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
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-22 18:39 +0000
1from datetime import timedelta
3import pytest
4from google.protobuf import empty_pb2
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
21@pytest.fixture(autouse=True)
22def _(testconfig):
23 pass
26UPCOMING_STATUSES = [
27 conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
28 conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED,
29]
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))
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
56def test_GetDashboardV2_matches_individual_rpcs(db, moderator):
57 user1, token1 = generate_user()
58 user2, token2 = generate_user()
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 )
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())
101 with dashboard_session(token1) as api:
102 res = api.GetDashboardV2(dashboard_pb2.GetDashboardV2Req())
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
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
118def test_GetDashboardV2_buckets_by_role(db, moderator):
119 user1, token1 = generate_user()
120 user2, token2 = generate_user()
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 )
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
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()
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
154 start = now()
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 )
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
171 moderator.approve_event_occurrence(e_attending)
172 moderator.approve_event_occurrence(e_community_only)
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 )
179 with dashboard_session(token1) as api:
180 res = api.GetDashboardV2(dashboard_pb2.GetDashboardV2Req())
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]
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