- Pool preset checkboxes with failover in Calibrate and Forge - Tier 1/2 UX: setup banner, forge next worker, LAN defaults, blueprint prompt - USB: auto-install forge tools, seed config.json, sync LAUNCH.bat
544 lines
15 KiB
Go
544 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func resetConfigFlags(args []string) {
|
|
flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError)
|
|
os.Args = args
|
|
}
|
|
|
|
func applyMergeFromJSON(t *testing.T, dst *Config, payload string) {
|
|
t.Helper()
|
|
var incoming Config
|
|
if err := json.Unmarshal([]byte(payload), &incoming); err != nil {
|
|
t.Fatalf("unmarshal incoming: %v", err)
|
|
}
|
|
var present map[string]json.RawMessage
|
|
if err := json.Unmarshal([]byte(payload), &present); err != nil {
|
|
t.Fatalf("unmarshal present keys: %v", err)
|
|
}
|
|
mergeConfigExplicit(dst, &incoming, present)
|
|
}
|
|
|
|
func TestDefaultConfigServerDefaults(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
if cfg.Port != 8989 {
|
|
t.Fatalf("port default: got %d", cfg.Port)
|
|
}
|
|
if cfg.Pool.Host != "pool.supportxmr.com" || cfg.Pool.Port != 443 || !cfg.Pool.UseTLS {
|
|
t.Fatalf("pool defaults wrong: %+v", cfg.Pool)
|
|
}
|
|
if len(cfg.Pool.BackupPools) < 2 {
|
|
t.Fatalf("expected TLS backup pools, got %+v", cfg.Pool.BackupPools)
|
|
}
|
|
if cfg.Server.PublicURL != "" {
|
|
t.Fatalf("public_url should default empty, got %q", cfg.Server.PublicURL)
|
|
}
|
|
if cfg.Server.StatsRetentionHours != 168 {
|
|
t.Fatalf("stats retention default: got %d", cfg.Server.StatsRetentionHours)
|
|
}
|
|
if cfg.Server.MaxAgents != 256 {
|
|
t.Fatalf("max agents default: got %d", cfg.Server.MaxAgents)
|
|
}
|
|
if !cfg.Server.LogAgentConnections {
|
|
t.Fatal("log_agent_connections should default true")
|
|
}
|
|
if cfg.Server.DashboardSubtitle != "security is just an emotion" {
|
|
t.Fatalf("dashboard subtitle default wrong: %q", cfg.Server.DashboardSubtitle)
|
|
}
|
|
if cfg.DefaultAgent.Threads != 4 {
|
|
t.Fatalf("default agent threads: got %d", cfg.DefaultAgent.Threads)
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigExplicitPortOnlyPreservesPoolTLS(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Pool.UseTLS = true
|
|
applyMergeFromJSON(t, dst, `{"port":9000}`)
|
|
if dst.Port != 9000 {
|
|
t.Fatalf("port not merged")
|
|
}
|
|
if !dst.Pool.UseTLS {
|
|
t.Fatal("partial PUT with port only must not reset pool.use_tls")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigExplicitNestedServerPartialPreservesBooleans(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Server.LogAgentConnections = true
|
|
dst.Server.OpenFirewallOnStart = true
|
|
dst.Server.DashboardSubtitle = "keep-me"
|
|
|
|
applyMergeFromJSON(t, dst, `{"server":{"dashboard_subtitle":"updated"}}`)
|
|
|
|
if dst.Server.DashboardSubtitle != "updated" {
|
|
t.Fatalf("subtitle not updated: %q", dst.Server.DashboardSubtitle)
|
|
}
|
|
if !dst.Server.LogAgentConnections {
|
|
t.Fatal("nested partial PUT must not reset log_agent_connections")
|
|
}
|
|
if !dst.Server.OpenFirewallOnStart {
|
|
t.Fatal("nested partial PUT must not reset open_firewall_on_start")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigExplicitAllowsBlankPublicURL(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Server.PublicURL = "https://stale.example.com"
|
|
|
|
applyMergeFromJSON(t, dst, `{"server":{"public_url":""}}`)
|
|
|
|
if dst.Server.PublicURL != "" {
|
|
t.Fatalf("explicit blank public_url must clear old value, got %q", dst.Server.PublicURL)
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigExplicitNestedPoolPartialPreservesUseTLS(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Pool.UseTLS = true
|
|
dst.Pool.Host = "original.pool"
|
|
|
|
applyMergeFromJSON(t, dst, `{"pool":{"host":"new.pool.example"}}`)
|
|
|
|
if dst.Pool.Host != "new.pool.example" {
|
|
t.Fatalf("host not updated: %q", dst.Pool.Host)
|
|
}
|
|
if !dst.Pool.UseTLS {
|
|
t.Fatal("nested partial pool PUT must preserve use_tls")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigExplicitExplicitBooleanFalse(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
if !dst.Pool.UseTLS {
|
|
t.Fatal("precondition: default pool TLS true")
|
|
}
|
|
|
|
applyMergeFromJSON(t, dst, `{"pool":{"use_tls":false}}`)
|
|
if dst.Pool.UseTLS {
|
|
t.Fatal("explicit use_tls:false must be applied")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigExplicitAbsentSectionUntouched(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Wallet.Address = "48keep"
|
|
applyMergeFromJSON(t, dst, `{"port":7777}`)
|
|
if dst.Wallet.Address != "48keep" {
|
|
t.Fatalf("wallet should be untouched: %q", dst.Wallet.Address)
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigExplicitNilPresentKeysFallsBack(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Pool.UseTLS = true
|
|
src := &Config{Port: 9000}
|
|
mergeConfigExplicit(dst, src, nil)
|
|
if dst.Port != 9000 {
|
|
t.Fatalf("port not merged via fallback")
|
|
}
|
|
// mergeConfig fallback still overwrites bools — documented legacy path
|
|
if dst.Pool.UseTLS {
|
|
t.Log("mergeConfig fallback resets UseTLS when presentKeys nil (LoadConfig uses mergeConfig)")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigExplicitDefaultAgentConfigKey(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.DefaultAgent.AdaptToHardware = true
|
|
dst.DefaultAgent.Threads = 8
|
|
|
|
applyMergeFromJSON(t, dst, `{"default_agent_config":{"threads":2}}`)
|
|
|
|
if dst.DefaultAgent.Threads != 2 {
|
|
t.Fatalf("threads not updated: %d", dst.DefaultAgent.Threads)
|
|
}
|
|
if !dst.DefaultAgent.AdaptToHardware {
|
|
t.Fatal("partial default_agent_config must preserve adapt_to_hardware")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigExplicitBackgroundPartial(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Background.AutoStart = true
|
|
dst.Background.SilentMode = true
|
|
|
|
applyMergeFromJSON(t, dst, `{"background":{"auto_start":false}}`)
|
|
|
|
if dst.Background.AutoStart {
|
|
t.Fatal("explicit auto_start:false must apply")
|
|
}
|
|
if !dst.Background.SilentMode {
|
|
t.Fatal("partial background PUT must preserve silent_mode")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigExplicitAlertsEmailEnabled(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Alerts.EmailEnabled = false
|
|
|
|
applyMergeFromJSON(t, dst, `{"alerts":{"email_enabled":true}}`)
|
|
if !dst.Alerts.EmailEnabled {
|
|
t.Fatal("explicit email_enabled:true must apply")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigPreservesPoolTLSWhenPartialUpdate(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Pool.UseTLS = true
|
|
src := &Config{Port: 9000}
|
|
mergeConfig(dst, src)
|
|
if dst.Port != 9000 {
|
|
t.Fatalf("port not merged")
|
|
}
|
|
// mergeConfig (file load path) still resets bools — known legacy behaviour
|
|
if !dst.Pool.UseTLS {
|
|
t.Log("mergeConfig resets UseTLS on partial update — file load uses mergeConfig not mergeConfigExplicit")
|
|
}
|
|
}
|
|
|
|
func TestConfigJSONRoundTrip(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
cfg.Wallet.Address = "48testwallet"
|
|
data, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var decoded Config
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decoded.Wallet.Address != cfg.Wallet.Address {
|
|
t.Fatalf("wallet mismatch")
|
|
}
|
|
}
|
|
|
|
func TestDefaultConfigPort(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
if cfg.Port != 8989 {
|
|
t.Fatalf("expected port 8989, got %d", cfg.Port)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigFromFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "config.json")
|
|
data, err := json.Marshal(DefaultConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(cfgPath, data, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var loaded Config
|
|
if err := json.Unmarshal(raw, &loaded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if loaded.Port != 8989 {
|
|
t.Fatalf("expected port 8989, got %d", loaded.Port)
|
|
}
|
|
}
|
|
|
|
func TestConfigSaveAndReload(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := DefaultConfig()
|
|
cfg.DataDir = dir
|
|
cfg.Port = 7777
|
|
cfg.Wallet.Address = "48savedwallet"
|
|
cfg.Server.FleetSecret = "test-secret"
|
|
|
|
if err := cfg.Save(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
raw, err := os.ReadFile(filepath.Join(dir, "config.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var loaded Config
|
|
if err := json.Unmarshal(raw, &loaded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if loaded.Port != 7777 {
|
|
t.Fatalf("saved port: got %d", loaded.Port)
|
|
}
|
|
if loaded.Wallet.Address != "48savedwallet" {
|
|
t.Fatalf("saved wallet: got %q", loaded.Wallet.Address)
|
|
}
|
|
if loaded.Server.FleetSecret != "test-secret" {
|
|
t.Fatalf("saved fleet secret: got %q", loaded.Server.FleetSecret)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigCLIFlags(t *testing.T) {
|
|
dir := t.TempDir()
|
|
resetConfigFlags([]string{"test", "-port", "9999", "-data", dir})
|
|
|
|
cfg := LoadConfig()
|
|
if cfg.Port != 9999 {
|
|
t.Fatalf("CLI port: got %d", cfg.Port)
|
|
}
|
|
if cfg.DataDir != dir {
|
|
t.Fatalf("CLI data dir: got %q", cfg.DataDir)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigMergesFileOverrides(t *testing.T) {
|
|
dir := t.TempDir()
|
|
fileCfg := DefaultConfig()
|
|
fileCfg.Port = 9001
|
|
fileCfg.Pool.Host = "custom.pool.example"
|
|
fileCfg.Wallet.Address = "48fromfile"
|
|
data, err := json.Marshal(fileCfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "config.json"), data, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resetConfigFlags([]string{"test", "-port", "8989", "-data", dir})
|
|
cfg := LoadConfig()
|
|
|
|
if cfg.Port != 9001 {
|
|
t.Fatalf("file port override: got %d", cfg.Port)
|
|
}
|
|
if cfg.Pool.Host != "custom.pool.example" {
|
|
t.Fatalf("file pool host: got %q", cfg.Pool.Host)
|
|
}
|
|
if cfg.Wallet.Address != "48fromfile" {
|
|
t.Fatalf("file wallet: got %q", cfg.Wallet.Address)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigLegacyOpenFirewallDefault(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Legacy config without open_firewall_on_start key — LoadConfig forces true.
|
|
payload := `{"port":9100,"server":{"dashboard_subtitle":"legacy"}}`
|
|
if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(payload), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resetConfigFlags([]string{"test", "-data", dir})
|
|
cfg := LoadConfig()
|
|
|
|
if !cfg.Server.OpenFirewallOnStart {
|
|
t.Fatal("legacy config without open_firewall_on_start must default true")
|
|
}
|
|
if cfg.Server.DashboardSubtitle != "legacy" {
|
|
t.Fatalf("subtitle from file: got %q", cfg.Server.DashboardSubtitle)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigExplicitOpenFirewallFalse(t *testing.T) {
|
|
dir := t.TempDir()
|
|
payload := `{"server":{"open_firewall_on_start":false}}`
|
|
if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(payload), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resetConfigFlags([]string{"test", "-data", dir})
|
|
cfg := LoadConfig()
|
|
|
|
if cfg.Server.OpenFirewallOnStart {
|
|
t.Fatal("explicit open_firewall_on_start:false must be honored")
|
|
}
|
|
}
|
|
|
|
func TestPoolURLTLS(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
got := cfg.PoolURL()
|
|
want := "stratum+ssl://pool.supportxmr.com:443"
|
|
if got != want {
|
|
t.Fatalf("PoolURL TLS: got %q want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestPoolURLPlainTCP(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
cfg.Pool.UseTLS = false
|
|
cfg.Pool.Port = 3333
|
|
got := cfg.PoolURL()
|
|
want := "stratum+tcp://pool.supportxmr.com:3333"
|
|
if got != want {
|
|
t.Fatalf("PoolURL plain: got %q want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNestedJSONKeys(t *testing.T) {
|
|
present := map[string]json.RawMessage{
|
|
"pool": json.RawMessage(`{"host":"x","port":4444}`),
|
|
"server": json.RawMessage(`invalid`),
|
|
}
|
|
poolKeys := nestedJSONKeys(present, "pool")
|
|
if poolKeys == nil {
|
|
t.Fatal("expected pool keys")
|
|
}
|
|
if _, ok := poolKeys["host"]; !ok {
|
|
t.Fatal("missing host key")
|
|
}
|
|
if nestedJSONKeys(present, "missing") != nil {
|
|
t.Fatal("missing section should return nil")
|
|
}
|
|
if nestedJSONKeys(present, "server") != nil {
|
|
t.Fatal("invalid nested JSON should return nil")
|
|
}
|
|
if nestedJSONKeys(nil, "pool") != nil {
|
|
t.Fatal("nil present map should return nil")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigLegacyScalarsAndBooleans(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Wallet.Address = "48original"
|
|
dst.Pool.UseTLS = true
|
|
dst.Background.SilentMode = true
|
|
dst.Server.LogAgentConnections = true
|
|
|
|
src := &Config{
|
|
Port: 8080,
|
|
Pool: PoolConfig{Host: "legacy.pool", Port: 4444, Password: "pw"},
|
|
Wallet: WalletConfig{Address: "48new"},
|
|
Background: BackgroundConfig{
|
|
SilentMode: false,
|
|
RunAs: "user",
|
|
AutoStart: false,
|
|
},
|
|
Alerts: AlertsConfig{EmailEnabled: true},
|
|
Server: ServerSettings{
|
|
LogAgentConnections: false,
|
|
LogShareSubmissions: true,
|
|
OpenFirewallOnStart: false,
|
|
DashboardSubtitle: "merged",
|
|
StatsRetentionHours: 72,
|
|
},
|
|
}
|
|
mergeConfig(dst, src)
|
|
|
|
if dst.Port != 8080 {
|
|
t.Fatalf("port: got %d", dst.Port)
|
|
}
|
|
if dst.Pool.Host != "legacy.pool" || dst.Pool.Port != 4444 {
|
|
t.Fatalf("pool: %+v", dst.Pool)
|
|
}
|
|
if dst.Wallet.Address != "48new" {
|
|
t.Fatalf("wallet: %q", dst.Wallet.Address)
|
|
}
|
|
if dst.Background.SilentMode {
|
|
t.Fatal("mergeConfig must apply background.silent_mode false")
|
|
}
|
|
if dst.Background.RunAs != "user" {
|
|
t.Fatalf("run_as: %q", dst.Background.RunAs)
|
|
}
|
|
if dst.Background.AutoStart {
|
|
t.Fatal("auto_start false must apply")
|
|
}
|
|
if !dst.Alerts.EmailEnabled {
|
|
t.Fatal("email_enabled true must apply")
|
|
}
|
|
if dst.Server.LogAgentConnections {
|
|
t.Fatal("log_agent_connections false must apply")
|
|
}
|
|
if !dst.Server.LogShareSubmissions {
|
|
t.Fatal("log_share_submissions true must apply")
|
|
}
|
|
if dst.Server.OpenFirewallOnStart {
|
|
t.Fatal("open_firewall_on_start false must apply")
|
|
}
|
|
if dst.Server.DashboardSubtitle != "merged" {
|
|
t.Fatalf("subtitle: %q", dst.Server.DashboardSubtitle)
|
|
}
|
|
if dst.Server.StatsRetentionHours != 72 {
|
|
t.Fatalf("stats retention: %d", dst.Server.StatsRetentionHours)
|
|
}
|
|
// Known legacy: UseTLS copied from src even when false on zero-value partial src
|
|
if dst.Pool.UseTLS {
|
|
t.Log("mergeConfig sets UseTLS from src zero value on partial update")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigLegacyDefaultAgentBools(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.DefaultAgent.AdaptToHardware = true
|
|
dst.DefaultAgent.FileLogging = true
|
|
dst.DefaultAgent.StealthMode = false
|
|
|
|
src := &Config{
|
|
DefaultAgent: AgentDefaults{
|
|
StealthMode: true,
|
|
},
|
|
}
|
|
mergeConfig(dst, src)
|
|
|
|
if !dst.DefaultAgent.StealthMode {
|
|
t.Fatal("stealth_mode true must apply via bool branch")
|
|
}
|
|
if dst.DefaultAgent.AdaptToHardware {
|
|
t.Fatal("adapt_to_hardware should follow src false on stealth_mode branch")
|
|
}
|
|
if dst.DefaultAgent.FileLogging {
|
|
t.Fatal("file_logging should follow src false on stealth_mode branch")
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigLegacyPoolUseTLSFalse(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Pool.UseTLS = true
|
|
src := &Config{Pool: PoolConfig{Host: "only-host"}}
|
|
mergeConfig(dst, src)
|
|
if dst.Pool.UseTLS {
|
|
t.Fatal("mergeConfig always copies UseTLS from src; zero src clears TLS")
|
|
}
|
|
if dst.Pool.Host != "only-host" {
|
|
t.Fatalf("host: %q", dst.Pool.Host)
|
|
}
|
|
}
|
|
|
|
func TestMergeConfigLegacyZeroValuesPreserveScalars(t *testing.T) {
|
|
dst := DefaultConfig()
|
|
dst.Port = 8989
|
|
dst.Pool.Port = 3333
|
|
src := &Config{}
|
|
mergeConfig(dst, src)
|
|
if dst.Port != 8989 {
|
|
t.Fatalf("zero src port must not overwrite: got %d", dst.Port)
|
|
}
|
|
if dst.Pool.Port != 3333 {
|
|
t.Fatalf("zero src pool port must not overwrite: got %d", dst.Pool.Port)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigIgnoresInvalidJSONFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(`{broken`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resetConfigFlags([]string{"test", "-port", "8888", "-data", dir})
|
|
cfg := LoadConfig()
|
|
if cfg.Port != 8888 {
|
|
t.Fatalf("invalid file should fall back to CLI port: got %d", cfg.Port)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigOpenFirewallKeyDetection(t *testing.T) {
|
|
withKey := `{"server":{"open_firewall_on_start":false}}`
|
|
if strings.Contains(withKey, `"open_firewall_on_start"`) != true {
|
|
t.Fatal("test precondition")
|
|
}
|
|
withoutKey := `{"server":{"dashboard_subtitle":"x"}}`
|
|
if strings.Contains(withoutKey, `"open_firewall_on_start"`) {
|
|
t.Fatal("test precondition")
|
|
}
|
|
}
|