test_archive_scope_per_run.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. """A caller limited to some printers sees an archive by its prints (#1727, archive redesign).
  2. Plate 1 of one archive printed on printer A, then plate 2 on B: the archive
  3. names B. A user of A sees the archive by plate 1 and only plate 1 of it: its
  4. outcome, video and finish photos. What changes the whole archive is refused
  5. to them, as it would change B's print too.
  6. """
  7. from __future__ import annotations
  8. from datetime import datetime, timedelta
  9. from types import SimpleNamespace
  10. import pytest
  11. from backend.app.core.config import settings as app_settings
  12. from backend.app.models.archive import PrintArchive
  13. from backend.app.models.print_log import RUN_OPEN, PrintLogEntry
  14. from backend.tests.integration.test_archives_last_per_printer import _admin_token, _auth, _user
  15. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  16. T1 = datetime(2026, 10, 30, 9)
  17. T2 = T1 + timedelta(hours=1)
  18. DIR = "archives/scoped"
  19. PERMISSIONS = ["archives:read_all", "archives:update_all", "archives:delete_all", "printers:read", "projects:read"]
  20. @pytest.fixture
  21. def files(tmp_path, monkeypatch):
  22. monkeypatch.setattr(app_settings, "base_dir", tmp_path)
  23. monkeypatch.setattr(app_settings, "archive_dir", tmp_path / "archive")
  24. (tmp_path / DIR / "photos").mkdir(parents=True)
  25. for name in ("a.mp4", "b.mp4", "photos/finish_a.jpg", "photos/finish_b.jpg", "photos/0a1b2c3d.jpg"):
  26. (tmp_path / DIR / name).write_bytes(b"x")
  27. return tmp_path
  28. @pytest.fixture
  29. async def plates(async_client, db_session, printer_factory, files):
  30. """Plate 1 completed on A with its video and photo, then plate 2 failed
  31. on B with its own; one photo uploaded by hand. Users scoped to A, to B
  32. and to a printer C that printed nothing."""
  33. a, b, c = await printer_factory(name="A"), await printer_factory(name="B"), await printer_factory(name="C")
  34. archive = PrintArchive(
  35. printer_id=b.id,
  36. filename="two.gcode.3mf",
  37. file_path=f"{DIR}/two.gcode.3mf",
  38. file_size=1,
  39. print_name="two",
  40. status="failed",
  41. started_at=T2,
  42. timelapse_path=f"{DIR}/b.mp4",
  43. photos=["finish_a.jpg", "finish_b.jpg", "0a1b2c3d.jpg"],
  44. )
  45. db_session.add(archive)
  46. await db_session.flush()
  47. first = PrintLogEntry(
  48. archive_id=archive.id,
  49. printer_id=a.id,
  50. plate_id=1,
  51. status="completed",
  52. started_at=T1,
  53. photos=["finish_a.jpg"],
  54. timelapse_path=f"{DIR}/a.mp4",
  55. )
  56. second = PrintLogEntry(
  57. archive_id=archive.id,
  58. printer_id=b.id,
  59. plate_id=2,
  60. status="failed",
  61. started_at=T2,
  62. photos=["finish_b.jpg"],
  63. timelapse_path=f"{DIR}/b.mp4",
  64. )
  65. db_session.add_all([first, second])
  66. await db_session.commit()
  67. admin = await _admin_token(async_client)
  68. users = {}
  69. for name, printer in (("of_a", a), ("of_b", b), ("of_c", c)):
  70. jwt, _ = await _user(async_client, admin, name, PERMISSIONS, printer_ids=[printer.id])
  71. users[name] = _auth(jwt)
  72. return SimpleNamespace(
  73. archive=archive.id, a=a.id, b=b.id, c=c.id, first=first.id, second=second.id, admin=_auth(admin), **users
  74. )
  75. def _url(plates, path=""):
  76. return f"/api/v1/archives/{plates.archive}{path}"
  77. async def _row(db_session, model, row_id):
  78. db_session.expire_all()
  79. return await db_session.get(model, row_id)
  80. class TestSeeing:
  81. async def test_a_user_of_a_sees_the_archive_by_its_plate(self, async_client, plates):
  82. listed = (await async_client.get("/api/v1/archives/", headers=plates.of_a)).json()
  83. assert [a["id"] for a in listed] == [plates.archive]
  84. shown = listed[0]
  85. assert (shown["printer_id"], shown["status"], shown["run_id"]) == (plates.a, "completed", plates.first)
  86. assert shown["timelapse_path"] == f"{DIR}/a.mp4"
  87. assert shown["photos"] == ["finish_a.jpg"]
  88. assert shown["run_count"] == 1
  89. async def test_the_archive_page_shows_their_plate(self, async_client, plates):
  90. response = await async_client.get(_url(plates), headers=plates.of_a)
  91. assert response.status_code == 200, response.text
  92. body = response.json()
  93. assert (body["printer_id"], body["status"], body["run_id"]) == (plates.a, "completed", plates.first)
  94. async def test_the_other_plate_is_not_theirs(self, async_client, plates):
  95. response = await async_client.get(_url(plates, f"?run_id={plates.second}"), headers=plates.of_a)
  96. assert response.status_code == 404
  97. async def test_its_print_log_lists_their_plate_only(self, async_client, plates):
  98. runs = (await async_client.get(_url(plates, "/runs"), headers=plates.of_a)).json()
  99. assert [r["id"] for r in runs["items"]] == [plates.first]
  100. async def test_a_user_of_b_sees_its_own_and_the_uploads(self, async_client, plates):
  101. body = (await async_client.get(_url(plates), headers=plates.of_b)).json()
  102. assert (body["printer_id"], body["status"]) == (plates.b, "failed")
  103. assert body["timelapse_path"] == f"{DIR}/b.mp4"
  104. assert body["photos"] == ["finish_b.jpg", "0a1b2c3d.jpg"]
  105. async def test_the_card_counts_every_prints_video(self, async_client, db_session, plates):
  106. # The archive holds plate 2's video only; plate 1's is on its print.
  107. listed = (await async_client.get("/api/v1/archives/", headers=plates.admin)).json()
  108. assert listed[0]["timelapse_count"] == 2
  109. assert (await async_client.get(_url(plates), headers=plates.admin)).json()["timelapse_count"] == 2
  110. # A scoped user counts their prints' videos only.
  111. assert (await async_client.get("/api/v1/archives/", headers=plates.of_a)).json()[0]["timelapse_count"] == 1
  112. assert (await async_client.get(_url(plates), headers=plates.of_b)).json()["timelapse_count"] == 1
  113. async def test_a_video_no_print_holds_counts_too(self, async_client, db_session, plates):
  114. archive = await _row(db_session, PrintArchive, plates.archive)
  115. archive.timelapse_path = f"{DIR}/hand.mp4"
  116. await db_session.commit()
  117. assert (await async_client.get(_url(plates), headers=plates.admin)).json()["timelapse_count"] == 3
  118. # Of the archive, not of a print of theirs: hidden from A's user, as
  119. # its path is.
  120. assert (await async_client.get(_url(plates), headers=plates.of_a)).json()["timelapse_count"] == 1
  121. async def test_no_video_counts_none(self, async_client, db_session, plates):
  122. db_session.expire_all()
  123. for model, row_id in (
  124. (PrintArchive, plates.archive),
  125. (PrintLogEntry, plates.first),
  126. (PrintLogEntry, plates.second),
  127. ):
  128. (await db_session.get(model, row_id)).timelapse_path = None
  129. await db_session.commit()
  130. assert (await async_client.get(_url(plates), headers=plates.admin)).json()["timelapse_count"] == 0
  131. async def test_a_printer_it_never_printed_on_sees_nothing(self, async_client, plates):
  132. assert (await async_client.get("/api/v1/archives/", headers=plates.of_c)).json() == []
  133. assert (await async_client.get(_url(plates), headers=plates.of_c)).status_code == 404
  134. async def test_a_plate_printing_now_counts(self, async_client, db_session, plates):
  135. db_session.add(
  136. PrintLogEntry(
  137. archive_id=plates.archive, printer_id=plates.c, status=RUN_OPEN, started_at=T2 + timedelta(hours=1)
  138. )
  139. )
  140. await db_session.commit()
  141. listed = (await async_client.get("/api/v1/archives/", headers=plates.of_c)).json()
  142. assert [a["id"] for a in listed] == [plates.archive]
  143. assert (await async_client.get(_url(plates), headers=plates.of_c)).status_code == 200
  144. async def test_search_finds_it_by_their_plate(self, async_client, plates):
  145. response = await async_client.get("/api/v1/archives/search?q=two", headers=plates.of_a)
  146. assert response.status_code == 200, response.text
  147. assert [a["id"] for a in response.json()] == [plates.archive]
  148. assert response.json()[0]["photos"] == ["finish_a.jpg"]
  149. async def test_the_admin_sees_it_whole(self, async_client, plates):
  150. body = (await async_client.get(_url(plates), headers=plates.admin)).json()
  151. assert (body["printer_id"], body["run_count"]) == (plates.b, 2)
  152. assert body["photos"] == ["finish_a.jpg", "finish_b.jpg", "0a1b2c3d.jpg"]
  153. class TestMedia:
  154. async def test_the_other_plates_video_is_not_theirs(self, async_client, plates):
  155. assert (await async_client.get(_url(plates, "/timelapse"), headers=plates.of_a)).status_code == 404
  156. assert (await async_client.get(_url(plates, "/timelapse"), headers=plates.of_b)).status_code == 200
  157. async def test_the_other_plates_photo_is_not_theirs(self, async_client, plates):
  158. assert (await async_client.get(_url(plates, "/photos/finish_b.jpg"), headers=plates.of_a)).status_code == 404
  159. assert (await async_client.get(_url(plates, "/photos/finish_a.jpg"), headers=plates.of_a)).status_code == 200
  160. assert (await async_client.get(_url(plates, "/photos/0a1b2c3d.jpg"), headers=plates.of_a)).status_code == 200
  161. async def test_nor_theirs_to_delete(self, async_client, db_session, plates):
  162. response = await async_client.delete(_url(plates, "/photos/finish_b.jpg"), headers=plates.of_a)
  163. assert response.status_code == 404
  164. assert "finish_b.jpg" in (await _row(db_session, PrintArchive, plates.archive)).photos
  165. async def test_a_finish_photo_on_no_runs_list_is_the_archives_latest(self, async_client, db_session, plates):
  166. """Logged before runs kept their own photos: the archive's, here B's."""
  167. archive = await _row(db_session, PrintArchive, plates.archive)
  168. archive.photos = [*archive.photos, "finish_old.jpg"]
  169. await db_session.commit()
  170. assert "finish_old.jpg" not in (await async_client.get(_url(plates), headers=plates.of_a)).json()["photos"]
  171. assert "finish_old.jpg" in (await async_client.get(_url(plates), headers=plates.of_b)).json()["photos"]
  172. async def test_a_plate_logged_before_runs_kept_media_shows_none_of_the_others(
  173. self, async_client, db_session, files, plates
  174. ):
  175. """Its run has no video or photo list of its own: the archive's are
  176. B's latest, and only the photo uploaded by hand is everyone's."""
  177. first = await _row(db_session, PrintLogEntry, plates.first)
  178. first.photos, first.timelapse_path = None, None
  179. await db_session.commit()
  180. body = (await async_client.get(_url(plates), headers=plates.of_a)).json()
  181. assert (body["run_id"], body["timelapse_path"]) == (plates.first, None)
  182. assert body["photos"] == ["0a1b2c3d.jpg"]
  183. assert (await async_client.get(_url(plates, "/photos/finish_a.jpg"), headers=plates.of_a)).status_code == 404
  184. class TestWholeArchive:
  185. async def test_deleting_it_is_refused(self, async_client, db_session, plates):
  186. response = await async_client.delete(_url(plates), headers=plates.of_a)
  187. assert response.status_code == 403, response.text
  188. assert (await _row(db_session, PrintArchive, plates.archive)).deleted_at is None
  189. async def test_also_to_the_user_of_its_own_printer(self, async_client, plates):
  190. """B is the archive's printer; plate 1 printed on A."""
  191. assert (await async_client.delete(_url(plates), headers=plates.of_b)).status_code == 403
  192. async def test_deleting_its_video_is_refused(self, async_client, db_session, plates):
  193. response = await async_client.delete(_url(plates, "/timelapse"), headers=plates.of_b)
  194. assert response.status_code == 403
  195. assert (await _row(db_session, PrintArchive, plates.archive)).timelapse_path == f"{DIR}/b.mp4"
  196. async def test_its_notes_are_theirs_to_edit(self, async_client, db_session, plates):
  197. response = await async_client.patch(_url(plates), headers=plates.of_a, json={"notes": "nice", "tags": "x"})
  198. assert response.status_code == 200, response.text
  199. assert (await _row(db_session, PrintArchive, plates.archive)).notes == "nice"
  200. async def test_its_cost_is_not(self, async_client, db_session, plates):
  201. response = await async_client.patch(_url(plates), headers=plates.of_a, json={"cost": 9.0})
  202. assert response.status_code == 403
  203. assert (await _row(db_session, PrintArchive, plates.archive)).cost is None
  204. async def test_nor_its_outcome_without_their_run(self, async_client, db_session, plates):
  205. response = await async_client.patch(_url(plates), headers=plates.of_a, json={"user_verdict": "good"})
  206. assert response.status_code == 403
  207. async def test_their_plates_outcome_is(self, async_client, db_session, plates):
  208. response = await async_client.patch(
  209. _url(plates), headers=plates.of_a, json={"user_verdict": "good", "run_id": plates.first}
  210. )
  211. assert response.status_code == 200, response.text
  212. assert (await _row(db_session, PrintLogEntry, plates.first)).user_verdict == "good"
  213. async def test_the_admin_may_delete_it(self, async_client, db_session, plates):
  214. response = await async_client.delete(_url(plates), headers=plates.admin)
  215. assert response.status_code == 200, response.text
  216. @pytest.mark.parametrize(
  217. ("method", "path", "kwargs"),
  218. [
  219. ("post", "/timelapse/select?filename=v.mp4", {}),
  220. ("post", "/timelapse/upload", {"files": {"file": ("v.mp4", b"x", "video/mp4")}}),
  221. ("post", "/timelapse/process", {"data": {"speed": "2"}}),
  222. ("delete", "/timelapse/job", {}),
  223. ("post", "/source", {"files": {"file": ("s.3mf", b"x", "application/octet-stream")}}),
  224. ("delete", "/source", {}),
  225. ("post", "/f3d", {"files": {"file": ("d.f3d", b"x", "application/octet-stream")}}),
  226. ("delete", "/f3d", {}),
  227. ],
  228. )
  229. async def test_its_file_and_video_are_not_theirs_to_change(self, async_client, plates, method, path, kwargs):
  230. """B's user: the archive's own printer, but plate 1 printed on A."""
  231. response = await getattr(async_client, method)(_url(plates, path), headers=plates.of_b, **kwargs)
  232. assert response.status_code == 403, response.text
  233. async def test_scanning_for_its_video_is_not_theirs(self, async_client, db_session, plates):
  234. """A scan attaches a video to the archive: only without one."""
  235. archive = await _row(db_session, PrintArchive, plates.archive)
  236. archive.timelapse_path = None
  237. await db_session.commit()
  238. response = await async_client.post(_url(plates, "/timelapse/scan"), headers=plates.of_b)
  239. assert response.status_code == 403, response.text
  240. class TestTheOtherPlatesVideo:
  241. @pytest.mark.parametrize(
  242. ("method", "path"),
  243. [
  244. ("get", "/timelapse/info"),
  245. ("get", "/timelapse/thumbnails"),
  246. ("post", "/media-download-token"),
  247. ],
  248. )
  249. async def test_is_missing_for_them(self, async_client, plates, method, path):
  250. response = await getattr(async_client, method)(_url(plates, path), headers=plates.of_a)
  251. assert response.status_code == 404, response.text
  252. async def test_is_not_offered_as_attached(self, async_client, plates):
  253. response = await async_client.get(_url(plates, "/printer-media"), headers=plates.of_a)
  254. assert response.status_code == 200, response.text
  255. assert response.json()["local_timelapse"] is None
  256. class TestReviewFixes:
  257. async def test_a_null_run_id_is_no_run(self, async_client, db_session, plates):
  258. response = await async_client.patch(
  259. _url(plates), headers=plates.of_a, json={"run_id": None, "status": "completed", "failure_reason": "x"}
  260. )
  261. assert response.status_code == 403, response.text
  262. assert (await _row(db_session, PrintArchive, plates.archive)).status == "failed"
  263. async def test_its_project_page_is_its_file(self, async_client, plates):
  264. response = await async_client.patch(_url(plates, "/project-page"), headers=plates.of_a, json={"title": "x"})
  265. assert response.status_code == 403, response.text
  266. async def test_compare_and_similar_leave_it_out(self, async_client, db_session, plates):
  267. """Both show each archive's latest print as it is: B's."""
  268. other = PrintArchive(
  269. printer_id=plates.a, filename="two.gcode.3mf", file_path=f"{DIR}/o.3mf", file_size=1, print_name="two"
  270. )
  271. db_session.add(other)
  272. await db_session.commit()
  273. other_id = other.id
  274. compare = await async_client.get(
  275. f"/api/v1/archives/compare?archive_ids={plates.archive},{other_id}", headers=plates.of_a
  276. )
  277. assert compare.status_code == 404, compare.text
  278. similar = (await async_client.get(f"/api/v1/archives/{other_id}/similar", headers=plates.of_a)).json()
  279. assert plates.archive not in [s["archive"]["id"] for s in similar]
  280. async def test_the_project_previews_leave_it_out(self, async_client, db_session, plates):
  281. from backend.app.models.project import Project
  282. project = Project(name="P")
  283. db_session.add(project)
  284. await db_session.flush()
  285. (await db_session.get(PrintArchive, plates.archive)).project_id = project.id
  286. await db_session.commit()
  287. projects = (await async_client.get("/api/v1/projects/", headers=plates.of_a)).json()
  288. assert [p.get("archives", []) for p in projects] == [[]]
  289. async def test_its_photo_responses_show_theirs(self, async_client, plates):
  290. uploaded = await async_client.post(
  291. _url(plates, "/photos"), headers=plates.of_a, files={"file": ("p.jpg", b"\xff\xd8\xff", "image/jpeg")}
  292. )
  293. assert uploaded.status_code == 200, uploaded.text
  294. assert "finish_b.jpg" not in uploaded.json()["photos"]
  295. deleted = await async_client.delete(_url(plates, "/photos/0a1b2c3d.jpg"), headers=plates.of_a)
  296. assert deleted.status_code == 200, deleted.text
  297. assert "finish_b.jpg" not in deleted.json()["photos"]
  298. async def test_printer_media_does_not_name_the_other_printer(self, async_client, plates):
  299. response = await async_client.get(_url(plates, "/printer-media"), headers=plates.of_a)
  300. assert response.json()["printer_id"] is None
  301. async def test_its_energy_and_time_are_their_prints(self, async_client, db_session, plates):
  302. archive = await _row(db_session, PrintArchive, plates.archive)
  303. archive.energy_kwh, archive.wear_cost = 5.0, 2.0
  304. archive.status, archive.completed_at, archive.print_time_seconds = "completed", T2 + timedelta(hours=1), 3600
  305. first = await db_session.get(PrintLogEntry, plates.first)
  306. first.energy_kwh, first.wear_cost = 1.0, 0.5
  307. await db_session.commit()
  308. body = (await async_client.get(_url(plates), headers=plates.of_a)).json()
  309. assert (body["energy_kwh"], body["wear_cost"]) == (1.0, 0.5)
  310. assert body["actual_time_seconds"] is None
  311. whole = (await async_client.get(_url(plates), headers=plates.admin)).json()
  312. assert (whole["energy_kwh"], whole["wear_cost"]) == (5.0, 2.0)
  313. async def test_queueing_it_is_theirs(db_session, plates, test_engine):
  314. """It prints on their printer (archive redesign)."""
  315. from unittest.mock import patch
  316. from fastapi import HTTPException
  317. from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
  318. from backend.app.api.routes.print_queue import _assert_can_queue_archive
  319. from backend.app.core.printer_scope import PrinterScope
  320. archive = await db_session.get(PrintArchive, plates.archive)
  321. maker = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
  322. with patch("backend.app.core.database.async_session", maker):
  323. await _assert_can_queue_archive(archive, None, PrinterScope(frozenset({plates.a})))
  324. with pytest.raises(HTTPException) as refused:
  325. await _assert_can_queue_archive(archive, None, PrinterScope(frozenset({plates.c})))
  326. assert refused.value.status_code == 404
  327. class TestLiveUpdates:
  328. async def test_reach_the_printers_its_prints_ran_on(self, db_session, plates, test_engine):
  329. from unittest.mock import patch
  330. from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
  331. from backend.app.core import websocket
  332. db_session.add(
  333. PrintLogEntry(
  334. archive_id=plates.archive, printer_id=plates.c, status=RUN_OPEN, started_at=T2 + timedelta(hours=1)
  335. )
  336. )
  337. await db_session.commit()
  338. maker = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
  339. with patch("backend.app.core.database.async_session", maker):
  340. assert await websocket._archive_printer_ids(plates.archive) == (plates.b, {plates.a, plates.b, plates.c})
  341. async def test_the_project_list_shows_their_plate(async_client, db_session, plates):
  342. from backend.app.models.project import Project
  343. project = Project(name="P")
  344. db_session.add(project)
  345. await db_session.flush()
  346. archive = await db_session.get(PrintArchive, plates.archive)
  347. archive.project_id = project.id
  348. project_id = project.id
  349. await db_session.commit()
  350. response = await async_client.get(f"/api/v1/projects/{project_id}/archives", headers=plates.of_a)
  351. assert response.status_code == 200, response.text
  352. assert [(a["id"], a["printer_id"], a["photos"]) for a in response.json()] == [
  353. (plates.archive, plates.a, ["finish_a.jpg"])
  354. ]
  355. class TestLatestRun:
  356. """The run the archive's per-print fields come from (archive redesign P3)."""
  357. async def test_is_the_latest_print(self, async_client, plates):
  358. listed = (await async_client.get("/api/v1/archives/", headers=plates.admin)).json()
  359. detail = (await async_client.get(_url(plates), headers=plates.admin)).json()
  360. assert [a["latest_run_id"] for a in listed] == [plates.second]
  361. assert detail["latest_run_id"] == plates.second
  362. async def test_is_the_callers_latest_print(self, async_client, plates):
  363. detail = (await async_client.get(_url(plates), headers=plates.of_a)).json()
  364. assert (detail["latest_run_id"], detail["run_id"]) == (plates.first, plates.first)
  365. async def test_the_per_print_fields_are_flagged_deprecated(async_client):
  366. schema = (await async_client.get("/openapi.json")).json()["components"]["schemas"]["ArchiveResponse"]["properties"]
  367. deprecated = {name for name, field in schema.items() if field.get("deprecated")}
  368. assert {"status", "started_at", "completed_at", "failure_reason", "user_verdict", "printer_id"} <= deprecated
  369. assert not {"filename", "print_name", "notes", "tags", "latest_run_id", "run_count"} & deprecated
  370. class TestLatestRunFollowsTheShownPrint:
  371. async def test_a_print_running_now_is_the_latest(self, async_client, db_session, plates):
  372. running = PrintLogEntry(
  373. archive_id=plates.archive, printer_id=plates.b, status=RUN_OPEN, started_at=T2 + timedelta(hours=1)
  374. )
  375. db_session.add(running)
  376. await db_session.commit()
  377. running_id = running.id
  378. detail = (await async_client.get(_url(plates), headers=plates.admin)).json()
  379. assert detail["latest_run_id"] == running_id
  380. async def test_their_print_when_the_latest_is_not_theirs(self, async_client, db_session, plates):
  381. """The archive names A (as after a merge) but its latest print ran on B."""
  382. archive = await _row(db_session, PrintArchive, plates.archive)
  383. archive.printer_id = plates.a
  384. await db_session.commit()
  385. body = (await async_client.get(_url(plates), headers=plates.of_a)).json()
  386. assert (body["run_id"], body["status"], body["latest_run_id"]) == (plates.first, "completed", plates.first)
  387. assert body["failure_reason"] is None
  388. async def test_the_favourite_toggle_answers_with_the_latest_print(self, async_client, plates):
  389. response = await async_client.post(_url(plates, "/favorite"), headers=plates.admin)
  390. assert response.status_code == 200, response.text
  391. assert response.json()["latest_run_id"] == plates.second
  392. class TestTheArchivesPrinterFollowsItsLatestPrint:
  393. async def test_after_its_latest_print_moves(self, db_session, plates):
  394. from backend.app.services.print_log import mirror_latest_run
  395. archive = await _row(db_session, PrintArchive, plates.archive)
  396. archive.printer_id = plates.a
  397. await db_session.commit()
  398. await mirror_latest_run(db_session, plates.archive)
  399. await db_session.commit()
  400. assert (await _row(db_session, PrintArchive, plates.archive)).printer_id == plates.b
  401. async def test_not_to_a_printer_since_deleted(self, db_session, plates):
  402. from backend.app.services.print_log import mirror_latest_run
  403. run = await _row(db_session, PrintLogEntry, plates.second)
  404. run.printer_id = 999999
  405. await db_session.commit()
  406. await mirror_latest_run(db_session, plates.archive)
  407. await db_session.commit()
  408. assert (await _row(db_session, PrintArchive, plates.archive)).printer_id == plates.b