From 6748bbd637be766cdd03a1d7647fd43bca628a51 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 29 Sep 2026 10:54:50 +0000 Subject: [PATCH] Test that only the image routes send CORS headers (closes #98) A new server test sends requests with an Origin header through the server's routes and expects Access-Control-Allow-Origin, set to the configured origin, on both image routes, a preflight OPTIONS request included, and no such header on the login and URL generator pages. It fails for now: the CORS middleware still wraps every route. The encrypted image path the maintenance tests use is now a named constant, shared with the new test; what they check is unchanged. Model: opus-5-5 --- internal/server/cors_internal_test.go | 64 ++++++++++++++++++++ internal/server/maintenance_internal_test.go | 8 ++- 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 internal/server/cors_internal_test.go diff --git a/internal/server/cors_internal_test.go b/internal/server/cors_internal_test.go new file mode 100644 index 0000000..6232588 --- /dev/null +++ b/internal/server/cors_internal_test.go @@ -0,0 +1,64 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// TestCORSOnlyOnImageRoutes verifies that the image routes answer with the +// configured access_control_allow_origin, a preflight request included, and +// that the login and URL generator pages send no Access-Control-Allow-Origin, +// so no other site can read them. /metrics is left out: its middleware +// registers with the process-wide Prometheus registry, which only one test +// in this package can do. +func TestCORSOnlyOnImageRoutes(t *testing.T) { + t.Parallel() + + const appOrigin = "https://app.example.com" + + s := newTestServer(t) + s.config.AccessControlAllowOrigin = appOrigin + s.SetupRoutes() + + requests := []struct { + method string + path string + want string + }{ + {http.MethodGet, unsignedImagePath, appOrigin}, + {http.MethodHead, unsignedImagePath, appOrigin}, + {http.MethodOptions, unsignedImagePath, appOrigin}, + {http.MethodGet, encryptedImagePath, appOrigin}, + {http.MethodGet, "/", ""}, + {http.MethodOptions, "/", ""}, + {http.MethodPost, "/generate", ""}, + {http.MethodGet, "/logout", ""}, + } + + for _, tc := range requests { + t.Run(tc.method+" "+tc.path, func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequestWithContext( + t.Context(), tc.method, tc.path, nil) + req.Header.Set("Origin", appOrigin) + + // An OPTIONS request naming the method it asks about is the + // preflight a browser sends before some cross-origin requests. + if tc.method == http.MethodOptions { + req.Header.Set("Access-Control-Request-Method", http.MethodGet) + } + + rec := httptest.NewRecorder() + s.ServeHTTP(rec, req) + t.Logf("status %d", rec.Code) + + got := rec.Header().Get("Access-Control-Allow-Origin") + if got != tc.want { + t.Errorf("Access-Control-Allow-Origin = %q, want %q", + got, tc.want) + } + }) + } +} diff --git a/internal/server/maintenance_internal_test.go b/internal/server/maintenance_internal_test.go index 96b7272..f800005 100644 --- a/internal/server/maintenance_internal_test.go +++ b/internal/server/maintenance_internal_test.go @@ -13,6 +13,10 @@ import ( // unsignedImagePath is an image URL that carries no signature. const unsignedImagePath = "/v1/image/cdn.example.com/cat.jpg/100x100.jpeg" +// encryptedImagePath is an encrypted image URL whose token cannot be +// decrypted. +const encryptedImagePath = "/v1/e/token/cat.jpg" + // TestMaintenanceModeRefusesImageRequests verifies that while maintenance // mode is on, both image routes answer 503 Service Unavailable with a // Retry-After header and the JSON error body the image handlers send. @@ -28,7 +32,7 @@ func TestMaintenanceModeRefusesImageRequests(t *testing.T) { }{ {http.MethodGet, unsignedImagePath}, {http.MethodHead, unsignedImagePath}, - {http.MethodGet, "/v1/e/token/cat.jpg"}, + {http.MethodGet, encryptedImagePath}, } for _, tc := range requests { @@ -95,7 +99,7 @@ func TestImageRequestsServedWithoutMaintenanceMode(t *testing.T) { }{ {http.MethodGet, unsignedImagePath, http.StatusUnauthorized}, {http.MethodHead, unsignedImagePath, http.StatusUnauthorized}, - {http.MethodGet, "/v1/e/token/cat.jpg", http.StatusBadRequest}, + {http.MethodGet, encryptedImagePath, http.StatusBadRequest}, } for _, tc := range requests {