test_spoolman_service.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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 patch.object(client, "find_spool_by_tag", AsyncMock(return_value=existing_spool)):
  49. with patch.object(client, "update_spool", AsyncMock(return_value={"id": 42})) as mock_update:
  50. await client.sync_ams_tray(sample_tray, "TestPrinter")
  51. mock_update.assert_called_once()
  52. call_kwargs = mock_update.call_args.kwargs
  53. assert "remaining_weight" in call_kwargs
  54. assert call_kwargs["remaining_weight"] == 500.0 # 50% of 1000g
  55. assert "location" in call_kwargs
  56. @pytest.mark.asyncio
  57. async def test_sync_ams_tray_skips_weight_when_disabled(self, client, sample_tray, existing_spool):
  58. """Verify sync_ams_tray skips remaining_weight when disable_weight_sync=True."""
  59. with patch.object(client, "find_spool_by_tag", AsyncMock(return_value=existing_spool)):
  60. with patch.object(client, "update_spool", AsyncMock(return_value={"id": 42})) as mock_update:
  61. await client.sync_ams_tray(sample_tray, "TestPrinter", disable_weight_sync=True)
  62. mock_update.assert_called_once()
  63. call_kwargs = mock_update.call_args.kwargs
  64. # remaining_weight should be None (not updated)
  65. assert call_kwargs.get("remaining_weight") is None
  66. # location should still be updated
  67. assert "location" in call_kwargs
  68. assert "TestPrinter" in call_kwargs["location"]
  69. @pytest.mark.asyncio
  70. async def test_sync_ams_tray_new_spool_always_includes_weight(
  71. self, client, sample_tray, mock_filament
  72. ):
  73. """Verify new spool creation always includes remaining_weight even when disabled."""
  74. with patch.object(client, "find_spool_by_tag", AsyncMock(return_value=None)):
  75. with patch.object(client, "_find_or_create_filament", AsyncMock(return_value=mock_filament)):
  76. with patch.object(client, "create_spool", AsyncMock(return_value={"id": 99})) as mock_create:
  77. await client.sync_ams_tray(sample_tray, "TestPrinter", disable_weight_sync=True)
  78. mock_create.assert_called_once()
  79. call_kwargs = mock_create.call_args.kwargs
  80. # New spools should ALWAYS include remaining_weight
  81. assert "remaining_weight" in call_kwargs
  82. assert call_kwargs["remaining_weight"] == 500.0 # 50% of 1000g
  83. @pytest.mark.asyncio
  84. async def test_sync_ams_tray_location_format(self, client, sample_tray, existing_spool):
  85. """Verify location format is correct when updating spool."""
  86. with patch.object(client, "find_spool_by_tag", AsyncMock(return_value=existing_spool)):
  87. with patch.object(client, "update_spool", AsyncMock(return_value={"id": 42})) as mock_update:
  88. await client.sync_ams_tray(sample_tray, "My Printer", disable_weight_sync=True)
  89. call_kwargs = mock_update.call_args.kwargs
  90. # Location should follow pattern: "PrinterName - AMS A1"
  91. assert "location" in call_kwargs
  92. assert "My Printer" in call_kwargs["location"]
  93. assert "AMS" in call_kwargs["location"]
  94. @pytest.mark.asyncio
  95. async def test_sync_ams_tray_skips_non_bambu_spool(self, client):
  96. """Verify non-Bambu Lab spools are skipped."""
  97. # Third-party spool without proper identifiers
  98. tray = AMSTray(
  99. ams_id=0,
  100. tray_id=0,
  101. tray_type="PLA",
  102. tray_sub_brands="Third Party PLA",
  103. tray_color="FF0000FF",
  104. remain=50,
  105. tag_uid="",
  106. tray_uuid="",
  107. tray_info_idx="", # No Bambu Lab preset ID
  108. tray_weight=1000,
  109. )
  110. result = await client.sync_ams_tray(tray, "TestPrinter")
  111. assert result is None
  112. @pytest.mark.asyncio
  113. async def test_sync_ams_tray_weight_calculation(self, client, existing_spool):
  114. """Verify remaining weight is calculated correctly for various percentages."""
  115. test_cases = [
  116. (100, 1000, 1000.0), # Full spool
  117. (50, 1000, 500.0), # Half spool
  118. (25, 1000, 250.0), # Quarter spool
  119. (0, 1000, 0.0), # Empty spool
  120. (75, 500, 375.0), # Different spool weight
  121. ]
  122. for remain, weight, expected in test_cases:
  123. tray = AMSTray(
  124. ams_id=0,
  125. tray_id=0,
  126. tray_type="PLA",
  127. tray_sub_brands="PLA Basic",
  128. tray_color="FF0000FF",
  129. remain=remain,
  130. tag_uid="",
  131. tray_uuid="A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4",
  132. tray_info_idx="GFA00",
  133. tray_weight=weight,
  134. )
  135. with patch.object(client, "find_spool_by_tag", AsyncMock(return_value=existing_spool)):
  136. with patch.object(client, "update_spool", AsyncMock(return_value={"id": 42})) as mock_update:
  137. await client.sync_ams_tray(tray, "TestPrinter", disable_weight_sync=False)
  138. call_kwargs = mock_update.call_args.kwargs
  139. assert call_kwargs["remaining_weight"] == expected, (
  140. f"Expected {expected}g for {remain}% of {weight}g, got {call_kwargs['remaining_weight']}"
  141. )