baton/tests/test_baton_006.py

235 lines
10 KiB
Python
Raw Permalink 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-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}