test_maintenance_api.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. """Integration tests for Maintenance API endpoints."""
  2. import pytest
  3. from httpx import AsyncClient
  4. class TestMaintenanceTypesAPI:
  5. """Integration tests for /api/v1/maintenance/types endpoints."""
  6. @pytest.mark.asyncio
  7. @pytest.mark.integration
  8. async def test_list_maintenance_types(self, async_client: AsyncClient):
  9. """Verify maintenance types list returns data with defaults."""
  10. response = await async_client.get("/api/v1/maintenance/types")
  11. assert response.status_code == 200
  12. data = response.json()
  13. assert isinstance(data, list)
  14. # Should have default system types
  15. assert len(data) >= 1
  16. @pytest.mark.asyncio
  17. @pytest.mark.integration
  18. async def test_list_includes_system_types(self, async_client: AsyncClient):
  19. """Verify default system types are created."""
  20. response = await async_client.get("/api/v1/maintenance/types")
  21. assert response.status_code == 200
  22. data = response.json()
  23. names = [t["name"] for t in data]
  24. # Check for some default types
  25. assert "Lubricate Linear Rails" in names or len(data) > 0
  26. @pytest.mark.asyncio
  27. @pytest.mark.integration
  28. async def test_create_custom_maintenance_type(self, async_client: AsyncClient):
  29. """Verify custom maintenance type can be created."""
  30. data = {
  31. "name": "Custom Test Task",
  32. "description": "Test description",
  33. "default_interval_hours": 200.0,
  34. "interval_type": "hours",
  35. "icon": "Wrench",
  36. }
  37. response = await async_client.post("/api/v1/maintenance/types", json=data)
  38. assert response.status_code == 200
  39. result = response.json()
  40. assert result["name"] == "Custom Test Task"
  41. assert result["is_system"] is False
  42. @pytest.mark.asyncio
  43. @pytest.mark.integration
  44. async def test_create_custom_type_persists_wiki_url(self, async_client: AsyncClient):
  45. """#1596: pre-fix, the POST handler hard-coded every constructor field
  46. by name and silently dropped `wiki_url`. The schema accepted the value,
  47. the response echoed `null`, and the row landed without it. Pin the
  48. contract so the constructor doesn't drift again."""
  49. data = {
  50. "name": "Wiki URL Persistence Test",
  51. "default_interval_hours": 50.0,
  52. "interval_type": "hours",
  53. "wiki_url": "https://wiki.example.com/lubrication",
  54. }
  55. response = await async_client.post("/api/v1/maintenance/types", json=data)
  56. assert response.status_code == 200
  57. assert response.json()["wiki_url"] == "https://wiki.example.com/lubrication"
  58. # Verify it persists through a separate GET round-trip — the POST
  59. # response could have echoed the request body without committing.
  60. list_response = await async_client.get("/api/v1/maintenance/types")
  61. assert list_response.status_code == 200
  62. matching = [t for t in list_response.json() if t["name"] == data["name"]]
  63. assert len(matching) == 1
  64. assert matching[0]["wiki_url"] == "https://wiki.example.com/lubrication"
  65. @pytest.mark.asyncio
  66. @pytest.mark.integration
  67. async def test_update_maintenance_type(self, async_client: AsyncClient):
  68. """Verify maintenance type can be updated."""
  69. # First create a custom type
  70. create_data = {
  71. "name": "Update Test",
  72. "description": "Original",
  73. "default_interval_hours": 100.0,
  74. }
  75. create_response = await async_client.post("/api/v1/maintenance/types", json=create_data)
  76. assert create_response.status_code == 200
  77. type_id = create_response.json()["id"]
  78. # Update it
  79. update_data = {"description": "Updated description"}
  80. response = await async_client.patch(f"/api/v1/maintenance/types/{type_id}", json=update_data)
  81. assert response.status_code == 200
  82. assert response.json()["description"] == "Updated description"
  83. @pytest.mark.asyncio
  84. @pytest.mark.integration
  85. async def test_delete_custom_maintenance_type(self, async_client: AsyncClient):
  86. """Verify custom maintenance type can be deleted."""
  87. # Create a custom type
  88. create_data = {
  89. "name": "Delete Test",
  90. "description": "To be deleted",
  91. "default_interval_hours": 50.0,
  92. }
  93. create_response = await async_client.post("/api/v1/maintenance/types", json=create_data)
  94. type_id = create_response.json()["id"]
  95. # Delete it
  96. response = await async_client.delete(f"/api/v1/maintenance/types/{type_id}")
  97. assert response.status_code == 200
  98. class TestPrinterMaintenanceAPI:
  99. """Integration tests for /api/v1/maintenance/printers endpoints."""
  100. @pytest.mark.asyncio
  101. @pytest.mark.integration
  102. async def test_get_printer_maintenance_not_found(self, async_client: AsyncClient):
  103. """Verify 404 for non-existent printer."""
  104. response = await async_client.get("/api/v1/maintenance/printers/9999")
  105. assert response.status_code == 404
  106. @pytest.mark.asyncio
  107. @pytest.mark.integration
  108. async def test_get_printer_maintenance(self, async_client: AsyncClient, printer_factory, db_session):
  109. """Verify maintenance overview for a printer."""
  110. printer = await printer_factory(name="Maintenance Test Printer")
  111. response = await async_client.get(f"/api/v1/maintenance/printers/{printer.id}")
  112. assert response.status_code == 200
  113. data = response.json()
  114. assert data["printer_id"] == printer.id
  115. assert data["printer_name"] == "Maintenance Test Printer"
  116. assert "maintenance_items" in data
  117. assert "total_print_hours" in data
  118. @pytest.mark.asyncio
  119. @pytest.mark.integration
  120. async def test_get_all_maintenance_overview(self, async_client: AsyncClient, printer_factory, db_session):
  121. """Verify overview endpoint returns all printers."""
  122. await printer_factory(name="Overview Printer 1")
  123. await printer_factory(name="Overview Printer 2")
  124. response = await async_client.get("/api/v1/maintenance/overview")
  125. assert response.status_code == 200
  126. data = response.json()
  127. assert isinstance(data, list)
  128. @pytest.mark.asyncio
  129. @pytest.mark.integration
  130. async def test_get_maintenance_summary(self, async_client: AsyncClient):
  131. """Verify summary endpoint returns counts."""
  132. response = await async_client.get("/api/v1/maintenance/summary")
  133. assert response.status_code == 200
  134. data = response.json()
  135. assert "total_due" in data
  136. assert "total_warning" in data
  137. assert "printers_with_issues" in data
  138. class TestMaintenanceItemsAPI:
  139. """Integration tests for /api/v1/maintenance/items endpoints."""
  140. @pytest.fixture
  141. async def maintenance_item(self, async_client: AsyncClient, printer_factory, db_session):
  142. """Create a maintenance item for testing."""
  143. printer = await printer_factory(name="Item Test Printer")
  144. # Get the printer's maintenance overview to create items
  145. response = await async_client.get(f"/api/v1/maintenance/printers/{printer.id}")
  146. assert response.status_code == 200
  147. data = response.json()
  148. # Return the first maintenance item
  149. if data["maintenance_items"]:
  150. return data["maintenance_items"][0]
  151. return None
  152. @pytest.mark.asyncio
  153. @pytest.mark.integration
  154. async def test_update_maintenance_item(self, async_client: AsyncClient, maintenance_item):
  155. """Verify maintenance item can be updated."""
  156. if not maintenance_item:
  157. pytest.skip("No maintenance items available")
  158. item_id = maintenance_item["id"]
  159. response = await async_client.patch(
  160. f"/api/v1/maintenance/items/{item_id}", json={"custom_interval_hours": 150.0}
  161. )
  162. assert response.status_code == 200
  163. @pytest.mark.asyncio
  164. @pytest.mark.integration
  165. async def test_disable_maintenance_item(self, async_client: AsyncClient, maintenance_item):
  166. """Verify maintenance item can be disabled."""
  167. if not maintenance_item:
  168. pytest.skip("No maintenance items available")
  169. item_id = maintenance_item["id"]
  170. response = await async_client.patch(f"/api/v1/maintenance/items/{item_id}", json={"enabled": False})
  171. assert response.status_code == 200
  172. assert response.json()["enabled"] is False
  173. @pytest.mark.asyncio
  174. @pytest.mark.integration
  175. async def test_perform_reports_the_item_s_own_interval_type(self, async_client: AsyncClient, printer_factory):
  176. """dbcol-09: an item set to days on an hours-based type was answered in
  177. hours by "performed", while the list honoured the item's setting."""
  178. printer = await printer_factory(name="Interval Type Printer")
  179. overview = (await async_client.get(f"/api/v1/maintenance/printers/{printer.id}")).json()
  180. item = next(i for i in overview["maintenance_items"] if i["interval_type"] == "hours")
  181. patched = await async_client.patch(
  182. f"/api/v1/maintenance/items/{item['id']}",
  183. json={"custom_interval_type": "days", "custom_interval_hours": 30},
  184. )
  185. assert patched.status_code == 200
  186. data = (await async_client.post(f"/api/v1/maintenance/items/{item['id']}/perform", json={})).json()
  187. assert data["interval_type"] == "days"
  188. assert data["days_until_due"] == 30
  189. @pytest.mark.asyncio
  190. @pytest.mark.integration
  191. async def test_perform_maintenance(self, async_client: AsyncClient, maintenance_item):
  192. """Verify maintenance can be marked as performed."""
  193. if not maintenance_item:
  194. pytest.skip("No maintenance items available")
  195. item_id = maintenance_item["id"]
  196. response = await async_client.post(
  197. f"/api/v1/maintenance/items/{item_id}/perform", json={"notes": "Test maintenance performed"}
  198. )
  199. assert response.status_code == 200
  200. data = response.json()
  201. assert data["last_performed_at"] is not None
  202. @pytest.mark.asyncio
  203. @pytest.mark.integration
  204. async def test_get_maintenance_history(self, async_client: AsyncClient, maintenance_item):
  205. """Verify maintenance history can be retrieved."""
  206. if not maintenance_item:
  207. pytest.skip("No maintenance items available")
  208. item_id = maintenance_item["id"]
  209. # First perform maintenance to create history
  210. await async_client.post(f"/api/v1/maintenance/items/{item_id}/perform", json={"notes": "History test"})
  211. response = await async_client.get(f"/api/v1/maintenance/items/{item_id}/history")
  212. assert response.status_code == 200
  213. history = response.json()
  214. assert isinstance(history, list)
  215. @pytest.mark.asyncio
  216. @pytest.mark.integration
  217. async def test_update_maintenance_item_not_found(self, async_client: AsyncClient):
  218. """Verify 404 for non-existent maintenance item."""
  219. response = await async_client.patch("/api/v1/maintenance/items/9999", json={"enabled": False})
  220. assert response.status_code == 404
  221. class TestPrinterHoursAPI:
  222. """Integration tests for /api/v1/maintenance/printers/{id}/hours endpoint."""
  223. @pytest.mark.asyncio
  224. @pytest.mark.integration
  225. async def test_set_printer_hours(self, async_client: AsyncClient, printer_factory, db_session):
  226. """Verify printer hours can be set."""
  227. printer = await printer_factory(name="Hours Test Printer")
  228. response = await async_client.patch(
  229. f"/api/v1/maintenance/printers/{printer.id}/hours", params={"total_hours": 500.0}
  230. )
  231. assert response.status_code == 200
  232. data = response.json()
  233. assert data["total_hours"] == 500.0
  234. @pytest.mark.asyncio
  235. @pytest.mark.integration
  236. async def test_set_printer_hours_not_found(self, async_client: AsyncClient):
  237. """Verify 404 for non-existent printer."""
  238. response = await async_client.patch("/api/v1/maintenance/printers/9999/hours", params={"total_hours": 100.0})
  239. assert response.status_code == 404