test_spoolman_k_profiles.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. """Integration tests for Spoolman K-profile endpoints.
  2. Covers:
  3. GET /api/v1/spoolman/inventory/spools/{id}/k-profiles
  4. PUT /api/v1/spoolman/inventory/spools/{id}/k-profiles
  5. GET /api/v1/spoolman/inventory/spools/{id} — k_profiles enrichment
  6. GET /api/v1/spoolman/inventory/spools — k_profiles enrichment (batch)
  7. """
  8. from unittest.mock import AsyncMock, MagicMock, patch
  9. import pytest
  10. from httpx import AsyncClient
  11. SAMPLE_SPOOL = {
  12. "id": 7,
  13. "filament": {
  14. "id": 1,
  15. "name": "PETG CF",
  16. "material": "PETG",
  17. "weight": 1000,
  18. "color_hex": "000000",
  19. "vendor": {"id": 1, "name": "BrandX"},
  20. },
  21. "remaining_weight": 600.0,
  22. "used_weight": 400.0,
  23. "location": None,
  24. "comment": None,
  25. "first_used": None,
  26. "last_used": None,
  27. "registered": "2024-01-01T00:00:00+00:00",
  28. "archived": False,
  29. "price": None,
  30. "extra": {},
  31. }
  32. @pytest.fixture
  33. async def kp_settings(db_session):
  34. from backend.app.models.settings import Settings
  35. db_session.add(Settings(key="spoolman_enabled", value="true"))
  36. db_session.add(Settings(key="spoolman_url", value="http://localhost:7912"))
  37. await db_session.commit()
  38. @pytest.fixture
  39. async def test_printer(db_session):
  40. from backend.app.models.printer import Printer
  41. printer = Printer(
  42. name="KP Printer",
  43. serial_number="KPTEST001",
  44. ip_address="192.168.1.77",
  45. access_code="12345678",
  46. )
  47. db_session.add(printer)
  48. await db_session.commit()
  49. await db_session.refresh(printer)
  50. return printer
  51. @pytest.fixture
  52. def mock_spoolman_client():
  53. client = MagicMock()
  54. client.base_url = "http://localhost:7912"
  55. client.health_check = AsyncMock(return_value=True)
  56. client.get_spool = AsyncMock(return_value=SAMPLE_SPOOL)
  57. client.get_all_spools = AsyncMock(return_value=[SAMPLE_SPOOL])
  58. client.get_distinct_locations = AsyncMock(return_value=[])
  59. with patch(
  60. "backend.app.api.routes.spoolman_inventory._get_client",
  61. AsyncMock(return_value=client),
  62. ):
  63. yield client
  64. class TestGetSpoolmanKProfiles:
  65. @pytest.mark.asyncio
  66. @pytest.mark.integration
  67. async def test_returns_empty_list_when_none(self, async_client: AsyncClient, kp_settings, mock_spoolman_client):
  68. """GET /spools/7/k-profiles returns [] when no profiles exist."""
  69. response = await async_client.get("/api/v1/spoolman/inventory/spools/7/k-profiles")
  70. assert response.status_code == 200
  71. assert response.json() == []
  72. @pytest.mark.asyncio
  73. @pytest.mark.integration
  74. async def test_returns_existing_profiles(
  75. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer, db_session
  76. ):
  77. """GET /spools/7/k-profiles returns saved profiles."""
  78. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  79. kp = SpoolmanKProfile(
  80. spoolman_spool_id=7,
  81. printer_id=test_printer.id,
  82. extruder=0,
  83. nozzle_diameter="0.4",
  84. k_value=0.025,
  85. cali_idx=3,
  86. )
  87. db_session.add(kp)
  88. await db_session.commit()
  89. response = await async_client.get("/api/v1/spoolman/inventory/spools/7/k-profiles")
  90. assert response.status_code == 200
  91. profiles = response.json()
  92. assert len(profiles) == 1
  93. assert profiles[0]["spool_id"] == 7
  94. assert profiles[0]["printer_id"] == test_printer.id
  95. assert profiles[0]["k_value"] == pytest.approx(0.025)
  96. assert profiles[0]["cali_idx"] == 3
  97. class TestSaveSpoolmanKProfiles:
  98. @pytest.mark.asyncio
  99. @pytest.mark.integration
  100. async def test_put_creates_profiles(
  101. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer
  102. ):
  103. """PUT /spools/7/k-profiles saves profiles and returns them."""
  104. response = await async_client.put(
  105. "/api/v1/spoolman/inventory/spools/7/k-profiles",
  106. json=[
  107. {
  108. "printer_id": test_printer.id,
  109. "extruder": 0,
  110. "nozzle_diameter": "0.4",
  111. "k_value": 0.02,
  112. "cali_idx": 1,
  113. }
  114. ],
  115. )
  116. assert response.status_code == 200
  117. saved = response.json()
  118. assert len(saved) == 1
  119. assert saved[0]["spool_id"] == 7
  120. assert saved[0]["k_value"] == pytest.approx(0.02)
  121. @pytest.mark.asyncio
  122. @pytest.mark.integration
  123. async def test_put_replaces_existing_profiles(
  124. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer, db_session
  125. ):
  126. """PUT /spools/7/k-profiles with new data deletes old rows first."""
  127. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  128. old = SpoolmanKProfile(
  129. spoolman_spool_id=7,
  130. printer_id=test_printer.id,
  131. extruder=0,
  132. nozzle_diameter="0.4",
  133. k_value=0.99,
  134. cali_idx=99,
  135. )
  136. db_session.add(old)
  137. await db_session.commit()
  138. response = await async_client.put(
  139. "/api/v1/spoolman/inventory/spools/7/k-profiles",
  140. json=[
  141. {
  142. "printer_id": test_printer.id,
  143. "extruder": 0,
  144. "nozzle_diameter": "0.4",
  145. "k_value": 0.03,
  146. "cali_idx": 7,
  147. }
  148. ],
  149. )
  150. assert response.status_code == 200
  151. saved = response.json()
  152. assert len(saved) == 1
  153. assert saved[0]["k_value"] == pytest.approx(0.03)
  154. assert saved[0]["cali_idx"] == 7
  155. @pytest.mark.asyncio
  156. @pytest.mark.integration
  157. async def test_put_empty_clears_profiles(
  158. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer, db_session
  159. ):
  160. """PUT /spools/7/k-profiles with [] clears all existing profiles."""
  161. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  162. kp = SpoolmanKProfile(
  163. spoolman_spool_id=7,
  164. printer_id=test_printer.id,
  165. extruder=0,
  166. nozzle_diameter="0.4",
  167. k_value=0.02,
  168. cali_idx=1,
  169. )
  170. db_session.add(kp)
  171. await db_session.commit()
  172. response = await async_client.put(
  173. "/api/v1/spoolman/inventory/spools/7/k-profiles",
  174. json=[],
  175. )
  176. assert response.status_code == 200
  177. assert response.json() == []
  178. # Verify gone in DB
  179. get_resp = await async_client.get("/api/v1/spoolman/inventory/spools/7/k-profiles")
  180. assert get_resp.json() == []
  181. class TestSpoolKProfileEnrichment:
  182. @pytest.mark.asyncio
  183. @pytest.mark.integration
  184. async def test_get_spool_includes_k_profiles(
  185. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer, db_session
  186. ):
  187. """GET /spools/7 includes k_profiles from local DB."""
  188. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  189. kp = SpoolmanKProfile(
  190. spoolman_spool_id=7,
  191. printer_id=test_printer.id,
  192. extruder=0,
  193. nozzle_diameter="0.6",
  194. k_value=0.018,
  195. cali_idx=2,
  196. )
  197. db_session.add(kp)
  198. await db_session.commit()
  199. response = await async_client.get("/api/v1/spoolman/inventory/spools/7")
  200. assert response.status_code == 200
  201. body = response.json()
  202. assert "k_profiles" in body
  203. assert len(body["k_profiles"]) == 1
  204. assert body["k_profiles"][0]["k_value"] == pytest.approx(0.018)
  205. @pytest.mark.asyncio
  206. @pytest.mark.integration
  207. async def test_list_spools_includes_k_profiles(
  208. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer, db_session
  209. ):
  210. """GET /spools includes k_profiles for each spool from local DB."""
  211. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  212. kp = SpoolmanKProfile(
  213. spoolman_spool_id=7,
  214. printer_id=test_printer.id,
  215. extruder=0,
  216. nozzle_diameter="0.4",
  217. k_value=0.021,
  218. cali_idx=4,
  219. )
  220. db_session.add(kp)
  221. await db_session.commit()
  222. response = await async_client.get("/api/v1/spoolman/inventory/spools")
  223. assert response.status_code == 200
  224. spools = response.json()
  225. assert len(spools) == 1
  226. assert "k_profiles" in spools[0]
  227. assert len(spools[0]["k_profiles"]) == 1
  228. assert spools[0]["k_profiles"][0]["cali_idx"] == 4
  229. class TestPutSpoolmanKProfilesValidation:
  230. @pytest.mark.asyncio
  231. @pytest.mark.integration
  232. async def test_put_k_profiles_duplicate_raises_422(
  233. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer
  234. ):
  235. """Two profiles with identical (printer_id, extruder, nozzle_diameter) → UNIQUE violation → 422."""
  236. profiles = [
  237. {"printer_id": test_printer.id, "extruder": 0, "nozzle_diameter": "0.4", "k_value": 0.02},
  238. {"printer_id": test_printer.id, "extruder": 0, "nozzle_diameter": "0.4", "k_value": 0.03},
  239. ]
  240. response = await async_client.put(
  241. "/api/v1/spoolman/inventory/spools/7/k-profiles",
  242. json=profiles,
  243. )
  244. assert response.status_code == 422