test_available_filaments.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. """Integration tests for GET /api/v1/printers/available-filaments endpoint.
  2. Tests that the endpoint returns deduplicated filaments with tray_sub_brands,
  3. correctly distinguishing subtypes like "PLA Basic" vs "PLA Matte".
  4. """
  5. from unittest.mock import MagicMock, patch
  6. import pytest
  7. from httpx import AsyncClient
  8. def _make_mock_status(ams_data: list, vt_tray: list | None = None, ams_extruder_map: dict | None = None) -> MagicMock:
  9. """Create a mock printer status with raw_data containing AMS info."""
  10. status = MagicMock()
  11. raw = {"ams": ams_data}
  12. if vt_tray is not None:
  13. raw["vt_tray"] = vt_tray
  14. if ams_extruder_map is not None:
  15. raw["ams_extruder_map"] = ams_extruder_map
  16. else:
  17. raw["ams_extruder_map"] = {}
  18. status.raw_data = raw
  19. return status
  20. class TestAvailableFilaments:
  21. """Tests for /api/v1/printers/available-filaments endpoint."""
  22. @pytest.mark.asyncio
  23. @pytest.mark.integration
  24. async def test_returns_tray_sub_brands(self, async_client: AsyncClient, printer_factory):
  25. """Verify tray_sub_brands is included in the response."""
  26. await printer_factory(name="Test Printer", model="X1C")
  27. status = _make_mock_status(
  28. ams_data=[
  29. {
  30. "id": 0,
  31. "tray": [
  32. {
  33. "id": 0,
  34. "tray_type": "PLA",
  35. "tray_color": "000000FF",
  36. "tray_info_idx": "GFL99",
  37. "tray_sub_brands": "PLA Basic",
  38. },
  39. ],
  40. },
  41. ]
  42. )
  43. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  44. mock_pm.get_status.return_value = status
  45. response = await async_client.get("/api/v1/printers/available-filaments?model=X1C")
  46. assert response.status_code == 200
  47. data = response.json()
  48. assert len(data) == 1
  49. assert data[0]["tray_sub_brands"] == "PLA Basic"
  50. assert data[0]["type"] == "PLA"
  51. @pytest.mark.asyncio
  52. @pytest.mark.integration
  53. async def test_dedup_distinguishes_subtypes(self, async_client: AsyncClient, printer_factory):
  54. """PLA Basic Black and PLA Matte Black should be separate entries."""
  55. await printer_factory(name="Printer 1", model="X1C")
  56. status = _make_mock_status(
  57. ams_data=[
  58. {
  59. "id": 0,
  60. "tray": [
  61. {
  62. "id": 0,
  63. "tray_type": "PLA",
  64. "tray_color": "000000FF",
  65. "tray_info_idx": "GFL99",
  66. "tray_sub_brands": "PLA Basic",
  67. },
  68. {
  69. "id": 1,
  70. "tray_type": "PLA",
  71. "tray_color": "000000FF",
  72. "tray_info_idx": "GFL05",
  73. "tray_sub_brands": "PLA Matte",
  74. },
  75. ],
  76. },
  77. ]
  78. )
  79. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  80. mock_pm.get_status.return_value = status
  81. response = await async_client.get("/api/v1/printers/available-filaments?model=X1C")
  82. assert response.status_code == 200
  83. data = response.json()
  84. # Same type + color but different tray_sub_brands → 2 entries
  85. assert len(data) == 2
  86. sub_brands = {d["tray_sub_brands"] for d in data}
  87. assert sub_brands == {"PLA Basic", "PLA Matte"}
  88. @pytest.mark.asyncio
  89. @pytest.mark.integration
  90. async def test_dedup_same_subtype_same_color(self, async_client: AsyncClient, printer_factory):
  91. """Same subtype + same color across two printers should be deduped to one entry."""
  92. await printer_factory(name="Printer 1", model="X1C")
  93. await printer_factory(name="Printer 2", model="X1C")
  94. status1 = _make_mock_status(
  95. ams_data=[
  96. {
  97. "id": 0,
  98. "tray": [
  99. {
  100. "id": 0,
  101. "tray_type": "PLA",
  102. "tray_color": "FF0000FF",
  103. "tray_info_idx": "GFL99",
  104. "tray_sub_brands": "PLA Basic",
  105. }
  106. ],
  107. },
  108. ]
  109. )
  110. status2 = _make_mock_status(
  111. ams_data=[
  112. {
  113. "id": 0,
  114. "tray": [
  115. {
  116. "id": 0,
  117. "tray_type": "PLA",
  118. "tray_color": "FF0000FF",
  119. "tray_info_idx": "GFL99",
  120. "tray_sub_brands": "PLA Basic",
  121. }
  122. ],
  123. },
  124. ]
  125. )
  126. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  127. mock_pm.get_status.side_effect = [status1, status2]
  128. response = await async_client.get("/api/v1/printers/available-filaments?model=X1C")
  129. assert response.status_code == 200
  130. data = response.json()
  131. assert len(data) == 1
  132. @pytest.mark.asyncio
  133. @pytest.mark.integration
  134. async def test_empty_sub_brands_handled(self, async_client: AsyncClient, printer_factory):
  135. """Filaments with empty/missing tray_sub_brands should still be returned."""
  136. await printer_factory(name="Test Printer", model="X1C")
  137. status = _make_mock_status(
  138. ams_data=[
  139. {
  140. "id": 0,
  141. "tray": [
  142. {"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF", "tray_info_idx": "GFL99"},
  143. ],
  144. },
  145. ]
  146. )
  147. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  148. mock_pm.get_status.return_value = status
  149. response = await async_client.get("/api/v1/printers/available-filaments?model=X1C")
  150. assert response.status_code == 200
  151. data = response.json()
  152. assert len(data) == 1
  153. assert data[0]["tray_sub_brands"] == ""
  154. @pytest.mark.asyncio
  155. @pytest.mark.integration
  156. async def test_external_spool_includes_sub_brands(self, async_client: AsyncClient, printer_factory):
  157. """External spools (vt_tray) should also include tray_sub_brands."""
  158. await printer_factory(name="Test Printer", model="X1C")
  159. status = _make_mock_status(
  160. ams_data=[],
  161. vt_tray=[
  162. {
  163. "id": 254,
  164. "tray_type": "PETG",
  165. "tray_color": "00FF00FF",
  166. "tray_info_idx": "GFG00",
  167. "tray_sub_brands": "PETG HF",
  168. },
  169. ],
  170. )
  171. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  172. mock_pm.get_status.return_value = status
  173. response = await async_client.get("/api/v1/printers/available-filaments?model=X1C")
  174. assert response.status_code == 200
  175. data = response.json()
  176. assert len(data) == 1
  177. assert data[0]["tray_sub_brands"] == "PETG HF"
  178. assert data[0]["type"] == "PETG"
  179. @pytest.mark.asyncio
  180. @pytest.mark.integration
  181. async def test_no_printers_returns_empty(self, async_client: AsyncClient):
  182. """Verify empty list when no printers match the model."""
  183. response = await async_client.get("/api/v1/printers/available-filaments?model=X1C")
  184. assert response.status_code == 200
  185. assert response.json() == []