195 lines
7.7 KiB
Python
195 lines
7.7 KiB
Python
|
|
"""
|
|||
|
|
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"
|
|||
|
|
)
|