defaults_test.go
Rama main · réplica de lectura del repositorio original.
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())
}
}