70 lines
2.5 KiB
Python
70 lines
2.5 KiB
Python
"""SSE activity stream: resumable event ids, reconnect without duplicate/lost
|
|
events, and parity with the polling fallback."""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from photo_pipeline.api.app import create_app
|
|
from photo_pipeline.config import Config
|
|
from photo_pipeline.db import run_migrations
|
|
from photo_pipeline.services.jobs import ItemState, JobService, JobState
|
|
|
|
|
|
@pytest.fixture
|
|
def client(tmp_path):
|
|
(tmp_path / "data").mkdir()
|
|
config = Config.from_env({"PHOTO_PIPELINE_DATA_DIR": str(tmp_path / "data")})
|
|
run_migrations(config.database_url)
|
|
app = create_app(config)
|
|
with TestClient(app) as test_client:
|
|
test_client.app = app
|
|
yield test_client
|
|
|
|
|
|
def _finished_job(client):
|
|
job = client.post("/api/v1/jobs", json={"job_type": "scan", "items": ["a", "b"]}).json()
|
|
service = JobService(client.app.state.session_factory)
|
|
service.claim(["scan"], "w1")
|
|
for item in ("a", "b"):
|
|
service.set_item(job["id"], item, ItemState.RUNNING)
|
|
service.set_item(job["id"], item, ItemState.SUCCEEDED)
|
|
service.transition(job["id"], JobState.SUCCEEDED, worker_id="w1")
|
|
return job["id"]
|
|
|
|
|
|
def _read_ids(client, job_id, headers=None):
|
|
ids, saw_done = [], False
|
|
with client.stream(
|
|
"GET", f"/api/v1/jobs/{job_id}/events/stream", headers=headers or {}
|
|
) as stream:
|
|
assert stream.headers["content-type"].startswith("text/event-stream")
|
|
for line in stream.iter_lines():
|
|
if line.startswith("id:"):
|
|
ids.append(int(line.split(":", 1)[1]))
|
|
elif line.startswith("event: done"):
|
|
saw_done = True
|
|
return ids, saw_done
|
|
|
|
|
|
def test_sse_streams_all_events_then_closes(client):
|
|
job_id = _finished_job(client)
|
|
ids, saw_done = _read_ids(client, job_id)
|
|
assert ids == sorted(ids) and len(ids) >= 3
|
|
assert saw_done # terminal job closes the stream
|
|
|
|
|
|
def test_sse_reconnect_has_no_duplicate_or_lost_events(client):
|
|
job_id = _finished_job(client)
|
|
full, _ = _read_ids(client, job_id)
|
|
resume_from = full[1]
|
|
resumed, saw_done = _read_ids(client, job_id, headers={"Last-Event-ID": str(resume_from)})
|
|
assert resumed == [i for i in full if i > resume_from] # no dup, none lost
|
|
assert saw_done
|
|
|
|
|
|
def test_sse_matches_polling(client):
|
|
job_id = _finished_job(client)
|
|
sse_ids, _ = _read_ids(client, job_id)
|
|
polled = client.get(f"/api/v1/jobs/{job_id}/events").json()["events"]
|
|
assert sse_ids == [e["seq"] for e in polled]
|