ghost-shopper-api / python /tests /test_visit_model.py
azzaraqi
Deploy FastAPI backend with Supabase pooler support
b84ea83
Raw
History Blame Contribute Delete
5.15 kB
"""
Unit Tests β€” Pydantic Visit/Checklist/Photo Schemas
Tests:
- VisitCreate: required fields, validation rules, defaults
- VisitUpdate: all-optional, partial updates
- VisitResponse: from_attributes (ORM β†’ Pydantic)
- ChecklistCreate / ChecklistUpdate
- PhotoModel schema
"""
from datetime import datetime, timezone
import pytest
from pydantic import ValidationError
from models.visit_model import Visit, VisitCreate, VisitUpdate, VisitResponse
from models.checklist_model import Checklist, ChecklistCreate, ChecklistUpdate
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# VisitCreate
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class TestVisitCreate:
def _valid(self, **overrides):
return {
"title": "Site Inspection",
"visit_date": datetime(2026, 5, 1, 9, 0, tzinfo=timezone.utc),
**overrides,
}
def test_valid_minimal(self):
v = VisitCreate(**self._valid())
assert v.title == "Site Inspection"
assert v.status == "pending" # default
assert v.description is None
assert v.location is None
def test_valid_full(self):
v = VisitCreate(**self._valid(
description="Monthly check",
location="Gedung A",
status="in_progress",
))
assert v.location == "Gedung A"
assert v.status == "in_progress"
def test_title_required(self):
with pytest.raises(ValidationError) as exc_info:
VisitCreate(visit_date=datetime(2026, 5, 1, 9, 0))
errors = exc_info.value.errors()
assert any(e["loc"] == ("title",) for e in errors)
def test_title_empty_string_invalid(self):
with pytest.raises(ValidationError):
VisitCreate(**self._valid(title=""))
def test_title_too_long(self):
with pytest.raises(ValidationError):
VisitCreate(**self._valid(title="x" * 256))
def test_visit_date_required(self):
with pytest.raises(ValidationError):
VisitCreate(title="Test")
def test_location_max_length(self):
with pytest.raises(ValidationError):
VisitCreate(**self._valid(location="L" * 256))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# VisitUpdate
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class TestVisitUpdate:
def test_all_optional_empty(self):
u = VisitUpdate()
assert u.title is None
assert u.status is None
def test_partial_update(self):
u = VisitUpdate(status="completed")
assert u.status == "completed"
assert u.title is None
def test_title_empty_invalid(self):
with pytest.raises(ValidationError):
VisitUpdate(title="")
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# VisitResponse β€” ORM round-trip
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class TestVisitResponse:
def test_from_orm(self, sample_visit: Visit):
"""VisitResponse must serialize from an ORM object (from_attributes)."""
resp = VisitResponse.model_validate(sample_visit)
assert str(resp.id) == str(sample_visit.id)
assert resp.title == sample_visit.title
assert resp.status == sample_visit.status
# Mark as async because sample_visit is an async fixture
@pytest.mark.asyncio
async def test_from_orm_async(self, sample_visit):
resp = VisitResponse.model_validate(sample_visit)
assert resp.title == "Test Site Inspection"
assert resp.location == "Gedung A"
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ChecklistCreate / ChecklistUpdate
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class TestChecklistSchemas:
def test_create_minimal(self):
c = ChecklistCreate(item_name="Panel check")
assert c.item_name == "Panel check"
assert c.is_checked is False # default
assert c.notes is None
def test_create_with_notes(self):
c = ChecklistCreate(item_name="Grounding", is_checked=True, notes="OK")
assert c.is_checked is True
assert c.notes == "OK"
def test_create_empty_name_invalid(self):
with pytest.raises(ValidationError):
ChecklistCreate(item_name="")
def test_update_all_optional(self):
u = ChecklistUpdate()
assert u.is_checked is None
assert u.item_name is None
def test_update_toggle(self):
u = ChecklistUpdate(is_checked=True)
assert u.is_checked is True