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

1"""Response fields distinguish optional metadata from required dataset structure.""" 

2 

3import pytest 

4from pydantic import ValidationError 

5 

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 

16 

17 

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) 

66 

67 

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) 

74 

75 

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) 

90 

91 

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") 

96 

97 

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"