From 5401363ea9bf102cad055bb5a0046f277ba0e210 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Sat, 21 Mar 2026 09:37:57 +0200 Subject: [PATCH] =?UTF-8?q?kin:=20BATON-FIX-013=20CORS=20allow=5Fmethods:?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20GET=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20/health=20=D1=8D=D0=BD=D0=B4=D0=BF=D0=BE?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_fix_013.py | 194 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 tests/test_fix_013.py diff --git a/tests/test_fix_013.py b/tests/test_fix_013.py new file mode 100644 index 0000000..5445895 --- /dev/null +++ b/tests/test_fix_013.py @@ -0,0 +1,194 @@ +""" +Tests for BATON-FIX-013: CORS allow_methods — добавить GET для /health эндпоинтов. + +Acceptance criteria: +1. CORSMiddleware в main.py содержит "GET" в allow_methods. +2. OPTIONS preflight /health с Origin и Access-Control-Request-Method: GET + возвращает 200/204 и содержит GET в Access-Control-Allow-Methods. +3. OPTIONS preflight /api/health — аналогично. +4. GET /health возвращает 200 (regression guard vs. allow_methods=['POST'] only). +""" +from __future__ import annotations + +import os + +os.environ.setdefault("BOT_TOKEN", "test-bot-token") +os.environ.setdefault("CHAT_ID", "-1001234567890") +os.environ.setdefault("WEBHOOK_SECRET", "test-webhook-secret") +os.environ.setdefault("WEBHOOK_URL", "https://example.com/api/webhook/telegram") +os.environ.setdefault("FRONTEND_ORIGIN", "http://localhost:3000") +os.environ.setdefault("ADMIN_TOKEN", "test-admin-token") +os.environ.setdefault("ADMIN_CHAT_ID", "5694335584") + +import pytest + +from tests.conftest import make_app_client + +_ORIGIN = "http://localhost:3000" +# allow_headers = ["Content-Type", "Authorization"] — X-Custom-Header не разрешён, +# поэтому preflight с X-Custom-Header вернёт 400. Используем Content-Type. +_PREFLIGHT_HEADER = "Content-Type" + + +# --------------------------------------------------------------------------- +# Criterion 1 — Static: CORSMiddleware.allow_methods must contain "GET" +# --------------------------------------------------------------------------- + + +def test_cors_middleware_allow_methods_contains_get() -> None: + """app.user_middleware CORSMiddleware должен содержать 'GET' в allow_methods.""" + from fastapi.middleware.cors import CORSMiddleware + + from backend.main import app + + cors_mw = next( + (m for m in app.user_middleware if m.cls is CORSMiddleware), None + ) + assert cors_mw is not None, "CORSMiddleware не найден в app.user_middleware" + allow_methods = cors_mw.kwargs.get("allow_methods", []) + assert "GET" in allow_methods, ( + f"CORSMiddleware.allow_methods={allow_methods!r} не содержит 'GET'" + ) + + +def test_cors_middleware_allow_methods_contains_head() -> None: + """allow_methods должен содержать 'HEAD' для корректной работы preflight.""" + from fastapi.middleware.cors import CORSMiddleware + + from backend.main import app + + cors_mw = next( + (m for m in app.user_middleware if m.cls is CORSMiddleware), None + ) + assert cors_mw is not None + allow_methods = cors_mw.kwargs.get("allow_methods", []) + assert "HEAD" in allow_methods, ( + f"CORSMiddleware.allow_methods={allow_methods!r} не содержит 'HEAD'" + ) + + +def test_cors_middleware_allow_methods_contains_options() -> None: + """allow_methods должен содержать 'OPTIONS' для корректной обработки preflight.""" + from fastapi.middleware.cors import CORSMiddleware + + from backend.main import app + + cors_mw = next( + (m for m in app.user_middleware if m.cls is CORSMiddleware), None + ) + assert cors_mw is not None + allow_methods = cors_mw.kwargs.get("allow_methods", []) + assert "OPTIONS" in allow_methods, ( + f"CORSMiddleware.allow_methods={allow_methods!r} не содержит 'OPTIONS'" + ) + + +# --------------------------------------------------------------------------- +# Criterion 2 — Preflight OPTIONS /health includes GET in Allow-Methods +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_health_preflight_options_returns_success_status() -> None: + """OPTIONS preflight /health должен вернуть 200 или 204.""" + async with make_app_client() as client: + response = await client.options( + "/health", + headers={ + "Origin": _ORIGIN, + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": _PREFLIGHT_HEADER, + }, + ) + assert response.status_code in (200, 204), ( + f"OPTIONS /health вернул {response.status_code}, ожидался 200 или 204" + ) + + +@pytest.mark.asyncio +async def test_health_preflight_options_allow_methods_contains_get() -> None: + """OPTIONS preflight /health: Access-Control-Allow-Methods должен содержать GET.""" + async with make_app_client() as client: + response = await client.options( + "/health", + headers={ + "Origin": _ORIGIN, + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": _PREFLIGHT_HEADER, + }, + ) + allow_methods_header = response.headers.get("access-control-allow-methods", "") + assert "GET" in allow_methods_header, ( + f"Access-Control-Allow-Methods={allow_methods_header!r} не содержит 'GET'" + ) + + +# --------------------------------------------------------------------------- +# Criterion 3 — Preflight OPTIONS /api/health includes GET in Allow-Methods +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_api_health_preflight_options_returns_success_status() -> None: + """OPTIONS preflight /api/health должен вернуть 200 или 204.""" + async with make_app_client() as client: + response = await client.options( + "/api/health", + headers={ + "Origin": _ORIGIN, + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": _PREFLIGHT_HEADER, + }, + ) + assert response.status_code in (200, 204), ( + f"OPTIONS /api/health вернул {response.status_code}, ожидался 200 или 204" + ) + + +@pytest.mark.asyncio +async def test_api_health_preflight_options_allow_methods_contains_get() -> None: + """OPTIONS preflight /api/health: Access-Control-Allow-Methods должен содержать GET.""" + async with make_app_client() as client: + response = await client.options( + "/api/health", + headers={ + "Origin": _ORIGIN, + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": _PREFLIGHT_HEADER, + }, + ) + allow_methods_header = response.headers.get("access-control-allow-methods", "") + assert "GET" in allow_methods_header, ( + f"Access-Control-Allow-Methods={allow_methods_header!r} не содержит 'GET'" + ) + + +# --------------------------------------------------------------------------- +# Criterion 4 — GET /health returns 200 (regression guard) +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_health_get_returns_200_regression_guard() -> None: + """GET /health должен вернуть 200 — regression guard против allow_methods=['POST'] only.""" + async with make_app_client() as client: + response = await client.get( + "/health", + headers={"Origin": _ORIGIN}, + ) + assert response.status_code == 200, ( + f"GET /health вернул {response.status_code}, ожидался 200" + ) + + +@pytest.mark.asyncio +async def test_api_health_get_returns_200_regression_guard() -> None: + """GET /api/health должен вернуть 200 — regression guard против allow_methods=['POST'] only.""" + async with make_app_client() as client: + response = await client.get( + "/api/health", + headers={"Origin": _ORIGIN}, + ) + assert response.status_code == 200, ( + f"GET /api/health вернул {response.status_code}, ожидался 200" + )