test_printers_api.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. """Integration tests for Printers API endpoints.
  2. Tests the full request/response cycle for /api/v1/printers/ endpoints.
  3. """
  4. from unittest.mock import AsyncMock, MagicMock, patch
  5. import pytest
  6. from httpx import AsyncClient
  7. class TestPrintersAPI:
  8. """Integration tests for /api/v1/printers/ endpoints."""
  9. # ========================================================================
  10. # List endpoints
  11. # ========================================================================
  12. @pytest.mark.asyncio
  13. @pytest.mark.integration
  14. async def test_list_printers_empty(self, async_client: AsyncClient):
  15. """Verify empty list is returned when no printers exist."""
  16. response = await async_client.get("/api/v1/printers/")
  17. assert response.status_code == 200
  18. assert response.json() == []
  19. @pytest.mark.asyncio
  20. @pytest.mark.integration
  21. async def test_list_printers_with_data(self, async_client: AsyncClient, printer_factory, db_session):
  22. """Verify list returns existing printers."""
  23. await printer_factory(name="Test Printer")
  24. response = await async_client.get("/api/v1/printers/")
  25. assert response.status_code == 200
  26. data = response.json()
  27. assert len(data) >= 1
  28. assert any(p["name"] == "Test Printer" for p in data)
  29. # ========================================================================
  30. # Create endpoints
  31. # ========================================================================
  32. @pytest.mark.asyncio
  33. @pytest.mark.integration
  34. async def test_create_printer(self, async_client: AsyncClient):
  35. """Verify printer can be created."""
  36. data = {
  37. "name": "New Printer",
  38. "serial_number": "00M09A111111111",
  39. "ip_address": "192.168.1.100",
  40. "access_code": "12345678",
  41. "is_active": True,
  42. "model": "X1C",
  43. }
  44. response = await async_client.post("/api/v1/printers/", json=data)
  45. assert response.status_code == 200
  46. result = response.json()
  47. assert result["name"] == "New Printer"
  48. assert result["serial_number"] == "00M09A111111111"
  49. assert result["model"] == "X1C"
  50. @pytest.mark.asyncio
  51. @pytest.mark.integration
  52. async def test_create_printer_duplicate_serial(self, async_client: AsyncClient, printer_factory, db_session):
  53. """Verify duplicate serial number is rejected."""
  54. await printer_factory(serial_number="00M09A222222222")
  55. data = {
  56. "name": "Duplicate Printer",
  57. "serial_number": "00M09A222222222",
  58. "ip_address": "192.168.1.101",
  59. "access_code": "12345678",
  60. }
  61. response = await async_client.post("/api/v1/printers/", json=data)
  62. # Should fail due to duplicate serial
  63. assert response.status_code in [400, 409, 422, 500]
  64. # ========================================================================
  65. # Get single endpoint
  66. # ========================================================================
  67. @pytest.mark.asyncio
  68. @pytest.mark.integration
  69. async def test_get_printer(self, async_client: AsyncClient, printer_factory, db_session):
  70. """Verify single printer can be retrieved."""
  71. printer = await printer_factory(name="Get Test Printer")
  72. response = await async_client.get(f"/api/v1/printers/{printer.id}")
  73. assert response.status_code == 200
  74. result = response.json()
  75. assert result["id"] == printer.id
  76. assert result["name"] == "Get Test Printer"
  77. @pytest.mark.asyncio
  78. @pytest.mark.integration
  79. async def test_get_printer_not_found(self, async_client: AsyncClient):
  80. """Verify 404 for non-existent printer."""
  81. response = await async_client.get("/api/v1/printers/9999")
  82. assert response.status_code == 404
  83. # ========================================================================
  84. # Update endpoints
  85. # ========================================================================
  86. @pytest.mark.asyncio
  87. @pytest.mark.integration
  88. async def test_update_printer_name(self, async_client: AsyncClient, printer_factory, db_session):
  89. """Verify printer name can be updated."""
  90. printer = await printer_factory(name="Original Name")
  91. response = await async_client.patch(f"/api/v1/printers/{printer.id}", json={"name": "Updated Name"})
  92. assert response.status_code == 200
  93. assert response.json()["name"] == "Updated Name"
  94. @pytest.mark.asyncio
  95. @pytest.mark.integration
  96. async def test_update_printer_active_status(self, async_client: AsyncClient, printer_factory, db_session):
  97. """Verify printer active status can be updated."""
  98. printer = await printer_factory(is_active=True)
  99. response = await async_client.patch(f"/api/v1/printers/{printer.id}", json={"is_active": False})
  100. assert response.status_code == 200
  101. assert response.json()["is_active"] is False
  102. @pytest.mark.asyncio
  103. @pytest.mark.integration
  104. async def test_update_printer_auto_archive(self, async_client: AsyncClient, printer_factory, db_session):
  105. """Verify auto_archive setting can be updated."""
  106. printer = await printer_factory(auto_archive=True)
  107. response = await async_client.patch(f"/api/v1/printers/{printer.id}", json={"auto_archive": False})
  108. assert response.status_code == 200
  109. assert response.json()["auto_archive"] is False
  110. @pytest.mark.asyncio
  111. @pytest.mark.integration
  112. async def test_update_nonexistent_printer(self, async_client: AsyncClient):
  113. """Verify updating non-existent printer returns 404."""
  114. response = await async_client.patch("/api/v1/printers/9999", json={"name": "New Name"})
  115. assert response.status_code == 404
  116. # ========================================================================
  117. # Delete endpoints
  118. # ========================================================================
  119. @pytest.mark.asyncio
  120. @pytest.mark.integration
  121. async def test_delete_printer(self, async_client: AsyncClient, printer_factory, db_session):
  122. """Verify printer can be deleted."""
  123. printer = await printer_factory()
  124. printer_id = printer.id
  125. response = await async_client.delete(f"/api/v1/printers/{printer_id}")
  126. assert response.status_code == 200
  127. # Verify deleted
  128. response = await async_client.get(f"/api/v1/printers/{printer_id}")
  129. assert response.status_code == 404
  130. @pytest.mark.asyncio
  131. @pytest.mark.integration
  132. async def test_delete_nonexistent_printer(self, async_client: AsyncClient):
  133. """Verify deleting non-existent printer returns 404."""
  134. response = await async_client.delete("/api/v1/printers/9999")
  135. assert response.status_code == 404
  136. # ========================================================================
  137. # Status endpoint
  138. # ========================================================================
  139. @pytest.mark.asyncio
  140. @pytest.mark.integration
  141. async def test_get_printer_status(
  142. self, async_client: AsyncClient, printer_factory, mock_printer_manager, db_session
  143. ):
  144. """Verify printer status can be retrieved."""
  145. printer = await printer_factory()
  146. response = await async_client.get(f"/api/v1/printers/{printer.id}/status")
  147. assert response.status_code == 200
  148. result = response.json()
  149. assert "connected" in result
  150. assert "state" in result
  151. @pytest.mark.asyncio
  152. @pytest.mark.integration
  153. async def test_get_printer_status_not_found(self, async_client: AsyncClient):
  154. """Verify 404 for status of non-existent printer."""
  155. response = await async_client.get("/api/v1/printers/9999/status")
  156. assert response.status_code == 404
  157. # ========================================================================
  158. # Test connection endpoint
  159. # ========================================================================
  160. class TestPrinterDataIntegrity:
  161. """Tests for printer data integrity."""
  162. @pytest.mark.asyncio
  163. @pytest.mark.integration
  164. async def test_printer_stores_all_fields(self, async_client: AsyncClient, printer_factory, db_session):
  165. """Verify printer stores all fields correctly."""
  166. printer = await printer_factory(
  167. name="Full Test Printer",
  168. serial_number="00M09A444444444",
  169. ip_address="192.168.1.150",
  170. model="P1S",
  171. is_active=True,
  172. auto_archive=False,
  173. )
  174. response = await async_client.get(f"/api/v1/printers/{printer.id}")
  175. assert response.status_code == 200
  176. result = response.json()
  177. assert result["name"] == "Full Test Printer"
  178. assert result["serial_number"] == "00M09A444444444"
  179. assert result["ip_address"] == "192.168.1.150"
  180. assert result["model"] == "P1S"
  181. assert result["is_active"] is True
  182. assert result["auto_archive"] is False
  183. @pytest.mark.asyncio
  184. @pytest.mark.integration
  185. async def test_printer_update_persists(self, async_client: AsyncClient, printer_factory, db_session):
  186. """CRITICAL: Verify printer updates persist."""
  187. printer = await printer_factory(name="Original", is_active=True)
  188. # Update
  189. await async_client.patch(f"/api/v1/printers/{printer.id}", json={"name": "Updated", "is_active": False})
  190. # Verify persistence
  191. response = await async_client.get(f"/api/v1/printers/{printer.id}")
  192. result = response.json()
  193. assert result["name"] == "Updated"
  194. assert result["is_active"] is False
  195. # ========================================================================
  196. # Refresh status endpoint
  197. # ========================================================================
  198. @pytest.mark.asyncio
  199. @pytest.mark.integration
  200. async def test_refresh_status_not_found(self, async_client: AsyncClient):
  201. """Verify 404 for non-existent printer."""
  202. response = await async_client.post("/api/v1/printers/99999/refresh-status")
  203. assert response.status_code == 404
  204. @pytest.mark.asyncio
  205. @pytest.mark.integration
  206. async def test_refresh_status_not_connected(self, async_client: AsyncClient, printer_factory):
  207. """Verify 400 when printer is not connected."""
  208. printer = await printer_factory(name="Disconnected Printer")
  209. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  210. mock_pm.request_status_update.return_value = False
  211. response = await async_client.post(f"/api/v1/printers/{printer.id}/refresh-status")
  212. assert response.status_code == 400
  213. assert "not connected" in response.json()["detail"].lower()
  214. @pytest.mark.asyncio
  215. @pytest.mark.integration
  216. async def test_refresh_status_success(self, async_client: AsyncClient, printer_factory):
  217. """Verify successful refresh request."""
  218. printer = await printer_factory(name="Connected Printer")
  219. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  220. mock_pm.request_status_update.return_value = True
  221. response = await async_client.post(f"/api/v1/printers/{printer.id}/refresh-status")
  222. assert response.status_code == 200
  223. assert response.json()["status"] == "refresh_requested"
  224. mock_pm.request_status_update.assert_called_once_with(printer.id)
  225. # ========================================================================
  226. # Current print user endpoint (Issue #206)
  227. # ========================================================================
  228. @pytest.mark.asyncio
  229. @pytest.mark.integration
  230. async def test_get_current_print_user_not_found(self, async_client: AsyncClient):
  231. """Verify 404 for non-existent printer."""
  232. response = await async_client.get("/api/v1/printers/99999/current-print-user")
  233. assert response.status_code == 404
  234. @pytest.mark.asyncio
  235. @pytest.mark.integration
  236. async def test_get_current_print_user_returns_empty_when_no_user(self, async_client: AsyncClient, printer_factory):
  237. """Verify empty object returned when no user is tracked."""
  238. printer = await printer_factory(name="Test Printer")
  239. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  240. mock_pm.get_current_print_user.return_value = None
  241. response = await async_client.get(f"/api/v1/printers/{printer.id}/current-print-user")
  242. assert response.status_code == 200
  243. assert response.json() == {}
  244. @pytest.mark.asyncio
  245. @pytest.mark.integration
  246. async def test_get_current_print_user_returns_user_info(self, async_client: AsyncClient, printer_factory):
  247. """Verify user info is returned when tracked."""
  248. printer = await printer_factory(name="Test Printer")
  249. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  250. mock_pm.get_current_print_user.return_value = {"user_id": 42, "username": "testuser"}
  251. response = await async_client.get(f"/api/v1/printers/{printer.id}/current-print-user")
  252. assert response.status_code == 200
  253. result = response.json()
  254. assert result["user_id"] == 42
  255. assert result["username"] == "testuser"
  256. class TestPrintControlAPI:
  257. """Integration tests for print control endpoints (stop, pause, resume)."""
  258. # ========================================================================
  259. # Stop print endpoint
  260. # ========================================================================
  261. @pytest.mark.asyncio
  262. @pytest.mark.integration
  263. async def test_stop_print_not_found(self, async_client: AsyncClient):
  264. """Verify 404 for non-existent printer."""
  265. response = await async_client.post("/api/v1/printers/99999/print/stop")
  266. assert response.status_code == 404
  267. @pytest.mark.asyncio
  268. @pytest.mark.integration
  269. async def test_stop_print_not_connected(self, async_client: AsyncClient, printer_factory):
  270. """Verify error when printer is not connected."""
  271. printer = await printer_factory(name="Disconnected Printer")
  272. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  273. mock_pm.get_client.return_value = None
  274. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/stop")
  275. assert response.status_code == 400
  276. assert "not connected" in response.json()["detail"].lower()
  277. @pytest.mark.asyncio
  278. @pytest.mark.integration
  279. async def test_stop_print_success(self, async_client: AsyncClient, printer_factory):
  280. """Verify successful stop print request."""
  281. printer = await printer_factory(name="Printing Printer")
  282. mock_client = MagicMock()
  283. mock_client.stop_print.return_value = True
  284. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  285. mock_pm.get_client.return_value = mock_client
  286. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/stop")
  287. assert response.status_code == 200
  288. assert response.json()["success"] is True
  289. mock_client.stop_print.assert_called_once()
  290. # ========================================================================
  291. # Pause print endpoint
  292. # ========================================================================
  293. @pytest.mark.asyncio
  294. @pytest.mark.integration
  295. async def test_pause_print_not_found(self, async_client: AsyncClient):
  296. """Verify 404 for non-existent printer."""
  297. response = await async_client.post("/api/v1/printers/99999/print/pause")
  298. assert response.status_code == 404
  299. @pytest.mark.asyncio
  300. @pytest.mark.integration
  301. async def test_pause_print_not_connected(self, async_client: AsyncClient, printer_factory):
  302. """Verify error when printer is not connected."""
  303. printer = await printer_factory(name="Disconnected Printer")
  304. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  305. mock_pm.get_client.return_value = None
  306. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/pause")
  307. assert response.status_code == 400
  308. assert "not connected" in response.json()["detail"].lower()
  309. @pytest.mark.asyncio
  310. @pytest.mark.integration
  311. async def test_pause_print_success(self, async_client: AsyncClient, printer_factory):
  312. """Verify successful pause print request."""
  313. printer = await printer_factory(name="Printing Printer")
  314. mock_client = MagicMock()
  315. mock_client.pause_print.return_value = True
  316. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  317. mock_pm.get_client.return_value = mock_client
  318. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/pause")
  319. assert response.status_code == 200
  320. assert response.json()["success"] is True
  321. mock_client.pause_print.assert_called_once()
  322. # ========================================================================
  323. # Resume print endpoint
  324. # ========================================================================
  325. @pytest.mark.asyncio
  326. @pytest.mark.integration
  327. async def test_resume_print_not_found(self, async_client: AsyncClient):
  328. """Verify 404 for non-existent printer."""
  329. response = await async_client.post("/api/v1/printers/99999/print/resume")
  330. assert response.status_code == 404
  331. @pytest.mark.asyncio
  332. @pytest.mark.integration
  333. async def test_resume_print_not_connected(self, async_client: AsyncClient, printer_factory):
  334. """Verify error when printer is not connected."""
  335. printer = await printer_factory(name="Disconnected Printer")
  336. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  337. mock_pm.get_client.return_value = None
  338. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/resume")
  339. assert response.status_code == 400
  340. assert "not connected" in response.json()["detail"].lower()
  341. @pytest.mark.asyncio
  342. @pytest.mark.integration
  343. async def test_resume_print_success(self, async_client: AsyncClient, printer_factory):
  344. """Verify successful resume print request."""
  345. printer = await printer_factory(name="Paused Printer")
  346. mock_client = MagicMock()
  347. mock_client.resume_print.return_value = True
  348. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  349. mock_pm.get_client.return_value = mock_client
  350. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/resume")
  351. assert response.status_code == 200
  352. assert response.json()["success"] is True
  353. mock_client.resume_print.assert_called_once()
  354. class TestAMSRefreshAPI:
  355. """Integration tests for AMS slot refresh endpoint."""
  356. @pytest.mark.asyncio
  357. @pytest.mark.integration
  358. async def test_ams_refresh_not_found(self, async_client: AsyncClient):
  359. """Verify 404 for non-existent printer."""
  360. response = await async_client.post("/api/v1/printers/99999/ams/0/slot/0/refresh")
  361. assert response.status_code == 404
  362. @pytest.mark.asyncio
  363. @pytest.mark.integration
  364. async def test_ams_refresh_not_connected(self, async_client: AsyncClient, printer_factory):
  365. """Verify error when printer is not connected."""
  366. printer = await printer_factory(name="Disconnected Printer")
  367. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  368. mock_pm.get_client.return_value = None
  369. response = await async_client.post(f"/api/v1/printers/{printer.id}/ams/0/slot/0/refresh")
  370. assert response.status_code == 400
  371. assert "not connected" in response.json()["detail"].lower()
  372. @pytest.mark.asyncio
  373. @pytest.mark.integration
  374. async def test_ams_refresh_success(self, async_client: AsyncClient, printer_factory):
  375. """Verify successful AMS refresh request."""
  376. printer = await printer_factory(name="Printer with AMS")
  377. mock_client = MagicMock()
  378. mock_client.ams_refresh_tray.return_value = (True, "Refreshing AMS 0 tray 1")
  379. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  380. mock_pm.get_client.return_value = mock_client
  381. response = await async_client.post(f"/api/v1/printers/{printer.id}/ams/0/slot/1/refresh")
  382. assert response.status_code == 200
  383. result = response.json()
  384. assert result["success"] is True
  385. mock_client.ams_refresh_tray.assert_called_once_with(0, 1)
  386. @pytest.mark.asyncio
  387. @pytest.mark.integration
  388. async def test_ams_refresh_filament_loaded(self, async_client: AsyncClient, printer_factory):
  389. """Verify error when filament is loaded (can't refresh while loaded)."""
  390. printer = await printer_factory(name="Printer with AMS")
  391. mock_client = MagicMock()
  392. mock_client.ams_refresh_tray.return_value = (False, "Please unload filament first")
  393. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  394. mock_pm.get_client.return_value = mock_client
  395. response = await async_client.post(f"/api/v1/printers/{printer.id}/ams/0/slot/0/refresh")
  396. assert response.status_code == 400
  397. assert "unload" in response.json()["detail"].lower()
  398. class TestSkipObjectsAPI:
  399. """Integration tests for skip objects endpoints."""
  400. # ========================================================================
  401. # Get printable objects endpoint
  402. # ========================================================================
  403. @pytest.mark.asyncio
  404. @pytest.mark.integration
  405. async def test_get_objects_not_found(self, async_client: AsyncClient):
  406. """Verify 404 for non-existent printer."""
  407. response = await async_client.get("/api/v1/printers/99999/print/objects")
  408. assert response.status_code == 404
  409. @pytest.mark.asyncio
  410. @pytest.mark.integration
  411. async def test_get_objects_not_connected(self, async_client: AsyncClient, printer_factory):
  412. """Verify error when printer is not connected."""
  413. printer = await printer_factory(name="Disconnected Printer")
  414. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  415. mock_pm.get_client.return_value = None
  416. response = await async_client.get(f"/api/v1/printers/{printer.id}/print/objects")
  417. assert response.status_code == 400
  418. assert "not connected" in response.json()["detail"].lower()
  419. @pytest.mark.asyncio
  420. @pytest.mark.integration
  421. async def test_get_objects_empty(self, async_client: AsyncClient, printer_factory):
  422. """Verify empty objects list when no print is active."""
  423. printer = await printer_factory(name="Idle Printer")
  424. mock_client = MagicMock()
  425. mock_client.state.printable_objects = {}
  426. mock_client.state.skipped_objects = []
  427. mock_client.state.state = "IDLE"
  428. mock_client.state.subtask_name = None # Prevent FTP download attempt
  429. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  430. mock_pm.get_client.return_value = mock_client
  431. response = await async_client.get(f"/api/v1/printers/{printer.id}/print/objects")
  432. assert response.status_code == 200
  433. result = response.json()
  434. assert result["objects"] == []
  435. assert result["total"] == 0
  436. assert result["skipped_count"] == 0
  437. assert result["is_printing"] is False
  438. @pytest.mark.asyncio
  439. @pytest.mark.integration
  440. async def test_get_objects_with_data(self, async_client: AsyncClient, printer_factory):
  441. """Verify objects list when print is active."""
  442. printer = await printer_factory(name="Printing Printer")
  443. mock_client = MagicMock()
  444. mock_client.state.printable_objects = {100: "Part A", 200: "Part B", 300: "Part C"}
  445. mock_client.state.skipped_objects = [200]
  446. mock_client.state.state = "RUNNING"
  447. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  448. mock_pm.get_client.return_value = mock_client
  449. response = await async_client.get(f"/api/v1/printers/{printer.id}/print/objects")
  450. assert response.status_code == 200
  451. result = response.json()
  452. assert result["total"] == 3
  453. assert result["skipped_count"] == 1
  454. assert result["is_printing"] is True
  455. # Check objects have correct structure
  456. objects_by_id = {obj["id"]: obj for obj in result["objects"]}
  457. assert objects_by_id[100]["name"] == "Part A"
  458. assert objects_by_id[100]["skipped"] is False
  459. assert objects_by_id[200]["name"] == "Part B"
  460. assert objects_by_id[200]["skipped"] is True
  461. assert objects_by_id[300]["name"] == "Part C"
  462. assert objects_by_id[300]["skipped"] is False
  463. # ========================================================================
  464. # Skip objects endpoint
  465. # ========================================================================
  466. @pytest.mark.asyncio
  467. @pytest.mark.integration
  468. async def test_get_objects_with_positions(self, async_client: AsyncClient, printer_factory):
  469. """Verify objects list includes position data when available."""
  470. printer = await printer_factory(name="Printing Printer")
  471. # New format with position data
  472. mock_client = MagicMock()
  473. mock_client.state.printable_objects = {
  474. 100: {"name": "Part A", "x": 50.0, "y": 100.0},
  475. 200: {"name": "Part B", "x": 150.0, "y": 100.0},
  476. }
  477. mock_client.state.skipped_objects = []
  478. mock_client.state.state = "RUNNING"
  479. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  480. mock_pm.get_client.return_value = mock_client
  481. response = await async_client.get(f"/api/v1/printers/{printer.id}/print/objects")
  482. assert response.status_code == 200
  483. result = response.json()
  484. assert result["total"] == 2
  485. # Check objects have position data
  486. objects_by_id = {obj["id"]: obj for obj in result["objects"]}
  487. assert objects_by_id[100]["name"] == "Part A"
  488. assert objects_by_id[100]["x"] == 50.0
  489. assert objects_by_id[100]["y"] == 100.0
  490. assert objects_by_id[200]["name"] == "Part B"
  491. assert objects_by_id[200]["x"] == 150.0
  492. assert objects_by_id[200]["y"] == 100.0
  493. # ========================================================================
  494. # Skip objects endpoint
  495. # ========================================================================
  496. @pytest.mark.asyncio
  497. @pytest.mark.integration
  498. async def test_skip_objects_not_found(self, async_client: AsyncClient):
  499. """Verify 404 for non-existent printer."""
  500. response = await async_client.post("/api/v1/printers/99999/print/skip-objects", json=[100])
  501. assert response.status_code == 404
  502. @pytest.mark.asyncio
  503. @pytest.mark.integration
  504. async def test_skip_objects_not_connected(self, async_client: AsyncClient, printer_factory):
  505. """Verify error when printer is not connected."""
  506. printer = await printer_factory(name="Disconnected Printer")
  507. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  508. mock_pm.get_client.return_value = None
  509. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/skip-objects", json=[100])
  510. assert response.status_code == 400
  511. assert "not connected" in response.json()["detail"].lower()
  512. @pytest.mark.asyncio
  513. @pytest.mark.integration
  514. async def test_skip_objects_empty_list(self, async_client: AsyncClient, printer_factory):
  515. """Verify error when no object IDs provided."""
  516. printer = await printer_factory(name="Printing Printer")
  517. mock_client = MagicMock()
  518. mock_client.state.printable_objects = {100: "Part A"}
  519. mock_client.state.skipped_objects = []
  520. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  521. mock_pm.get_client.return_value = mock_client
  522. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/skip-objects", json=[])
  523. assert response.status_code == 400
  524. assert "no object" in response.json()["detail"].lower()
  525. @pytest.mark.asyncio
  526. @pytest.mark.integration
  527. async def test_skip_objects_invalid_id(self, async_client: AsyncClient, printer_factory):
  528. """Verify error when object ID doesn't exist."""
  529. printer = await printer_factory(name="Printing Printer")
  530. mock_client = MagicMock()
  531. mock_client.state.printable_objects = {100: "Part A"}
  532. mock_client.state.skipped_objects = []
  533. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  534. mock_pm.get_client.return_value = mock_client
  535. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/skip-objects", json=[999])
  536. assert response.status_code == 400
  537. assert "invalid" in response.json()["detail"].lower()
  538. @pytest.mark.asyncio
  539. @pytest.mark.integration
  540. async def test_skip_objects_success(self, async_client: AsyncClient, printer_factory):
  541. """Verify successful skip objects request."""
  542. printer = await printer_factory(name="Printing Printer")
  543. mock_client = MagicMock()
  544. mock_client.state.printable_objects = {100: "Part A", 200: "Part B"}
  545. mock_client.state.skipped_objects = []
  546. mock_client.skip_objects.return_value = True
  547. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  548. mock_pm.get_client.return_value = mock_client
  549. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/skip-objects", json=[100])
  550. assert response.status_code == 200
  551. result = response.json()
  552. assert result["success"] is True
  553. assert 100 in result["skipped_objects"]
  554. mock_client.skip_objects.assert_called_once_with([100])
  555. @pytest.mark.asyncio
  556. @pytest.mark.integration
  557. async def test_skip_objects_multiple(self, async_client: AsyncClient, printer_factory):
  558. """Verify skipping multiple objects at once."""
  559. printer = await printer_factory(name="Printing Printer")
  560. mock_client = MagicMock()
  561. mock_client.state.printable_objects = {100: "Part A", 200: "Part B", 300: "Part C"}
  562. mock_client.state.skipped_objects = []
  563. mock_client.skip_objects.return_value = True
  564. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  565. mock_pm.get_client.return_value = mock_client
  566. response = await async_client.post(f"/api/v1/printers/{printer.id}/print/skip-objects", json=[100, 200])
  567. assert response.status_code == 200
  568. result = response.json()
  569. assert result["success"] is True
  570. assert 100 in result["skipped_objects"]
  571. assert 200 in result["skipped_objects"]
  572. mock_client.skip_objects.assert_called_once_with([100, 200])
  573. class TestChamberLightAPI:
  574. """Integration tests for chamber light control endpoint."""
  575. @pytest.mark.asyncio
  576. @pytest.mark.integration
  577. async def test_chamber_light_not_found(self, async_client: AsyncClient):
  578. """Verify 404 for non-existent printer."""
  579. response = await async_client.post("/api/v1/printers/99999/chamber-light?on=true")
  580. assert response.status_code == 404
  581. @pytest.mark.asyncio
  582. @pytest.mark.integration
  583. async def test_chamber_light_not_connected(self, async_client: AsyncClient, printer_factory):
  584. """Verify error when printer is not connected."""
  585. printer = await printer_factory(name="Disconnected Printer")
  586. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  587. mock_pm.get_client.return_value = None
  588. response = await async_client.post(f"/api/v1/printers/{printer.id}/chamber-light?on=true")
  589. assert response.status_code == 400
  590. assert "not connected" in response.json()["detail"].lower()
  591. @pytest.mark.asyncio
  592. @pytest.mark.integration
  593. async def test_chamber_light_on_success(self, async_client: AsyncClient, printer_factory):
  594. """Verify successful chamber light on request."""
  595. printer = await printer_factory(name="Test Printer")
  596. mock_client = MagicMock()
  597. mock_client.set_chamber_light.return_value = True
  598. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  599. mock_pm.get_client.return_value = mock_client
  600. response = await async_client.post(f"/api/v1/printers/{printer.id}/chamber-light?on=true")
  601. assert response.status_code == 200
  602. result = response.json()
  603. assert result["success"] is True
  604. assert "on" in result["message"].lower()
  605. mock_client.set_chamber_light.assert_called_once_with(True)
  606. @pytest.mark.asyncio
  607. @pytest.mark.integration
  608. async def test_chamber_light_off_success(self, async_client: AsyncClient, printer_factory):
  609. """Verify successful chamber light off request."""
  610. printer = await printer_factory(name="Test Printer")
  611. mock_client = MagicMock()
  612. mock_client.set_chamber_light.return_value = True
  613. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  614. mock_pm.get_client.return_value = mock_client
  615. response = await async_client.post(f"/api/v1/printers/{printer.id}/chamber-light?on=false")
  616. assert response.status_code == 200
  617. result = response.json()
  618. assert result["success"] is True
  619. assert "off" in result["message"].lower()
  620. mock_client.set_chamber_light.assert_called_once_with(False)
  621. @pytest.mark.asyncio
  622. @pytest.mark.integration
  623. async def test_chamber_light_failure(self, async_client: AsyncClient, printer_factory):
  624. """Verify error handling when chamber light control fails."""
  625. printer = await printer_factory(name="Test Printer")
  626. mock_client = MagicMock()
  627. mock_client.set_chamber_light.return_value = False
  628. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  629. mock_pm.get_client.return_value = mock_client
  630. response = await async_client.post(f"/api/v1/printers/{printer.id}/chamber-light?on=true")
  631. assert response.status_code == 500
  632. assert "failed" in response.json()["detail"].lower()