package main import ( "context" "log/slog" "testing" "time" "github.com/pingular/pingularity/internal/config" "github.com/pingular/pingularity/internal/settings" "github.com/pingular/pingularity/internal/store" ) // TestDefaultSettings executes the fresh-install configuration literal - which no // other test does - and pins two things a silent edit would otherwise ship: // // 1. that each config duration/int maps to the RIGHT settings field. Every input // is distinct, so swapping any two same-typed fields (three retention // durations and two intervals sit adjacent in the literal) changes an asserted // value. The retention swap is the real hazard: it would keep latency data for // a year and delete speed history after a day, with the symptom weeks away. // 2. the hardcoded shipped defaults, so a change to any of them is deliberate. func TestDefaultSettings(t *testing.T) { cfg := config.Config{ Interval: 7 * time.Second, SpeedtestInterval: 42 * time.Minute, Timeout: 9 * time.Second, Retention: 11 * time.Hour, SpeedRetention: 22 * time.Hour, DowntimeRetention: 33 * time.Hour, DownAfter: 3, UpAfter: 4, LatencyEnabled: true, SpeedtestEnabled: true, SpeedtestOnReconnect: true, IPv6Mode: "on", } v := defaultSettings(cfg) // Mapping: each distinct input lands on its own field. A swap flips one of these. checks := []struct { name string got time.Duration want time.Duration }{ {"Latency<-Interval", v.Latency, cfg.Interval}, {"Speed<-SpeedtestInterval", v.Speed, cfg.SpeedtestInterval}, {"Timeout<-Timeout", v.Timeout, cfg.Timeout}, {"Retention<-Retention", v.Retention, cfg.Retention}, {"SpeedRetention<-SpeedRetention", v.SpeedRetention, cfg.SpeedRetention}, {"DowntimeRetention<-DowntimeRetention", v.DowntimeRetention, cfg.DowntimeRetention}, } for _, c := range checks { if c.got != c.want { t.Errorf("%s = %v, want %v (field mapping swapped?)", c.name, c.got, c.want) } } if v.DownAfter != cfg.DownAfter || v.UpAfter != cfg.UpAfter { t.Errorf("DownAfter/UpAfter = %d/%d, want %d/%d", v.DownAfter, v.UpAfter, cfg.DownAfter, cfg.UpAfter) } if v.IPv6Mode != cfg.IPv6Mode { t.Errorf("IPv6Mode = %q, want %q", v.IPv6Mode, cfg.IPv6Mode) } // Shipped defaults - each is a product decision, not incidental. wantBool := map[string]bool{ "Monitoring": v.Monitoring, "DNSProbe": v.DNSProbe, "NetinfoEnabled": v.NetinfoEnabled, "UpdateCheckEnabled": v.UpdateCheckEnabled, "LogRedactPII": v.LogRedactPII, "IperfUDP": v.IperfUDP, "OoklaLoss": v.OoklaLoss, "SpeedDiscardLosers": v.SpeedDiscardLosers, } for name, got := range wantBool { if !got { t.Errorf("default %s = false, want true", name) } } if v.SpeedtestRecordFailures { t.Error("SpeedtestRecordFailures defaults to true; recording failed tests must be opt-in (with it off, nothing new is written)") } if v.SpeedBestOfCount != 1 { t.Errorf("SpeedBestOfCount defaults to %d; a round costs N times the data and must be opt-in (1 = a single server)", v.SpeedBestOfCount) } if v.LogLevel != "off" { t.Errorf("LogLevel default = %q, want \"off\"", v.LogLevel) } if v.SpeedEngine != "ookla" { t.Errorf("SpeedEngine default = %q, want \"ookla\"", v.SpeedEngine) } if v.SpeedDirection != "both" { t.Errorf("SpeedDirection default = %q, want \"both\"", v.SpeedDirection) } if v.ExitTarget != "1.1.1.1" { t.Errorf("ExitTarget default = %q, want \"1.1.1.1\"", v.ExitTarget) } if v.DegradedPingMS != 150 || v.SpeedBusyMbps != 5 { t.Errorf("DegradedPingMS/SpeedBusyMbps = %v/%v, want 150/5", v.DegradedPingMS, v.SpeedBusyMbps) } // Probe readings are written in batches by default, on a new install and // on one that upgrades alike: neither has the key stored. The figure is in // the dashboard's help, its blank-field fallback and the docs. if v.SaveEvery != 5*time.Minute { t.Errorf("SaveEvery default = %v, want 5m", v.SaveEvery) } // Access is EXPLICIT, not guessed: loopback-only by default everywhere, and // off ONLY when the operator set -access network. No container heuristic. if !defaultSettings(cfg).AccessLocalOnly { t.Error("default (no -access) must be loopback-only everywhere, containers included") } net := cfg net.Access = "network" if defaultSettings(net).AccessLocalOnly { t.Error("-access network must default AccessLocalOnly=false") } loc := cfg loc.Access = "local" if !defaultSettings(loc).AccessLocalOnly { t.Error("-access local must default AccessLocalOnly=true") } } // reconcileAccess: an EXPLICIT -access / PINGULARITY_ACCESS must override a // disagreeing STORED access_local_only at boot - in either direction - and // persist the result; without explicit input the stored value always wins. // This is the recovery path for a bridged container that persisted // local_only=true under the old warn-only regime and is 403'd off its own // published port on upgrade, with no shell in the distroless image to repair // the database from. // // It is also the ONLY path that persists an access decision (pinned // structurally by TestOnlyExplicitInputPersistsAccess): the container // grandfather that used to write access_local_only=false from an inference // about a store's age is gone, because that inference could not tell a // container born private under a build too old to record its birth from a // genuinely 0.61-or-earlier one, and opening the wrong one put an // unauthenticated dashboard on the LAN. Ambiguity now fails closed and only // warns, which makes this override the way back in. func TestReconcileAccess(t *testing.T) { ctx := context.Background() newSet := func(t *testing.T, storedLocalOnly bool) *settings.Controller { t.Helper() st, err := store.Open(":memory:") if err != nil { t.Fatalf("open store: %v", err) } t.Cleanup(func() { st.Close() }) set, err := settings.New(ctx, st, settings.Values{ Latency: 5 * time.Second, Speed: time.Hour, Timeout: 2 * time.Second, DownAfter: 3, UpAfter: 2, }) if err != nil { t.Fatalf("new settings: %v", err) } // Simulate the upgraded install: the value sits in the STORE, where the // overlay makes it win over the flag-seeded default. if err := set.SetAccessLocalOnly(ctx, storedLocalOnly); err != nil { t.Fatalf("seed stored access_local_only: %v", err) } return set } cases := []struct { name string stored bool cfg config.Config wantChanged bool wantLocal bool }{ {"explicit network overrides stored local-only (the upgrade lockout)", true, config.Config{Access: "network", AccessExplicit: true}, true, false}, {"explicit local overrides stored network", false, config.Config{Access: "local", AccessExplicit: true}, true, true}, {"explicit and agreeing: nothing to do", false, config.Config{Access: "network", AccessExplicit: true}, false, false}, {"silent default never overrides stored local-only", true, config.Config{Access: "local", AccessExplicit: false}, false, true}, {"silent default leaves stored network alone too", false, config.Config{Access: "local", AccessExplicit: false}, false, false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { set := newSet(t, c.stored) changed, err := reconcileAccess(ctx, c.cfg, set) if err != nil { t.Fatalf("reconcileAccess: %v", err) } if changed != c.wantChanged { t.Errorf("changed = %v, want %v", changed, c.wantChanged) } if got := set.AccessLocalOnly(); got != c.wantLocal { t.Errorf("AccessLocalOnly = %v, want %v", got, c.wantLocal) } // The override must PERSIST (survive a reload), not just flip memory. if err := set.Reload(ctx); err != nil { t.Fatalf("reload: %v", err) } if got := set.AccessLocalOnly(); got != c.wantLocal { t.Errorf("after reload: AccessLocalOnly = %v, want %v (override not persisted)", got, c.wantLocal) } }) } } // TestLogLevelOffKeepsErrors pins B2: the default log level "off" must still // surface WARN and ERROR. It silences routine INFO/DEBUG, but a failed sample // write or a wedged webhook must not vanish just because logging is at its // default - those Errors are the only evidence a later support request has. // Regression proof: map "off" back to logLevelOff and both sub-tests fail. func TestLogLevelOffKeepsErrors(t *testing.T) { lvl := new(slog.LevelVar) applyLogLevel(lvl, "off") if lvl.Level() > slog.LevelError { t.Errorf("\"off\" set level %v, above LevelError - Errors would be dropped", lvl.Level()) } if lvl.Level() > slog.LevelWarn { t.Errorf("\"off\" set level %v, above LevelWarn - Warnings would be dropped", lvl.Level()) } // Still quiet for the routine stuff: INFO and DEBUG must not pass at "off". if lvl.Level() <= slog.LevelInfo { t.Errorf("\"off\" set level %v, at or below LevelInfo - routine chatter would print", lvl.Level()) } applyLogLevel(lvl, "on") if lvl.Level() != slog.LevelDebug { t.Errorf("\"on\" set level %v, want LevelDebug", lvl.Level()) } }