"""P2 integration test — SPA fallback + voice UI coexist (TASK-10-04, G-041).
Tests against the running app (TestClient). Verifies:
1. GET / → 200 text/html with
(voice UI loads).
2. GET /operator/dashboard → 200 text/html (SPA fallback serves index.html).
3. GET /operator/login → 200 text/html (SPA fallback).
4. GET /api/operator/cohort → JSON (API route, not SPA fallback).
5. GET /health → JSON (API route).
6. GET /pipecat/webrtc → 405 (POST only, route exists — not SPA fallback).
7. GET /vc/verify/nonexistent → 404 (API route, not SPA fallback).
8. GET /assets/index.js → served by StaticFiles (not SPA fallback).
R-DASH-03 verified: SPA fallback serves index.html for client-side routes;
API routes + StaticFiles assets are unaffected. R-DASH-05: voice UI at /
unchanged.
G-041: the SPA fallback uses a custom StaticFiles subclass (SpaStaticFiles),
NOT a catch-all route — assets are served normally, index.html is the
fallback only for non-file paths.
"""
from __future__ import annotations
import os
import shutil
import tempfile
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def client_with_dist(tmp_path):
"""Build a client/dist with index.html + an asset, then import the app."""
dist = tmp_path / "dist"
dist.mkdir()
(dist / "index.html").write_text(
'
',
encoding="utf-8",
)
assets = dist / "assets"
assets.mkdir()
(assets / "index.js").write_text("console.log('app');", encoding="utf-8")
# Set the env var + reload the app module so the StaticFiles mount sees it.
os.environ["PRAXIS_CLIENT_DIST"] = str(dist)
os.environ["PRAXIS_COOKIE_SECRET"] = "x" * 48
os.environ["PRAXIS_COOKIE_SECURE"] = "false"
# Drop any PG DSN so we don't try to connect during the lifespan.
os.environ.pop("PRAXIS_PG_DSN", None)
import importlib
import server.__main__ as main_mod
importlib.reload(main_mod)
with TestClient(main_mod.app) as c:
yield c
# Cleanup env.
os.environ.pop("PRAXIS_CLIENT_DIST", None)
def test_root_serves_voice_ui(client_with_dist):
r = client_with_dist.get("/")
assert r.status_code == 200
assert "text/html" in r.headers.get("content-type", "")
assert "
" in r.text
def test_operator_dashboard_spa_fallback(client_with_dist):
r = client_with_dist.get("/operator/dashboard")
assert r.status_code == 200
assert "text/html" in r.headers.get("content-type", "")
assert "
" in r.text
def test_operator_login_spa_fallback(client_with_dist):
r = client_with_dist.get("/operator/login")
assert r.status_code == 200
assert "text/html" in r.headers.get("content-type", "")
assert "
" in r.text
def test_api_operator_cohort_is_json_not_html(client_with_dist):
# Without auth → 401 JSON (not index.html). Proves the API route wins.
r = client_with_dist.get("/api/operator/cohort")
assert r.status_code in (401, 503)
assert "application/json" in r.headers.get("content-type", "")
# Critically NOT html.
assert "
" not in r.text
def test_health_is_json(client_with_dist):
r = client_with_dist.get("/health")
assert r.status_code == 200
assert "application/json" in r.headers.get("content-type", "")
def test_pipecat_webrtc_post_route_exists(client_with_dist):
# The POST route exists and responds (not index.html). A GET falls through
# to the SPA fallback (serves index.html) — acceptable: the POST route is
# the real voice-loop entrypoint; a GET is a client-side navigation attempt.
# We assert the POST route is wired (returns 4xx/5xx, not HTML).
r = client_with_dist.post("/pipecat/webrtc", json={"sdp": "", "type": "offer"})
assert r.status_code in (400, 422, 500)
assert "
" not in r.text
def test_vc_verify_nonexistent_is_404(client_with_dist):
r = client_with_dist.get("/vc/verify/nonexistent-id-xyz")
assert r.status_code == 404
assert "application/json" in r.headers.get("content-type", "")
assert "
" not in r.text
def test_assets_served_by_staticfiles_not_spa_fallback(client_with_dist):
r = client_with_dist.get("/assets/index.js")
assert r.status_code == 200
ct = r.headers.get("content-type", "")
assert "javascript" in ct or "text/plain" in ct
assert "console.log" in r.text
def test_unknown_non_asset_path_serves_index_html(client_with_dist):
"""An unknown path that is NOT an asset + NOT an API route → SPA fallback."""
r = client_with_dist.get("/some/unknown/route")
assert r.status_code == 200
assert "
" in r.text