test_slicer_pipelines_api.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. """Integration tests for the Slicer Pipelines API (#1425 PR A)."""
  2. import pytest
  3. from httpx import AsyncClient
  4. def _preset_ref(source: str, id_: str) -> dict:
  5. return {"source": source, "id": id_}
  6. def _payload(**overrides) -> dict:
  7. payload = {
  8. "name": "Production Batch",
  9. "description": "High speed PLA on X1C",
  10. "printer_preset": _preset_ref("local", "42"),
  11. "process_preset": _preset_ref("local", "7"),
  12. "filament_presets": [_preset_ref("local", "11"), _preset_ref("standard", "PLA Basic")],
  13. "bed_type": "Textured PEI Plate",
  14. }
  15. payload.update(overrides)
  16. return payload
  17. class TestSlicerPipelinesAPI:
  18. """CRUD + edge cases for /api/v1/slicer-pipelines."""
  19. @pytest.mark.asyncio
  20. @pytest.mark.integration
  21. async def test_list_empty(self, async_client: AsyncClient):
  22. """Empty list response uses the canonical {pipelines: []} envelope."""
  23. resp = await async_client.get("/api/v1/slicer-pipelines/")
  24. assert resp.status_code == 200
  25. data = resp.json()
  26. assert data == {"pipelines": []}
  27. @pytest.mark.asyncio
  28. @pytest.mark.integration
  29. async def test_create_and_list(self, async_client: AsyncClient):
  30. """A newly-created pipeline appears in the list with its full shape."""
  31. resp = await async_client.post("/api/v1/slicer-pipelines/", json=_payload())
  32. assert resp.status_code == 201, resp.text
  33. created = resp.json()
  34. assert created["name"] == "Production Batch"
  35. assert created["printer_preset"] == _preset_ref("local", "42")
  36. assert created["process_preset"] == _preset_ref("local", "7")
  37. assert created["filament_presets"] == [
  38. _preset_ref("local", "11"),
  39. _preset_ref("standard", "PLA Basic"),
  40. ]
  41. assert created["bed_type"] == "Textured PEI Plate"
  42. # PR A defaults persisted but not user-set
  43. assert created["target_kind"] == "printer_class"
  44. assert created["target_printer_id"] is None
  45. assert created["fanout_strategy"] == "max_parallel"
  46. list_resp = await async_client.get("/api/v1/slicer-pipelines/")
  47. assert list_resp.status_code == 200
  48. ids = [p["id"] for p in list_resp.json()["pipelines"]]
  49. assert created["id"] in ids
  50. @pytest.mark.asyncio
  51. @pytest.mark.integration
  52. async def test_get_by_id(self, async_client: AsyncClient):
  53. """Round-trips the preset slots through JSON storage faithfully."""
  54. created = (await async_client.post("/api/v1/slicer-pipelines/", json=_payload())).json()
  55. resp = await async_client.get(f"/api/v1/slicer-pipelines/{created['id']}")
  56. assert resp.status_code == 200
  57. fetched = resp.json()
  58. assert fetched["printer_preset"] == _preset_ref("local", "42")
  59. assert fetched["filament_presets"] == [
  60. _preset_ref("local", "11"),
  61. _preset_ref("standard", "PLA Basic"),
  62. ]
  63. @pytest.mark.asyncio
  64. @pytest.mark.integration
  65. async def test_get_not_found(self, async_client: AsyncClient):
  66. resp = await async_client.get("/api/v1/slicer-pipelines/99999")
  67. assert resp.status_code == 404
  68. @pytest.mark.asyncio
  69. @pytest.mark.integration
  70. async def test_update_partial(self, async_client: AsyncClient):
  71. """PUT writes only fields that are present; others stay unchanged."""
  72. created = (await async_client.post("/api/v1/slicer-pipelines/", json=_payload())).json()
  73. resp = await async_client.put(
  74. f"/api/v1/slicer-pipelines/{created['id']}",
  75. json={"name": "Renamed", "bed_type": "Cool Plate"},
  76. )
  77. assert resp.status_code == 200
  78. updated = resp.json()
  79. assert updated["name"] == "Renamed"
  80. assert updated["bed_type"] == "Cool Plate"
  81. # Untouched fields preserved
  82. assert updated["printer_preset"] == _preset_ref("local", "42")
  83. assert updated["filament_presets"] == created["filament_presets"]
  84. @pytest.mark.asyncio
  85. @pytest.mark.integration
  86. async def test_update_filament_list_replaces_wholesale(self, async_client: AsyncClient):
  87. """Setting filament_presets replaces the entire list."""
  88. created = (await async_client.post("/api/v1/slicer-pipelines/", json=_payload())).json()
  89. new_filaments = [_preset_ref("cloud", "PFUS1"), _preset_ref("cloud", "PFUS2"), _preset_ref("cloud", "PFUS3")]
  90. resp = await async_client.put(
  91. f"/api/v1/slicer-pipelines/{created['id']}",
  92. json={"filament_presets": new_filaments},
  93. )
  94. assert resp.status_code == 200
  95. assert resp.json()["filament_presets"] == new_filaments
  96. @pytest.mark.asyncio
  97. @pytest.mark.integration
  98. async def test_delete_is_soft(self, async_client: AsyncClient):
  99. """DELETE hides from list + GET-by-id but doesn't drop the row (PR B+
  100. run history must still resolve pipeline metadata)."""
  101. created = (await async_client.post("/api/v1/slicer-pipelines/", json=_payload())).json()
  102. resp = await async_client.delete(f"/api/v1/slicer-pipelines/{created['id']}")
  103. assert resp.status_code == 204
  104. # Hidden from list
  105. list_resp = await async_client.get("/api/v1/slicer-pipelines/")
  106. assert created["id"] not in [p["id"] for p in list_resp.json()["pipelines"]]
  107. # Hidden from GET
  108. get_resp = await async_client.get(f"/api/v1/slicer-pipelines/{created['id']}")
  109. assert get_resp.status_code == 404
  110. @pytest.mark.asyncio
  111. @pytest.mark.integration
  112. async def test_delete_not_found(self, async_client: AsyncClient):
  113. resp = await async_client.delete("/api/v1/slicer-pipelines/99999")
  114. assert resp.status_code == 404
  115. @pytest.mark.asyncio
  116. @pytest.mark.integration
  117. async def test_create_rejects_empty_filament_list(self, async_client: AsyncClient):
  118. """The schema requires at least one filament slot."""
  119. payload = _payload(filament_presets=[])
  120. resp = await async_client.post("/api/v1/slicer-pipelines/", json=payload)
  121. assert resp.status_code == 422
  122. @pytest.mark.asyncio
  123. @pytest.mark.integration
  124. async def test_create_rejects_invalid_preset_source(self, async_client: AsyncClient):
  125. """PresetRef.source is constrained to the four known tiers."""
  126. bad = _preset_ref("bogus_source", "1")
  127. payload = _payload(printer_preset=bad)
  128. resp = await async_client.post("/api/v1/slicer-pipelines/", json=payload)
  129. assert resp.status_code == 422
  130. @pytest.mark.asyncio
  131. @pytest.mark.integration
  132. async def test_list_orders_newest_first(self, async_client: AsyncClient):
  133. first = (await async_client.post("/api/v1/slicer-pipelines/", json=_payload(name="First"))).json()
  134. second = (await async_client.post("/api/v1/slicer-pipelines/", json=_payload(name="Second"))).json()
  135. third = (await async_client.post("/api/v1/slicer-pipelines/", json=_payload(name="Third"))).json()
  136. listing = (await async_client.get("/api/v1/slicer-pipelines/")).json()["pipelines"]
  137. # Filter to the three we just made (DB may have other rows from other tests)
  138. ours = [p for p in listing if p["id"] in {first["id"], second["id"], third["id"]}]
  139. assert [p["name"] for p in ours] == ["Third", "Second", "First"]