Coverage for tests/conftest.py: 98%

55 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-13 19:28 +0000

1"""Offline upstream fixtures for API smoke tests.""" 

2 

3from datetime import datetime, timezone 

4from unittest.mock import AsyncMock 

5 

6import httpx 

7import pytest 

8 

9from data_api.data.manager import nthudata 

10from data_api.data.nthudata import Snapshot 

11from data_api.models.courses import CourseData 

12 

13 

14def course_payload(**overrides): 

15 data = { 

16 "id": "11510TEST100000", 

17 "chinese_title": "Test course", 

18 "english_title": "Test Course", 

19 "note": "", 

20 "limit_note": "", 

21 "object": "", 

22 "ge_type": "", 

23 "ge_category": "", 

24 "no_extra_selection": "", 

25 "semester_code": "11510", 

26 "department_code": "TEST", 

27 "course_number": "100000", 

28 "department_name": None, 

29 "college_name": None, 

30 "course_level_code": None, 

31 "course_level": None, 

32 "remote_teaching": None, 

33 "credits": 3, 

34 "capacity": None, 

35 "capacity_unlimited": True, 

36 "enrolled_count": None, 

37 "enrollment_after_selection": None, 

38 "enrollment_after_withdrawal": None, 

39 "reserved_for_freshmen": 0, 

40 "is_cancelled": False, 

41 "language_code": "zh", 

42 "lecturers": [], 

43 "meetings": [], 

44 "expertises": [], 

45 "programs": [], 

46 "curriculum_requirements": [], 

47 "prerequisite_text": "", 

48 "parsing_issues": [], 

49 } 

50 return data | overrides 

51 

52 

53def make_course(data=None, **overrides): 

54 return CourseData.model_validate(course_payload(**((data or {}) | overrides))) 

55 

56 

57def set_courses(monkeypatch, courses, version="course-test-version"): 

58 from data_api.domain.courses.services import courses_service 

59 

60 snapshot = Snapshot([], courses, version, datetime.now(timezone.utc)) 

61 monkeypatch.setattr(courses_service, "get_snapshot", AsyncMock(return_value=snapshot)) 

62 return snapshot 

63 

64 

65NEWSLETTER_NAMES = ["NTHU-Newsletter", "教務處綜合教務組電子報"] 

66 

67 

68@pytest.fixture(autouse=True) 

69def no_external_network(monkeypatch): 

70 async def unavailable(self, request): 

71 return httpx.Response(503, request=request) 

72 

73 monkeypatch.setattr(httpx.AsyncHTTPTransport, "handle_async_request", unavailable) 

74 

75 

76@pytest.fixture 

77def mock_upstream(monkeypatch, no_external_network): 

78 def install(handler): 

79 transport = httpx.MockTransport(handler) 

80 

81 async def handle_request(self, request): 

82 return await transport.handle_async_request(request) 

83 

84 monkeypatch.setattr(httpx.AsyncHTTPTransport, "handle_async_request", handle_request) 

85 

86 return install 

87 

88 

89@pytest.fixture 

90def library_space_payload(): 

91 return { 

92 "resmsg": "成功", 

93 "rows": [ 

94 { 

95 "spacetype": "1", 

96 "spacetypename": "Study room", 

97 "zoneid": "main", 

98 "zonename": "Main library", 

99 "count": "12", 

100 } 

101 ], 

102 } 

103 

104 

105@pytest.fixture 

106def lost_items_html(): 

107 def make(count=12): 

108 header = "<tr><td>序號</td><td>拾獲時間</td><td>拾獲地點</td><td>描述</td></tr>" 

109 rows = "".join( 

110 f"<tr><td>{index}</td><td>2026-09-30</td><td>Main library</td>" 

111 "<td> Book\n title </td></tr>" 

112 for index in range(count) 

113 ) 

114 return f"<table>{header}{rows}</table>" 

115 

116 return make 

117 

118 

119@pytest.fixture 

120def lost_items_empty_page(): 

121 return '<div id="content"><h1>失物招領系統 Lost and Found System</h1>' "<br>目前無資料 !!</div>" 

122 

123 

124@pytest.fixture 

125async def dataset_runtime(): 

126 data = { 

127 "/courses/latest.json": [course_payload()], 

128 "/courses/sources.json": {"semesters": ["11510"]}, 

129 "/buses.json": {}, 

130 "/calendars.json": [], 

131 "/libraries/calendars.json": [], 

132 "/announcements.json": [], 

133 "/announcements_list.json": [], 

134 "/dining.json": [], 

135 "/directory.json": [], 

136 "/newsletters.json": [ 

137 {"name": name, "link": "https://example.com", "details": {}, "articles": []} 

138 for name in NEWSLETTER_NAMES 

139 ], 

140 "/maps.json": { 

141 "main": { 

142 name: {"latitude": "24.79", "longitude": "120.99"} 

143 for name in ["校門", "綜合", "台積", "台達"] 

144 } 

145 }, 

146 } 

147 

148 def handler(request): 

149 if request.url.path == "/file_details.json": 

150 return httpx.Response( 

151 200, 

152 json={ 

153 "file_details": { 

154 "/": [{"name": path.lstrip("/"), "version": "fixture"} for path in data] 

155 } 

156 }, 

157 ) 

158 if request.url.path in data: 

159 return httpx.Response(200, json=data[request.url.path]) 

160 return httpx.Response(404) 

161 

162 async with nthudata.lifespan(httpx.AsyncClient(transport=httpx.MockTransport(handler))): 

163 yield nthudata