Coverage for app/backend/src/tests/test_dashboard.py: 98%

98 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-22 18:39 +0000

1from datetime import timedelta 

2 

3import pytest 

4from google.protobuf import empty_pb2 

5 

6from couchers.constants import HOST_REQUEST_MIN_LENGTH_UTF16 

7from couchers.db import session_scope 

8from couchers.proto import conversations_pb2, dashboard_pb2, discussions_pb2, events_pb2, requests_pb2 

9from couchers.utils import Timestamp_from_datetime, now, today 

10from tests.fixtures.db import generate_user 

11from tests.fixtures.sessions import ( 

12 account_session, 

13 dashboard_session, 

14 discussions_session, 

15 events_session, 

16 requests_session, 

17) 

18from tests.test_communities import create_community 

19 

20 

21@pytest.fixture(autouse=True) 

22def _(testconfig): 

23 pass 

24 

25 

26UPCOMING_STATUSES = [ 

27 conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

28 conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED, 

29] 

30 

31 

32def valid_request_text(text: str = "Test request") -> str: 

33 """Pads a request text to a valid length (measured in utf-16 code units, matching the frontend).""" 

34 utf16_length = len(text.encode("utf-16-le")) // 2 

35 if utf16_length >= HOST_REQUEST_MIN_LENGTH_UTF16: 35 ↛ 36line 35 didn't jump to line 36 because the condition on line 35 was never true

36 return text 

37 return text + ("_" * (HOST_REQUEST_MIN_LENGTH_UTF16 - utf16_length)) 

38 

39 

40def _setup_accepted_host_request(token_surfer, host_user_id, moderator): 

41 from_date = today() + timedelta(days=2) 

42 to_date = today() + timedelta(days=3) 

43 with requests_session(token_surfer) as api: 

44 host_request_id = api.CreateHostRequest( 

45 requests_pb2.CreateHostRequestReq( 

46 host_user_id=host_user_id, 

47 from_date=from_date.isoformat(), 

48 to_date=to_date.isoformat(), 

49 text=valid_request_text(), 

50 ) 

51 ).host_request_id 

52 moderator.approve_host_request(host_request_id) 

53 return host_request_id 

54 

55 

56def test_GetDashboardV2_matches_individual_rpcs(db, moderator): 

57 user1, token1 = generate_user() 

58 user2, token2 = generate_user() 

59 

60 host_request_id = _setup_accepted_host_request(token1, user2.id, moderator) 

61 with requests_session(token2) as api: 

62 api.RespondHostRequest( 

63 requests_pb2.RespondHostRequestReq( 

64 host_request_id=host_request_id, 

65 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

66 text="Sure, come on over!", 

67 ) 

68 ) 

69 

70 # the dashboard response must be identical to fanning out to the individual RPCs with the 

71 # same parameters the web frontend uses 

72 with requests_session(token1) as api: 

73 surfing = api.ListHostRequests( 

74 requests_pb2.ListHostRequestsReq( 

75 only_sent=True, 

76 only_active=True, 

77 status_in=UPCOMING_STATUSES, 

78 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

79 ) 

80 ) 

81 hosting = api.ListHostRequests( 

82 requests_pb2.ListHostRequestsReq( 

83 only_received=True, 

84 only_active=True, 

85 status_in=UPCOMING_STATUSES, 

86 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

87 ) 

88 ) 

89 with events_session(token1) as api: 

90 my_events = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=3)) 

91 community_events = api.ListMyEvents( 

92 events_pb2.ListMyEventsReq( 

93 page_size=3, my_communities=True, my_communities_exclude_global=True, exclude_attending=True 

94 ) 

95 ) 

96 with discussions_session(token1) as api: 

97 discussions = api.ListMyCommunitiesDiscussions(discussions_pb2.ListMyCommunitiesDiscussionsReq(page_size=3)) 

98 with account_session(token1) as api: 

99 reminders = api.GetReminders(empty_pb2.Empty()) 

100 

101 with dashboard_session(token1) as api: 

102 res = api.GetDashboardV2(dashboard_pb2.GetDashboardV2Req()) 

103 

104 assert res.reminders == reminders 

105 assert res.surfing == surfing 

106 assert res.hosting == hosting 

107 assert res.my_events == my_events 

108 assert res.community_events == community_events 

109 assert res.discussions == discussions 

110 

111 # the surfer sees their upcoming trip under surfing, nothing under hosting 

112 assert len(res.surfing.host_requests) == 1 

113 assert res.surfing.host_requests[0].host_request_id == host_request_id 

114 assert res.surfing.host_requests[0].status == conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED 

115 assert len(res.hosting.host_requests) == 0 

116 

117 

118def test_GetDashboardV2_buckets_by_role(db, moderator): 

119 user1, token1 = generate_user() 

120 user2, token2 = generate_user() 

121 

122 host_request_id = _setup_accepted_host_request(token1, user2.id, moderator) 

123 with requests_session(token2) as api: 

124 api.RespondHostRequest( 

125 requests_pb2.RespondHostRequestReq( 

126 host_request_id=host_request_id, 

127 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

128 text="Sure, come on over!", 

129 ) 

130 ) 

131 

132 # the host sees the upcoming stay under hosting, nothing under surfing 

133 with dashboard_session(token2) as api: 

134 res = api.GetDashboardV2(dashboard_pb2.GetDashboardV2Req()) 

135 assert len(res.hosting.host_requests) == 1 

136 assert res.hosting.host_requests[0].host_request_id == host_request_id 

137 assert len(res.surfing.host_requests) == 0 

138 

139 

140def test_GetDashboardV2_community_events_excludes_attending(db, moderator): 

141 # Pins the exclude_attending parameter the web frontend sends: an event the user is 

142 # attending shows under my_events and must not also duplicate into community_events. 

143 user1, token1 = generate_user() 

144 user2, token2 = generate_user() 

145 

146 with session_scope() as session: 

147 # community_events excludes global-level communities, so nest down to a subregion 

148 world = create_community(session, 0, 100, "World", [user1, user2], [], None) 

149 macroregion = create_community(session, 0, 100, "Macroregion", [user1, user2], [], world) 

150 region = create_community(session, 0, 100, "Region", [user1, user2], [], macroregion) 

151 subregion = create_community(session, 0, 100, "Subregion", [user1, user2], [], region) 

152 community_id = subregion.id 

153 

154 start = now() 

155 

156 def make_event(hours: int) -> events_pb2.CreateEventReq: 

157 return events_pb2.CreateEventReq( 

158 title="Test Event", 

159 content="Test content.", 

160 location=events_pb2.EventLocation(address="Near Null Island", lat=0.1, lng=0.2), 

161 parent_community_id=community_id, 

162 timezone="UTC", 

163 start_time=Timestamp_from_datetime(start + timedelta(hours=hours)), 

164 end_time=Timestamp_from_datetime(start + timedelta(hours=hours + 1)), 

165 ) 

166 

167 with events_session(token2) as api: 

168 e_attending = api.CreateEvent(make_event(1)).event_id 

169 e_community_only = api.CreateEvent(make_event(2)).event_id 

170 

171 moderator.approve_event_occurrence(e_attending) 

172 moderator.approve_event_occurrence(e_community_only) 

173 

174 with events_session(token1) as api: 

175 api.SetEventAttendance( 

176 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

177 ) 

178 

179 with dashboard_session(token1) as api: 

180 res = api.GetDashboardV2(dashboard_pb2.GetDashboardV2Req()) 

181 

182 # the attended event shows under my_events (which with no flags includes all relationships)... 

183 assert e_attending in {e.event_id for e in res.my_events.events} 

184 # ...while community_events must exclude it (exclude_attending), showing only the rest 

185 assert [e.event_id for e in res.community_events.events] == [e_community_only] 

186 

187 

188def test_GetDashboardV2_empty(db): 

189 user, token = generate_user() 

190 with dashboard_session(token) as api: 

191 res = api.GetDashboardV2(dashboard_pb2.GetDashboardV2Req()) 

192 assert len(res.surfing.host_requests) == 0 

193 assert len(res.hosting.host_requests) == 0 

194 assert len(res.my_events.events) == 0 

195 assert len(res.community_events.events) == 0 

196 assert len(res.discussions.discussions) == 0 

197 assert len(res.reminders.reminders) == 0