test_spool_schemas_storage_location.py 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. """Schema tests for the spool storage_location field (#1291).
  2. Reporter @needo37: the `storage_location` column existed on the Spool ORM model
  3. but was missing from SpoolBase, SpoolUpdate, and SpoolResponse. Pydantic
  4. silently drops unknown fields, so PATCH writes never reached the DB and reads
  5. omitted the field entirely. The fix is purely additive on the schema layer.
  6. """
  7. import pytest
  8. from pydantic import ValidationError
  9. from backend.app.schemas.spool import SpoolCreate, SpoolResponse, SpoolUpdate
  10. class TestStorageLocationRoundtrips:
  11. """The bug was that storage_location wasn't on the schemas at all — pin
  12. the round-trip so a future refactor can't quietly drop it again."""
  13. def test_create_accepts_storage_location(self):
  14. spool = SpoolCreate(material="PLA", storage_location="Drybox #1")
  15. assert spool.storage_location == "Drybox #1"
  16. def test_create_storage_location_optional(self):
  17. spool = SpoolCreate(material="PLA")
  18. assert spool.storage_location is None
  19. def test_update_accepts_storage_location(self):
  20. update = SpoolUpdate(storage_location="Top shelf")
  21. assert update.storage_location == "Top shelf"
  22. def test_update_omits_unset_storage_location(self):
  23. """A PATCH that doesn't mention storage_location must NOT clear it —
  24. model_dump(exclude_unset=True) keeps the field out of the update dict
  25. so the route's setattr loop skips it."""
  26. update = SpoolUpdate.model_validate({})
  27. dumped = update.model_dump(exclude_unset=True)
  28. assert "storage_location" not in dumped
  29. def test_update_explicit_null_clears_storage_location(self):
  30. """A PATCH that explicitly sends storage_location=null must reach
  31. the route's update_data dict as None, so setattr writes NULL to the
  32. DB — that's how the UI clears the field."""
  33. update = SpoolUpdate.model_validate({"storage_location": None})
  34. dumped = update.model_dump(exclude_unset=True)
  35. assert "storage_location" in dumped
  36. assert dumped["storage_location"] is None
  37. def test_response_carries_storage_location(self):
  38. """SpoolResponse inherits from SpoolBase, so the field must surface
  39. on read too — otherwise the inventory table silently always shows '-'."""
  40. from datetime import datetime, timezone
  41. now = datetime.now(timezone.utc)
  42. response = SpoolResponse.model_validate(
  43. {
  44. "id": 1,
  45. "material": "PLA",
  46. "storage_location": "Drybox #1",
  47. "created_at": now,
  48. "updated_at": now,
  49. }
  50. )
  51. assert response.storage_location == "Drybox #1"
  52. class TestStorageLocationLength:
  53. """The DB column is String(255). Schema must enforce the same cap so the
  54. API rejects too-long input cleanly instead of letting SQLAlchemy raise."""
  55. def test_accepts_max_length(self):
  56. update = SpoolUpdate(storage_location="x" * 255)
  57. assert len(update.storage_location) == 255
  58. def test_rejects_over_max_length(self):
  59. with pytest.raises(ValidationError, match="storage_location"):
  60. SpoolUpdate(storage_location="x" * 256)