baton/tests/test_fix_007.py
2026-03-21 09:30:44 +02:00

155 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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