package config import ( "os" "path/filepath" "strings" "testing" ) func TestIsLoopbackAddr(t *testing.T) { cases := []struct { addr string want bool }{ {"127.0.0.1:8080", true}, {"localhost:8080", true}, {"[::1]:8080", true}, {"127.0.0.5:80", true}, // any 127/8 is loopback {"0.0.0.0:8080", false}, {":8443", false}, // bare port = all interfaces {"10.0.0.1:80", false}, {"example.com:443", false}, {"[2001:db8::1]:80", false}, {"", false}, {"not-an-addr", false}, // SplitHostPort fails } for _, tc := range cases { t.Run(tc.addr, func(t *testing.T) { if got := isLoopbackAddr(tc.addr); got != tc.want { t.Errorf("isLoopbackAddr(%q) = %v, want %v", tc.addr, got, tc.want) } }) } } func TestLoad(t *testing.T) { root := t.TempDir() // Drop a placeholder .zddc so subtests using this root don't trip the // "no root .zddc → refuse to start" safety check. Tests that explicitly // exercise the missing-.zddc path use a dedicated tmpdir without one. if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("admins: [test@example.com]\n"), 0o644); err != nil { t.Fatalf("seed root .zddc: %v", err) } // Pre-set the env so each subtest can override what it needs. type envSet map[string]string clearAll := func() { for _, k := range []string{"ZDDC_ROOT", "ZDDC_ADDR", "ZDDC_TLS_CERT", "ZDDC_TLS_KEY", "ZDDC_INSECURE_DIRECT", "ZDDC_INSECURE", "ZDDC_LOG_LEVEL", "ZDDC_INDEX_PATH", "ZDDC_EMAIL_HEADER", "ZDDC_CORS_ORIGIN"} { os.Unsetenv(k) } } apply := func(env envSet) { clearAll() for k, v := range env { os.Setenv(k, v) } } cases := []struct { name string env envSet wantErr bool errContains string check func(*testing.T, Config) }{ { name: "missing root defaults to CWD", // ZDDC_INSECURE=1 because the package's CWD has no .zddc; this test // is specifically about Root resolution falling back to CWD, not // about the .zddc safety check. env: envSet{"ZDDC_INSECURE": "1"}, // ZDDC_ROOT not set → Load falls back to os.Getwd(). check: func(t *testing.T, cfg Config) { cwd, err := os.Getwd() if err != nil { t.Fatalf("Getwd: %v", err) } // os.Stat resolves symlinks; so does Load via filepath behavior, so // just compare the resolved values. if cfg.Root != cwd { t.Errorf("Root = %q, want CWD %q", cfg.Root, cwd) } }, }, { name: "root not a directory", env: envSet{"ZDDC_ROOT": filepath.Join(root, "does-not-exist")}, wantErr: true, errContains: "not accessible", }, { name: "defaults applied with TLS self-signed", env: envSet{"ZDDC_ROOT": root}, check: func(t *testing.T, cfg Config) { if cfg.Addr != ":8443" { t.Errorf("Addr = %q, want :8443", cfg.Addr) } if cfg.TLSMode != "selfsigned" { t.Errorf("TLSMode = %q, want selfsigned", cfg.TLSMode) } if cfg.IndexPath != ".archive" { t.Errorf("IndexPath = %q, want .archive", cfg.IndexPath) } if cfg.EmailHeader != "X-Auth-Request-Email" { t.Errorf("EmailHeader = %q, want X-Auth-Request-Email", cfg.EmailHeader) } if len(cfg.CORSOrigins) != 0 { t.Errorf("CORSOrigins = %v, want empty (CORS disabled by default)", cfg.CORSOrigins) } }, }, { name: "CORS single origin override", env: envSet{ "ZDDC_ROOT": root, "ZDDC_CORS_ORIGIN": "https://tools.acme.com", }, check: func(t *testing.T, cfg Config) { if len(cfg.CORSOrigins) != 1 || cfg.CORSOrigins[0] != "https://tools.acme.com" { t.Errorf("CORSOrigins = %v, want [https://tools.acme.com]", cfg.CORSOrigins) } }, }, { name: "CORS multi-origin allowlist", env: envSet{ "ZDDC_ROOT": root, "ZDDC_CORS_ORIGIN": "https://a.example, https://b.example ,https://c.example", }, check: func(t *testing.T, cfg Config) { want := []string{"https://a.example", "https://b.example", "https://c.example"} if len(cfg.CORSOrigins) != len(want) { t.Fatalf("CORSOrigins = %v, want %v", cfg.CORSOrigins, want) } for i, w := range want { if cfg.CORSOrigins[i] != w { t.Errorf("CORSOrigins[%d] = %q, want %q", i, cfg.CORSOrigins[i], w) } } }, }, { name: "CORS disabled with empty value", env: envSet{ "ZDDC_ROOT": root, "ZDDC_CORS_ORIGIN": "", }, check: func(t *testing.T, cfg Config) { if len(cfg.CORSOrigins) != 0 { t.Errorf("CORSOrigins = %v, want empty (CORS disabled)", cfg.CORSOrigins) } }, }, { name: "TLS provided requires both cert and key", env: envSet{ "ZDDC_ROOT": root, "ZDDC_TLS_CERT": "/some/cert.pem", // missing key }, wantErr: true, errContains: "both be set or both be empty", }, { name: "plain HTTP on all interfaces without insecure flag is rejected", env: envSet{ "ZDDC_ROOT": root, "ZDDC_TLS_CERT": "none", "ZDDC_ADDR": ":8080", }, wantErr: true, errContains: "--insecure-direct", }, { name: "plain HTTP on 0.0.0.0 without insecure flag is rejected", env: envSet{ "ZDDC_ROOT": root, "ZDDC_TLS_CERT": "none", "ZDDC_ADDR": "0.0.0.0:8080", }, wantErr: true, errContains: "--insecure-direct", }, { name: "plain HTTP on loopback is allowed", env: envSet{ "ZDDC_ROOT": root, "ZDDC_TLS_CERT": "none", "ZDDC_ADDR": "127.0.0.1:8080", }, check: func(t *testing.T, cfg Config) { if cfg.TLSMode != "none" { t.Errorf("TLSMode = %q, want none", cfg.TLSMode) } }, }, { name: "plain HTTP on non-loopback with explicit opt-in is allowed", env: envSet{ "ZDDC_ROOT": root, "ZDDC_TLS_CERT": "none", "ZDDC_ADDR": ":8080", "ZDDC_INSECURE_DIRECT": "1", }, check: func(t *testing.T, cfg Config) { if cfg.TLSMode != "none" { t.Errorf("TLSMode = %q, want none", cfg.TLSMode) } }, }, { name: "ZDDC_INSECURE_DIRECT set to non-1 is not opt-in", env: envSet{ "ZDDC_ROOT": root, "ZDDC_TLS_CERT": "none", "ZDDC_ADDR": ":8080", "ZDDC_INSECURE_DIRECT": "true", // must be exactly "1" }, wantErr: true, errContains: "--insecure-direct", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { apply(tc.env) defer clearAll() cfg, err := Load([]string{}) if tc.wantErr { if err == nil { t.Fatalf("Load() = nil error, want error containing %q", tc.errContains) } if tc.errContains != "" && !strings.Contains(err.Error(), tc.errContains) { t.Errorf("Load() error = %v, want substring %q", err, tc.errContains) } return } if err != nil { t.Fatalf("Load() unexpected error: %v", err) } if tc.check != nil { tc.check(t, cfg) } }) } } // tmpRootWithZddc creates a temp dir and seeds a minimal .zddc so the // post-Root-stat safety check (refuse to start with no root .zddc) does // not fire. Tests that exercise the safety check explicitly use // t.TempDir() directly without seeding. func tmpRootWithZddc(t *testing.T) string { t.Helper() dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, ".zddc"), []byte("admins: [test@example.com]\n"), 0o644); err != nil { t.Fatalf("seed root .zddc: %v", err) } return dir } // TestLoadFlags_OverrideEnv: --root flag wins over ZDDC_ROOT env var. func TestLoadFlags_OverrideEnv(t *testing.T) { envRoot := tmpRootWithZddc(t) flagRoot := tmpRootWithZddc(t) os.Setenv("ZDDC_ROOT", envRoot) defer os.Unsetenv("ZDDC_ROOT") cfg, err := Load([]string{"--root", flagRoot}) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Root != flagRoot { t.Errorf("Root = %q, want flag value %q", cfg.Root, flagRoot) } } // TestLoadFlags_AddrLogLevelFromFlags: arbitrary flags override env defaults. func TestLoadFlags_AddrLogLevelFromFlags(t *testing.T) { root := tmpRootWithZddc(t) cfg, err := Load([]string{ "--root", root, "--addr", "127.0.0.1:9999", "--log-level", "debug", "--index-path", ".myindex", "--email-header", "X-User-Email", }) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Addr != "127.0.0.1:9999" { t.Errorf("Addr=%q", cfg.Addr) } if cfg.LogLevel != "debug" { t.Errorf("LogLevel=%q", cfg.LogLevel) } if cfg.IndexPath != ".myindex" { t.Errorf("IndexPath=%q", cfg.IndexPath) } if cfg.EmailHeader != "X-User-Email" { t.Errorf("EmailHeader=%q", cfg.EmailHeader) } } // TestLoadFlags_CORSExplicitEmptyDisables: --cors-origin="" explicitly disables CORS. func TestLoadFlags_CORSExplicitEmptyDisables(t *testing.T) { root := tmpRootWithZddc(t) cfg, err := Load([]string{"--root", root, "--cors-origin", ""}) if err != nil { t.Fatalf("Load: %v", err) } if len(cfg.CORSOrigins) != 0 { t.Errorf("CORSOrigins = %v, want empty (CORS disabled by explicit empty flag)", cfg.CORSOrigins) } } // TestLoadFlags_HelpRequested: --help returns the sentinel error. func TestLoadFlags_HelpRequested(t *testing.T) { _, err := Load([]string{"--help"}) if !strings.Contains(err.Error(), "help requested") && err != ErrHelpRequested { t.Errorf("got err=%v, want ErrHelpRequested", err) } } // TestLoadFlags_VersionRequested: --version returns the sentinel error. func TestLoadFlags_VersionRequested(t *testing.T) { _, err := Load([]string{"--version"}) if !strings.Contains(err.Error(), "version requested") && err != ErrVersionRequested { t.Errorf("got err=%v, want ErrVersionRequested", err) } } // TestLoadFlags_RootFlagDefaultsToCWD: with no --root and no ZDDC_ROOT, falls back to CWD. func TestLoadFlags_RootFlagDefaultsToCWD(t *testing.T) { os.Unsetenv("ZDDC_ROOT") // ZDDC_INSECURE=1 because the package's CWD has no .zddc; this test is // specifically about the CWD fallback, not the .zddc safety check. os.Setenv("ZDDC_INSECURE", "1") defer os.Unsetenv("ZDDC_INSECURE") cfg, err := Load([]string{}) if err != nil { t.Fatalf("Load: %v", err) } cwd, _ := os.Getwd() if cfg.Root != cwd { t.Errorf("Root=%q, want CWD=%q", cfg.Root, cwd) } } // TestLoad_MissingRootZddcRefusesStartByDefault: with no .zddc at root and no // --insecure, Load refuses to start (the public-by-default footgun). func TestLoad_MissingRootZddcRefusesStartByDefault(t *testing.T) { root := t.TempDir() // no .zddc seeded os.Setenv("ZDDC_ROOT", root) defer os.Unsetenv("ZDDC_ROOT") _, err := Load([]string{}) if err == nil { t.Fatal("Load() = nil error, want error about missing root .zddc") } if !strings.Contains(err.Error(), ".zddc") || !strings.Contains(err.Error(), "publicly accessible") { t.Errorf("Load() error = %v, want substring about missing .zddc and public access", err) } } // TestLoad_MissingRootZddcAllowedWithInsecure: --insecure allows startup // when the root .zddc is missing (acknowledges the public-tree shape). func TestLoad_MissingRootZddcAllowedWithInsecure(t *testing.T) { root := t.TempDir() // no .zddc seeded os.Setenv("ZDDC_ROOT", root) os.Setenv("ZDDC_INSECURE", "1") defer os.Unsetenv("ZDDC_ROOT") defer os.Unsetenv("ZDDC_INSECURE") cfg, err := Load([]string{}) if err != nil { t.Fatalf("Load() unexpected error: %v", err) } if !cfg.Insecure { t.Errorf("cfg.Insecure = false, want true (set via ZDDC_INSECURE=1)") } } // TestLoad_MissingRootZddcAllowedWithInsecureFlag: same but via --insecure flag. func TestLoad_MissingRootZddcAllowedWithInsecureFlag(t *testing.T) { root := t.TempDir() // no .zddc seeded cfg, err := Load([]string{"--root", root, "--insecure"}) if err != nil { t.Fatalf("Load() unexpected error: %v", err) } if !cfg.Insecure { t.Errorf("cfg.Insecure = false, want true (set via --insecure)") } } // TestLoad_ClientModeAddrDefaultsToLoopback confirms that --upstream // triggers the client-mode loopback default for --addr. The cache // layer forwards the configured bearer to upstream without checking // the local caller, so a non-loopback default would be a confused- // deputy open proxy on whatever interface the host happens to bind. func TestLoad_ClientModeAddrDefaultsToLoopback(t *testing.T) { root := t.TempDir() bearer := filepath.Join(root, "tok") if err := os.WriteFile(bearer, []byte("xyz"), 0o600); err != nil { t.Fatalf("seed bearer: %v", err) } cfg, err := Load([]string{ "--root", root, "--upstream", "https://master.example.com", "--bearer-file", bearer, }) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Addr != "127.0.0.1:8443" { t.Errorf("cfg.Addr = %q, want 127.0.0.1:8443 (loopback default in client mode)", cfg.Addr) } } // TestLoad_ClientModeNonLoopbackBindRefusesWithoutInsecureDirect locks // down the confused-deputy guard: a non-loopback bind in client mode // with a configured bearer must require the operator to acknowledge // the deployment shape via --insecure-direct. func TestLoad_ClientModeNonLoopbackBindRefusesWithoutInsecureDirect(t *testing.T) { root := t.TempDir() bearer := filepath.Join(root, "tok") _ = os.WriteFile(bearer, []byte("xyz"), 0o600) _, err := Load([]string{ "--root", root, "--upstream", "https://master.example.com", "--bearer-file", bearer, "--addr", "0.0.0.0:8444", }) if err == nil { t.Fatal("Load() succeeded; want refusal for non-loopback client-mode bind with bearer") } if !strings.Contains(err.Error(), "--insecure-direct") { t.Errorf("error should mention --insecure-direct as the acknowledgement flag; got: %v", err) } } // TestLoad_ClientModeNonLoopbackBindAllowedWithInsecureDirect confirms // the operator can opt out of the loopback guard via the existing flag // (matches the helm chart path which sets ZDDC_INSECURE_DIRECT=1). func TestLoad_ClientModeNonLoopbackBindAllowedWithInsecureDirect(t *testing.T) { root := t.TempDir() bearer := filepath.Join(root, "tok") _ = os.WriteFile(bearer, []byte("xyz"), 0o600) cfg, err := Load([]string{ "--root", root, "--upstream", "https://master.example.com", "--bearer-file", bearer, "--addr", "0.0.0.0:8444", "--insecure-direct", }) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Addr != "0.0.0.0:8444" { t.Errorf("cfg.Addr = %q, want 0.0.0.0:8444", cfg.Addr) } } // TestLoad_ClientModeNoBearerSkipsGuard documents the narrower scope: // without a bearer file, there's no credential to launder, so a non- // loopback bind is the operator's call (the deployment still gets the // X-ZDDC-Cache: hit/miss metadata-disclosure surface, but no credential // forwarding). Refusing in this case would needlessly block proxy-mode // deployments that don't authenticate to upstream at all. func TestLoad_ClientModeNoBearerSkipsGuard(t *testing.T) { root := t.TempDir() cfg, err := Load([]string{ "--root", root, "--upstream", "https://master.example.com", "--addr", "0.0.0.0:8444", }) if err != nil { t.Fatalf("Load (client, non-loopback, no bearer): %v", err) } if cfg.Addr != "0.0.0.0:8444" { t.Errorf("Addr = %q", cfg.Addr) } }