test_spoolman_service.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. """Unit tests for Spoolman service.
  2. These tests specifically target the sync_ams_tray method's disable_weight_sync
  3. functionality that controls whether remaining_weight is updated.
  4. """
  5. from unittest.mock import AsyncMock, patch
  6. import pytest
  7. from backend.app.services.spoolman import AMSTray, SpoolmanClient
  8. class TestSpoolmanClient:
  9. """Tests for SpoolmanClient class."""
  10. @pytest.fixture
  11. def client(self):
  12. """Create a SpoolmanClient instance."""
  13. return SpoolmanClient("http://localhost:7912")
  14. @pytest.fixture
  15. def sample_tray(self):
  16. """Create a sample AMSTray for testing."""
  17. return AMSTray(
  18. ams_id=0,
  19. tray_id=0,
  20. tray_type="PLA",
  21. tray_sub_brands="PLA Basic",
  22. tray_color="FF0000FF",
  23. remain=50,
  24. tag_uid="",
  25. tray_uuid="A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4",
  26. tray_info_idx="GFA00",
  27. tray_weight=1000,
  28. )
  29. @pytest.fixture
  30. def existing_spool(self):
  31. """Create a mock existing spool response."""
  32. return {
  33. "id": 42,
  34. "remaining_weight": 800,
  35. "extra": {"tag": '"A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4"'},
  36. "filament": {"id": 1, "name": "PLA Red", "material": "PLA"},
  37. }
  38. @pytest.fixture
  39. def mock_filament(self):
  40. """Create a mock filament response."""
  41. return {"id": 1, "name": "PLA Basic", "material": "PLA"}
  42. # ========================================================================
  43. # Tests for sync_ams_tray with disable_weight_sync
  44. # ========================================================================
  45. @pytest.mark.asyncio
  46. async def test_sync_ams_tray_updates_weight_by_default(self, client, sample_tray, existing_spool):
  47. """Verify sync_ams_tray updates remaining_weight by default."""
  48. with (
  49. patch.object(client, "find_spool_by_tag", AsyncMock(return_value=existing_spool)),
  50. patch.object(client, "update_spool", AsyncMock(return_value={"id": 42})) as mock_update,
  51. ):
  52. await client.sync_ams_tray(sample_tray, "TestPrinter")
  53. mock_update.assert_called_once()
  54. call_kwargs = mock_update.call_args.kwargs
  55. assert "remaining_weight" in call_kwargs
  56. assert call_kwargs["remaining_weight"] == 500.0 # 50% of 1000g
  57. assert "location" in call_kwargs
  58. @pytest.mark.asyncio
  59. async def test_sync_ams_tray_skips_weight_when_disabled(self, client, sample_tray, existing_spool):
  60. """Verify sync_ams_tray skips remaining_weight when disable_weight_sync=True."""
  61. with (
  62. patch.object(client, "find_spool_by_tag", AsyncMock(return_value=existing_spool)),
  63. patch.object(client, "update_spool", AsyncMock(return_value={"id": 42})) as mock_update,
  64. ):
  65. await client.sync_ams_tray(sample_tray, "TestPrinter", disable_weight_sync=True)
  66. mock_update.assert_called_once()
  67. call_kwargs = mock_update.call_args.kwargs
  68. # remaining_weight should be None (not updated)
  69. assert call_kwargs.get("remaining_weight") is None
  70. # location should still be updated
  71. assert "location" in call_kwargs
  72. assert "TestPrinter" in call_kwargs["location"]
  73. @pytest.mark.asyncio
  74. async def test_sync_ams_tray_new_spool_always_includes_weight(
  75. self, client, sample_tray, mock_filament
  76. ):
  77. """Verify new spool creation always includes remaining_weight even when disabled."""
  78. with (
  79. patch.object(client, "find_spool_by_tag", AsyncMock(return_value=None)),
  80. patch.object(client, "_find_or_create_filament", AsyncMock(return_value=mock_filament)),
  81. patch.object(client, "create_spool", AsyncMock(return_value={"id": 99})) as mock_create,
  82. ):
  83. await client.sync_ams_tray(sample_tray, "TestPrinter", disable_weight_sync=True)
  84. mock_create.assert_called_once()
  85. call_kwargs = mock_create.call_args.kwargs
  86. # New spools should ALWAYS include remaining_weight
  87. assert "remaining_weight" in call_kwargs
  88. assert call_kwargs["remaining_weight"] == 500.0 # 50% of 1000g
  89. @pytest.mark.asyncio
  90. async def test_sync_ams_tray_location_format(self, client, sample_tray, existing_spool):
  91. """Verify location format is correct when updating spool."""
  92. with (
  93. patch.object(client, "find_spool_by_tag", AsyncMock(return_value=existing_spool)),
  94. patch.object(client, "update_spool", AsyncMock(return_value={"id": 42})) as mock_update,
  95. ):
  96. await client.sync_ams_tray(sample_tray, "My Printer", disable_weight_sync=True)
  97. call_kwargs = mock_update.call_args.kwargs
  98. # Location should follow pattern: "PrinterName - AMS A1"
  99. assert "location" in call_kwargs
  100. assert "My Printer" in call_kwargs["location"]
  101. assert "AMS" in call_kwargs["location"]
  102. @pytest.mark.asyncio
  103. async def test_sync_ams_tray_skips_non_bambu_spool(self, client):
  104. """Verify non-Bambu Lab spools are skipped."""
  105. # Third-party spool without proper identifiers
  106. tray = AMSTray(
  107. ams_id=0,
  108. tray_id=0,
  109. tray_type="PLA",
  110. tray_sub_brands="Third Party PLA",
  111. tray_color="FF0000FF",
  112. remain=50,
  113. tag_uid="",
  114. tray_uuid="",
  115. tray_info_idx="", # No Bambu Lab preset ID
  116. tray_weight=1000,
  117. )
  118. result = await client.sync_ams_tray(tray, "TestPrinter")
  119. assert result is None
  120. @pytest.mark.asyncio
  121. async def test_sync_ams_tray_weight_calculation(self, client, existing_spool):
  122. """Verify remaining weight is calculated correctly for various percentages."""
  123. test_cases = [
  124. (100, 1000, 1000.0), # Full spool
  125. (50, 1000, 500.0), # Half spool
  126. (25, 1000, 250.0), # Quarter spool
  127. (0, 1000, 0.0), # Empty spool
  128. (75, 500, 375.0), # Different spool weight
  129. ]
  130. for remain, weight, expected in test_cases:
  131. tray = AMSTray(
  132. ams_id=0,
  133. tray_id=0,
  134. tray_type="PLA",
  135. tray_sub_brands="PLA Basic",
  136. tray_color="FF0000FF",
  137. remain=remain,
  138. tag_uid="",
  139. tray_uuid="A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4",
  140. tray_info_idx="GFA00",
  141. tray_weight=weight,
  142. )
  143. with (
  144. patch.object(client, "find_spool_by_tag", AsyncMock(return_value=existing_spool)),
  145. patch.object(client, "update_spool", AsyncMock(return_value={"id": 42})) as mock_update,
  146. ):
  147. await client.sync_ams_tray(tray, "TestPrinter", disable_weight_sync=False)
  148. call_kwargs = mock_update.call_args.kwargs
  149. assert call_kwargs["remaining_weight"] == expected, (
  150. f"Expected {expected}g for {remain}% of {weight}g, got {call_kwargs['remaining_weight']}"
  151. )