""" 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" )