test_archives_api.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. """Integration tests for Archives API endpoints.
  2. Tests the full request/response cycle for /api/v1/archives/ endpoints.
  3. """
  4. import pytest
  5. from httpx import AsyncClient
  6. class TestArchivesAPI:
  7. """Integration tests for /api/v1/archives/ endpoints."""
  8. # ========================================================================
  9. # List endpoints
  10. # ========================================================================
  11. @pytest.mark.asyncio
  12. @pytest.mark.integration
  13. async def test_list_archives_empty(self, async_client: AsyncClient):
  14. """Verify empty list is returned when no archives exist."""
  15. response = await async_client.get("/api/v1/archives/")
  16. assert response.status_code == 200
  17. data = response.json()
  18. assert isinstance(data, list)
  19. assert len(data) == 0
  20. @pytest.mark.asyncio
  21. @pytest.mark.integration
  22. async def test_list_archives_with_data(
  23. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  24. ):
  25. """Verify list returns existing archives."""
  26. printer = await printer_factory()
  27. await archive_factory(printer.id, print_name="Test Archive")
  28. response = await async_client.get("/api/v1/archives/")
  29. assert response.status_code == 200
  30. data = response.json()
  31. assert isinstance(data, list)
  32. assert len(data) >= 1
  33. assert any(a["print_name"] == "Test Archive" for a in data)
  34. @pytest.mark.asyncio
  35. @pytest.mark.integration
  36. async def test_list_archives_pagination(
  37. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  38. ):
  39. """Verify pagination works correctly."""
  40. printer = await printer_factory()
  41. # Create 5 archives
  42. for i in range(5):
  43. await archive_factory(printer.id, print_name=f"Archive {i}")
  44. # Get first page with limit 2
  45. response = await async_client.get("/api/v1/archives/?limit=2&offset=0")
  46. assert response.status_code == 200
  47. data = response.json()
  48. assert isinstance(data, list)
  49. assert len(data) == 2
  50. @pytest.mark.asyncio
  51. @pytest.mark.integration
  52. async def test_list_archives_filter_by_printer(
  53. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  54. ):
  55. """Verify filtering by printer_id works."""
  56. printer1 = await printer_factory(name="Printer 1", serial_number="00M09A000000001")
  57. printer2 = await printer_factory(name="Printer 2", serial_number="00M09A000000002")
  58. await archive_factory(printer1.id, print_name="Printer 1 Archive")
  59. await archive_factory(printer2.id, print_name="Printer 2 Archive")
  60. response = await async_client.get(f"/api/v1/archives/?printer_id={printer1.id}")
  61. assert response.status_code == 200
  62. data = response.json()
  63. assert all(a["printer_id"] == printer1.id for a in data)
  64. # ========================================================================
  65. # Get single endpoint
  66. # ========================================================================
  67. @pytest.mark.asyncio
  68. @pytest.mark.integration
  69. async def test_get_archive(self, async_client: AsyncClient, archive_factory, printer_factory, db_session):
  70. """Verify single archive can be retrieved."""
  71. printer = await printer_factory()
  72. archive = await archive_factory(printer.id, print_name="Get Test Archive")
  73. response = await async_client.get(f"/api/v1/archives/{archive.id}")
  74. assert response.status_code == 200
  75. result = response.json()
  76. assert result["id"] == archive.id
  77. assert result["print_name"] == "Get Test Archive"
  78. @pytest.mark.asyncio
  79. @pytest.mark.integration
  80. async def test_get_archive_not_found(self, async_client: AsyncClient):
  81. """Verify 404 for non-existent archive."""
  82. response = await async_client.get("/api/v1/archives/9999")
  83. assert response.status_code == 404
  84. # ========================================================================
  85. # Update endpoints
  86. # ========================================================================
  87. @pytest.mark.asyncio
  88. @pytest.mark.integration
  89. async def test_update_archive_name(self, async_client: AsyncClient, archive_factory, printer_factory, db_session):
  90. """Verify archive name can be updated."""
  91. printer = await printer_factory()
  92. archive = await archive_factory(printer.id, print_name="Original Name")
  93. response = await async_client.patch(f"/api/v1/archives/{archive.id}", json={"print_name": "Updated Name"})
  94. assert response.status_code == 200
  95. assert response.json()["print_name"] == "Updated Name"
  96. @pytest.mark.asyncio
  97. @pytest.mark.integration
  98. async def test_update_archive_notes(self, async_client: AsyncClient, archive_factory, printer_factory, db_session):
  99. """Verify archive notes can be updated."""
  100. printer = await printer_factory()
  101. archive = await archive_factory(printer.id)
  102. response = await async_client.patch(f"/api/v1/archives/{archive.id}", json={"notes": "Great print!"})
  103. assert response.status_code == 200
  104. assert response.json()["notes"] == "Great print!"
  105. @pytest.mark.asyncio
  106. @pytest.mark.integration
  107. async def test_update_archive_favorite(
  108. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  109. ):
  110. """Verify archive favorite status can be updated."""
  111. printer = await printer_factory()
  112. archive = await archive_factory(printer.id)
  113. response = await async_client.patch(f"/api/v1/archives/{archive.id}", json={"is_favorite": True})
  114. assert response.status_code == 200
  115. assert response.json()["is_favorite"] is True
  116. # ========================================================================
  117. # Delete endpoints
  118. # ========================================================================
  119. @pytest.mark.asyncio
  120. @pytest.mark.integration
  121. async def test_delete_archive(self, async_client: AsyncClient, archive_factory, printer_factory, db_session):
  122. """Verify archive can be deleted."""
  123. printer = await printer_factory()
  124. archive = await archive_factory(printer.id)
  125. archive_id = archive.id
  126. response = await async_client.delete(f"/api/v1/archives/{archive_id}")
  127. assert response.status_code == 200
  128. # Verify deleted
  129. response = await async_client.get(f"/api/v1/archives/{archive_id}")
  130. assert response.status_code == 404
  131. @pytest.mark.asyncio
  132. @pytest.mark.integration
  133. async def test_delete_nonexistent_archive(self, async_client: AsyncClient):
  134. """Verify deleting non-existent archive returns 404."""
  135. response = await async_client.delete("/api/v1/archives/9999")
  136. assert response.status_code == 404
  137. # ========================================================================
  138. # Statistics endpoints
  139. # ========================================================================
  140. @pytest.mark.asyncio
  141. @pytest.mark.integration
  142. async def test_get_archive_stats(self, async_client: AsyncClient, archive_factory, printer_factory, db_session):
  143. """Verify archive statistics can be retrieved."""
  144. printer = await printer_factory()
  145. await archive_factory(
  146. printer.id,
  147. status="completed",
  148. print_time_seconds=3600,
  149. filament_used_grams=50.0,
  150. )
  151. await archive_factory(
  152. printer.id,
  153. status="completed",
  154. print_time_seconds=7200,
  155. filament_used_grams=100.0,
  156. )
  157. response = await async_client.get("/api/v1/archives/stats")
  158. assert response.status_code == 200
  159. result = response.json()
  160. # Check for actual stats fields
  161. assert "total_prints" in result
  162. assert "successful_prints" in result
  163. class TestArchiveDataIntegrity:
  164. """Tests for archive data integrity."""
  165. @pytest.mark.asyncio
  166. @pytest.mark.integration
  167. async def test_archive_linked_to_printer(
  168. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  169. ):
  170. """Verify archive is properly linked to printer."""
  171. printer = await printer_factory(name="My Printer")
  172. archive = await archive_factory(printer.id)
  173. response = await async_client.get(f"/api/v1/archives/{archive.id}")
  174. assert response.status_code == 200
  175. result = response.json()
  176. assert result["printer_id"] == printer.id
  177. @pytest.mark.asyncio
  178. @pytest.mark.integration
  179. async def test_archive_stores_print_data(
  180. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  181. ):
  182. """Verify archive stores all print data correctly."""
  183. printer = await printer_factory()
  184. archive = await archive_factory(
  185. printer.id,
  186. print_name="Test Print",
  187. filename="test.3mf",
  188. status="completed",
  189. filament_type="PLA",
  190. filament_used_grams=75.5,
  191. print_time_seconds=5400,
  192. )
  193. response = await async_client.get(f"/api/v1/archives/{archive.id}")
  194. assert response.status_code == 200
  195. result = response.json()
  196. assert result["print_name"] == "Test Print"
  197. assert result["filename"] == "test.3mf"
  198. assert result["status"] == "completed"
  199. assert result["filament_type"] == "PLA"
  200. assert result["filament_used_grams"] == 75.5
  201. assert result["print_time_seconds"] == 5400
  202. @pytest.mark.asyncio
  203. @pytest.mark.integration
  204. async def test_archive_update_persists(
  205. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  206. ):
  207. """CRITICAL: Verify archive updates persist."""
  208. printer = await printer_factory()
  209. archive = await archive_factory(printer.id, notes="Original notes")
  210. # Update
  211. await async_client.patch(f"/api/v1/archives/{archive.id}", json={"notes": "Updated notes", "is_favorite": True})
  212. # Verify persistence
  213. response = await async_client.get(f"/api/v1/archives/{archive.id}")
  214. result = response.json()
  215. assert result["notes"] == "Updated notes"
  216. assert result["is_favorite"] is True
  217. class TestArchiveF3DEndpoints:
  218. """Tests for F3D (Fusion 360 design file) attachment endpoints."""
  219. @pytest.mark.asyncio
  220. @pytest.mark.integration
  221. async def test_archive_response_includes_f3d_path(
  222. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  223. ):
  224. """Verify f3d_path is included in archive response."""
  225. printer = await printer_factory()
  226. archive = await archive_factory(printer.id, f3d_path="archives/test/design.f3d")
  227. response = await async_client.get(f"/api/v1/archives/{archive.id}")
  228. assert response.status_code == 200
  229. result = response.json()
  230. assert "f3d_path" in result
  231. assert result["f3d_path"] == "archives/test/design.f3d"
  232. @pytest.mark.asyncio
  233. @pytest.mark.integration
  234. async def test_archive_response_f3d_path_null_when_not_set(
  235. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  236. ):
  237. """Verify f3d_path is null when no F3D file attached."""
  238. printer = await printer_factory()
  239. archive = await archive_factory(printer.id)
  240. response = await async_client.get(f"/api/v1/archives/{archive.id}")
  241. assert response.status_code == 200
  242. result = response.json()
  243. assert "f3d_path" in result
  244. assert result["f3d_path"] is None
  245. @pytest.mark.asyncio
  246. @pytest.mark.integration
  247. async def test_upload_f3d_to_nonexistent_archive(self, async_client: AsyncClient):
  248. """Verify 404 when uploading F3D to non-existent archive."""
  249. # Create a minimal file-like upload
  250. files = {"file": ("design.f3d", b"fake f3d content", "application/octet-stream")}
  251. response = await async_client.post("/api/v1/archives/9999/f3d", files=files)
  252. assert response.status_code == 404
  253. @pytest.mark.asyncio
  254. @pytest.mark.integration
  255. async def test_download_f3d_not_found_when_no_file(
  256. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  257. ):
  258. """Verify 404 when downloading F3D from archive without F3D file."""
  259. printer = await printer_factory()
  260. archive = await archive_factory(printer.id)
  261. response = await async_client.get(f"/api/v1/archives/{archive.id}/f3d")
  262. assert response.status_code == 404
  263. @pytest.mark.asyncio
  264. @pytest.mark.integration
  265. async def test_download_f3d_nonexistent_archive(self, async_client: AsyncClient):
  266. """Verify 404 when downloading F3D from non-existent archive."""
  267. response = await async_client.get("/api/v1/archives/9999/f3d")
  268. assert response.status_code == 404
  269. @pytest.mark.asyncio
  270. @pytest.mark.integration
  271. async def test_delete_f3d_nonexistent_archive(self, async_client: AsyncClient):
  272. """Verify 404 when deleting F3D from non-existent archive."""
  273. response = await async_client.delete("/api/v1/archives/9999/f3d")
  274. assert response.status_code == 404
  275. @pytest.mark.asyncio
  276. @pytest.mark.integration
  277. async def test_delete_f3d_when_no_file(
  278. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  279. ):
  280. """Verify 404 when deleting F3D from archive without F3D file."""
  281. printer = await printer_factory()
  282. archive = await archive_factory(printer.id)
  283. response = await async_client.delete(f"/api/v1/archives/{archive.id}/f3d")
  284. assert response.status_code == 404
  285. @pytest.mark.asyncio
  286. @pytest.mark.integration
  287. async def test_list_archives_includes_f3d_path(
  288. self, async_client: AsyncClient, archive_factory, printer_factory, db_session
  289. ):
  290. """Verify f3d_path is included in archive list responses."""
  291. printer = await printer_factory()
  292. await archive_factory(printer.id, print_name="With F3D", f3d_path="archives/test/design.f3d")
  293. await archive_factory(printer.id, print_name="Without F3D")
  294. response = await async_client.get("/api/v1/archives/")
  295. assert response.status_code == 200
  296. data = response.json()
  297. assert len(data) >= 2
  298. with_f3d = next((a for a in data if a["print_name"] == "With F3D"), None)
  299. without_f3d = next((a for a in data if a["print_name"] == "Without F3D"), None)
  300. assert with_f3d is not None
  301. assert with_f3d["f3d_path"] == "archives/test/design.f3d"
  302. assert without_f3d is not None
  303. assert without_f3d["f3d_path"] is None
  304. # ========================================================================
  305. # Multi-Plate 3MF endpoints (Issue #93)
  306. # ========================================================================
  307. @pytest.mark.asyncio
  308. @pytest.mark.integration
  309. async def test_get_archive_plates_not_found(self, async_client: AsyncClient):
  310. """Verify 404 when fetching plates for non-existent archive."""
  311. response = await async_client.get("/api/v1/archives/999999/plates")
  312. assert response.status_code == 404
  313. @pytest.mark.asyncio
  314. @pytest.mark.integration
  315. async def test_get_plate_thumbnail_not_found(self, async_client: AsyncClient):
  316. """Verify 404 when fetching plate thumbnail for non-existent archive."""
  317. response = await async_client.get("/api/v1/archives/999999/plate-thumbnail/1")
  318. assert response.status_code == 404
  319. @pytest.mark.asyncio
  320. @pytest.mark.integration
  321. async def test_filament_requirements_not_found(self, async_client: AsyncClient):
  322. """Verify filament-requirements returns 404 for non-existent archive."""
  323. response = await async_client.get("/api/v1/archives/999999/filament-requirements")
  324. assert response.status_code == 404
  325. @pytest.mark.asyncio
  326. @pytest.mark.integration
  327. async def test_filament_requirements_with_plate_id_not_found(self, async_client: AsyncClient):
  328. """Verify filament-requirements with plate_id returns 404 for non-existent archive."""
  329. response = await async_client.get("/api/v1/archives/999999/filament-requirements?plate_id=1")
  330. assert response.status_code == 404