test_spoolman_k_profiles.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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. with patch(
  59. "backend.app.api.routes.spoolman_inventory._get_client",
  60. AsyncMock(return_value=client),
  61. ):
  62. yield client
  63. class TestGetSpoolmanKProfiles:
  64. @pytest.mark.asyncio
  65. @pytest.mark.integration
  66. async def test_returns_empty_list_when_none(self, async_client: AsyncClient, kp_settings, mock_spoolman_client):
  67. """GET /spools/7/k-profiles returns [] when no profiles exist."""
  68. response = await async_client.get("/api/v1/spoolman/inventory/spools/7/k-profiles")
  69. assert response.status_code == 200
  70. assert response.json() == []
  71. @pytest.mark.asyncio
  72. @pytest.mark.integration
  73. async def test_returns_existing_profiles(
  74. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer, db_session
  75. ):
  76. """GET /spools/7/k-profiles returns saved profiles."""
  77. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  78. kp = SpoolmanKProfile(
  79. spoolman_spool_id=7,
  80. printer_id=test_printer.id,
  81. extruder=0,
  82. nozzle_diameter="0.4",
  83. k_value=0.025,
  84. cali_idx=3,
  85. )
  86. db_session.add(kp)
  87. await db_session.commit()
  88. response = await async_client.get("/api/v1/spoolman/inventory/spools/7/k-profiles")
  89. assert response.status_code == 200
  90. profiles = response.json()
  91. assert len(profiles) == 1
  92. assert profiles[0]["spool_id"] == 7
  93. assert profiles[0]["printer_id"] == test_printer.id
  94. assert profiles[0]["k_value"] == pytest.approx(0.025)
  95. assert profiles[0]["cali_idx"] == 3
  96. class TestSaveSpoolmanKProfiles:
  97. @pytest.mark.asyncio
  98. @pytest.mark.integration
  99. async def test_put_creates_profiles(
  100. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer
  101. ):
  102. """PUT /spools/7/k-profiles saves profiles and returns them."""
  103. response = await async_client.put(
  104. "/api/v1/spoolman/inventory/spools/7/k-profiles",
  105. json=[
  106. {
  107. "printer_id": test_printer.id,
  108. "extruder": 0,
  109. "nozzle_diameter": "0.4",
  110. "k_value": 0.02,
  111. "cali_idx": 1,
  112. }
  113. ],
  114. )
  115. assert response.status_code == 200
  116. saved = response.json()
  117. assert len(saved) == 1
  118. assert saved[0]["spool_id"] == 7
  119. assert saved[0]["k_value"] == pytest.approx(0.02)
  120. @pytest.mark.asyncio
  121. @pytest.mark.integration
  122. async def test_put_replaces_existing_profiles(
  123. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer, db_session
  124. ):
  125. """PUT /spools/7/k-profiles with new data deletes old rows first."""
  126. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  127. old = SpoolmanKProfile(
  128. spoolman_spool_id=7,
  129. printer_id=test_printer.id,
  130. extruder=0,
  131. nozzle_diameter="0.4",
  132. k_value=0.99,
  133. cali_idx=99,
  134. )
  135. db_session.add(old)
  136. await db_session.commit()
  137. response = await async_client.put(
  138. "/api/v1/spoolman/inventory/spools/7/k-profiles",
  139. json=[
  140. {
  141. "printer_id": test_printer.id,
  142. "extruder": 0,
  143. "nozzle_diameter": "0.4",
  144. "k_value": 0.03,
  145. "cali_idx": 7,
  146. }
  147. ],
  148. )
  149. assert response.status_code == 200
  150. saved = response.json()
  151. assert len(saved) == 1
  152. assert saved[0]["k_value"] == pytest.approx(0.03)
  153. assert saved[0]["cali_idx"] == 7
  154. @pytest.mark.asyncio
  155. @pytest.mark.integration
  156. async def test_put_empty_clears_profiles(
  157. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer, db_session
  158. ):
  159. """PUT /spools/7/k-profiles with [] clears all existing profiles."""
  160. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  161. kp = SpoolmanKProfile(
  162. spoolman_spool_id=7,
  163. printer_id=test_printer.id,
  164. extruder=0,
  165. nozzle_diameter="0.4",
  166. k_value=0.02,
  167. cali_idx=1,
  168. )
  169. db_session.add(kp)
  170. await db_session.commit()
  171. response = await async_client.put(
  172. "/api/v1/spoolman/inventory/spools/7/k-profiles",
  173. json=[],
  174. )
  175. assert response.status_code == 200
  176. assert response.json() == []
  177. # Verify gone in DB
  178. get_resp = await async_client.get("/api/v1/spoolman/inventory/spools/7/k-profiles")
  179. assert get_resp.json() == []
  180. class TestSpoolKProfileEnrichment:
  181. @pytest.mark.asyncio
  182. @pytest.mark.integration
  183. async def test_get_spool_includes_k_profiles(
  184. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer, db_session
  185. ):
  186. """GET /spools/7 includes k_profiles from local DB."""
  187. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  188. kp = SpoolmanKProfile(
  189. spoolman_spool_id=7,
  190. printer_id=test_printer.id,
  191. extruder=0,
  192. nozzle_diameter="0.6",
  193. k_value=0.018,
  194. cali_idx=2,
  195. )
  196. db_session.add(kp)
  197. await db_session.commit()
  198. response = await async_client.get("/api/v1/spoolman/inventory/spools/7")
  199. assert response.status_code == 200
  200. body = response.json()
  201. assert "k_profiles" in body
  202. assert len(body["k_profiles"]) == 1
  203. assert body["k_profiles"][0]["k_value"] == pytest.approx(0.018)
  204. @pytest.mark.asyncio
  205. @pytest.mark.integration
  206. async def test_list_spools_includes_k_profiles(
  207. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer, db_session
  208. ):
  209. """GET /spools includes k_profiles for each spool from local DB."""
  210. from backend.app.models.spoolman_k_profile import SpoolmanKProfile
  211. kp = SpoolmanKProfile(
  212. spoolman_spool_id=7,
  213. printer_id=test_printer.id,
  214. extruder=0,
  215. nozzle_diameter="0.4",
  216. k_value=0.021,
  217. cali_idx=4,
  218. )
  219. db_session.add(kp)
  220. await db_session.commit()
  221. response = await async_client.get("/api/v1/spoolman/inventory/spools")
  222. assert response.status_code == 200
  223. spools = response.json()
  224. assert len(spools) == 1
  225. assert "k_profiles" in spools[0]
  226. assert len(spools[0]["k_profiles"]) == 1
  227. assert spools[0]["k_profiles"][0]["cali_idx"] == 4
  228. class TestPutSpoolmanKProfilesValidation:
  229. @pytest.mark.asyncio
  230. @pytest.mark.integration
  231. async def test_put_k_profiles_duplicate_raises_422(
  232. self, async_client: AsyncClient, kp_settings, mock_spoolman_client, test_printer
  233. ):
  234. """Two profiles with identical (printer_id, extruder, nozzle_diameter) → UNIQUE violation → 422."""
  235. profiles = [
  236. {"printer_id": test_printer.id, "extruder": 0, "nozzle_diameter": "0.4", "k_value": 0.02},
  237. {"printer_id": test_printer.id, "extruder": 0, "nozzle_diameter": "0.4", "k_value": 0.03},
  238. ]
  239. response = await async_client.put(
  240. "/api/v1/spoolman/inventory/spools/7/k-profiles",
  241. json=profiles,
  242. )
  243. assert response.status_code == 422