Coverage for tests/test_schemas.py: 100%
43 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-13 19:28 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-13 19:28 +0000
1"""Response fields distinguish optional metadata from required dataset structure."""
3import pytest
4from pydantic import ValidationError
6from data_api.api.schemas.announcements import AnnouncementArticle, AnnouncementSource
7from data_api.api.schemas.calendars import Calendar, CalendarEvent
8from data_api.api.schemas.departments import DepartmentPerson
9from data_api.api.schemas.dining import DiningRestaurant
10from data_api.api.schemas.libraries import (
11 LibraryRssData,
12 LibraryRssImage,
13 LibraryRssItem,
14)
15from data_api.api.schemas.newsletters import NewsletterArticle
18@pytest.mark.parametrize(
19 "model,payload,optional_fields",
20 [
21 (AnnouncementArticle, {}, {"title", "link", "date"}),
22 (NewsletterArticle, {}, {"title", "link", "date"}),
23 (DepartmentPerson, {"name": "Test"}, {"title", "extension", "email", "note"}),
24 (
25 DiningRestaurant,
26 {"area": "Test", "name": "Test", "note": "", "phone": "", "schedule": {}},
27 {"image"},
28 ),
29 (LibraryRssImage, {}, {"url", "title", "link"}),
30 (
31 LibraryRssItem,
32 {"title": "Test", "description": ""},
33 {"guid", "category", "link", "pubDate", "author", "image"},
34 ),
35 (LibraryRssData, {"link": "https://example.com"}, {"title", "date"}),
36 (
37 Calendar,
38 {"id": "library-main"},
39 {"name", "description", "timezone", "url", "category", "source"},
40 ),
41 (
42 CalendarEvent,
43 {
44 "id": "event",
45 "title": "Test",
46 "start": "2026-09-28",
47 "end": "2026-09-29",
48 "all_day": True,
49 },
50 {"description"},
51 ),
52 ],
53)
54def test_optional_metadata_can_be_omitted_or_null(model, payload, optional_fields):
55 omitted = model.model_validate(payload)
56 explicit_null = model.model_validate({**payload, **{name: None for name in optional_fields}})
57 assert omitted.model_dump() == explicit_null.model_dump()
58 for name in optional_fields:
59 assert getattr(omitted, name) is None
60 assert not model.model_fields[name].is_required()
61 for name in payload:
62 assert model.model_fields[name].is_required()
63 missing_required = {key: value for key, value in payload.items() if key != name}
64 with pytest.raises(ValidationError):
65 model.model_validate(missing_required)
68@pytest.mark.parametrize("invalid_link", ["https://", "not a url"])
69def test_optional_urls_still_validate_supplied_values(invalid_link):
70 with pytest.raises(ValidationError):
71 AnnouncementArticle(link=invalid_link)
72 with pytest.raises(ValidationError):
73 Calendar(id="library-main", url=invalid_link)
76@pytest.mark.parametrize("field", ["title", "link", "language", "department"])
77def test_announcement_source_requires_metadata(field):
78 payload = {
79 "title": "News",
80 "link": "https://example.com/news",
81 "language": "en",
82 "department": "Office",
83 }
84 missing_field_payload = {key: value for key, value in payload.items() if key != field}
85 with pytest.raises(ValidationError):
86 AnnouncementSource.model_validate(missing_field_payload)
87 null_field_payload = {**payload, field: None}
88 with pytest.raises(ValidationError):
89 AnnouncementSource.model_validate(null_field_payload)
92@pytest.mark.parametrize("link", ["https://", "not a url", "/news", 42, True, [], {}])
93def test_announcement_source_rejects_invalid_http_urls(link):
94 with pytest.raises(ValidationError):
95 AnnouncementSource(title="News", link=link, language="en", department="Office")
98def test_announcement_source_corrects_protocol_relative_urls():
99 source = AnnouncementSource(
100 title="News", link="//example.com/news", language="en", department="Office"
101 )
102 assert source.model_dump(mode="json")["link"] == "https://example.com/news"