Files
summercms/surf/cors_coverage_test.go
Jakub Zych ef448da1cc test(10-05): cover every Phase 10 Go change with branch-level tests
- bouncer TestPhase10CookieGuard: cookie read without Bearer, Bearer wins,
  empty cookie, frontend audience and blacklisted jti rejected
- boardwalk TestPhase10BoardwalkServing: HEAD, query strings, encoded
  traversal, index by name, nested prefix, MIME fallback, constructor errors
- cabana TestPhase10Coverage: mounted unsafe routes vs the CSRF walk, option
  and filter edges, read-only labels, relation message defaults, bundle
  fallback locale, cookie refresh of an expired token in the refresh window
- phrasebook override precedence, new locale, Bundle merge order, Forms shapes
- surf prefix collision for deeper paths and the default /backend prefix
- swagger2openapi TestUnionRewrite and converter branch tests
- framework tests no longer name the application (acme fixtures instead)
2026-09-27 18:05:28 +02:00

93 lines
3.1 KiB
Go

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 acme
// groups; this fixture is framework-only (/healthz vs api/*).
func TestCORSPathScopedNoMatchIndependentOfAcme(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)
}
})
}