internal/middleware/cors_test.go
Rama main · réplica de lectura del repositorio original.
package middleware
import (
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
const uiOrigin = "https://ui.example.com"
// corsChain mirrors cmd/server's ordering: CORS runs before API key auth, so a
// preflight is answered without needing the key.
func corsChain(t testing.TB, origins []string, production bool, apiKey string) (http.Handler, CORSMode) {
cors, mode := CORS(origins, production, apiKey != "")
auth, _ := newAuth(t, apiKey)
api := auth.HTTPMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
return cors(api), mode
}
func preflight(h http.Handler, origin, headers string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodOptions, "/v1/webhooks", nil)
req.Header.Set("Origin", origin)
req.Header.Set("Access-Control-Request-Method", http.MethodGet)
req.Header.Set("Access-Control-Request-Headers", headers)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
func get(h http.Handler, origin, apiKey string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "/v1/webhooks", nil)
req.Header.Set("Origin", origin)
if apiKey != "" {
req.Header.Set(APIKeyHeader, apiKey)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
func TestCORSModes(t *testing.T) {
cases := []struct {
name string
origins []string
production bool
auth bool
want CORSMode
}{
{"allow list", []string{uiOrigin}, true, true, CORSAllowList},
{"allow list in dev", []string{uiOrigin}, false, false, CORSAllowList},
{"production default", nil, true, true, CORSBlockAll},
{"production blank entries", []string{"", " "}, true, false, CORSBlockAll},
{"dev default with key", nil, false, true, CORSAllowAll},
{"dev default without key", nil, false, false, CORSLoopbackOnly},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if _, got := CORS(tc.origins, tc.production, tc.auth); got != tc.want {
t.Fatalf("mode = %v, want %v", got, tc.want)
}
})
}
}
func TestCORSProductionDefaultBlocksCrossOrigin(t *testing.T) {
// Regression: rs/cors treats an empty AllowedOrigins as "*", so the
// production lockdown used to allow every origin.
h, _ := corsChain(t, nil, true, "")
if got := preflight(h, "https://evil.example.com", "x-api-key").Header().Get("Access-Control-Allow-Origin"); got != "" {
t.Fatalf("preflight Access-Control-Allow-Origin = %q, want none", got)
}
if got := get(h, "https://evil.example.com", "").Header().Get("Access-Control-Allow-Origin"); got != "" {
t.Fatalf("GET Access-Control-Allow-Origin = %q, want none", got)
}
}
func TestCORSAllowListSplitDeploymentWithAPIKey(t *testing.T) {
// UI on its own origin, server in production with an API key: the
// preflight must succeed without the key, the real request needs it.
h, _ := corsChain(t, []string{uiOrigin + "/"}, true, "secret")
// Browsers send the requested headers lowercased and sorted.
rec := preflight(h, uiOrigin, "content-type,x-api-key")
if rec.Code != http.StatusNoContent {
t.Fatalf("preflight status = %d, want 204 (must not hit API key auth)", rec.Code)
}
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != uiOrigin {
t.Fatalf("preflight Access-Control-Allow-Origin = %q, want %q", got, uiOrigin)
}
if got := rec.Header().Get("Access-Control-Allow-Headers"); got == "" {
t.Fatal("preflight did not allow the X-API-Key header")
}
if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "" {
t.Fatalf("Access-Control-Allow-Credentials = %q, want none (auth is header based)", got)
}
rec = get(h, uiOrigin, "secret")
if rec.Code != http.StatusOK || rec.Header().Get("Access-Control-Allow-Origin") != uiOrigin {
t.Fatalf("GET with key: status %d, ACAO %q", rec.Code, rec.Header().Get("Access-Control-Allow-Origin"))
}
// A 401 must still carry CORS headers so the browser lets the UI read it
// and show the API key prompt instead of an opaque network error.
rec = get(h, uiOrigin, "")
if rec.Code != http.StatusUnauthorized || rec.Header().Get("Access-Control-Allow-Origin") != uiOrigin {
t.Fatalf("GET without key: status %d, ACAO %q", rec.Code, rec.Header().Get("Access-Control-Allow-Origin"))
}
if got := preflight(h, "https://other.example.com", "x-api-key").Header().Get("Access-Control-Allow-Origin"); got != "" {
t.Fatalf("unlisted origin got Access-Control-Allow-Origin = %q", got)
}
}
func TestCORSDevAllowsAnyOriginWithAPIKeyHeader(t *testing.T) {
// `make run` + `make run-web`: vite on :5173 calling :8080.
h, _ := corsChain(t, nil, false, "secret")
rec := preflight(h, "http://localhost:5173", "x-api-key")
if rec.Code != http.StatusNoContent || rec.Header().Get("Access-Control-Allow-Origin") == "" {
t.Fatalf("preflight: status %d, ACAO %q", rec.Code, rec.Header().Get("Access-Control-Allow-Origin"))
}
if got := get(h, "http://localhost:5173", "secret").Code; got != http.StatusOK {
t.Fatalf("GET status = %d, want 200", got)
}
}
func TestCORSDevWithoutKeyAllowsOnlyLoopback(t *testing.T) {
// No key means every caller is root: a page on another site must not be
// able to read responses, while `make run-web` on localhost still works.
h, _ := corsChain(t, nil, false, "")
for _, origin := range []string{"http://localhost:5173", "http://127.0.0.1:3000", "http://[::1]:5173"} {
if got := preflight(h, origin, "content-type").Header().Get("Access-Control-Allow-Origin"); got != origin {
t.Errorf("%s: preflight ACAO = %q, want it allowed", origin, got)
}
}
for _, origin := range []string{"https://evil.example.com", "http://localhost.evil.example.com", "http://10.0.0.5:5173", "null"} {
if got := preflight(h, origin, "content-type").Header().Get("Access-Control-Allow-Origin"); got != "" {
t.Errorf("%s: preflight ACAO = %q, want none", origin, got)
}
if got := get(h, origin, "").Header().Get("Access-Control-Allow-Origin"); got != "" {
t.Errorf("%s: GET ACAO = %q, want none", origin, got)
}
}
}
func TestNormalizeOrigins(t *testing.T) {
got := NormalizeOrigins([]string{" https://a.example.com/ ", "", "https://b.example.com//", " "})
want := []string{"https://a.example.com", "https://b.example.com"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("NormalizeOrigins = %v, want %v", got, want)
}
}