ZDDC/zddc/internal/config/config_test.go
ZDDC 4ede42010a feat(zddc-server): CLI flags, --version, CWD-default ZDDC_ROOT
Adds command-line flags to zddc-server alongside the existing env vars.
Each setting can be set via --<flag-name> or ZDDC_<NAME>; the flag wins
on conflict, the env var wins over the hard-coded default.

  --root          / ZDDC_ROOT          (now defaults to CWD if both unset)
  --addr          / ZDDC_ADDR          (:8443)
  --tls-cert      / ZDDC_TLS_CERT      ("none" / empty / path)
  --tls-key       / ZDDC_TLS_KEY
  --log-level     / ZDDC_LOG_LEVEL     (info)
  --index-path    / ZDDC_INDEX_PATH    (.archive)
  --email-header  / ZDDC_EMAIL_HEADER  (X-Auth-Request-Email)
  --cors-origin   / ZDDC_CORS_ORIGIN   (https://zddc.varasys.io; "" disables)
  --insecure-direct / ZDDC_INSECURE_DIRECT (false)
  --help          (prints flag list to stderr, exits 0)
  --version       (prints binary + embedded tool versions, exits 0)

So an operator can `cd /srv/zddc && zddc-server` with zero config — the
served root defaults to the current directory, and TLS defaults to a
self-signed cert. config.Load now takes []string (test-friendly: nil
skips flag parsing entirely; tests pass an empty slice for env-only
loads).

Adds a `version` package-level var in main.go injected at link time via
`-ldflags="-X main.version=..."`. The build.sh runs git describe against
zddc-server-v* tags; for in-flight commits between releases it produces
e.g. zddc-server-v0.0.7-19-gadb6904-dirty.

Adds an embedded versions manifest:
  - Each tool's compute_build_label (in shared/build-lib.sh) writes a
    sidecar <tool>.label to $BUILD_LABELS_DIR if that env var is set.
  - Top-level build.sh sets BUILD_LABELS_DIR before running each tool's
    build, then assembles zddc/internal/apps/embedded/versions.txt as
    one `<app>=<build label>` line per app.
  - apps.EmbeddedVersions() loads the manifest at runtime.
  - main.go logs a compact summary on every startup; --version dumps
    the full per-app label.

Removes the old cfg.BuildVersion field — the X-ZDDC-Source: embedded
header now uses the package-level main.version directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:43:31 -05:00

322 lines
8.6 KiB
Go

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()
// 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_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",
env: envSet{},
// 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) != 1 || cfg.CORSOrigins[0] != "https://zddc.varasys.io" {
t.Errorf("CORSOrigins = %v, want [https://zddc.varasys.io]", 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)
}
})
}
}
// TestLoadFlags_OverrideEnv: --root flag wins over ZDDC_ROOT env var.
func TestLoadFlags_OverrideEnv(t *testing.T) {
envRoot := t.TempDir()
flagRoot := t.TempDir()
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 := t.TempDir()
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 := t.TempDir()
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")
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)
}
}