"""Smoke-тесты M0: health, защита API, redirect OAuth-флоу."""
from fastapi.testclient import TestClient
from app.config import get_settings
from app.main import app
client = TestClient(app)
def test_health() -> None:
assert client.get("/api/health").json() == {"status": "ok"}
def test_auth_me_requires_auth() -> None:
assert client.get("/auth/me").status_code == 401
def test_login_redirects_to_sso() -> None:
base_url = get_settings().gauth_base_url
resp = client.get("/auth/login", follow_redirects=False)
assert resp.status_code in (302, 307)
location = resp.headers["location"]
assert location.startswith(base_url)
assert "state=" in location and "code_challenge=" in location
def test_safe_return_to() -> None:
"""Open redirect: наружный return_to превращается в корень приложения."""
from app.auth.routes import _safe_return_to
assert _safe_return_to("/tasks/5") == "/tasks/5"
assert _safe_return_to("https://evil.com") == "/"
assert _safe_return_to("//evil.com") == "/"
assert _safe_return_to("") == "/"
def test_default_session_secret_rejected() -> None:
"""Забытый SESSION_SECRET не должен поднимать приложение молча."""
import pytest as _pytest
from app.config import Settings
with _pytest.raises(ValueError, match="SESSION_SECRET"):
Settings(_env_file=None, session_secret="change-me-session-secret")
def test_api_requires_auth() -> None:
"""Регрессия: все /api эндпоинты должны требовать сессию."""
from app.dependencies import require_user
saved = app.dependency_overrides.pop(require_user, None)
try:
# Без `with`: lifespan запускает MCP session manager — только один раз
# на инстанс, см. test_mcp.py::test_mcp_requires_bearer_token.
c = TestClient(app)
assert c.get("/api/tasks").status_code == 401
assert c.get("/api/projects").status_code == 401
assert c.get("/api/tags").status_code == 401
assert c.post("/api/tasks", json={"title": "x"}).status_code == 401
assert c.delete("/api/tags/1").status_code == 401
finally:
if saved is not None:
app.dependency_overrides[require_user] = saved
def test_every_api_route_requires_user() -> None:
"""Каждый /api-роут обязан иметь require_user в зависимостях: один новый
эндпоинт без UserDep (как было с DELETE /api/tags) не должен проходить мимо."""
from fastapi.routing import APIRoute
from app.dependencies import require_user
bare = []
for route in app.routes:
if not isinstance(route, APIRoute):
continue
if not route.path.startswith("/api/") or route.path == "/api/health":
continue
deps = [d.call for d in route.dependant.dependencies]
if require_user not in deps:
bare.append(route.path)
assert bare == [], f"роуты без авторизации: {bare}"