package surf import ( "net/http" "net/http/httptest" "testing" ) // Gap (d): pathScopedCORS with a path matching NONE of the configured // globs. TestCORSPathScopedHeaders already covers the two named fonoteka // groups; this fixture is framework-only (/healthz vs api/*). func TestCORSPathScopedNoMatchIndependentOfFonoteka(t *testing.T) { cfg := CORSConfig{ Paths: []string{"api/*", "oauth/mcp/*"}, AllowedMethods: []string{"*"}, AllowedOrigins: []string{"*"}, AllowedHeaders: []string{"*"}, } inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) }) h := pathScopedCORS(cfg, inner) rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil)) if rec.Code != http.StatusNoContent { t.Fatalf("status = %d", rec.Code) } if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" { t.Fatalf("unmatched path must not set ACAO, got %q", got) } } func TestCORSAllowOriginExactAndPattern(t *testing.T) { cfg := CORSConfig{ Paths: []string{"api/*"}, AllowedMethods: []string{"GET", "POST"}, AllowedOrigins: []string{"https://app.example.test"}, AllowedOriginsPatterns: []string{`^https://.*\.example\.test$`}, AllowedHeaders: []string{"Authorization", "Content-Type"}, ExposedHeaders: []string{"X-RateLimit-Limit"}, MaxAge: 600, SupportsCredentials: true, } inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) h := pathScopedCORS(cfg, inner) t.Run("exact origin", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/items", nil) req.Header.Set("Origin", "https://app.example.test") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Header().Get("Access-Control-Allow-Origin") != "https://app.example.test" { t.Fatalf("ACAO = %q", rec.Header().Get("Access-Control-Allow-Origin")) } if rec.Header().Get("Vary") != "Origin" { t.Fatalf("Vary = %q", rec.Header().Get("Vary")) } if rec.Header().Get("Access-Control-Allow-Credentials") != "true" { t.Fatal("missing credentials header") } if rec.Header().Get("Access-Control-Max-Age") != "600" { t.Fatalf("Max-Age = %q", rec.Header().Get("Access-Control-Max-Age")) } }) t.Run("pattern origin", func(t *testing.T) { req := httptest.NewRequest(http.MethodOptions, "/api/v1/items", nil) req.Header.Set("Origin", "https://admin.example.test") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("preflight status = %d", rec.Code) } if rec.Header().Get("Access-Control-Allow-Origin") != "https://admin.example.test" { t.Fatalf("ACAO = %q", rec.Header().Get("Access-Control-Allow-Origin")) } }) t.Run("disallowed origin", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/items", nil) req.Header.Set("Origin", "https://evil.test") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" { t.Fatalf("disallowed origin ACAO = %q", got) } }) }