package test import ( "RenewCMS/api/middleware" "net/http" "net/http/httptest" "strings" "testing" ) type corsExchange struct { rec *httptest.ResponseRecorder reached bool } func corsRequest(origins []string, method string, headers map[string]string) corsExchange { exchange := corsExchange{} handler := middleware.CORS(origins)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { exchange.reached = true w.WriteHeader(http.StatusOK) })) req := httptest.NewRequest(method, "/v1/article", nil) for name, value := range headers { req.Header.Set(name, value) } exchange.rec = httptest.NewRecorder() handler.ServeHTTP(exchange.rec, req) return exchange } func corsPreflight(origins []string, origin string, method string, requestHeaders string) corsExchange { headers := map[string]string{"Origin": origin, "Access-Control-Request-Method": method} if requestHeaders != "" { headers["Access-Control-Request-Headers"] = requestHeaders } return corsRequest(origins, http.MethodOptions, headers) } func TestCORSActualRequests(t *testing.T) { origins := []string{"https://app.example.com", "https://*.preview.example.com"} t.Run("An allowed origin is echoed and the exposed headers are listed", func(t *testing.T) { got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://app.example.com"}) if got.rec.Header().Get("Access-Control-Allow-Origin") != "https://app.example.com" { t.Errorf("unexpected allow-origin %q", got.rec.Header().Get("Access-Control-Allow-Origin")) } if got.rec.Header().Get("Access-Control-Expose-Headers") != "Link, X-Total-Count" { t.Errorf("unexpected expose-headers %q", got.rec.Header().Get("Access-Control-Expose-Headers")) } if !got.reached { t.Error("the request must reach the handler") } }) t.Run("Credentials are never allowed", func(t *testing.T) { got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://app.example.com"}) if got.rec.Header().Get("Access-Control-Allow-Credentials") != "" { t.Error("Access-Control-Allow-Credentials must not be set") } }) t.Run("The match is case-insensitive", func(t *testing.T) { got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "HTTPS://APP.EXAMPLE.COM"}) if got.rec.Header().Get("Access-Control-Allow-Origin") == "" { t.Error("expected the origin to be allowed") } }) t.Run("A wildcard subdomain pattern matches", func(t *testing.T) { got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://pr-12.preview.example.com"}) if got.rec.Header().Get("Access-Control-Allow-Origin") != "https://pr-12.preview.example.com" { t.Errorf("unexpected allow-origin %q", got.rec.Header().Get("Access-Control-Allow-Origin")) } }) t.Run("A wildcard pattern does not match a look-alike", func(t *testing.T) { for _, origin := range []string{"https://preview.example.com", "https://x.preview.example.com.evil.net", "http://x.preview.example.com"} { got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": origin}) if got.rec.Header().Get("Access-Control-Allow-Origin") != "" { t.Errorf("origin %q must not be allowed", origin) } } }) t.Run("An unknown origin gets no CORS headers but still reaches the handler", func(t *testing.T) { got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://evil.example.net"}) if got.rec.Header().Get("Access-Control-Allow-Origin") != "" || !got.reached { t.Errorf("unexpected exchange %v reached=%v", got.rec.Header(), got.reached) } }) t.Run("A request with no Origin is untouched", func(t *testing.T) { got := corsRequest(origins, http.MethodGet, nil) if got.rec.Header().Get("Vary") != "" || got.rec.Header().Get("Access-Control-Allow-Origin") != "" || !got.reached { t.Errorf("unexpected headers %v", got.rec.Header()) } }) t.Run("Responses vary on Origin so shared caches stay correct", func(t *testing.T) { got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://evil.example.net"}) if got.rec.Header().Get("Vary") != "Origin" { t.Errorf("unexpected Vary %q", got.rec.Header().Get("Vary")) } }) t.Run("A lone star allows any origin", func(t *testing.T) { got := corsRequest([]string{"*"}, http.MethodGet, map[string]string{"Origin": "https://anything.example.net"}) if got.rec.Header().Get("Access-Control-Allow-Origin") != "https://anything.example.net" { t.Errorf("unexpected allow-origin %q", got.rec.Header().Get("Access-Control-Allow-Origin")) } }) t.Run("Blank entries never match the empty origin", func(t *testing.T) { got := corsRequest([]string{"", " "}, http.MethodGet, map[string]string{"Origin": "https://app.example.com"}) if got.rec.Header().Get("Access-Control-Allow-Origin") != "" { t.Error("blank entries must be ignored") } }) } func TestCORSPreflight(t *testing.T) { origins := []string{"https://app.example.com"} t.Run("An allowed preflight is answered 204 without reaching the handler", func(t *testing.T) { got := corsPreflight(origins, "https://app.example.com", http.MethodPatch, "Content-Type, Authorization") header := got.rec.Header() if got.rec.Code != http.StatusNoContent || got.reached { t.Fatalf("expected 204 and no handler, got %d reached=%v", got.rec.Code, got.reached) } if header.Get("Access-Control-Allow-Origin") != "https://app.example.com" { t.Errorf("unexpected allow-origin %q", header.Get("Access-Control-Allow-Origin")) } if header.Get("Access-Control-Allow-Methods") != "PATCH" { t.Errorf("unexpected allow-methods %q", header.Get("Access-Control-Allow-Methods")) } if header.Get("Access-Control-Max-Age") != "300" { t.Errorf("unexpected max-age %q", header.Get("Access-Control-Max-Age")) } for _, allowed := range []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"} { if !strings.Contains(header.Get("Access-Control-Allow-Headers"), allowed) { t.Errorf("allow-headers misses %s: %q", allowed, header.Get("Access-Control-Allow-Headers")) } } }) t.Run("Every supported method is allowed", func(t *testing.T) { for _, method := range []string{"GET", "POST", "PUT", "PATCH", "DELETE"} { got := corsPreflight(origins, "https://app.example.com", method, "") if got.rec.Header().Get("Access-Control-Allow-Methods") != method { t.Errorf("method %s not allowed", method) } } }) t.Run("The requested method is matched case-insensitively", func(t *testing.T) { got := corsPreflight(origins, "https://app.example.com", "patch", "") if got.rec.Header().Get("Access-Control-Allow-Methods") != "PATCH" { t.Errorf("unexpected allow-methods %q", got.rec.Header().Get("Access-Control-Allow-Methods")) } }) t.Run("A method outside the list is refused", func(t *testing.T) { got := corsPreflight(origins, "https://app.example.com", "TRACE", "") if got.rec.Header().Get("Access-Control-Allow-Origin") != "" || got.rec.Header().Get("Access-Control-Allow-Methods") != "" { t.Errorf("unexpected headers %v", got.rec.Header()) } }) t.Run("A header outside the list is refused", func(t *testing.T) { got := corsPreflight(origins, "https://app.example.com", "GET", "Content-Type, X-Evil") if got.rec.Header().Get("Access-Control-Allow-Origin") != "" { t.Errorf("unexpected headers %v", got.rec.Header()) } }) t.Run("Requested headers are matched case-insensitively and tolerate spacing", func(t *testing.T) { got := corsPreflight(origins, "https://app.example.com", "GET", "content-type ,AUTHORIZATION,") if got.rec.Header().Get("Access-Control-Allow-Origin") == "" { t.Error("expected the preflight to be allowed") } }) t.Run("An unknown origin is refused", func(t *testing.T) { got := corsPreflight(origins, "https://evil.example.net", "GET", "") if got.rec.Header().Get("Access-Control-Allow-Origin") != "" || got.reached { t.Errorf("unexpected exchange %v reached=%v", got.rec.Header(), got.reached) } }) t.Run("The preflight response varies on every request header it depends on", func(t *testing.T) { got := corsPreflight(origins, "https://app.example.com", "GET", "") vary := strings.Join(got.rec.Header().Values("Vary"), ",") for _, field := range []string{"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"} { if !strings.Contains(vary, field) { t.Errorf("Vary misses %s: %q", field, vary) } } }) t.Run("An OPTIONS request without a requested method is treated as a normal request", func(t *testing.T) { got := corsRequest(origins, http.MethodOptions, map[string]string{"Origin": "https://app.example.com"}) if !got.reached { t.Error("expected the handler to be reached") } }) }