1 parent
cc7466823b
commit
e8c5b414d9
44 files changed
+7649
-344
No files matched your search
@@ -0,0 +1,148 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// configMapBody extracts the `config.yaml: |` block from a k8s manifest. The
|
||||
// ConfigMap is the thing an operator actually applies, and nothing in the test
|
||||
// suite loads it, so a typo in it is invisible until a pod crash-loops.
|
||||
func configMapBody(t *testing.T, manifest, key string) string {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(manifest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lines := strings.Split(string(raw), "\n")
|
||||
header := " " + key + ": |"
|
||||
start := -1
|
||||
for i, line := range lines {
|
||||
if strings.TrimRight(line, " \r") == header {
|
||||
start = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if start < 0 {
|
||||
t.Fatalf("%s has no %q block", manifest, key)
|
||||
}
|
||||
var body []string
|
||||
for _, line := range lines[start:] {
|
||||
// The block scalar's content is indented past the key; the first
|
||||
// line that is not ends the block.
|
||||
if strings.TrimSpace(line) != "" && !strings.HasPrefix(line, " ") {
|
||||
break
|
||||
}
|
||||
body = append(body, strings.TrimPrefix(line, " "))
|
||||
}
|
||||
if len(body) == 0 {
|
||||
t.Fatalf("%s: the %q block is empty", manifest, key)
|
||||
}
|
||||
return strings.Join(body, "\n")
|
||||
}
|
||||
|
||||
// writeTemp writes body to a temp config file and returns its path. Load
|
||||
// resolves relative paths against the file's directory, so the fixture has to
|
||||
// be a real file in a real directory rather than an in-memory string.
|
||||
func writeTemp(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// TestExampleConfigsValidate loads the two configuration files we ship and
|
||||
// requires both to survive Load, which ends in Validate. Everything else in
|
||||
// this file is free to be wrong; a shipped example that cannot start is not.
|
||||
func TestExampleConfigsValidate(t *testing.T) {
|
||||
t.Run("deploy/config.example.yaml", func(t *testing.T) {
|
||||
cfg, err := Load(filepath.Join("..", "..", "deploy", "config.example.yaml"))
|
||||
if err != nil {
|
||||
t.Fatalf("the shipped example config does not load: %v", err)
|
||||
}
|
||||
// Spot-check that the example is not merely valid but is saying what
|
||||
// its comments say: the API is open for writes here, metrics are on,
|
||||
// and the three blocklist sources are the three the file lists.
|
||||
if !cfg.API.AllowConfigWrite {
|
||||
t.Error("config.example.yaml documents config writes but sets allow_config_write: false")
|
||||
}
|
||||
if !cfg.Metrics.Enabled || !cfg.Metrics.Protect {
|
||||
t.Error("config.example.yaml should ship metrics enabled and protected")
|
||||
}
|
||||
if got := len(cfg.Block.Lists); got != 3 {
|
||||
t.Errorf("blocklist lists = %d, want 3 (haas, easylist, local-rpz)", got)
|
||||
}
|
||||
if got := cfg.Memory.Reserve; got != 256*1024*1024 {
|
||||
t.Errorf("memory.reserve = %d, want 256MiB as documented", int64(got))
|
||||
}
|
||||
// The example must not carry a key that looks like a real one. This
|
||||
// is the file people copy, and a working key in it is a published
|
||||
// credential.
|
||||
for _, k := range cfg.API.Auth.APIKeys {
|
||||
if len(k) > 12 && !strings.Contains(k, "CHANGE-ME") {
|
||||
t.Errorf("config.example.yaml ships an API key that is not a "+
|
||||
"placeholder: %q", k)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("deploy/k8s/00-config.yaml ConfigMap", func(t *testing.T) {
|
||||
manifest := filepath.Join("..", "..", "deploy", "k8s", "00-config.yaml")
|
||||
body := configMapBody(t, manifest, "config.yaml")
|
||||
// The key comes from a Secret, so the manifest's ${FANCYDNS_API_KEY}
|
||||
// has to be set for the config to load at all — which is the point:
|
||||
// a missing Secret is a crash loop, not an open console.
|
||||
t.Setenv("FANCYDNS_API_KEY", "k8s-manifest-fixture-key")
|
||||
|
||||
cfg, err := Load(writeTemp(t, body))
|
||||
if err != nil {
|
||||
t.Fatalf("the k8s ConfigMap does not load: %v", err)
|
||||
}
|
||||
if got := cfg.API.Auth.APIKeys; len(got) != 1 || got[0] != "k8s-manifest-fixture-key" {
|
||||
t.Errorf("api keys = %q, want the expanded ${FANCYDNS_API_KEY}", got)
|
||||
}
|
||||
if cfg.API.Auth.TrustLoopback {
|
||||
t.Error("the k8s manifest exposes the API on 0.0.0.0, so trust_loopback must be false")
|
||||
}
|
||||
if cfg.API.AllowConfigWrite {
|
||||
t.Error("the k8s manifest sets allow_config_write: false; a ConfigMap " +
|
||||
"is not writable from the pod")
|
||||
}
|
||||
// The memory envelope is matched to the StatefulSet's 1Gi limit, and a
|
||||
// limit above the cgroup's is the cgroup kill AGENTS.md warns about.
|
||||
if got := int64(cfg.Memory.Limit); got != 1024*1024*1024 {
|
||||
t.Errorf("memory.limit = %d, want 1GiB", got)
|
||||
}
|
||||
if got := int64(cfg.Memory.CacheMaxBytes); got != 384*1024*1024 {
|
||||
t.Errorf("memory.cache_max_bytes = %d, want 384MiB", got)
|
||||
}
|
||||
if got := cfg.Cache.Size; got != 500000 {
|
||||
t.Errorf("cache.size = %d, want the 500000 the manifest documents", got)
|
||||
}
|
||||
if !cfg.DNS.DoT.Enabled || !cfg.DNS.DoH.Enabled {
|
||||
t.Error("the k8s manifest enables both DoT and DoH; both are required for the Ingress")
|
||||
}
|
||||
if !cfg.Metrics.Protect {
|
||||
t.Error("metrics.protect must stay true in the cluster")
|
||||
}
|
||||
if got := cfg.Log.Format; got != "json" {
|
||||
t.Errorf("log.format = %q, want json in a cluster", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an unset API key variable is still fatal", func(t *testing.T) {
|
||||
// The control for the case above: the expansion does not fall back to
|
||||
// something harmless when the Secret is missing.
|
||||
manifest := filepath.Join("..", "..", "deploy", "k8s", "00-config.yaml")
|
||||
t.Setenv("FANCYDNS_API_KEY", "")
|
||||
os.Unsetenv("FANCYDNS_API_KEY")
|
||||
if _, err := Load(writeTemp(t, configMapBody(t, manifest, "config.yaml"))); err == nil {
|
||||
t.Fatal("the k8s ConfigMap loaded with no API key in the environment; " +
|
||||
"a missing Secret must be a startup failure")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user