package config import ( "os" "path/filepath" "testing" ) // loadYAML writes body to a temp file and loads it, failing the test if Load // errors. Every test here wants "Load accepted this", so the helper asserts it // once instead of repeating the check. func loadYAML(t *testing.T, body string) *Config { t.Helper() path := filepath.Join(t.TempDir(), "config.yaml") if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } cfg, err := Load(path) if err != nil { t.Fatalf("Load rejected the config: %v", err) } return cfg } // TestLoadIgnoresUnknownKeys pins a decision STATUS.md lists as open: Load // unmarshals leniently, so a key the struct does not know is silently dropped // instead of rejected. yaml.KnownFields(true) would catch it, and was // deliberately NOT adopted: it is a behaviour change for anyone carrying extra // keys in their config. // // The reason this is pinned rather than left alone is that leniency has already // cost this project twice (two test fixtures were caught by a stray key, not by // what they were testing). If the decision ever flips, the failure below is the // reminder to update STATUS.md, AGENTS.md and the README together. func TestLoadIgnoresUnknownKeys(t *testing.T) { t.Run("a misspelled top-level block is dropped", func(t *testing.T) { // The real trap from the field: the struct tag is "blocklist", so // writing "block:" produces a plausible-looking server that has // quietly kept every blocklist default. The correctly spelled // block below disables the blocklist so the config validates at all // — which is itself the sharper half of the pin: if the typo were // honoured, block: {enabled: true} with no sources would fail // Validate and the server would refuse to start. cfg := loadYAML(t, ` blocklist: enabled: false block: enabled: true response_ip: 10.0.0.1 response_ttl: 5s `) if cfg.Block.Enabled { t.Fatal("the misspelled block: block was applied; Load is no longer lenient " + "and this pin is stale") } if cfg.Block.ResponseIP != Default().Block.ResponseIP { t.Fatalf("blocklist.response_ip = %q, want the default %q", cfg.Block.ResponseIP, Default().Block.ResponseIP) } }) t.Run("the same block spelled correctly is applied", func(t *testing.T) { // The control for the case above: the lenient path is the // difference between these two keys, not Load ignoring the file. cfg := loadYAML(t, ` blocklist: enabled: false response_ip: 10.0.0.1 response_ttl: 5s `) if cfg.Block.Enabled { t.Fatal("blocklist.enabled: false was not applied") } if cfg.Block.ResponseIP != "10.0.0.1" { t.Fatalf("blocklist.response_ip = %q, want 10.0.0.1", cfg.Block.ResponseIP) } }) t.Run("a misspelled nested key is dropped", func(t *testing.T) { cfg := loadYAML(t, ` blocklist: enabled: false api: basepath: /console `) if cfg.API.BasePath != Default().API.BasePath { t.Fatalf("api.base_path = %q, want the default %q — leniency changed", cfg.API.BasePath, Default().API.BasePath) } }) t.Run("an unknown top-level key is dropped", func(t *testing.T) { cfg := loadYAML(t, ` blocklist: enabled: false telemetry: endpoint: https://example.invalid `) if cfg.API.Enabled != Default().API.Enabled { t.Fatalf("an unknown key changed the config: api.enabled = %v", cfg.API.Enabled) } }) t.Run("a known key with a bad TYPE is still fatal", func(t *testing.T) { // The asymmetry is the point: a typo is invisible, a wrong value type // is a startup failure. Leniency covers unknown keys only. path := filepath.Join(t.TempDir(), "config.yaml") if err := os.WriteFile(path, []byte("blocklist:\n enabled: false\nserver:\n max_concurrent: many\n"), 0o600); err != nil { t.Fatal(err) } if _, err := Load(path); err == nil { t.Fatal("Load accepted a string where a number belongs; the strictness " + "contrast this pin documents is gone") } }) }