package app import ( "go/ast" "go/parser" "go/token" "net/http" "net/http/httptest" "os" "path/filepath" "sort" "strconv" "strings" "testing" ) func TestConfigValidationRequiresTrustedProxyCIDRs(t *testing.T) { cfg := Config{TrustProxy: true} if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "TRUSTED_PROXY_CIDRS") { t.Fatalf("expected trusted proxy validation error, got %v", err) } } func TestConfigValidationRequiresMetricsToken(t *testing.T) { cfg := Config{MetricsEnabled: true, MetricsToken: "short"} if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "METRICS_TOKEN") { t.Fatalf("expected metrics token validation error, got %v", err) } } func TestConfigValidationLegalStrict(t *testing.T) { cfg := Config{LegalStrict: true} if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "LEGAL_NAME") { t.Fatalf("expected legal validation error, got %v", err) } cfg.LegalName = "Example GmbH" cfg.LegalAddress = "Example Street 1\n12345 Example" cfg.LegalEmail = "legal@example.org" cfg.DataProtectionContact = "privacy@example.org" cfg.HostingProvider = "Example Hosting GmbH" cfg.HostingAddress = "Hosting Street 1\n12345 Example" if err := cfg.Validate(); err != nil { t.Fatalf("valid strict legal configuration rejected: %v", err) } } func TestMetricsDisabledAndProtected(t *testing.T) { h := testHandlerConfig(t, Config{ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de"}) r := httptest.NewRequest(http.MethodGet, "/metrics", nil) w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusNotFound { t.Fatalf("disabled metrics status %d", w.Code) } cfg := Config{ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", MetricsEnabled: true, MetricsToken: strings.Repeat("x", 40)} h = testHandlerConfig(t, cfg) r = httptest.NewRequest(http.MethodGet, "/metrics", nil) w = httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusUnauthorized { t.Fatalf("metrics without token status %d", w.Code) } r = httptest.NewRequest(http.MethodGet, "/metrics", nil) r.Header.Set("Authorization", "Bearer "+cfg.MetricsToken) w = httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "ai_disclosure_uptime_seconds") { t.Fatalf("authorized metrics failed: %d %s", w.Code, w.Body.String()) } } func TestOperatorPagesAndHSTS(t *testing.T) { cfg := Config{ ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Example", DefaultLanguage: "de", EnableHSTS: true, LegalName: "Example GmbH", LegalAddress: "Example Street 1", LegalEmail: "legal@example.org", DataProtectionContact: "privacy@example.org", HostingProvider: "Host GmbH", HostingAddress: "Host Street 1", AccessibilityContact: "access@example.org", AccessibilityStatus: "Teilweise konform getestet", } h := testHandlerConfig(t, cfg) for _, tc := range []struct{ path, want string }{{"/legal?lang=de", "Example GmbH"}, {"/privacy?lang=en", "Privacy information"}, {"/accessibility?lang=fr", "Accessibilité"}} { r := httptest.NewRequest(http.MethodGet, tc.path, nil) w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), tc.want) { t.Fatalf("%s: %d, missing %q", tc.path, w.Code, tc.want) } if got := w.Header().Get("Strict-Transport-Security"); got == "" { t.Fatalf("%s missing HSTS", tc.path) } } } func TestEnvExampleDocumentsRuntimeConfiguration(t *testing.T) { root := filepath.Join("..", "..") data, err := os.ReadFile(filepath.Join(root, ".env.example")) if err != nil { t.Fatal(err) } text := string(data) keys := map[string]struct{}{} files := []string{filepath.Join("config.go"), filepath.Join(root, "cmd", "server", "main.go")} helpers := map[string]bool{ "env": true, "envAllowEmpty": true, "boolEnv": true, "durationEnv": true, "intEnv": true, "serviceModeEnv": true, "disputeStatusEnv": true, "cidrEnv": true, "secretEnv": true, } fset := token.NewFileSet() for _, filename := range files { file, err := parser.ParseFile(fset, filename, nil, 0) if err != nil { t.Fatalf("parse %s: %v", filename, err) } ast.Inspect(file, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok || len(call.Args) == 0 { return true } name := "" switch fn := call.Fun.(type) { case *ast.Ident: if helpers[fn.Name] { name = fn.Name } case *ast.SelectorExpr: if ident, ok := fn.X.(*ast.Ident); ok && ident.Name == "os" && (fn.Sel.Name == "Getenv" || fn.Sel.Name == "LookupEnv") { name = fn.Sel.Name } } if name == "" { return true } lit, ok := call.Args[0].(*ast.BasicLit) if !ok || lit.Kind != token.STRING { return true } key, err := strconv.Unquote(lit.Value) if err != nil || key == "" { return true } keys[key] = struct{}{} if name == "secretEnv" { keys[key+"_FILE"] = struct{}{} } return true }) } missing := make([]string, 0) for key := range keys { if !strings.Contains(text, key+"=") { missing = append(missing, key) } } sort.Strings(missing) if len(missing) > 0 { t.Fatalf(".env.example is missing runtime variables: %s", strings.Join(missing, ", ")) } }