|
|
@@ -0,0 +1,76 @@
|
|
|
+"""Schema tests for the spool storage_location field (#1291).
|
|
|
+
|
|
|
+Reporter @needo37: the `storage_location` column existed on the Spool ORM model
|
|
|
+but was missing from SpoolBase, SpoolUpdate, and SpoolResponse. Pydantic
|
|
|
+silently drops unknown fields, so PATCH writes never reached the DB and reads
|
|
|
+omitted the field entirely. The fix is purely additive on the schema layer.
|
|
|
+"""
|
|
|
+
|
|
|
+import pytest
|
|
|
+from pydantic import ValidationError
|
|
|
+
|
|
|
+from backend.app.schemas.spool import SpoolCreate, SpoolResponse, SpoolUpdate
|
|
|
+
|
|
|
+
|
|
|
+class TestStorageLocationRoundtrips:
|
|
|
+ """The bug was that storage_location wasn't on the schemas at all — pin
|
|
|
+ the round-trip so a future refactor can't quietly drop it again."""
|
|
|
+
|
|
|
+ def test_create_accepts_storage_location(self):
|
|
|
+ spool = SpoolCreate(material="PLA", storage_location="Drybox #1")
|
|
|
+ assert spool.storage_location == "Drybox #1"
|
|
|
+
|
|
|
+ def test_create_storage_location_optional(self):
|
|
|
+ spool = SpoolCreate(material="PLA")
|
|
|
+ assert spool.storage_location is None
|
|
|
+
|
|
|
+ def test_update_accepts_storage_location(self):
|
|
|
+ update = SpoolUpdate(storage_location="Top shelf")
|
|
|
+ assert update.storage_location == "Top shelf"
|
|
|
+
|
|
|
+ def test_update_omits_unset_storage_location(self):
|
|
|
+ """A PATCH that doesn't mention storage_location must NOT clear it —
|
|
|
+ model_dump(exclude_unset=True) keeps the field out of the update dict
|
|
|
+ so the route's setattr loop skips it."""
|
|
|
+ update = SpoolUpdate.model_validate({})
|
|
|
+ dumped = update.model_dump(exclude_unset=True)
|
|
|
+ assert "storage_location" not in dumped
|
|
|
+
|
|
|
+ def test_update_explicit_null_clears_storage_location(self):
|
|
|
+ """A PATCH that explicitly sends storage_location=null must reach
|
|
|
+ the route's update_data dict as None, so setattr writes NULL to the
|
|
|
+ DB — that's how the UI clears the field."""
|
|
|
+ update = SpoolUpdate.model_validate({"storage_location": None})
|
|
|
+ dumped = update.model_dump(exclude_unset=True)
|
|
|
+ assert "storage_location" in dumped
|
|
|
+ assert dumped["storage_location"] is None
|
|
|
+
|
|
|
+ def test_response_carries_storage_location(self):
|
|
|
+ """SpoolResponse inherits from SpoolBase, so the field must surface
|
|
|
+ on read too — otherwise the inventory table silently always shows '-'."""
|
|
|
+ from datetime import datetime, timezone
|
|
|
+
|
|
|
+ now = datetime.now(timezone.utc)
|
|
|
+ response = SpoolResponse.model_validate(
|
|
|
+ {
|
|
|
+ "id": 1,
|
|
|
+ "material": "PLA",
|
|
|
+ "storage_location": "Drybox #1",
|
|
|
+ "created_at": now,
|
|
|
+ "updated_at": now,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ assert response.storage_location == "Drybox #1"
|
|
|
+
|
|
|
+
|
|
|
+class TestStorageLocationLength:
|
|
|
+ """The DB column is String(255). Schema must enforce the same cap so the
|
|
|
+ API rejects too-long input cleanly instead of letting SQLAlchemy raise."""
|
|
|
+
|
|
|
+ def test_accepts_max_length(self):
|
|
|
+ update = SpoolUpdate(storage_location="x" * 255)
|
|
|
+ assert len(update.storage_location) == 255
|
|
|
+
|
|
|
+ def test_rejects_over_max_length(self):
|
|
|
+ with pytest.raises(ValidationError, match="storage_location"):
|
|
|
+ SpoolUpdate(storage_location="x" * 256)
|