M2-T11: serve React SPA from FastAPI and remove Jinja pages

- app/main.py serves the SPA build (SPA_DIST_DIR, default frontend/dist):
  mounts /assets and a GET catch-all returning index.html for client routes;
  catch-all 404s on /api/*, never swallows /docs, /openapi.json, /static, assets,
  ingestion/ticktick/status; skips SPA serving when dist absent (backend-only CI)
- delete app/api/routes/pages.py, app/api/routes/auth.py, app/templates/
  (all replaced by /api/* + SPA; auth service layer kept)
- remove/replace Jinja page tests (JSON coverage already in test_api_*);
  add tests/test_spa_hosting.py for the fallback contract
- regenerate openapi/ (Jinja paths gone) and frontend schema.d.ts
This commit is contained in:
2026-06-13 11:29:14 +02:00
parent 8aa7316b26
commit a9830c42d8
18 changed files with 319 additions and 2094 deletions
+2 -22
View File
@@ -1,8 +1,6 @@
"""Tests for M2-T02: GET /api/session, POST /api/auth/login, /logout, /password."""
from __future__ import annotations
import re
from fastapi.testclient import TestClient
@@ -11,24 +9,6 @@ from fastapi.testclient import TestClient
# ---------------------------------------------------------------------------
def _extract_csrf_token(html: str) -> str:
match = re.search(r'name="csrf_token" value="([^"]+)"', html)
assert match is not None, "csrf_token not found in HTML"
return match.group(1)
def _jinja_login(client: TestClient) -> None:
"""Log in via the existing Jinja form so the client has a session cookie."""
login_page = client.get("/login")
csrf_token = _extract_csrf_token(login_page.text)
resp = client.post(
"/login",
data={"username": "admin", "password": "test-password", "csrf_token": csrf_token},
follow_redirects=False,
)
assert resp.status_code == 303, f"Jinja login failed: {resp.status_code}"
def _api_login(client: TestClient, *, username: str = "admin", password: str = "test-password"):
"""Log in via POST /api/auth/login and return the response."""
return client.post(
@@ -53,7 +33,7 @@ def test_get_session_unauthenticated_returns_401(client: TestClient) -> None:
def test_get_session_authenticated_returns_user_and_csrf(client: TestClient) -> None:
_jinja_login(client)
_api_login(client)
response = client.get("/api/session")
@@ -68,7 +48,7 @@ def test_get_session_authenticated_returns_user_and_csrf(client: TestClient) ->
def test_get_session_does_not_leak_password(client: TestClient) -> None:
_jinja_login(client)
_api_login(client)
response = client.get("/api/session")
body_str = str(response.json())
assert "test-password" not in body_str