87 lines
3.8 KiB
Python
87 lines
3.8 KiB
Python
from types import SimpleNamespace
|
|
from uuid import uuid4
|
|
import asyncio
|
|
|
|
from fastapi.testclient import TestClient
|
|
import pytest
|
|
|
|
from app.api.v1.endpoints.timeseries import playback
|
|
from app.auth.project_dependencies import ProjectContext
|
|
from tests.conftest import build_test_app
|
|
|
|
|
|
def client(monkeypatch):
|
|
context = ProjectContext(project_id=uuid4(), project_code="test", user_id=uuid4(), project_role="viewer")
|
|
app = build_test_app(playback.router, "/api/v1")
|
|
app.dependency_overrides[playback.get_project_context] = lambda: context
|
|
app.dependency_overrides[playback.get_project_pg_connection] = lambda: object()
|
|
app.dependency_overrides[playback.get_timescale_connection] = lambda: object()
|
|
async def geometry(*_):
|
|
return SimpleNamespace(version="a" * 24, content=b"geometry")
|
|
monkeypatch.setattr(playback, "geometry_for_project", geometry)
|
|
return TestClient(app)
|
|
|
|
|
|
def test_geometry_is_private_and_frames_reject_a_different_model_version(monkeypatch):
|
|
http = client(monkeypatch)
|
|
response = http.get("/api/v1/timeseries/realtime/playback/geometry")
|
|
assert response.status_code == 200
|
|
assert response.headers["cache-control"] == "private, no-store"
|
|
assert response.content == b"geometry"
|
|
response = http.get("/api/v1/timeseries/realtime/playback/frames", params={
|
|
"start_time": "2026-07-16T00:00:00+08:00", "model_version": "b" * 24,
|
|
})
|
|
assert response.status_code == 409
|
|
|
|
|
|
@pytest.mark.parametrize("params", [
|
|
{"start_time": "2026-07-16T00:00:00"},
|
|
{"start_time": "2026-07-16T00:00:00Z", "frame_count": 9},
|
|
{"start_time": "2026-07-16T00:00:00Z", "step_seconds": 0},
|
|
])
|
|
def test_frames_require_timezone_and_bound_the_block_size(monkeypatch, params):
|
|
http = client(monkeypatch)
|
|
response = http.get("/api/v1/timeseries/realtime/playback/frames", params={"model_version": "a" * 24, **params})
|
|
assert response.status_code == 422
|
|
|
|
|
|
def test_frames_pass_exact_utc_time_and_counts_to_reader(monkeypatch):
|
|
http = client(monkeypatch)
|
|
async def frames(_connection, geometry, start, count, step):
|
|
assert geometry.version == "a" * 24
|
|
assert start.isoformat() == "2026-07-15T16:00:00+00:00"
|
|
assert count == 4 and step == 900
|
|
return b"frames"
|
|
monkeypatch.setattr(playback, "read_frames", frames)
|
|
response = http.get("/api/v1/timeseries/realtime/playback/frames", params={
|
|
"start_time": "2026-07-16T00:00:00+08:00", "model_version": "a" * 24,
|
|
})
|
|
assert response.status_code == 200 and response.content == b"frames"
|
|
|
|
|
|
def test_cache_separates_projects_and_database_routes(monkeypatch):
|
|
playback._cache.clear()
|
|
calls = []
|
|
async def read(conn):
|
|
calls.append(conn.info.dbname)
|
|
return SimpleNamespace(version=conn.info.dbname)
|
|
monkeypatch.setattr(playback, "read_geometry", read)
|
|
ctx = SimpleNamespace(project_id=uuid4())
|
|
async def run():
|
|
for name in ["one", "one", "two"]:
|
|
connection = SimpleNamespace(info=SimpleNamespace(host="host", port=5432, dbname=name))
|
|
result = await playback.geometry_for_project(ctx, connection)
|
|
assert result.version == name
|
|
other = SimpleNamespace(project_id=uuid4())
|
|
await playback.geometry_for_project(other, SimpleNamespace(info=SimpleNamespace(host="host", port=5432, dbname="one")))
|
|
asyncio.run(run())
|
|
assert calls == ["one", "two", "one"]
|
|
playback._cache.clear()
|
|
|
|
|
|
def test_contract_advertises_binary_packets_instead_of_json():
|
|
schema = build_test_app(playback.router, "/api/v1").openapi()
|
|
for suffix in ("geometry", "frames"):
|
|
content = schema["paths"][f"/api/v1/timeseries/realtime/playback/{suffix}"]["get"]["responses"]["200"]["content"]
|
|
assert content == {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}}
|