Compare commits
No commits in common. "257631436a701e5df758f599f4feb1c5d67adf51" and "36087c3d9eba40de6342a1e45d98c0ae702cb32b" have entirely different histories.
257631436a
...
36087c3d9e
2 changed files with 1 additions and 156 deletions
|
|
@ -120,7 +120,7 @@ app = FastAPI(lifespan=lifespan)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=[config.FRONTEND_ORIGIN],
|
allow_origins=[config.FRONTEND_ORIGIN],
|
||||||
allow_methods=["GET", "HEAD", "OPTIONS", "POST"],
|
allow_methods=["POST"],
|
||||||
allow_headers=["Content-Type", "Authorization"],
|
allow_headers=["Content-Type", "Authorization"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
"""
|
|
||||||
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}"
|
|
||||||
)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue