test_spool_reset_usage.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. """Reset-usage endpoint regressions (#1390 follow-up).
  2. The per-spool and bulk reset endpoints zero `weight_used` without touching
  3. `weight_locked`. They exist because PATCH /spools/{id} auto-locks the spool
  4. when weight_used is set explicitly, and that's wrong for the "clean-slate
  5. my Total Consumed stat" workflow — the user wants the spool to keep
  6. receiving AMS auto-sync updates from the next print onward.
  7. """
  8. import pytest
  9. from httpx import AsyncClient
  10. from sqlalchemy import select
  11. from sqlalchemy.ext.asyncio import AsyncSession
  12. from backend.app.models.spool import Spool
  13. @pytest.fixture
  14. async def spool_factory(db_session: AsyncSession):
  15. """Create a Spool with sensible defaults."""
  16. async def _create(**kwargs):
  17. defaults = {
  18. "material": "PLA",
  19. "subtype": "Basic",
  20. "brand": "Bambu",
  21. "color_name": "Red",
  22. "rgba": "FF0000FF",
  23. "label_weight": 1000,
  24. "weight_used": 0,
  25. "weight_locked": False,
  26. }
  27. defaults.update(kwargs)
  28. spool = Spool(**defaults)
  29. db_session.add(spool)
  30. await db_session.commit()
  31. await db_session.refresh(spool)
  32. return spool
  33. return _create
  34. class TestResetSpoolUsage:
  35. @pytest.mark.asyncio
  36. @pytest.mark.integration
  37. async def test_reset_zeroes_weight_used(self, async_client: AsyncClient, spool_factory, db_session):
  38. """Endpoint sets weight_used to 0."""
  39. spool = await spool_factory(weight_used=234.5)
  40. response = await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-usage")
  41. assert response.status_code == 200
  42. assert response.json()["weight_used"] == 0
  43. await db_session.refresh(spool)
  44. assert spool.weight_used == 0
  45. @pytest.mark.asyncio
  46. @pytest.mark.integration
  47. async def test_reset_does_not_lock_spool(self, async_client: AsyncClient, spool_factory, db_session):
  48. """Reset must leave weight_locked alone.
  49. PATCH /spools/{id} auto-locks when weight_used is set explicitly;
  50. the dedicated reset endpoint must NOT, because the user's intent
  51. is "track fresh from zero", not "freeze at zero forever".
  52. """
  53. spool = await spool_factory(weight_used=100.0, weight_locked=False)
  54. response = await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-usage")
  55. assert response.status_code == 200
  56. await db_session.refresh(spool)
  57. assert spool.weight_used == 0
  58. assert spool.weight_locked is False, "Reset must not auto-lock the spool"
  59. @pytest.mark.asyncio
  60. @pytest.mark.integration
  61. async def test_reset_preserves_existing_lock(self, async_client: AsyncClient, spool_factory, db_session):
  62. """If the user previously locked the spool, the lock is preserved."""
  63. spool = await spool_factory(weight_used=500.0, weight_locked=True)
  64. response = await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-usage")
  65. assert response.status_code == 200
  66. await db_session.refresh(spool)
  67. assert spool.weight_used == 0
  68. assert spool.weight_locked is True, "Pre-existing lock must be preserved"
  69. @pytest.mark.asyncio
  70. @pytest.mark.integration
  71. async def test_reset_404_for_missing_spool(self, async_client: AsyncClient):
  72. response = await async_client.post("/api/v1/inventory/spools/99999/reset-usage")
  73. assert response.status_code == 404
  74. class TestBulkResetSpoolUsage:
  75. @pytest.mark.asyncio
  76. @pytest.mark.integration
  77. async def test_bulk_reset_zeroes_only_listed_spools(self, async_client: AsyncClient, spool_factory, db_session):
  78. """Only spools in the request are reset; others are untouched."""
  79. target1 = await spool_factory(weight_used=100.0)
  80. target2 = await spool_factory(weight_used=200.0)
  81. untouched = await spool_factory(weight_used=300.0)
  82. response = await async_client.post(
  83. "/api/v1/inventory/spools/reset-usage-bulk",
  84. json={"spool_ids": [target1.id, target2.id]},
  85. )
  86. assert response.status_code == 200
  87. assert response.json() == {"reset": 2}
  88. # The endpoint commits via its own session — expire our session so the
  89. # next read pulls fresh values rather than the cached pre-reset state.
  90. db_session.expire_all()
  91. spools = (await db_session.execute(select(Spool))).scalars().all()
  92. by_id = {s.id: s for s in spools}
  93. assert by_id[target1.id].weight_used == 0
  94. assert by_id[target2.id].weight_used == 0
  95. assert by_id[untouched.id].weight_used == 300.0, "Spool not in request must keep its usage"
  96. @pytest.mark.asyncio
  97. @pytest.mark.integration
  98. async def test_bulk_reset_rejects_empty_list(self, async_client: AsyncClient):
  99. """Empty list must be rejected — guards against accidental wildcard wipes."""
  100. response = await async_client.post(
  101. "/api/v1/inventory/spools/reset-usage-bulk",
  102. json={"spool_ids": []},
  103. )
  104. assert response.status_code == 400
  105. @pytest.mark.asyncio
  106. @pytest.mark.integration
  107. async def test_bulk_reset_rejects_missing_field(self, async_client: AsyncClient):
  108. """Missing spool_ids field must be rejected."""
  109. response = await async_client.post(
  110. "/api/v1/inventory/spools/reset-usage-bulk",
  111. json={},
  112. )
  113. assert response.status_code == 400
  114. @pytest.mark.asyncio
  115. @pytest.mark.integration
  116. async def test_bulk_reset_does_not_lock_spools(self, async_client: AsyncClient, spool_factory, db_session):
  117. """Bulk reset preserves weight_locked across all targets."""
  118. unlocked = await spool_factory(weight_used=100.0, weight_locked=False)
  119. locked = await spool_factory(weight_used=200.0, weight_locked=True)
  120. response = await async_client.post(
  121. "/api/v1/inventory/spools/reset-usage-bulk",
  122. json={"spool_ids": [unlocked.id, locked.id]},
  123. )
  124. assert response.status_code == 200
  125. await db_session.refresh(unlocked)
  126. await db_session.refresh(locked)
  127. assert unlocked.weight_used == 0 and unlocked.weight_locked is False
  128. assert locked.weight_used == 0 and locked.weight_locked is True