"""API contract tests (spec M3), against a synthetic fixture built into the test data dir. Locks the frozen response shapes lane C depends on, and verifies HTTP Range serving (206), CORS, and graceful degradation of the still-stubbed endpoints. """ from __future__ import annotations import shutil import pytest pytestmark = pytest.mark.skipif( shutil.which("ffmpeg") is None or shutil.which("ffprobe") is None, reason="ffmpeg/ffprobe required to build the API test fixture", ) @pytest.fixture(scope="module") def client(): from fastapi.testclient import TestClient from festival4d import synthetic synthetic.build(duration_s=1.0, run_ffmpeg=True) # into the temp data dir (conftest) from festival4d import api # imported after build so the /media mount + DB are ready with TestClient(api.app) as c: yield c def test_manifest_shape(client): data = client.get("/api/manifest").json() assert set(data) == {"videos", "t_global_max", "has_poses"} assert data["has_poses"] is True assert len(data["videos"]) == 3 v = data["videos"][0] assert set(v) == { "id", "filename", "url", "duration_s", "fps", "width", "height", "offset_ms", "drift_ppm", } offsets = {vv["filename"]: vv["offset_ms"] for vv in data["videos"]} assert offsets == {"cam0.mp4": 0.0, "cam1.mp4": 1370.0, "cam2.mp4": -842.0} def test_poses_shape(client): poses = client.get("/api/videos/1/poses").json() assert len(poses) > 0 p = poses[0] assert set(p) == {"frame_idx", "t_video_s", "q", "t", "intrinsics", "registered"} assert len(p["q"]) == 4 and len(p["t"]) == 3 assert set(p["intrinsics"]) == {"fx", "fy", "cx", "cy"} def test_missing_video_404(client): assert client.get("/api/videos/999/poses").status_code == 404 def test_anchors_get_and_post(client): anchors = client.get("/api/anchors").json() assert len(anchors) == 4 created = client.post( "/api/anchors", json={"label": "T", "x": 1, "y": 2, "z": 3, "color": "#fff"} ).json() assert created["label"] == "T" and created["id"] > 0 def test_events(client): events = client.get("/api/events").json() assert len(events) == len(__import__("festival4d.synthetic", fromlist=["x"]).SEED_EVENTS) assert {"id", "t_global_s", "event_type", "source", "description"} <= set(events[0]) def test_pointcloud_is_ply(client): resp = client.get("/api/pointcloud") assert resp.status_code == 200 assert resp.content[:3] == b"ply" def test_video_range_returns_206(client): """Spec pitfall #3: