package http import ( "net/http" "net/http/httptest" "testing" ) func corsHandler(t *testing.T, cfg CORSConfig) http.Handler { t.Helper() middleware, err := CORS(cfg) if err != nil { t.Fatalf("CORS(%+v): %v", cfg, err) } return middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) } func request(h http.Handler, method, origin string, preflight bool) *httptest.ResponseRecorder { r := httptest.NewRequest(method, "/jobs", nil) if origin != "" { r.Header.Set("Origin", origin) } if preflight { r.Header.Set("Access-Control-Request-Method", "POST") } w := httptest.NewRecorder() h.ServeHTTP(w, r) return w } // A wildcard origin combined with credentials would let any site issue // credentialed requests and read the responses, so it must not be constructible. func TestCORSRejectsWildcardWithCredentials(t *testing.T) { if _, err := CORS(CORSConfig{AllowedOrigins: []string{"*"}, AllowCredentials: true}); err == nil { t.Fatal("expected an error for wildcard origins combined with credentials") } } func TestCORSRejectsSchemelessOrigin(t *testing.T) { if _, err := CORS(CORSConfig{AllowedOrigins: []string{"app.example.com"}}); err == nil { t.Fatal("expected an error for an origin without a scheme") } } func TestCORSDoesNotEchoDisallowedOrigin(t *testing.T) { h := corsHandler(t, CORSConfig{ AllowedOrigins: []string{"https://app.example.com"}, AllowCredentials: true, }) w := request(h, http.MethodGet, "https://evil.example.com", false) if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" { t.Errorf("Access-Control-Allow-Origin = %q, want empty for a disallowed origin", got) } if got := w.Header().Get("Access-Control-Allow-Credentials"); got != "" { t.Errorf("Access-Control-Allow-Credentials = %q, want empty for a disallowed origin", got) } if w.Code != http.StatusOK { t.Errorf("status = %d, want %d: the handler should still run", w.Code, http.StatusOK) } } func TestCORSAllowsListedOrigin(t *testing.T) { h := corsHandler(t, CORSConfig{ AllowedOrigins: []string{"https://app.example.com"}, AllowCredentials: true, }) w := request(h, http.MethodGet, "https://app.example.com", false) if got, want := w.Header().Get("Access-Control-Allow-Origin"), "https://app.example.com"; got != want { t.Errorf("Access-Control-Allow-Origin = %q, want %q", got, want) } if got, want := w.Header().Get("Access-Control-Allow-Credentials"), "true"; got != want { t.Errorf("Access-Control-Allow-Credentials = %q, want %q", got, want) } // Without Vary, a shared cache could serve one origin's response to another. if got, want := w.Header().Get("Vary"), "Origin"; got != want { t.Errorf("Vary = %q, want %q", got, want) } } func TestCORSOriginMatchIsCaseInsensitive(t *testing.T) { h := corsHandler(t, CORSConfig{AllowedOrigins: []string{"https://App.Example.com"}}) w := request(h, http.MethodGet, "https://app.example.com", false) if w.Header().Get("Access-Control-Allow-Origin") == "" { t.Error("origin differing only in case should match the allowlist") } } // An unconfigured service must fail closed rather than allowing every origin. func TestCORSEmptyAllowlistDeniesCrossOrigin(t *testing.T) { h := corsHandler(t, CORSConfig{}) w := request(h, http.MethodGet, "https://anything.example.com", false) if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" { t.Errorf("Access-Control-Allow-Origin = %q, want empty when no origins are configured", got) } } func TestCORSPreflightShortCircuits(t *testing.T) { h := corsHandler(t, CORSConfig{AllowedOrigins: []string{"https://app.example.com"}}) w := request(h, http.MethodOptions, "https://app.example.com", true) if w.Code != http.StatusNoContent { t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent) } // Defaults must apply to a directly constructed config, not only to one // built by LoadCORSConfigFromEnv. if w.Header().Get("Access-Control-Allow-Methods") == "" { t.Error("preflight response is missing Access-Control-Allow-Methods") } if w.Header().Get("Access-Control-Allow-Headers") == "" { t.Error("preflight response is missing Access-Control-Allow-Headers") } } func TestCORSPlainOptionsReachesHandler(t *testing.T) { h := corsHandler(t, CORSConfig{AllowedOrigins: []string{"https://app.example.com"}}) // OPTIONS without Access-Control-Request-Method is not a preflight. w := request(h, http.MethodOptions, "https://app.example.com", false) if w.Code != http.StatusOK { t.Errorf("status = %d, want %d", w.Code, http.StatusOK) } }