kin: BATON-006 не работает фронт: {'detail':'Not Found'}
This commit is contained in:
parent
68a1c90541
commit
5fcfc3a76b
1 changed files with 235 additions and 0 deletions
235
tests/test_baton_006.py
Normal file
235
tests/test_baton_006.py
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
"""
|
||||||
|
Tests for BATON-006: не работает фронт — {"detail":"Not Found"}
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
1. nginx/baton.conf содержит location /api/ (prefix match), проксирует на FastAPI.
|
||||||
|
2. nginx/baton.conf содержит location /health, проксирует на FastAPI.
|
||||||
|
3. nginx/baton.conf содержит location / с root и try_files (SPA-поведение).
|
||||||
|
4. GET / на FastAPI возвращает 404 (маршрут / не зарегистрирован в main.py —
|
||||||
|
статику должен отдавать nginx, а не FastAPI).
|
||||||
|
5. GET /health возвращает 200 (FastAPI-маршрут работает).
|
||||||
|
6. POST /api/register возвращает 200 (FastAPI-маршрут не сломан).
|
||||||
|
7. POST /api/signal возвращает 200 (FastAPI-маршрут не сломан).
|
||||||
|
8. POST /api/webhook/telegram возвращает 200 с корректным секретом.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.conftest import make_app_client
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
NGINX_CONF = PROJECT_ROOT / "nginx" / "baton.conf"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Criterion 1 — location /api/ proxies to FastAPI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_nginx_conf_exists() -> None:
|
||||||
|
"""nginx/baton.conf должен существовать в репозитории."""
|
||||||
|
assert NGINX_CONF.is_file(), f"nginx/baton.conf не найден: {NGINX_CONF}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_nginx_conf_has_api_location_block() -> None:
|
||||||
|
"""nginx/baton.conf должен содержать location /api/ (prefix match)."""
|
||||||
|
content = NGINX_CONF.read_text(encoding="utf-8")
|
||||||
|
assert re.search(r"location\s+/api/", content), (
|
||||||
|
"nginx/baton.conf не содержит блок location /api/"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_nginx_conf_api_location_proxies_to_fastapi() -> None:
|
||||||
|
"""Блок location /api/ должен делать proxy_pass на 127.0.0.1:8000."""
|
||||||
|
content = NGINX_CONF.read_text(encoding="utf-8")
|
||||||
|
# Ищем блок api и proxy_pass внутри
|
||||||
|
api_block = re.search(
|
||||||
|
r"location\s+/api/\s*\{([^}]+)\}", content, re.DOTALL
|
||||||
|
)
|
||||||
|
assert api_block is not None, "Блок location /api/ { ... } не найден"
|
||||||
|
assert re.search(r"proxy_pass\s+http://127\.0\.0\.1:8000", api_block.group(1)), (
|
||||||
|
"Блок location /api/ не содержит proxy_pass http://127.0.0.1:8000"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Criterion 2 — location /health proxies to FastAPI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_nginx_conf_has_health_location_block() -> None:
|
||||||
|
"""nginx/baton.conf должен содержать отдельный location /health."""
|
||||||
|
content = NGINX_CONF.read_text(encoding="utf-8")
|
||||||
|
assert re.search(r"location\s+/health\b", content), (
|
||||||
|
"nginx/baton.conf не содержит блок location /health"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_nginx_conf_health_location_proxies_to_fastapi() -> None:
|
||||||
|
"""Блок location /health должен делать proxy_pass на 127.0.0.1:8000."""
|
||||||
|
content = NGINX_CONF.read_text(encoding="utf-8")
|
||||||
|
health_block = re.search(
|
||||||
|
r"location\s+/health\s*\{([^}]+)\}", content, re.DOTALL
|
||||||
|
)
|
||||||
|
assert health_block is not None, "Блок location /health { ... } не найден"
|
||||||
|
assert re.search(r"proxy_pass\s+http://127\.0\.0\.1:8000", health_block.group(1)), (
|
||||||
|
"Блок location /health не содержит proxy_pass http://127.0.0.1:8000"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Criterion 3 — location / serves static files (SPA)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_nginx_conf_root_location_has_root_directive() -> None:
|
||||||
|
"""location / в nginx.conf должен содержать директиву root (статика)."""
|
||||||
|
content = NGINX_CONF.read_text(encoding="utf-8")
|
||||||
|
# Ищем последний блок location / (не /api/, не /health)
|
||||||
|
root_block = re.search(
|
||||||
|
r"location\s+/\s*\{([^}]+)\}", content, re.DOTALL
|
||||||
|
)
|
||||||
|
assert root_block is not None, "Блок location / { ... } не найден"
|
||||||
|
assert re.search(r"root\s+", root_block.group(1)), (
|
||||||
|
"Блок location / не содержит директиву root — SPA статика не настроена"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_nginx_conf_root_location_has_try_files_for_spa() -> None:
|
||||||
|
"""location / должен содержать try_files с fallback на /index.html (SPA)."""
|
||||||
|
content = NGINX_CONF.read_text(encoding="utf-8")
|
||||||
|
root_block = re.search(
|
||||||
|
r"location\s+/\s*\{([^}]+)\}", content, re.DOTALL
|
||||||
|
)
|
||||||
|
assert root_block is not None, "Блок location / { ... } не найден"
|
||||||
|
assert re.search(r"try_files\s+\$uri\s+/index\.html", root_block.group(1)), (
|
||||||
|
"Блок location / не содержит try_files $uri /index.html — "
|
||||||
|
"SPA-роутинг не работает"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_nginx_conf_root_location_does_not_proxy_to_fastapi() -> None:
|
||||||
|
"""location / НЕ должен делать proxy_pass на FastAPI (только статика)."""
|
||||||
|
content = NGINX_CONF.read_text(encoding="utf-8")
|
||||||
|
root_block = re.search(
|
||||||
|
r"location\s+/\s*\{([^}]+)\}", content, re.DOTALL
|
||||||
|
)
|
||||||
|
assert root_block is not None, "Блок location / { ... } не найден"
|
||||||
|
assert not re.search(r"proxy_pass", root_block.group(1)), (
|
||||||
|
"Блок location / содержит proxy_pass — GET / будет проксирован в FastAPI, "
|
||||||
|
"что вернёт 404 {'detail':'Not Found'} (исходная ошибка BATON-006)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Criterion 4 — FastAPI не имеет маршрута GET /
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fastapi_root_returns_404() -> None:
|
||||||
|
"""GET / должен возвращать 404 от FastAPI — маршрут не зарегистрирован.
|
||||||
|
|
||||||
|
Это ожидаемое поведение: статику отдаёт nginx (location / с root + try_files),
|
||||||
|
а не FastAPI. Регрессия: если когда-нибудь GET / начнёт возвращать 200 от FastAPI,
|
||||||
|
это нарушит архитектуру (FastAPI не должен отдавать статику).
|
||||||
|
"""
|
||||||
|
async with make_app_client() as client:
|
||||||
|
response = await client.get("/")
|
||||||
|
|
||||||
|
assert response.status_code == 404, (
|
||||||
|
f"GET / должен возвращать 404 от FastAPI (статику отдаёт nginx). "
|
||||||
|
f"Получено: {response.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Criterion 5 — GET /health работает
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_health_endpoint_returns_200() -> None:
|
||||||
|
"""GET /health должен возвращать 200 после изменений nginx-конфига."""
|
||||||
|
async with make_app_client() as client:
|
||||||
|
response = await client.get("/health")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json().get("status") == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Criterion 6 — POST /api/register не сломан
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_register_not_broken_after_nginx_change() -> None:
|
||||||
|
"""POST /api/register должен вернуть 200 — функция не сломана изменением nginx."""
|
||||||
|
async with make_app_client() as client:
|
||||||
|
response = await client.post(
|
||||||
|
"/api/register",
|
||||||
|
json={"uuid": "baton-006-uuid-001", "name": "TestUser"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["user_id"] > 0
|
||||||
|
assert data["uuid"] == "baton-006-uuid-001"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Criterion 7 — POST /api/signal не сломан
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_signal_not_broken_after_nginx_change() -> None:
|
||||||
|
"""POST /api/signal должен вернуть 200 — функция не сломана изменением nginx."""
|
||||||
|
async with make_app_client() as client:
|
||||||
|
# Сначала регистрируем пользователя
|
||||||
|
await client.post(
|
||||||
|
"/api/register",
|
||||||
|
json={"uuid": "baton-006-uuid-002", "name": "SignalUser"},
|
||||||
|
)
|
||||||
|
# Отправляем сигнал
|
||||||
|
response = await client.post(
|
||||||
|
"/api/signal",
|
||||||
|
json={
|
||||||
|
"user_id": "baton-006-uuid-002",
|
||||||
|
"timestamp": 1700000000000,
|
||||||
|
"geo": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json().get("status") == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Criterion 8 — POST /api/webhook/telegram не сломан
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_webhook_telegram_not_broken_after_nginx_change() -> None:
|
||||||
|
"""POST /api/webhook/telegram с корректным секретом должен вернуть 200."""
|
||||||
|
async with make_app_client() as client:
|
||||||
|
response = await client.post(
|
||||||
|
"/api/webhook/telegram",
|
||||||
|
json={"update_id": 200, "message": {"text": "hello"}},
|
||||||
|
headers={"X-Telegram-Bot-Api-Secret-Token": "test-webhook-secret"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"ok": True}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue