"""The library player's resume point (`library_states`). A checkpoint stores where the caller stopped in a library item they can play — their own download, or one shared with them — and the two library listings hand it back per item, so the player opens there. Too early or too close to the end clears it. Nobody else's checkpoint, and no item the caller cannot reach, is ever read or written. DB-backed (real Postgres). """ import pytest from sqlalchemy import select from app.downloads.models import DownloadJob, DownloadShare, LibraryState, MediaAsset from app.routes._common import ProgressIn, kept_position def _done_job(db, user) -> DownloadJob: asset = MediaAsset( source_kind="url", source_ref="https://example.com/a.mp4", format_sig=f"sig-{user.id}", status="ready", rel_path=f"a-{user.id}.mp4", ref_count=1, media_kind="video", container="mp4", ) db.add(asset) db.flush() job = DownloadJob( user_id=user.id, asset_id=asset.id, source_kind="url", source_ref="https://example.com/a.mp4", status="done", profile_snapshot={}, ) db.add(job) db.commit() return job def _checkpoint(client, job_id: int, position: float, duration: float = 3000) -> int: """Send a checkpoint the caller may make; the point the server kept (0 = cleared).""" r = client.post( f"/api/downloads/{job_id}/progress", json={"position_seconds": position, "duration_seconds": duration}, ) assert r.status_code == 200, r.text return r.json()["position_seconds"] def _states(db) -> list[tuple[int, int, int]]: db.expire_all() rows = db.execute( select(LibraryState.user_id, LibraryState.job_id, LibraryState.position_seconds) ).all() return [tuple(r) for r in rows] def test_a_checkpoint_is_listed_back_on_the_item(db, make_user, as_user): me = make_user() job = _done_job(db, me) c = as_user(me) r = c.post( f"/api/downloads/{job.id}/progress", json={"position_seconds": 754.9, "duration_seconds": 3000}, ) assert r.status_code == 200 and r.json() == { "ok": True, "job_id": job.id, "position_seconds": 754, } [item] = c.get("/api/files").json() assert item["position_seconds"] == 754 # A second checkpoint moves it rather than adding a row. assert _checkpoint(c, job.id, 900) == 900 assert _states(db) == [(me.id, job.id, 900)] def test_an_item_never_played_lists_zero(db, make_user, as_user): me = make_user() _done_job(db, me) [item] = as_user(me).get("/api/files").json() assert item["position_seconds"] == 0 def test_too_early_or_near_the_end_clears_it(db, make_user, as_user): me = make_user() job = _done_job(db, me) c = as_user(me) assert _checkpoint(c, job.id, 600) == 600 assert _checkpoint(c, job.id, 2995) == 0 assert _states(db) == [] assert _checkpoint(c, job.id, 600) == 600 assert _checkpoint(c, job.id, 3) == 0 assert _states(db) == [] # Clearing what is not there is not an error (the beacon and the last checkpoint of one stop). assert _checkpoint(c, job.id, 0) == 0 # The edges themselves, one second either side — the same rows as the client's # frontend/src/lib/resume.dom.test.ts: the rule must be applied alike on both sides, and the shared # constants alone (resume.contract.test.ts) would not see a `>=` that became `>` on one side. @pytest.mark.parametrize( ("position", "duration", "kept"), [(4, 3000, 0), (5, 3000, 5), (2990, 3000, 2990), (2991, 3000, 0)], ) def test_the_kept_point_at_its_exact_edges(position, duration, kept): body = ProgressIn(position_seconds=position, duration_seconds=duration) assert kept_position(body) == kept def test_a_recipient_keeps_their_own_place_in_a_shared_item(db, make_user, as_user): owner, friend = make_user(), make_user() job = _done_job(db, owner) db.add(DownloadShare(job_id=job.id, from_user_id=owner.id, to_user_id=friend.id)) db.commit() assert _checkpoint(as_user(owner), job.id, 100) == 100 assert _checkpoint(as_user(friend), job.id, 2000) == 2000 assert as_user(owner).get("/api/files").json()[0]["position_seconds"] == 100 assert ( as_user(friend).get("/api/files", params={"scope": "shared"}).json()[0]["position_seconds"] == 2000 ) def test_an_item_the_caller_cannot_play_is_a_404_and_writes_nothing(db, make_user, as_user): owner, stranger = make_user(), make_user() job = _done_job(db, owner) r = as_user(stranger).post( f"/api/downloads/{job.id}/progress", json={"position_seconds": 600, "duration_seconds": 3000}, ) assert r.status_code == 404 assert _states(db) == [] def test_deleting_the_job_deletes_its_places(db, make_user, as_user): me = make_user() job = _done_job(db, me) assert _checkpoint(as_user(me), job.id, 600) == 600 db.delete(db.get(DownloadJob, job.id)) db.commit() assert _states(db) == []