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.UseTLS { t.Fatalf("pool defaults wrong: %+v", cfg.Pool) } 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 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:3333" if got != want { t.Fatalf("PoolURL TLS: got %q want %q", got, want) } } func TestPoolURLPlainTCP(t *testing.T) { cfg := DefaultConfig() cfg.Pool.UseTLS = false 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") } }