Files
summercms/surf/cors_test.go
Jakub Zych 30539b954f feat(06-03): add path-scoped CORS and per-route body limits
- CORS matches Laravel path globs (api/* includes nested segments); unlisted paths get no headers
- Non-raw routes wrap http.MaxBytesReader from http.body_limits.default_bytes; body.limit:N overrides innermost
- Raw routes stay uncapped at this layer
2026-09-19 20:10:02 +02:00

120 lines
3.3 KiB
Go

package surf
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"git.golem15.com/golem15/summercms/backpack"
"git.golem15.com/golem15/summercms/compass"
"git.golem15.com/golem15/summercms/party"
)
func TestCORSLaravelGlobMatchesNestedPaths(t *testing.T) {
re := compileLaravelGlob("api/*")
if re == nil || !re.MatchString("api/v1/fonoteka/genres") {
t.Fatal("api/* must match api/v1/fonoteka/genres (Laravel Str::is, not Go path.Match)")
}
if re.MatchString("_fonoteka/api/v1/genres") {
t.Fatal("api/* must not match _fonoteka/api/v1/genres")
}
mcp := compileLaravelGlob("oauth/mcp/*")
if mcp == nil || !mcp.MatchString("oauth/mcp/token") {
t.Fatal("oauth/mcp/* must match oauth/mcp/token")
}
}
func TestCORSPathScopedHeaders(t *testing.T) {
cfg := CORSConfig{
Paths: []string{"api/*", "oauth/mcp/*"},
AllowedMethods: []string{"*"},
AllowedOrigins: []string{"*"},
AllowedHeaders: []string{"*"},
}
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/fonoteka/genres", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /_fonoteka/api/v1/genres", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
h := pathScopedCORS(cfg, mux)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/fonoteka/genres", nil))
if rec.Header().Get("Access-Control-Allow-Origin") != "*" {
t.Fatalf("token group ACAO = %q", rec.Header().Get("Access-Control-Allow-Origin"))
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/_fonoteka/api/v1/genres", nil))
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" {
t.Fatalf("JWT group ACAO = %q, want empty", got)
}
}
func TestCORSConfigLoadedFromHTTPSection(t *testing.T) {
cfg := writeHTTPConfig(t, `
cors:
paths: ["api/*"]
allowed_methods: ["*"]
allowed_origins: ["*"]
allowed_origins_patterns: []
allowed_headers: ["*"]
exposed_headers: []
max_age: 0
supports_credentials: false
`)
got, err := LoadCORSConfig(cfg)
if err != nil {
t.Fatal(err)
}
if len(got.Paths) != 1 || got.Paths[0] != "api/*" {
t.Fatalf("paths = %v", got.Paths)
}
if !containsStar(got.AllowedOrigins) {
t.Fatalf("origins = %v", got.AllowedOrigins)
}
}
func writeHTTPConfig(t *testing.T, httpYAML string) *compass.Config {
t.Helper()
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: cors-test\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "http.yaml"), []byte(httpYAML), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Open(compass.Options{
Dir: dir,
Environ: []string{"SUMMER_ENV=development"},
})
if err != nil {
t.Fatal(err)
}
return cfg
}
func TestCORSAssembleUsesConfig(t *testing.T) {
cfg := writeHTTPConfig(t, `
cors:
paths: ["api/*"]
allowed_methods: ["*"]
allowed_origins: ["*"]
allowed_headers: ["*"]
`)
p := assemblePlugin{id: "golem15.demo", path: "/genres"}
h, err := Assemble(backpack.New(cfg), []party.Plugin{p})
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/genres", nil))
if rec.Header().Get("Access-Control-Allow-Origin") != "*" {
t.Fatalf("ACAO = %q", rec.Header().Get("Access-Control-Allow-Origin"))
}
}