package middleware import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" ) // Тесты CORS: разрешённый origin получает точные заголовки, чужой и // пустой список — ничего, preflight отвечает 204 до авторизации. // TestParseOrigins — список источников через запятую, мусор отбрасывается. func TestParseOrigins(t *testing.T) { cases := []struct { raw string want int }{ {"", 0}, {"http://localhost:3000", 1}, {"http://a.test, http://b.test", 2}, {" http://a.test ,, ,http://b.test ", 2}, } for _, c := range cases { if got := ParseOrigins(c.raw); len(got) != c.want { t.Errorf("ParseOrigins(%q) = %v, want %d origins", c.raw, got, c.want) } } } // newCORSRouter — роутер с CORS и одним эндпоинтом. func newCORSRouter(allowed ...string) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.Use(CORS(allowed)) r.GET("/x", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) return r } // serve выполняет запрос и возвращает запись ответа. func serve(r *gin.Engine, method, origin string) *httptest.ResponseRecorder { w := httptest.NewRecorder() req := httptest.NewRequest(method, "/x", nil) if origin != "" { req.Header.Set("Origin", origin) } r.ServeHTTP(w, req) return w } func TestCORSAllowedOrigin(t *testing.T) { r := newCORSRouter("http://localhost:3000") // Настоящий запрос: заголовок ровно для своего origin + Vary: Origin. w := serve(r, http.MethodGet, "http://localhost:3000") if w.Code != http.StatusOK { t.Fatalf("code = %d, want 200", w.Code) } if got := w.Header().Get("Access-Control-Allow-Origin"); got != "http://localhost:3000" { t.Errorf("Allow-Origin = %q, want the exact origin", got) } if !strings.Contains(strings.Join(w.Header()["Vary"], ","), "Origin") { t.Error("no Vary: Origin — кэши смешают ответы разных источников") } if h := w.Header().Get("Access-Control-Allow-Headers"); !strings.Contains(h, "Authorization") { t.Errorf("Allow-Headers = %q, want Authorization", h) } // Preflight: 204 и без тела — браузер спрашивает до запроса с токеном. w2 := serve(r, http.MethodOptions, "http://localhost:3000") if w2.Code != http.StatusNoContent { t.Errorf("preflight code = %d, want 204", w2.Code) } if m := w2.Header().Get("Access-Control-Allow-Methods"); !strings.Contains(m, "PATCH") { t.Errorf("Allow-Methods = %q, want PATCH", m) } if w2.Body.Len() != 0 { t.Errorf("preflight body = %q, want empty", w2.Body.String()) } } func TestCORSDisallowedOrigin(t *testing.T) { // Чужой origin в списке разрешённых — заголовков нет, браузер закроет. r := newCORSRouter("http://localhost:3000") w := serve(r, http.MethodGet, "http://evil.test") if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" { t.Errorf("Allow-Origin for foreign origin = %q, want empty", got) } // Пустой список (CORS_ORIGINS не задан) — заголовков нет вообще. r2 := newCORSRouter() w2 := serve(r2, http.MethodGet, "http://localhost:3000") if got := w2.Header().Get("Access-Control-Allow-Origin"); got != "" { t.Errorf("Allow-Origin with empty list = %q, want empty", got) } // Запрос без Origin (тот же домен) живёт независимо от списка. w3 := serve(r2, http.MethodGet, "") if w3.Code != http.StatusOK { t.Errorf("same-origin request code = %d, want 200", w3.Code) } }