package main import ( "encoding/json" "os" "path/filepath" "testing" ) 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) } }