Ctrl K
ComunidadGuías…

defaults_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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())
	}
}