""" Tests for BATON-FIX-007: CORS OPTIONS preflight verification. Acceptance criteria: 1. OPTIONS preflight to /api/signal returns 200. 2. Preflight response includes Access-Control-Allow-Methods containing GET. 3. Preflight response includes Access-Control-Allow-Origin matching the configured origin. 4. Preflight response includes Access-Control-Allow-Headers with Authorization. 5. allow_methods in CORSMiddleware configuration explicitly contains GET. """ from __future__ import annotations import ast from pathlib import Path import pytest from tests.conftest import make_app_client _FRONTEND_ORIGIN = "http://localhost:3000" _BACKEND_DIR = Path(__file__).parent.parent / "backend" # --------------------------------------------------------------------------- # Static check — CORSMiddleware config contains GET in allow_methods # --------------------------------------------------------------------------- def test_main_py_cors_allow_methods_contains_get() -> None: """allow_methods в CORSMiddleware должен содержать 'GET'.""" source = (_BACKEND_DIR / "main.py").read_text(encoding="utf-8") tree = ast.parse(source, filename="main.py") for node in ast.walk(tree): if isinstance(node, ast.Call): func = node.func if isinstance(func, ast.Name) and func.id == "add_middleware": continue if not ( isinstance(func, ast.Attribute) and func.attr == "add_middleware" ): continue for kw in node.keywords: if kw.arg == "allow_methods": if isinstance(kw.value, ast.List): methods = [ elt.value for elt in kw.value.elts if isinstance(elt, ast.Constant) and isinstance(elt.value, str) ] assert "GET" in methods, ( f"allow_methods в CORSMiddleware не содержит 'GET': {methods}" ) return pytest.fail("add_middleware с CORSMiddleware и allow_methods не найден в main.py") def test_main_py_cors_allow_methods_contains_post() -> None: """allow_methods в CORSMiddleware должен содержать 'POST' (регрессия).""" source = (_BACKEND_DIR / "main.py").read_text(encoding="utf-8") assert '"POST"' in source or "'POST'" in source, ( "allow_methods в CORSMiddleware не содержит 'POST'" ) # --------------------------------------------------------------------------- # Functional — OPTIONS preflight request # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_options_preflight_signal_returns_200() -> None: """OPTIONS preflight к /api/signal должен возвращать 200.""" async with make_app_client() as client: resp = await client.options( "/api/signal", headers={ "Origin": _FRONTEND_ORIGIN, "Access-Control-Request-Method": "POST", "Access-Control-Request-Headers": "Content-Type, Authorization", }, ) assert resp.status_code == 200, ( f"Preflight OPTIONS /api/signal вернул {resp.status_code}, ожидался 200" ) @pytest.mark.asyncio async def test_options_preflight_allow_origin_header() -> None: """OPTIONS preflight должен вернуть Access-Control-Allow-Origin.""" async with make_app_client() as client: resp = await client.options( "/api/signal", headers={ "Origin": _FRONTEND_ORIGIN, "Access-Control-Request-Method": "POST", "Access-Control-Request-Headers": "Content-Type, Authorization", }, ) acao = resp.headers.get("access-control-allow-origin", "") assert acao == _FRONTEND_ORIGIN, ( f"Ожидался Access-Control-Allow-Origin: {_FRONTEND_ORIGIN!r}, получен: {acao!r}" ) @pytest.mark.asyncio async def test_options_preflight_allow_methods_contains_get() -> None: """OPTIONS preflight должен вернуть Access-Control-Allow-Methods, включающий GET.""" async with make_app_client() as client: resp = await client.options( "/api/signal", headers={ "Origin": _FRONTEND_ORIGIN, "Access-Control-Request-Method": "GET", "Access-Control-Request-Headers": "Authorization", }, ) acam = resp.headers.get("access-control-allow-methods", "") assert "GET" in acam, ( f"Access-Control-Allow-Methods не содержит GET: {acam!r}\n" "Decision #1268: allow_methods=['POST'] — GET отсутствует" ) @pytest.mark.asyncio async def test_options_preflight_allow_headers_contains_authorization() -> None: """OPTIONS preflight должен вернуть Access-Control-Allow-Headers, включающий Authorization.""" async with make_app_client() as client: resp = await client.options( "/api/signal", headers={ "Origin": _FRONTEND_ORIGIN, "Access-Control-Request-Method": "POST", "Access-Control-Request-Headers": "Authorization", }, ) acah = resp.headers.get("access-control-allow-headers", "") assert "authorization" in acah.lower(), ( f"Access-Control-Allow-Headers не содержит Authorization: {acah!r}" ) @pytest.mark.asyncio async def test_get_health_cors_header_present() -> None: """GET /health с Origin должен вернуть Access-Control-Allow-Origin (simple request).""" async with make_app_client() as client: resp = await client.get( "/health", headers={"Origin": _FRONTEND_ORIGIN}, ) assert resp.status_code == 200 acao = resp.headers.get("access-control-allow-origin", "") assert acao == _FRONTEND_ORIGIN, ( f"GET /health: ожидался CORS-заголовок {_FRONTEND_ORIGIN!r}, получен: {acao!r}" )