test_spoolbuddy_spoolman_mode_unusable.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. """SpoolBuddy with Spoolman switched on but not usable.
  2. With Spoolman enabled and a URL set, the spools are Spoolman's, even while
  3. that URL is refused. A tag lookup, a tag write and a scale reading then report
  4. Spoolman as unavailable instead of reading or writing the built-in inventory,
  5. whose spool ids are not Spoolman's. Switched on without a URL is the built-in
  6. inventory, as every SpoolBuddy screen shows it.
  7. """
  8. from datetime import datetime, timezone
  9. from unittest.mock import AsyncMock, patch
  10. import pytest
  11. from httpx import AsyncClient
  12. from sqlalchemy import select
  13. from backend.app.models.settings import Settings
  14. from backend.app.models.spool import Spool
  15. from backend.app.models.spoolbuddy_device import SpoolBuddyDevice
  16. API = "/api/v1/spoolbuddy"
  17. @pytest.fixture
  18. async def unusable_spoolman(db_session):
  19. db_session.add(Settings(key="spoolman_enabled", value="true"))
  20. # A cloud metadata address, which the URL guard refuses.
  21. db_session.add(Settings(key="spoolman_url", value="http://169.254.169.254"))
  22. db_session.add(
  23. SpoolBuddyDevice(
  24. device_id="sb-1",
  25. hostname="spoolbuddy-1",
  26. ip_address="10.0.0.1",
  27. firmware_version="1.0.0",
  28. has_nfc=True,
  29. has_scale=True,
  30. tare_offset=0,
  31. calibration_factor=1.0,
  32. last_seen=datetime.now(timezone.utc),
  33. )
  34. )
  35. spool = Spool(
  36. material="PLA",
  37. label_weight=1000,
  38. core_weight=250,
  39. weight_used=100,
  40. tag_uid="AABB1122",
  41. )
  42. db_session.add(spool)
  43. await db_session.commit()
  44. return spool.id
  45. @pytest.mark.asyncio
  46. @pytest.mark.integration
  47. async def test_a_scanned_tag_is_not_looked_up_in_the_built_in_inventory(async_client: AsyncClient, unusable_spoolman):
  48. with patch("backend.app.api.routes.spoolbuddy.ws_manager") as mock_ws:
  49. mock_ws.broadcast = AsyncMock()
  50. resp = await async_client.post(f"{API}/nfc/tag-scanned", json={"device_id": "sb-1", "tag_uid": "AABB1122"})
  51. assert resp.status_code == 200
  52. assert resp.json()["matched"] is False
  53. sent = [call.args[0]["type"] for call in mock_ws.broadcast.call_args_list]
  54. assert "spoolman_unavailable" in sent
  55. assert "spoolbuddy_tag_matched" not in sent
  56. @pytest.mark.asyncio
  57. @pytest.mark.integration
  58. async def test_a_tag_write_is_refused(async_client: AsyncClient, unusable_spoolman, db_session):
  59. with patch("backend.app.api.routes.spoolbuddy.ws_manager") as mock_ws:
  60. mock_ws.broadcast = AsyncMock()
  61. resp = await async_client.post(
  62. f"{API}/nfc/write-tag", json={"device_id": "sb-1", "spool_id": unusable_spoolman}
  63. )
  64. assert resp.status_code == 503
  65. device = await db_session.scalar(select(SpoolBuddyDevice).where(SpoolBuddyDevice.device_id == "sb-1"))
  66. await db_session.refresh(device)
  67. assert device.pending_command is None
  68. @pytest.mark.asyncio
  69. @pytest.mark.integration
  70. async def test_a_scale_reading_does_not_touch_the_built_in_spool(
  71. async_client: AsyncClient, unusable_spoolman, db_session
  72. ):
  73. resp = await async_client.post(
  74. f"{API}/scale/update-spool-weight", json={"spool_id": unusable_spoolman, "weight_grams": 600}
  75. )
  76. assert resp.status_code == 503
  77. spool = await db_session.get(Spool, unusable_spoolman)
  78. await db_session.refresh(spool)
  79. assert spool.weight_used == 100
  80. @pytest.mark.asyncio
  81. @pytest.mark.integration
  82. async def test_a_tag_written_after_spoolman_was_switched_off_reports_why(async_client: AsyncClient, db_session):
  83. """The write began in Spoolman mode; by the time the tag is written
  84. Spoolman is off, so the link cannot be stored."""
  85. import json
  86. db_session.add(
  87. SpoolBuddyDevice(
  88. device_id="sb-2",
  89. hostname="spoolbuddy-2",
  90. ip_address="10.0.0.2",
  91. firmware_version="1.0.0",
  92. has_nfc=True,
  93. has_scale=True,
  94. tare_offset=0,
  95. calibration_factor=1.0,
  96. last_seen=datetime.now(timezone.utc),
  97. pending_command="write_tag",
  98. pending_write_payload=json.dumps({"spool_id": 9, "ndef_data_hex": "deadbeef", "data_origin": "spoolman"}),
  99. )
  100. )
  101. await db_session.commit()
  102. with patch("backend.app.api.routes.spoolbuddy.ws_manager") as mock_ws:
  103. mock_ws.broadcast = AsyncMock()
  104. resp = await async_client.post(
  105. f"{API}/nfc/write-result",
  106. json={"device_id": "sb-2", "spool_id": 9, "tag_uid": "AABBCCDD", "success": True},
  107. )
  108. assert resp.status_code == 502
  109. msg = mock_ws.broadcast.call_args[0][0]
  110. assert (msg["type"], msg["reason"]) == ("spoolbuddy_tag_link_failed", "spoolman_not_configured")
  111. @pytest.mark.asyncio
  112. @pytest.mark.integration
  113. async def test_switched_on_without_a_url_is_the_built_in_inventory(async_client: AsyncClient, db_session):
  114. db_session.add(Settings(key="spoolman_enabled", value="true"))
  115. db_session.add(Settings(key="spoolman_url", value=""))
  116. spool = Spool(material="PLA", label_weight=1000, core_weight=250, weight_used=100)
  117. db_session.add(spool)
  118. await db_session.commit()
  119. resp = await async_client.post(f"{API}/scale/update-spool-weight", json={"spool_id": spool.id, "weight_grams": 600})
  120. assert resp.status_code == 200
  121. await db_session.refresh(spool)
  122. assert spool.weight_used == 650