package secheaders import ( "net/http" "net/http/httptest" "strings" "testing" ) func secureRequest(method, target string) *http.Request { req := httptest.NewRequest(method, target, nil) req.Header.Set("X-Forwarded-Proto", "https") return req } // Akzeptanzkriterium 1: CSP definiert ohne unsafe-inline/unsafe-eval. func TestDefaultConfig_CSPHasNoUnsafeDirectives(t *testing.T) { cfg := DefaultConfig() if strings.Contains(cfg.ContentSecurityPolicy, "unsafe-inline") { t.Fatal("csp enthaelt 'unsafe-inline', erwartet keine unsicheren direktiven") } if strings.Contains(cfg.ContentSecurityPolicy, "unsafe-eval") { t.Fatal("csp enthaelt 'unsafe-eval', erwartet keine unsicheren direktiven") } if !strings.Contains(cfg.ContentSecurityPolicy, "default-src 'self'") { t.Fatal("erwartet restriktive default-src 'self' als ausgangspunkt") } } // Akzeptanzkriterium 2: HSTS mit angemessener max-age gesetzt. func TestMiddleware_SetsHSTSWithSufficientMaxAge(t *testing.T) { cfg := DefaultConfig() handler := Middleware(cfg, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) rec := httptest.NewRecorder() handler(rec, secureRequest(http.MethodGet, "https://example.test/")) hsts := rec.Header().Get("Strict-Transport-Security") if !strings.Contains(hsts, "max-age=") { t.Fatalf("hsts-header fehlt max-age: %q", hsts) } // "Angemessen" heisst hier: mindestens 30 Tage (2592000s) — deutlich // ueber dem bekannten Anti-Pattern kurzer/fehlender max-age-Werte. if cfg.HSTSMaxAge.Seconds() < 2592000 { t.Fatalf("konfigurierte hsts-max-age = %v, erwartet mindestens 30 tage", cfg.HSTSMaxAge) } if !strings.Contains(hsts, "includeSubDomains") { t.Fatalf("erwartet includeSubDomains im hsts-header: %q", hsts) } } // Akzeptanzkriterium 3 + Pruefung 1: EINE gemeinsame Middleware setzt ALLE // Baseline-Header vollstaendig — automatisiert ueberprueft mit CheckCoverage. func TestMiddleware_ProducesFullHeaderCoverage(t *testing.T) { cfg := DefaultConfig() handler := Middleware(cfg, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) rec := httptest.NewRecorder() handler(rec, secureRequest(http.MethodGet, "https://example.test/")) if missing := CheckCoverage(rec.Header()); len(missing) != 0 { t.Fatalf("erwartet vollstaendige header-abdeckung, fehlen: %v", missing) } } // Pruefung 1 (Kehrseite): CheckCoverage erkennt fehlende Header // zuverlaessig — Voraussetzung dafuer, dass ein automatisierter Scan // gegen ein Modul-Frontend OHNE eingebundene Middleware ueberhaupt etwas // meldet. func TestCheckCoverage_DetectsMissingHeadersOnBareHandler(t *testing.T) { bareHandler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } rec := httptest.NewRecorder() bareHandler(rec, httptest.NewRequest(http.MethodGet, "/", nil)) missing := CheckCoverage(rec.Header()) if len(missing) != len(RequiredHeaders) { t.Fatalf("erwartet %d fehlende header bei unbehandeltem handler, habe %d: %v", len(RequiredHeaders), len(missing), missing) } } // Akzeptanzkriterium 2 + Pruefung 3: ein Aufruf ueber (simuliertes) HTTP // wird server-seitig auf HTTPS umgeleitet, statt sich allein auf den // HSTS-Browser-Cache zu verlassen (der erst nach dem ersten sicheren // Aufruf greift). func TestMiddleware_RedirectsInsecureRequestToHTTPS(t *testing.T) { cfg := DefaultConfig() called := false handler := Middleware(cfg, func(w http.ResponseWriter, r *http.Request) { called = true w.WriteHeader(http.StatusOK) }) req := httptest.NewRequest(http.MethodGet, "http://example.test/pfad?x=1", nil) // Kein X-Forwarded-Proto, kein r.TLS -> unsicherer Aufruf. rec := httptest.NewRecorder() handler(rec, req) if rec.Code != http.StatusMovedPermanently { t.Fatalf("status = %d, want 301 (redirect auf https)", rec.Code) } location := rec.Header().Get("Location") if !strings.HasPrefix(location, "https://") { t.Fatalf("location = %q, want https://-praefix", location) } if !strings.Contains(location, "/pfad?x=1") { t.Fatalf("location = %q, erwartet erhaltenen pfad+query", location) } if called { t.Fatal("eigentlicher handler haette bei unsicherem aufruf nicht erreicht werden duerfen") } }