Coverage for app/backend/src/tests/test_requests.py: 99%
986 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 17:51 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 17:51 +0000
1import html
2import re
3from datetime import date, timedelta
4from unittest.mock import patch
5from urllib.parse import parse_qs, urlparse
7import grpc
8import pytest
9from sqlalchemy import func, select
10from sqlalchemy_utils import refresh_materialized_view
12from couchers.constants import HOST_REQUEST_MIN_LENGTH_UTF16
13from couchers.crypto import b64decode
14from couchers.db import session_scope
15from couchers.i18n import LocalizationContext
16from couchers.models import (
17 Cluster,
18 ClusterRole,
19 ClusterSubscription,
20 HostRequest,
21 Message,
22 MessageType,
23 Node,
24 NodeType,
25 Notification,
26 RateLimitAction,
27)
28from couchers.models.public_trips import PublicTrip, PublicTripStatus
29from couchers.proto import (
30 api_pb2,
31 auth_pb2,
32 messages_pb2,
33 requests_pb2,
34)
35from couchers.proto.internal import unsubscribe_pb2
36from couchers.rate_limits.definitions import RATE_LIMIT_DEFINITIONS, RATE_LIMIT_HOURS
37from couchers.utils import create_coordinate, create_polygon_lat_lng, now, to_multi, today
38from tests.fixtures.db import generate_user
39from tests.fixtures.misc import EmailCollector, PushCollector
40from tests.fixtures.sessions import api_session, auth_api_session, requests_session
43@pytest.fixture(autouse=True)
44def _(testconfig):
45 pass
48def valid_request_text(text: str = "Test request") -> str:
49 """Pads a request text to a valid length."""
50 # Request lengths are measured in utf-16 code units to match the frontend.
51 utf16_length = len(text.encode("utf-16-le")) // 2
52 if utf16_length >= HOST_REQUEST_MIN_LENGTH_UTF16: 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 return text
54 padding_length = HOST_REQUEST_MIN_LENGTH_UTF16 - utf16_length
55 return text + ("_" * padding_length) # Each "_" adds one utf16 code unit.
58def test_create_request(db, moderator):
59 user1, token1 = generate_user()
60 hosting_city = "Morningside Heights, New York City"
61 hosting_lat = 40.8086
62 hosting_lng = -73.9616
63 hosting_radius = 500
64 user2, token2 = generate_user(
65 city=hosting_city,
66 geom=create_coordinate(hosting_lat, hosting_lng),
67 geom_radius=hosting_radius,
68 )
70 today_plus_2 = today() + timedelta(days=2)
71 today_plus_3 = today() + timedelta(days=3)
72 today_minus_2 = today() - timedelta(days=2)
73 today_minus_3 = today() - timedelta(days=3)
75 with requests_session(token1) as api:
76 with pytest.raises(grpc.RpcError) as e:
77 api.CreateHostRequest(
78 requests_pb2.CreateHostRequestReq(
79 host_user_id=user1.id,
80 from_date=today_plus_2.isoformat(),
81 to_date=today_plus_3.isoformat(),
82 text=valid_request_text(),
83 )
84 )
85 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
86 assert e.value.details() == "Can't request hosting from yourself."
88 with pytest.raises(grpc.RpcError) as e:
89 api.CreateHostRequest(
90 requests_pb2.CreateHostRequestReq(
91 host_user_id=999,
92 from_date=today_plus_2.isoformat(),
93 to_date=today_plus_3.isoformat(),
94 text=valid_request_text(),
95 )
96 )
97 assert e.value.code() == grpc.StatusCode.NOT_FOUND
98 assert e.value.details() == "Couldn't find that user."
100 with pytest.raises(grpc.RpcError) as e:
101 api.CreateHostRequest(
102 requests_pb2.CreateHostRequestReq(
103 host_user_id=user2.id,
104 from_date=today_plus_3.isoformat(),
105 to_date=today_plus_2.isoformat(),
106 text=valid_request_text(),
107 )
108 )
109 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
110 assert e.value.details() == "From date can't be after to date."
112 with pytest.raises(grpc.RpcError) as e:
113 api.CreateHostRequest(
114 requests_pb2.CreateHostRequestReq(
115 host_user_id=user2.id,
116 from_date=today_minus_3.isoformat(),
117 to_date=today_plus_2.isoformat(),
118 text=valid_request_text(),
119 )
120 )
121 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
122 assert e.value.details() == "From date must be today or later."
124 with pytest.raises(grpc.RpcError) as e:
125 api.CreateHostRequest(
126 requests_pb2.CreateHostRequestReq(
127 host_user_id=user2.id,
128 from_date=today_plus_2.isoformat(),
129 to_date=today_minus_2.isoformat(),
130 text=valid_request_text(),
131 )
132 )
133 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
134 assert e.value.details() == "From date can't be after to date."
136 with pytest.raises(grpc.RpcError) as e:
137 api.CreateHostRequest(
138 requests_pb2.CreateHostRequestReq(
139 host_user_id=user2.id,
140 from_date="2020-00-06",
141 to_date=today_minus_2.isoformat(),
142 text=valid_request_text(),
143 )
144 )
145 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
146 assert e.value.details() == "Invalid date."
148 with pytest.raises(grpc.RpcError) as e:
149 api.CreateHostRequest(
150 requests_pb2.CreateHostRequestReq(
151 host_user_id=user2.id,
152 from_date=today_plus_2.isoformat(),
153 to_date=today_plus_3.isoformat(),
154 text="Too short.",
155 )
156 )
157 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
158 assert e.value.details() == "Host request cannot be shorter than 250 characters."
160 res = api.CreateHostRequest(
161 requests_pb2.CreateHostRequestReq(
162 host_user_id=user2.id,
163 from_date=today_plus_2.isoformat(),
164 to_date=today_plus_3.isoformat(),
165 text=valid_request_text(),
166 )
167 )
168 host_request_id = res.host_request_id
170 moderator.approve_host_request(host_request_id)
172 with requests_session(token1) as api:
173 host_requests = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)).host_requests
175 assert len(host_requests) == 1
176 hr = host_requests[0]
178 assert hr.latest_message.text.text == valid_request_text()
180 assert hr.hosting_city == hosting_city
181 assert round(hr.hosting_lat, 4) == hosting_lat
182 assert round(hr.hosting_lng, 4) == hosting_lng
183 assert hr.hosting_radius == hosting_radius
185 today_ = today()
186 today_plus_one_year = today_ + timedelta(days=365)
187 today_plus_one_year_plus_2 = today_plus_one_year + timedelta(days=2)
188 today_plus_one_year_plus_3 = today_plus_one_year + timedelta(days=3)
189 with pytest.raises(grpc.RpcError) as e:
190 api.CreateHostRequest(
191 requests_pb2.CreateHostRequestReq(
192 host_user_id=user2.id,
193 from_date=today_plus_one_year_plus_2.isoformat(),
194 to_date=today_plus_one_year_plus_3.isoformat(),
195 text=valid_request_text("Test from date after one year"),
196 )
197 )
198 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
199 assert e.value.details() == "The start date must be within one year from today."
201 with pytest.raises(grpc.RpcError) as e:
202 api.CreateHostRequest(
203 requests_pb2.CreateHostRequestReq(
204 host_user_id=user2.id,
205 from_date=today_plus_2.isoformat(),
206 to_date=today_plus_one_year_plus_3.isoformat(),
207 text=valid_request_text("Test to date one year after from date"),
208 )
209 )
210 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
211 assert e.value.details() == "You cannot request to stay with someone for longer than one year."
214def test_create_host_request_rejects_date_past_in_host_timezone(db):
215 # When the host's timezone has already rolled over to the next day, a
216 # from_date of "today in UTC" is in the past from the host's perspective and
217 # must be rejected. The frontend blocks this date before submission; the
218 # backend enforces the same rule for consistency.
219 user1, token1 = generate_user()
220 # geom inside the fake Europe/Helsinki timezone polygon used in tests
221 user2, _ = generate_user(geom=create_coordinate(61, 25))
223 # Helsinki is already on 2026-01-16; requester submits 2026-01-15.
224 fake_today_by_tz = {"Europe/Helsinki": date(2026, 1, 16)}
226 with patch(
227 "couchers.servicers.requests.today_in_timezone",
228 side_effect=lambda tz: fake_today_by_tz.get(tz, date(2026, 1, 15)),
229 ):
230 with requests_session(token1) as api:
231 with pytest.raises(grpc.RpcError) as e:
232 api.CreateHostRequest(
233 requests_pb2.CreateHostRequestReq(
234 host_user_id=user2.id,
235 from_date="2026-01-15",
236 to_date="2026-01-18",
237 text=valid_request_text(),
238 )
239 )
240 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
243def test_create_host_request_date_valid_when_host_behind_requester(db):
244 # Simulate the opposite timezone direction: the host (America/New_York) is
245 # still on 2026-01-15 while the requester has already rolled into 2026-01-16.
246 # A from_date of 2026-01-16 is "today" for the requester and "tomorrow" for
247 # the host — must be accepted without issue.
248 user1, token1 = generate_user()
249 user2, _ = generate_user() # default geom resolves to America/New_York
251 fake_today_by_tz = {"America/New_York": date(2026, 1, 15)}
253 with patch(
254 "couchers.servicers.requests.today_in_timezone",
255 side_effect=lambda tz: fake_today_by_tz.get(tz, date(2026, 1, 15)),
256 ):
257 with requests_session(token1) as api:
258 res = api.CreateHostRequest(
259 requests_pb2.CreateHostRequestReq(
260 host_user_id=user2.id,
261 from_date="2026-01-16",
262 to_date="2026-01-20",
263 text=valid_request_text(),
264 )
265 )
266 assert res.host_request_id
269def test_create_request_incomplete_profile(db):
270 user1, token1 = generate_user(complete_profile=False)
271 user2, _ = generate_user()
272 today_plus_2 = today() + timedelta(days=2)
273 today_plus_3 = today() + timedelta(days=3)
274 with requests_session(token1) as api:
275 with pytest.raises(grpc.RpcError) as e:
276 api.CreateHostRequest(
277 requests_pb2.CreateHostRequestReq(
278 host_user_id=user2.id,
279 from_date=today_plus_2.isoformat(),
280 to_date=today_plus_3.isoformat(),
281 text=valid_request_text(),
282 )
283 )
284 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
285 assert e.value.details() == "You have to complete your profile before you can send a request."
288def test_excessive_requests_are_reported(db, email_collector: EmailCollector):
289 """Test that excessive host requests are first reported in a warning email and finally lead blocking of further requests."""
290 user, token = generate_user()
291 today_plus_2 = today() + timedelta(days=2)
292 today_plus_3 = today() + timedelta(days=3)
293 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.host_request]
294 with requests_session(token) as api:
295 # Test warning email
296 for _ in range(rate_limit_definition.warning_limit):
297 host_user, _ = generate_user()
298 _ = api.CreateHostRequest(
299 requests_pb2.CreateHostRequestReq(
300 host_user_id=host_user.id,
301 from_date=today_plus_2.isoformat(),
302 to_date=today_plus_3.isoformat(),
303 text=valid_request_text(),
304 )
305 )
307 assert email_collector.count_for_reports() == 0
308 host_user, _ = generate_user()
309 _ = api.CreateHostRequest(
310 requests_pb2.CreateHostRequestReq(
311 host_user_id=host_user.id,
312 from_date=today_plus_2.isoformat(),
313 to_date=today_plus_3.isoformat(),
314 text=valid_request_text("Excessive test request"),
315 )
316 )
318 email = email_collector.pop_for_reports(last=True)
319 assert email.plain.startswith(
320 f"User {user.username} has sent {rate_limit_definition.warning_limit} host requests in the past {RATE_LIMIT_HOURS} hours."
321 )
323 # Test ban after exceeding HOST_REQUEST_HARD_LIMIT
324 for _ in range(rate_limit_definition.hard_limit - rate_limit_definition.warning_limit - 1):
325 host_user, _ = generate_user()
326 _ = api.CreateHostRequest(
327 requests_pb2.CreateHostRequestReq(
328 host_user_id=host_user.id,
329 from_date=today_plus_2.isoformat(),
330 to_date=today_plus_3.isoformat(),
331 text=valid_request_text(),
332 )
333 )
335 assert email_collector.count_for_reports() == 0
337 host_user, _ = generate_user()
338 with pytest.raises(grpc.RpcError) as exc_info:
339 _ = api.CreateHostRequest(
340 requests_pb2.CreateHostRequestReq(
341 host_user_id=host_user.id,
342 from_date=today_plus_2.isoformat(),
343 to_date=today_plus_3.isoformat(),
344 text=valid_request_text("Excessive test request"),
345 )
346 )
347 assert exc_info.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
348 assert (
349 exc_info.value.details()
350 == "You have sent a lot of host requests in the past 24 hours. To avoid spam, you can't send any more for now."
351 )
353 email = email_collector.pop_for_reports(last=True)
354 assert email.plain.startswith(
355 f"User {user.username} has sent {rate_limit_definition.hard_limit} host requests in the past {RATE_LIMIT_HOURS} hours."
356 )
357 assert "The user has been blocked from sending further host requests for now." in email.plain
360def add_message(db, text, author_id, conversation_id):
361 with session_scope() as session:
362 message = Message(
363 conversation_id=conversation_id, author_id=author_id, text=text, message_type=MessageType.text
364 )
366 session.add(message)
369def test_GetHostRequest(db):
370 user1, token1 = generate_user()
371 user2, token2 = generate_user()
372 user3, token3 = generate_user()
373 today_plus_2 = today() + timedelta(days=2)
374 today_plus_3 = today() + timedelta(days=3)
375 with requests_session(token1) as api:
376 host_request_id = api.CreateHostRequest(
377 requests_pb2.CreateHostRequestReq(
378 host_user_id=user2.id,
379 from_date=today_plus_2.isoformat(),
380 to_date=today_plus_3.isoformat(),
381 text=valid_request_text("Test request 1"),
382 )
383 ).host_request_id
385 with pytest.raises(grpc.RpcError) as e:
386 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=999))
387 assert e.value.code() == grpc.StatusCode.NOT_FOUND
388 assert e.value.details() == "Couldn't find that host request."
390 api.SendHostRequestMessage(
391 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
392 )
394 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
395 assert res.latest_message.text.text == "Test message 1"
398def test_ListHostRequests(db, moderator):
399 user1, token1 = generate_user()
400 user2, token2 = generate_user()
401 user3, token3 = generate_user()
402 today_plus_2 = today() + timedelta(days=2)
403 today_plus_3 = today() + timedelta(days=3)
404 with requests_session(token1) as api:
405 host_request_1 = api.CreateHostRequest(
406 requests_pb2.CreateHostRequestReq(
407 host_user_id=user2.id,
408 from_date=today_plus_2.isoformat(),
409 to_date=today_plus_3.isoformat(),
410 text=valid_request_text("Test request 1"),
411 )
412 ).host_request_id
414 host_request_2 = api.CreateHostRequest(
415 requests_pb2.CreateHostRequestReq(
416 host_user_id=user3.id,
417 from_date=today_plus_2.isoformat(),
418 to_date=today_plus_3.isoformat(),
419 text=valid_request_text("Test request 2"),
420 )
421 ).host_request_id
423 moderator.approve_host_request(host_request_1)
424 moderator.approve_host_request(host_request_2)
426 with requests_session(token1) as api:
427 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
428 assert res.no_more
429 assert len(res.host_requests) == 2
431 with requests_session(token2) as api:
432 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
433 assert res.no_more
434 assert len(res.host_requests) == 1
435 assert res.host_requests[0].latest_message.text.text == valid_request_text("Test request 1")
436 assert res.host_requests[0].surfer_user_id == user1.id
437 assert res.host_requests[0].host_user_id == user2.id
438 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_PENDING
440 add_message(db, "Test request 1 message 1", user2.id, host_request_1)
441 add_message(db, "Test request 1 message 2", user2.id, host_request_1)
442 add_message(db, "Test request 1 message 3", user2.id, host_request_1)
444 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
445 assert res.host_requests[0].latest_message.text.text == "Test request 1 message 3"
447 host_request_3 = api.CreateHostRequest(
448 requests_pb2.CreateHostRequestReq(
449 host_user_id=user1.id,
450 from_date=today_plus_2.isoformat(),
451 to_date=today_plus_3.isoformat(),
452 text=valid_request_text("Test request 3"),
453 )
454 ).host_request_id
456 moderator.approve_host_request(host_request_3)
458 add_message(db, "Test request 2 message 1", user1.id, host_request_2)
459 add_message(db, "Test request 2 message 2", user3.id, host_request_2)
461 with requests_session(token3) as api:
462 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
463 assert res.no_more
464 assert len(res.host_requests) == 1
465 assert res.host_requests[0].latest_message.text.text == "Test request 2 message 2"
467 with requests_session(token1) as api:
468 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
469 assert len(res.host_requests) == 1
471 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq())
472 assert len(res.host_requests) == 3
475def test_ListHostRequests_pagination_regression(db, moderator):
476 """
477 ListHostRequests was skipping a request when getting multiple pages
478 """
479 user1, token1 = generate_user()
480 user2, token2 = generate_user()
481 today_plus_2 = today() + timedelta(days=2)
482 today_plus_3 = today() + timedelta(days=3)
483 with requests_session(token1) as api:
484 host_request_1 = api.CreateHostRequest(
485 requests_pb2.CreateHostRequestReq(
486 host_user_id=user2.id,
487 from_date=today_plus_2.isoformat(),
488 to_date=today_plus_3.isoformat(),
489 text=valid_request_text("Test request 1"),
490 )
491 ).host_request_id
493 host_request_2 = api.CreateHostRequest(
494 requests_pb2.CreateHostRequestReq(
495 host_user_id=user2.id,
496 from_date=today_plus_2.isoformat(),
497 to_date=today_plus_3.isoformat(),
498 text=valid_request_text("Test request 2"),
499 )
500 ).host_request_id
502 host_request_3 = api.CreateHostRequest(
503 requests_pb2.CreateHostRequestReq(
504 host_user_id=user2.id,
505 from_date=today_plus_2.isoformat(),
506 to_date=today_plus_3.isoformat(),
507 text=valid_request_text("Test request 3"),
508 )
509 ).host_request_id
511 moderator.approve_host_request(host_request_1)
512 moderator.approve_host_request(host_request_2)
513 moderator.approve_host_request(host_request_3)
515 with requests_session(token2) as api:
516 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
517 assert res.no_more
518 assert len(res.host_requests) == 3
519 assert res.host_requests[0].latest_message.text.text == valid_request_text("Test request 3")
520 assert res.host_requests[1].latest_message.text.text == valid_request_text("Test request 2")
521 assert res.host_requests[2].latest_message.text.text == valid_request_text("Test request 1")
523 with requests_session(token2) as api:
524 api.RespondHostRequest(
525 requests_pb2.RespondHostRequestReq(
526 host_request_id=host_request_2,
527 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
528 text="Accepting host request 2",
529 )
530 )
531 api.RespondHostRequest(
532 requests_pb2.RespondHostRequestReq(
533 host_request_id=host_request_1,
534 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
535 text="Accepting host request 1",
536 )
537 )
538 api.RespondHostRequest(
539 requests_pb2.RespondHostRequestReq(
540 host_request_id=host_request_3,
541 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
542 text="Accepting host request 3",
543 )
544 )
546 with requests_session(token2) as api:
547 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
548 assert res.no_more
549 assert len(res.host_requests) == 3
550 assert res.host_requests[0].latest_message.text.text == "Accepting host request 3"
551 assert res.host_requests[1].latest_message.text.text == "Accepting host request 1"
552 assert res.host_requests[2].latest_message.text.text == "Accepting host request 2"
554 with requests_session(token2) as api:
555 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True, number=1))
556 assert not res.no_more
557 assert len(res.host_requests) == 1
558 assert res.host_requests[0].latest_message.text.text == "Accepting host request 3"
559 res = api.ListHostRequests(
560 requests_pb2.ListHostRequestsReq(only_received=True, number=1, page_token=res.next_page_token)
561 )
562 assert not res.no_more
563 assert len(res.host_requests) == 1
564 assert res.host_requests[0].latest_message.text.text == "Accepting host request 1"
565 res = api.ListHostRequests(
566 requests_pb2.ListHostRequestsReq(only_received=True, number=1, page_token=res.next_page_token)
567 )
568 assert res.no_more
569 assert len(res.host_requests) == 1
570 assert res.host_requests[0].latest_message.text.text == "Accepting host request 2"
573def test_ListHostRequests_sort_by_from_date(db, moderator):
574 user1, token1 = generate_user()
575 user2, token2 = generate_user()
576 today_plus_2 = today() + timedelta(days=2)
577 today_plus_3 = today() + timedelta(days=3)
578 today_plus_5 = today() + timedelta(days=5)
579 today_plus_7 = today() + timedelta(days=7)
580 today_plus_10 = today() + timedelta(days=10)
582 with requests_session(token1) as api:
583 hr_late = api.CreateHostRequest(
584 requests_pb2.CreateHostRequestReq(
585 host_user_id=user2.id,
586 from_date=today_plus_7.isoformat(),
587 to_date=today_plus_10.isoformat(),
588 text=valid_request_text("Late request"),
589 )
590 ).host_request_id
592 hr_early = api.CreateHostRequest(
593 requests_pb2.CreateHostRequestReq(
594 host_user_id=user2.id,
595 from_date=today_plus_2.isoformat(),
596 to_date=today_plus_3.isoformat(),
597 text=valid_request_text("Early request"),
598 )
599 ).host_request_id
601 hr_mid = api.CreateHostRequest(
602 requests_pb2.CreateHostRequestReq(
603 host_user_id=user2.id,
604 from_date=today_plus_5.isoformat(),
605 to_date=today_plus_7.isoformat(),
606 text=valid_request_text("Mid request"),
607 )
608 ).host_request_id
610 moderator.approve_host_request(hr_late)
611 moderator.approve_host_request(hr_early)
612 moderator.approve_host_request(hr_mid)
614 with requests_session(token2) as api:
615 # default sort: latest message first (creation order reversed)
616 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
617 assert res.no_more
618 assert [r.host_request_id for r in res.host_requests] == [hr_mid, hr_early, hr_late]
620 # from_date sort: ascending by travel date
621 res = api.ListHostRequests(
622 requests_pb2.ListHostRequestsReq(
623 only_received=True,
624 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
625 )
626 )
627 assert res.no_more
628 assert [r.host_request_id for r in res.host_requests] == [hr_early, hr_mid, hr_late]
631def test_ListHostRequests_sort_by_from_date_pagination(db, moderator):
632 """Pagination cursor correctly handles both different and identical from_dates."""
633 user1, token1 = generate_user()
634 user2, token2 = generate_user()
635 today_plus_2 = today() + timedelta(days=2)
636 today_plus_3 = today() + timedelta(days=3)
637 today_plus_5 = today() + timedelta(days=5)
639 with requests_session(token1) as api:
640 hr_a = api.CreateHostRequest(
641 requests_pb2.CreateHostRequestReq(
642 host_user_id=user2.id,
643 from_date=today_plus_2.isoformat(),
644 to_date=today_plus_3.isoformat(),
645 text=valid_request_text("Request A"),
646 )
647 ).host_request_id
649 # Same from_date as A — tiebreaker by conversation_id
650 hr_b = api.CreateHostRequest(
651 requests_pb2.CreateHostRequestReq(
652 host_user_id=user2.id,
653 from_date=today_plus_2.isoformat(),
654 to_date=today_plus_3.isoformat(),
655 text=valid_request_text("Request B"),
656 )
657 ).host_request_id
659 hr_c = api.CreateHostRequest(
660 requests_pb2.CreateHostRequestReq(
661 host_user_id=user2.id,
662 from_date=today_plus_5.isoformat(),
663 to_date=(today_plus_5 + timedelta(days=2)).isoformat(),
664 text=valid_request_text("Request C"),
665 )
666 ).host_request_id
668 moderator.approve_host_request(hr_a)
669 moderator.approve_host_request(hr_b)
670 moderator.approve_host_request(hr_c)
672 with requests_session(token2) as api:
673 res = api.ListHostRequests(
674 requests_pb2.ListHostRequestsReq(
675 only_received=True,
676 number=1,
677 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
678 )
679 )
680 assert not res.no_more
681 assert len(res.host_requests) == 1
682 assert res.host_requests[0].host_request_id == hr_a
684 res = api.ListHostRequests(
685 requests_pb2.ListHostRequestsReq(
686 only_received=True,
687 number=1,
688 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
689 page_token=res.next_page_token,
690 )
691 )
692 assert not res.no_more
693 assert len(res.host_requests) == 1
694 assert res.host_requests[0].host_request_id == hr_b
696 res = api.ListHostRequests(
697 requests_pb2.ListHostRequestsReq(
698 only_received=True,
699 number=1,
700 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
701 page_token=res.next_page_token,
702 )
703 )
704 assert res.no_more
705 assert len(res.host_requests) == 1
706 assert res.host_requests[0].host_request_id == hr_c
709def test_ListHostRequests_active_filter(db, moderator):
710 user1, token1 = generate_user()
711 user2, token2 = generate_user()
712 today_plus_2 = today() + timedelta(days=2)
713 today_plus_3 = today() + timedelta(days=3)
715 with requests_session(token1) as api:
716 request_id = api.CreateHostRequest(
717 requests_pb2.CreateHostRequestReq(
718 host_user_id=user2.id,
719 from_date=today_plus_2.isoformat(),
720 to_date=today_plus_3.isoformat(),
721 text=valid_request_text("Test request 1"),
722 )
723 ).host_request_id
725 moderator.approve_host_request(request_id)
727 with requests_session(token1) as api:
728 api.RespondHostRequest(
729 requests_pb2.RespondHostRequestReq(
730 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
731 )
732 )
734 with requests_session(token2) as api:
735 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
736 assert len(res.host_requests) == 1
737 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True))
738 assert len(res.host_requests) == 0
741def test_ListHostRequests_active_filter_excludes_past(db, moderator):
742 """only_active must exclude requests whose end date has passed (regression test for <= bug)."""
743 user1, token1 = generate_user()
744 user2, token2 = generate_user()
745 today_plus_2 = today() + timedelta(days=2)
746 today_plus_3 = today() + timedelta(days=3)
748 with requests_session(token1) as api:
749 request_id = api.CreateHostRequest(
750 requests_pb2.CreateHostRequestReq(
751 host_user_id=user2.id,
752 from_date=today_plus_2.isoformat(),
753 to_date=today_plus_3.isoformat(),
754 text=valid_request_text("Past stay regression"),
755 )
756 ).host_request_id
758 moderator.approve_host_request(request_id)
760 with requests_session(token2) as api:
761 api.RespondHostRequest(
762 requests_pb2.RespondHostRequestReq(
763 host_request_id=request_id,
764 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
765 )
766 )
768 # Future request is visible with only_active
769 with requests_session(token2) as api:
770 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True))
771 assert len(res.host_requests) == 1
773 # Move dates into the past
774 with session_scope() as session:
775 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == request_id)).scalar_one()
776 hr.from_date = today() - timedelta(days=3)
777 hr.to_date = today() - timedelta(days=2)
779 # Past request must be excluded by only_active
780 with requests_session(token2) as api:
781 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True))
782 assert len(res.host_requests) == 0
784 # Still visible without the filter
785 with requests_session(token2) as api:
786 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
787 assert len(res.host_requests) == 1
790def test_ListHostRequests_status_in_filter(db, moderator):
791 """status_in must return only requests with the specified statuses."""
792 user1, token1 = generate_user()
793 user2, token2 = generate_user()
794 today_plus_2 = today() + timedelta(days=2)
795 today_plus_3 = today() + timedelta(days=3)
796 today_plus_4 = today() + timedelta(days=4)
797 today_plus_5 = today() + timedelta(days=5)
799 # Create a pending request
800 with requests_session(token1) as api:
801 pending_id = api.CreateHostRequest(
802 requests_pb2.CreateHostRequestReq(
803 host_user_id=user2.id,
804 from_date=today_plus_2.isoformat(),
805 to_date=today_plus_3.isoformat(),
806 text=valid_request_text("Pending"),
807 )
808 ).host_request_id
810 moderator.approve_host_request(pending_id)
812 # Create an accepted request
813 with requests_session(token1) as api:
814 accepted_id = api.CreateHostRequest(
815 requests_pb2.CreateHostRequestReq(
816 host_user_id=user2.id,
817 from_date=today_plus_4.isoformat(),
818 to_date=today_plus_5.isoformat(),
819 text=valid_request_text("Accepted"),
820 )
821 ).host_request_id
823 moderator.approve_host_request(accepted_id)
825 with requests_session(token2) as api:
826 api.RespondHostRequest(
827 requests_pb2.RespondHostRequestReq(
828 host_request_id=accepted_id,
829 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
830 )
831 )
833 # Filter to accepted only
834 with requests_session(token2) as api:
835 res = api.ListHostRequests(
836 requests_pb2.ListHostRequestsReq(
837 status_in=[messages_pb2.HOST_REQUEST_STATUS_ACCEPTED],
838 )
839 )
840 assert len(res.host_requests) == 1
841 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
843 # Filter to pending only
844 with requests_session(token2) as api:
845 res = api.ListHostRequests(
846 requests_pb2.ListHostRequestsReq(
847 status_in=[messages_pb2.HOST_REQUEST_STATUS_PENDING],
848 )
849 )
850 assert len(res.host_requests) == 1
851 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_PENDING
853 # Filter to accepted + pending — both appear
854 with requests_session(token2) as api:
855 res = api.ListHostRequests(
856 requests_pb2.ListHostRequestsReq(
857 status_in=[
858 messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
859 messages_pb2.HOST_REQUEST_STATUS_PENDING,
860 ],
861 )
862 )
863 assert len(res.host_requests) == 2
865 # Filter to confirmed — none appear
866 with requests_session(token2) as api:
867 res = api.ListHostRequests(
868 requests_pb2.ListHostRequestsReq(
869 status_in=[messages_pb2.HOST_REQUEST_STATUS_CONFIRMED],
870 )
871 )
872 assert len(res.host_requests) == 0
874 # Empty status_in — all requests returned (no filter applied)
875 with requests_session(token2) as api:
876 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
877 assert len(res.host_requests) == 2
880def test_RespondHostRequests(db, moderator):
881 user1, token1 = generate_user()
882 user2, token2 = generate_user()
883 user3, token3 = generate_user()
884 today_plus_2 = today() + timedelta(days=2)
885 today_plus_3 = today() + timedelta(days=3)
887 with requests_session(token1) as api:
888 request_id = api.CreateHostRequest(
889 requests_pb2.CreateHostRequestReq(
890 host_user_id=user2.id,
891 from_date=today_plus_2.isoformat(),
892 to_date=today_plus_3.isoformat(),
893 text=valid_request_text("Test request 1"),
894 )
895 ).host_request_id
897 moderator.approve_host_request(request_id)
899 # another user can't access
900 with requests_session(token3) as api:
901 with pytest.raises(grpc.RpcError) as e:
902 api.RespondHostRequest(
903 requests_pb2.RespondHostRequestReq(
904 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
905 )
906 )
907 assert e.value.code() == grpc.StatusCode.NOT_FOUND
908 assert e.value.details() == "Couldn't find that host request."
910 with requests_session(token1) as api:
911 with pytest.raises(grpc.RpcError) as e:
912 api.RespondHostRequest(
913 requests_pb2.RespondHostRequestReq(
914 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
915 )
916 )
917 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
918 assert e.value.details() == "You are not the host of this request."
920 with requests_session(token2) as api:
921 # non existing id
922 with pytest.raises(grpc.RpcError) as e:
923 api.RespondHostRequest(
924 requests_pb2.RespondHostRequestReq(
925 host_request_id=9999, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
926 )
927 )
928 assert e.value.code() == grpc.StatusCode.NOT_FOUND
930 # host can't confirm or cancel (host should accept/reject)
931 with pytest.raises(grpc.RpcError) as e:
932 api.RespondHostRequest(
933 requests_pb2.RespondHostRequestReq(
934 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
935 )
936 )
937 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
938 assert e.value.details() == "You can't set the host request status to that."
939 with pytest.raises(grpc.RpcError) as e:
940 api.RespondHostRequest(
941 requests_pb2.RespondHostRequestReq(
942 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
943 )
944 )
945 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
946 assert e.value.details() == "You can't set the host request status to that."
948 api.RespondHostRequest(
949 requests_pb2.RespondHostRequestReq(
950 host_request_id=request_id,
951 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED,
952 text="Test rejection message",
953 )
954 )
955 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=request_id))
956 assert res.messages[0].text.text == "Test rejection message"
957 assert res.messages[1].WhichOneof("content") == "host_request_status_changed"
958 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
959 # should be able to move from rejected -> accepted
960 api.RespondHostRequest(
961 requests_pb2.RespondHostRequestReq(
962 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
963 )
964 )
966 with requests_session(token1) as api:
967 # can't make pending
968 with pytest.raises(grpc.RpcError) as e:
969 api.RespondHostRequest(
970 requests_pb2.RespondHostRequestReq(
971 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_PENDING
972 )
973 )
974 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
975 assert e.value.details() == "You can't set the host request status to that."
977 # can confirm then cancel
978 api.RespondHostRequest(
979 requests_pb2.RespondHostRequestReq(
980 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
981 )
982 )
984 api.RespondHostRequest(
985 requests_pb2.RespondHostRequestReq(
986 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
987 )
988 )
990 # can't confirm after having cancelled
991 with pytest.raises(grpc.RpcError) as e:
992 api.RespondHostRequest(
993 requests_pb2.RespondHostRequestReq(
994 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
995 )
996 )
997 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
998 assert e.value.details() == "You can't set the host request status to that."
1000 # at this point there should be 7 messages
1001 # 2 for creation, 2 for the status change with message, 3 for the other status changed
1002 with requests_session(token1) as api:
1003 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=request_id))
1004 assert len(res.messages) == 7
1005 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1006 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1007 assert res.messages[2].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1008 assert res.messages[4].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
1009 assert res.messages[6].WhichOneof("content") == "chat_created"
1012def test_get_host_request_messages(db, moderator):
1013 user1, token1 = generate_user()
1014 user2, token2 = generate_user()
1015 today_plus_2 = today() + timedelta(days=2)
1016 today_plus_3 = today() + timedelta(days=3)
1017 with requests_session(token1) as api:
1018 res = api.CreateHostRequest(
1019 requests_pb2.CreateHostRequestReq(
1020 host_user_id=user2.id,
1021 from_date=today_plus_2.isoformat(),
1022 to_date=today_plus_3.isoformat(),
1023 text=valid_request_text("Test request 1"),
1024 )
1025 )
1026 conversation_id = res.host_request_id
1028 moderator.approve_host_request(conversation_id)
1030 add_message(db, "Test request 1 message 1", user1.id, conversation_id)
1031 add_message(db, "Test request 1 message 2", user1.id, conversation_id)
1032 add_message(db, "Test request 1 message 3", user1.id, conversation_id)
1034 with requests_session(token2) as api:
1035 api.RespondHostRequest(
1036 requests_pb2.RespondHostRequestReq(
1037 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1038 )
1039 )
1041 add_message(db, "Test request 1 message 4", user2.id, conversation_id)
1042 add_message(db, "Test request 1 message 5", user2.id, conversation_id)
1044 api.RespondHostRequest(
1045 requests_pb2.RespondHostRequestReq(
1046 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED
1047 )
1048 )
1050 with requests_session(token1) as api:
1051 # 9 including initial message
1052 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=conversation_id))
1053 assert len(res.messages) == 9
1054 assert res.no_more
1056 res = api.GetHostRequestMessages(
1057 requests_pb2.GetHostRequestMessagesReq(host_request_id=conversation_id, number=3)
1058 )
1059 assert not res.no_more
1060 assert len(res.messages) == 3
1061 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
1062 assert res.messages[0].WhichOneof("content") == "host_request_status_changed"
1063 assert res.messages[1].text.text == "Test request 1 message 5"
1064 assert res.messages[2].text.text == "Test request 1 message 4"
1066 res = api.GetHostRequestMessages(
1067 requests_pb2.GetHostRequestMessagesReq(
1068 host_request_id=conversation_id,
1069 last_message_id=res.messages[2].message_id,
1070 number=6,
1071 )
1072 )
1073 assert res.no_more
1074 assert len(res.messages) == 6
1075 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1076 assert res.messages[0].WhichOneof("content") == "host_request_status_changed"
1077 assert res.messages[1].text.text == "Test request 1 message 3"
1078 assert res.messages[2].text.text == "Test request 1 message 2"
1079 assert res.messages[3].text.text == "Test request 1 message 1"
1080 assert res.messages[4].text.text == valid_request_text("Test request 1")
1081 assert res.messages[5].WhichOneof("content") == "chat_created"
1084def test_SendHostRequestMessage(db, moderator):
1085 user1, token1 = generate_user()
1086 user2, token2 = generate_user()
1087 user3, token3 = generate_user()
1088 today_plus_2 = today() + timedelta(days=2)
1089 today_plus_3 = today() + timedelta(days=3)
1090 with requests_session(token1) as api:
1091 host_request_id = api.CreateHostRequest(
1092 requests_pb2.CreateHostRequestReq(
1093 host_user_id=user2.id,
1094 from_date=today_plus_2.isoformat(),
1095 to_date=today_plus_3.isoformat(),
1096 text=valid_request_text("Test request 1"),
1097 )
1098 ).host_request_id
1100 moderator.approve_host_request(host_request_id)
1102 with requests_session(token1) as api:
1103 with pytest.raises(grpc.RpcError) as e:
1104 api.SendHostRequestMessage(
1105 requests_pb2.SendHostRequestMessageReq(host_request_id=999, text="Test message 1")
1106 )
1107 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1109 with pytest.raises(grpc.RpcError) as e:
1110 api.SendHostRequestMessage(requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text=""))
1111 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1112 assert e.value.details() == "Invalid message."
1114 api.SendHostRequestMessage(
1115 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1116 )
1117 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id))
1118 assert res.messages[0].text.text == "Test message 1"
1119 assert res.messages[0].author_user_id == user1.id
1121 with requests_session(token3) as api:
1122 # other user can't send
1123 with pytest.raises(grpc.RpcError) as e:
1124 api.SendHostRequestMessage(
1125 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1126 )
1127 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1128 assert e.value.details() == "Couldn't find that host request."
1130 with requests_session(token2) as api:
1131 api.SendHostRequestMessage(
1132 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1133 )
1134 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id))
1135 # including 2 for creation control message and message
1136 assert len(res.messages) == 4
1137 assert res.messages[0].text.text == "Test message 2"
1138 assert res.messages[0].author_user_id == user2.id
1140 # CAN send messages to a rejected, confirmed or cancelled request, and for accepted
1141 api.RespondHostRequest(
1142 requests_pb2.RespondHostRequestReq(
1143 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED
1144 )
1145 )
1146 api.SendHostRequestMessage(
1147 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3")
1148 )
1150 api.RespondHostRequest(
1151 requests_pb2.RespondHostRequestReq(
1152 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1153 )
1154 )
1156 with requests_session(token1) as api:
1157 api.RespondHostRequest(
1158 requests_pb2.RespondHostRequestReq(
1159 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1160 )
1161 )
1162 api.SendHostRequestMessage(
1163 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3")
1164 )
1166 api.RespondHostRequest(
1167 requests_pb2.RespondHostRequestReq(
1168 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1169 )
1170 )
1171 api.SendHostRequestMessage(
1172 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3")
1173 )
1176def test_get_updates(db, moderator):
1177 user1, token1 = generate_user()
1178 user2, token2 = generate_user()
1179 user3, token3 = generate_user()
1180 today_plus_2 = today() + timedelta(days=2)
1181 today_plus_3 = today() + timedelta(days=3)
1182 with requests_session(token1) as api:
1183 host_request_id = api.CreateHostRequest(
1184 requests_pb2.CreateHostRequestReq(
1185 host_user_id=user2.id,
1186 from_date=today_plus_2.isoformat(),
1187 to_date=today_plus_3.isoformat(),
1188 text=valid_request_text("Test message 0"),
1189 )
1190 ).host_request_id
1192 moderator.approve_host_request(host_request_id)
1194 with requests_session(token1) as api:
1195 api.SendHostRequestMessage(
1196 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1197 )
1198 api.SendHostRequestMessage(
1199 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1200 )
1201 api.RespondHostRequest(
1202 requests_pb2.RespondHostRequestReq(
1203 host_request_id=host_request_id,
1204 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED,
1205 text="Test message 3",
1206 )
1207 )
1209 api.CreateHostRequest(
1210 requests_pb2.CreateHostRequestReq(
1211 host_user_id=user2.id,
1212 from_date=today_plus_2.isoformat(),
1213 to_date=today_plus_3.isoformat(),
1214 text=valid_request_text("Test message 4"),
1215 )
1216 )
1218 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id))
1219 assert len(res.messages) == 6
1220 assert res.messages[0].text.text == "Test message 3"
1221 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1222 assert res.messages[2].text.text == "Test message 2"
1223 assert res.messages[3].text.text == "Test message 1"
1224 assert res.messages[4].text.text == valid_request_text("Test message 0")
1225 message_id_3 = res.messages[0].message_id
1226 message_id_cancel = res.messages[1].message_id
1227 message_id_2 = res.messages[2].message_id
1228 message_id_1 = res.messages[3].message_id
1229 message_id_0 = res.messages[4].message_id
1231 with pytest.raises(grpc.RpcError) as e:
1232 api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=0))
1233 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1235 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1))
1236 assert res.no_more
1237 assert len(res.updates) == 5
1238 assert res.updates[0].message.text.text == "Test message 2"
1239 assert res.updates[1].message.host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1240 assert res.updates[1].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1241 assert res.updates[2].message.text.text == "Test message 3"
1242 assert res.updates[3].message.WhichOneof("content") == "chat_created"
1243 assert res.updates[3].status == messages_pb2.HOST_REQUEST_STATUS_PENDING
1244 assert res.updates[4].message.text.text == valid_request_text("Test message 4")
1246 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1, number=1))
1247 assert not res.no_more
1248 assert len(res.updates) == 1
1249 assert res.updates[0].message.text.text == "Test message 2"
1250 assert res.updates[0].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1252 with requests_session(token3) as api:
1253 # other user can't access
1254 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1))
1255 assert len(res.updates) == 0
1258def test_archive_host_request(db, moderator):
1259 user1, token1 = generate_user()
1260 user2, token2 = generate_user()
1262 today_plus_2 = today() + timedelta(days=2)
1263 today_plus_3 = today() + timedelta(days=3)
1265 with requests_session(token1) as api:
1266 host_request_id = api.CreateHostRequest(
1267 requests_pb2.CreateHostRequestReq(
1268 host_user_id=user2.id,
1269 from_date=today_plus_2.isoformat(),
1270 to_date=today_plus_3.isoformat(),
1271 text=valid_request_text("Test message 0"),
1272 )
1273 ).host_request_id
1275 api.SendHostRequestMessage(
1276 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1277 )
1278 api.SendHostRequestMessage(
1279 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1280 )
1282 moderator.approve_host_request(host_request_id)
1284 # happy path archiving host request
1285 with requests_session(token1) as api:
1286 api.RespondHostRequest(
1287 requests_pb2.RespondHostRequestReq(
1288 host_request_id=host_request_id,
1289 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED,
1290 text="Test message 3",
1291 )
1292 )
1293 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
1294 assert len(res.host_requests) == 1
1295 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1297 # Verify is_archived is False before archiving
1298 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1299 assert not res.is_archived
1301 api.SetHostRequestArchiveStatus(
1302 requests_pb2.SetHostRequestArchiveStatusReq(host_request_id=host_request_id, is_archived=True)
1303 )
1304 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_archived=True))
1305 assert len(res.host_requests) == 1
1307 # Verify is_archived is True after archiving
1308 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1309 assert res.is_archived
1312def test_mark_last_seen(db, moderator):
1313 user1, token1 = generate_user()
1314 user2, token2 = generate_user()
1315 user3, token3 = generate_user()
1316 today_plus_2 = today() + timedelta(days=2)
1317 today_plus_3 = today() + timedelta(days=3)
1318 with requests_session(token1) as api:
1319 host_request_id = api.CreateHostRequest(
1320 requests_pb2.CreateHostRequestReq(
1321 host_user_id=user2.id,
1322 from_date=today_plus_2.isoformat(),
1323 to_date=today_plus_3.isoformat(),
1324 text=valid_request_text("Test message 0"),
1325 )
1326 ).host_request_id
1328 host_request_id_2 = api.CreateHostRequest(
1329 requests_pb2.CreateHostRequestReq(
1330 host_user_id=user2.id,
1331 from_date=today_plus_2.isoformat(),
1332 to_date=today_plus_3.isoformat(),
1333 text=valid_request_text("Test message 0a"),
1334 )
1335 ).host_request_id
1337 moderator.approve_host_request(host_request_id)
1338 moderator.approve_host_request(host_request_id_2)
1340 with requests_session(token1) as api:
1341 api.SendHostRequestMessage(
1342 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1343 )
1344 api.SendHostRequestMessage(
1345 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1346 )
1347 api.RespondHostRequest(
1348 requests_pb2.RespondHostRequestReq(
1349 host_request_id=host_request_id,
1350 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED,
1351 text="Test message 3",
1352 )
1353 )
1355 moderator.approve_host_request(host_request_id)
1356 moderator.approve_host_request(host_request_id_2)
1358 # test Ping unseen host request count, should be automarked after sending
1359 with api_session(token1) as api:
1360 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 0
1361 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1363 with api_session(token2) as api:
1364 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 2
1365 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1367 with requests_session(token2) as api:
1368 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].last_seen_message_id == 0
1370 api.MarkLastSeenHostRequest(
1371 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=3)
1372 )
1374 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].last_seen_message_id == 3
1376 with pytest.raises(grpc.RpcError) as e:
1377 api.MarkLastSeenHostRequest(
1378 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=1)
1379 )
1380 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1381 assert e.value.details() == "You can't unsee messages."
1383 # this will be used to test sent request notifications
1384 host_request_id_3 = api.CreateHostRequest(
1385 requests_pb2.CreateHostRequestReq(
1386 host_user_id=user1.id,
1387 from_date=today_plus_2.isoformat(),
1388 to_date=today_plus_3.isoformat(),
1389 text=valid_request_text("Another test request"),
1390 )
1391 ).host_request_id
1393 moderator.approve_host_request(host_request_id_3)
1395 with requests_session(token2) as api:
1396 # this should make id_2 all read
1397 api.SendHostRequestMessage(
1398 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id_2, text="Test")
1399 )
1401 with api_session(token2) as api:
1402 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 1
1403 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1405 # make sure sent and received count for unseen notifications
1406 with requests_session(token1) as api:
1407 api.SendHostRequestMessage(
1408 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id_3, text="Test message")
1409 )
1411 with api_session(token2) as api:
1412 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 1
1413 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 1
1416def test_mark_last_seen_clears_notifications(db, moderator):
1417 user1, token1 = generate_user()
1418 user2, token2 = generate_user()
1419 today_plus_2 = today() + timedelta(days=2)
1420 today_plus_3 = today() + timedelta(days=3)
1422 with requests_session(token1) as api:
1423 host_request_id = api.CreateHostRequest(
1424 requests_pb2.CreateHostRequestReq(
1425 host_user_id=user2.id,
1426 from_date=today_plus_2.isoformat(),
1427 to_date=today_plus_3.isoformat(),
1428 text=valid_request_text("Test message"),
1429 )
1430 ).host_request_id
1432 moderator.approve_host_request(host_request_id)
1434 def unseen_notification_count(user_id):
1435 with session_scope() as session:
1436 return session.execute(
1437 select(func.count())
1438 .select_from(Notification)
1439 .where(Notification.user_id == user_id)
1440 .where(Notification.key == str(host_request_id))
1441 .where(Notification.is_seen == False)
1442 ).scalar_one()
1444 assert unseen_notification_count(user2.id) > 0
1446 with requests_session(token2) as api:
1447 api.MarkLastSeenHostRequest(
1448 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=1)
1449 )
1451 assert unseen_notification_count(user2.id) == 0
1454def test_response_rate(db, moderator):
1455 user1, token1 = generate_user()
1456 user2, token2 = generate_user()
1457 user3, token3 = generate_user(delete_user=True)
1459 today_plus_2 = today() + timedelta(days=2)
1460 today_plus_3 = today() + timedelta(days=3)
1462 with session_scope() as session:
1463 refresh_materialized_view(session, "user_response_rates")
1465 with requests_session(token1) as api:
1466 # deleted: not found
1467 with pytest.raises(grpc.RpcError) as e:
1468 api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user3.id))
1469 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1470 assert e.value.details() == "Couldn't find that user."
1472 # no requests: insufficient
1473 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1474 assert res.HasField("insufficient_data")
1476 # send a request and back date it by 36 hours
1477 host_request_1 = api.CreateHostRequest(
1478 requests_pb2.CreateHostRequestReq(
1479 host_user_id=user2.id,
1480 from_date=today_plus_2.isoformat(),
1481 to_date=today_plus_3.isoformat(),
1482 text=valid_request_text("Test request"),
1483 )
1484 ).host_request_id
1485 moderator.approve_host_request(host_request_1)
1486 with session_scope() as session:
1487 session.execute(
1488 select(Message)
1489 .where(Message.conversation_id == host_request_1)
1490 .where(Message.message_type == MessageType.chat_created)
1491 ).scalar_one().time = now() - timedelta(hours=36)
1492 refresh_materialized_view(session, "user_response_rates")
1494 # still insufficient
1495 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1496 assert res.HasField("insufficient_data")
1498 # send a request and back date it by 35 hours
1499 host_request_2 = api.CreateHostRequest(
1500 requests_pb2.CreateHostRequestReq(
1501 host_user_id=user2.id,
1502 from_date=today_plus_2.isoformat(),
1503 to_date=today_plus_3.isoformat(),
1504 text=valid_request_text("Test request"),
1505 )
1506 ).host_request_id
1507 moderator.approve_host_request(host_request_2)
1508 with session_scope() as session:
1509 session.execute(
1510 select(Message)
1511 .where(Message.conversation_id == host_request_2)
1512 .where(Message.message_type == MessageType.chat_created)
1513 ).scalar_one().time = now() - timedelta(hours=35)
1514 refresh_materialized_view(session, "user_response_rates")
1516 # still insufficient
1517 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1518 assert res.HasField("insufficient_data")
1520 # send a request and back date it by 34 hours
1521 host_request_3 = api.CreateHostRequest(
1522 requests_pb2.CreateHostRequestReq(
1523 host_user_id=user2.id,
1524 from_date=today_plus_2.isoformat(),
1525 to_date=today_plus_3.isoformat(),
1526 text=valid_request_text("Test request"),
1527 )
1528 ).host_request_id
1529 moderator.approve_host_request(host_request_3)
1530 with session_scope() as session:
1531 session.execute(
1532 select(Message)
1533 .where(Message.conversation_id == host_request_3)
1534 .where(Message.message_type == MessageType.chat_created)
1535 ).scalar_one().time = now() - timedelta(hours=34)
1536 refresh_materialized_view(session, "user_response_rates")
1538 # now low
1539 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1540 assert res.HasField("low")
1542 with requests_session(token2) as api:
1543 # accept a host req
1544 api.RespondHostRequest(
1545 requests_pb2.RespondHostRequestReq(
1546 host_request_id=host_request_2,
1547 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1548 text="Accepting host request",
1549 )
1550 )
1552 with session_scope() as session:
1553 refresh_materialized_view(session, "user_response_rates")
1555 with requests_session(token1) as api:
1556 # now some w p33 = 35h
1557 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1558 assert res.HasField("some")
1559 assert res.some.response_time_p33.ToTimedelta() == timedelta(hours=35)
1561 with requests_session(token2) as api:
1562 # accept another host req
1563 api.RespondHostRequest(
1564 requests_pb2.RespondHostRequestReq(
1565 host_request_id=host_request_3,
1566 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1567 text="Accepting host request",
1568 )
1569 )
1571 with session_scope() as session:
1572 refresh_materialized_view(session, "user_response_rates")
1574 with requests_session(token1) as api:
1575 # now most w p33 = 34h, p66 = 35h
1576 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1577 assert res.HasField("most")
1578 assert res.most.response_time_p33.ToTimedelta() == timedelta(hours=34)
1579 assert res.most.response_time_p66.ToTimedelta() == timedelta(hours=35)
1581 with requests_session(token2) as api:
1582 # accept last host req
1583 api.RespondHostRequest(
1584 requests_pb2.RespondHostRequestReq(
1585 host_request_id=host_request_1,
1586 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1587 text="Accepting host request",
1588 )
1589 )
1591 with session_scope() as session:
1592 refresh_materialized_view(session, "user_response_rates")
1594 with requests_session(token1) as api:
1595 # now all w p33 = 34h, p66 = 35h
1596 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1597 assert res.HasField("almost_all")
1598 assert res.almost_all.response_time_p33.ToTimedelta() == timedelta(hours=34)
1599 assert res.almost_all.response_time_p66.ToTimedelta() == timedelta(hours=35)
1601 # send a request and back date it by 2 hours
1602 host_request_4 = api.CreateHostRequest(
1603 requests_pb2.CreateHostRequestReq(
1604 host_user_id=user2.id,
1605 from_date=today_plus_2.isoformat(),
1606 to_date=today_plus_3.isoformat(),
1607 text=valid_request_text("Test request"),
1608 )
1609 ).host_request_id
1610 moderator.approve_host_request(host_request_4)
1611 with session_scope() as session:
1612 session.execute(
1613 select(Message)
1614 .where(Message.conversation_id == host_request_4)
1615 .where(Message.message_type == MessageType.chat_created)
1616 ).scalar_one().time = now() - timedelta(hours=2)
1617 refresh_materialized_view(session, "user_response_rates")
1619 # send a request and back date it by 4 hours
1620 host_request_5 = api.CreateHostRequest(
1621 requests_pb2.CreateHostRequestReq(
1622 host_user_id=user2.id,
1623 from_date=today_plus_2.isoformat(),
1624 to_date=today_plus_3.isoformat(),
1625 text=valid_request_text("Test request"),
1626 )
1627 ).host_request_id
1628 moderator.approve_host_request(host_request_5)
1629 with session_scope() as session:
1630 session.execute(
1631 select(Message)
1632 .where(Message.conversation_id == host_request_5)
1633 .where(Message.message_type == MessageType.chat_created)
1634 ).scalar_one().time = now() - timedelta(hours=4)
1635 refresh_materialized_view(session, "user_response_rates")
1637 # now some w p33 = 35h
1638 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1639 assert res.HasField("some")
1640 assert res.some.response_time_p33.ToTimedelta() == timedelta(hours=35)
1642 with requests_session(token2) as api:
1643 # accept host req
1644 api.RespondHostRequest(
1645 requests_pb2.RespondHostRequestReq(
1646 host_request_id=host_request_5,
1647 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1648 text="Accepting host request",
1649 )
1650 )
1652 with session_scope() as session:
1653 refresh_materialized_view(session, "user_response_rates")
1655 with requests_session(token1) as api:
1656 # now most w p33 = 34h, p66 = 36h
1657 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1658 assert res.HasField("most")
1659 assert res.most.response_time_p33.ToTimedelta() == timedelta(hours=34)
1660 assert res.most.response_time_p66.ToTimedelta() == timedelta(hours=36)
1662 with requests_session(token2) as api:
1663 # accept host req
1664 api.RespondHostRequest(
1665 requests_pb2.RespondHostRequestReq(
1666 host_request_id=host_request_4,
1667 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1668 text="Accepting host request",
1669 )
1670 )
1672 with session_scope() as session:
1673 refresh_materialized_view(session, "user_response_rates")
1675 with requests_session(token1) as api:
1676 # now most w p33 = 4h, p66 = 35h
1677 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1678 assert res.HasField("almost_all")
1679 assert res.almost_all.response_time_p33.ToTimedelta() == timedelta(hours=4)
1680 assert res.almost_all.response_time_p66.ToTimedelta() == timedelta(hours=35)
1683def test_request_notifications(db, email_collector: EmailCollector, push_collector: PushCollector, moderator):
1684 host, host_token = generate_user(complete_profile=True)
1685 surfer, surfer_token = generate_user(complete_profile=True)
1687 host_loc_context = LocalizationContext.from_user(host)
1688 surfer_loc_context = LocalizationContext.from_user(surfer)
1690 today_plus_2 = today() + timedelta(days=2)
1691 today_plus_3 = today() + timedelta(days=3)
1693 with requests_session(surfer_token) as api:
1694 hr_id = api.CreateHostRequest(
1695 requests_pb2.CreateHostRequestReq(
1696 host_user_id=host.id,
1697 from_date=today_plus_2.isoformat(),
1698 to_date=today_plus_3.isoformat(),
1699 text=valid_request_text("can i stay plz"),
1700 )
1701 ).host_request_id
1703 moderator.approve_host_request(hr_id)
1705 email = email_collector.pop_for_recipient(host.email, last=True)
1706 assert email.recipient == host.email
1707 assert "host request" in email.subject.lower()
1708 assert host.name in email.plain
1709 assert host.name in email.html
1710 assert "quick decline" in email.plain.lower(), email.plain
1711 assert "quick decline" in email.html.lower()
1712 assert surfer.name in email.plain
1713 assert surfer.name in email.html
1714 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
1715 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.html
1716 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
1717 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.html
1718 assert "http://localhost:5001/img/thumbnail/" not in email.plain
1719 assert "http://localhost:5001/img/thumbnail/" in email.html
1720 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
1721 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
1722 assert not email.attachments
1724 assert push_collector.pop_for_user(host.id, last=True).content.title == f"New host request from {surfer.name}"
1726 with requests_session(host_token) as api:
1727 api.RespondHostRequest(
1728 requests_pb2.RespondHostRequestReq(
1729 host_request_id=hr_id,
1730 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1731 text="Accepting host request",
1732 )
1733 )
1735 email = email_collector.pop_for_recipient(surfer.email, last=True)
1736 assert email.recipient == surfer.email
1737 assert "host request" in email.subject.lower()
1738 assert host.name in email.plain
1739 assert host.name in email.html
1740 assert surfer.name in email.plain
1741 assert surfer.name in email.html
1742 assert surfer_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
1743 assert surfer_loc_context.localize_date(today_plus_2, with_year=False) in email.html
1744 assert surfer_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
1745 assert surfer_loc_context.localize_date(today_plus_3, with_year=False) in email.html
1746 assert "http://localhost:5001/img/thumbnail/" not in email.plain
1747 assert "http://localhost:5001/img/thumbnail/" in email.html
1748 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
1749 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
1750 assert len(email.attachments or []) == 1
1752 assert push_collector.pop_for_user(surfer.id, last=True).content.title == f"{host.name} accepted your host request"
1755def test_quick_decline(db, email_collector: EmailCollector, push_collector: PushCollector, moderator):
1756 host, host_token = generate_user(complete_profile=True)
1757 surfer, surfer_token = generate_user(complete_profile=True)
1759 host_loc_context = LocalizationContext.from_user(host)
1761 today_plus_2 = today() + timedelta(days=2)
1762 today_plus_3 = today() + timedelta(days=3)
1764 with requests_session(surfer_token) as api:
1765 hr_id = api.CreateHostRequest(
1766 requests_pb2.CreateHostRequestReq(
1767 host_user_id=host.id,
1768 from_date=today_plus_2.isoformat(),
1769 to_date=today_plus_3.isoformat(),
1770 text=valid_request_text("can i stay plz"),
1771 )
1772 ).host_request_id
1774 moderator.approve_host_request(hr_id)
1776 email = email_collector.pop_for_recipient(host.email, last=True)
1777 assert email.recipient == host.email
1778 assert "host request" in email.subject.lower()
1779 assert host.name in email.plain
1780 assert host.name in email.html
1781 assert "quick decline" in email.plain.lower(), email.plain
1782 assert "quick decline" in email.html.lower()
1783 assert surfer.name in email.plain
1784 assert surfer.name in email.html
1785 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
1786 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.html
1787 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
1788 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.html
1789 assert "http://localhost:5001/img/thumbnail/" not in email.plain
1790 assert "http://localhost:5001/img/thumbnail/" in email.html
1791 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
1792 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
1794 assert push_collector.pop_for_user(host.id, last=True).content.title == f"New host request from {surfer.name}"
1796 # very ugly
1797 # http://localhost:3000/quick-link?payload=CAEiGAoOZnJpZW5kX3JlcXVlc3QSBmFjY2VwdA==&sig=BQdk024NTATm8zlR0krSXTBhP5U9TlFv7VhJeIHZtUg=
1798 for link in re.findall(r'<a href="(.*?)"', email.html): 1798 ↛ 1817line 1798 didn't jump to line 1817 because the loop on line 1798 didn't complete
1799 if "payload" not in link:
1800 continue
1801 print(link)
1802 url_parts = urlparse(html.unescape(link))
1803 params = parse_qs(url_parts.query)
1804 print(params["payload"][0])
1805 payload = unsubscribe_pb2.UnsubscribePayload.FromString(b64decode(params["payload"][0]))
1806 if payload.HasField("host_request_quick_decline"): 1806 ↛ 1798line 1806 didn't jump to line 1798 because the condition on line 1806 was always true
1807 with auth_api_session() as (auth_api, metadata_interceptor):
1808 res = auth_api.Unsubscribe(
1809 auth_pb2.UnsubscribeReq(
1810 payload=b64decode(params["payload"][0]),
1811 sig=b64decode(params["sig"][0]),
1812 )
1813 )
1814 assert res.response == "Thank you for responding to the host request!"
1815 break
1816 else:
1817 raise Exception("Didn't find link")
1819 with requests_session(surfer_token) as api:
1820 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
1821 assert res.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
1824def test_host_req_feedback(db, moderator):
1825 host, host_token = generate_user(complete_profile=True)
1826 host2, host2_token = generate_user(complete_profile=True)
1827 host3, host3_token = generate_user(complete_profile=True)
1828 surfer, surfer_token = generate_user(complete_profile=True)
1830 today_plus_2 = today() + timedelta(days=2)
1831 today_plus_3 = today() + timedelta(days=3)
1833 with requests_session(surfer_token) as api:
1834 hr_id = api.CreateHostRequest(
1835 requests_pb2.CreateHostRequestReq(
1836 host_user_id=host.id,
1837 from_date=today_plus_2.isoformat(),
1838 to_date=today_plus_3.isoformat(),
1839 text=valid_request_text("can i stay plz"),
1840 )
1841 ).host_request_id
1842 hr2_id = api.CreateHostRequest(
1843 requests_pb2.CreateHostRequestReq(
1844 host_user_id=host2.id,
1845 from_date=today_plus_2.isoformat(),
1846 to_date=today_plus_3.isoformat(),
1847 text=valid_request_text("can i stay plz"),
1848 )
1849 ).host_request_id
1850 hr3_id = api.CreateHostRequest(
1851 requests_pb2.CreateHostRequestReq(
1852 host_user_id=host3.id,
1853 from_date=today_plus_2.isoformat(),
1854 to_date=today_plus_3.isoformat(),
1855 text=valid_request_text("can i stay plz"),
1856 )
1857 ).host_request_id
1859 moderator.approve_host_request(hr_id)
1860 moderator.approve_host_request(hr2_id)
1861 moderator.approve_host_request(hr3_id)
1863 with requests_session(host_token) as api:
1864 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
1865 assert not res.need_host_request_feedback
1867 api.RespondHostRequest(
1868 requests_pb2.RespondHostRequestReq(
1869 host_request_id=hr_id,
1870 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED,
1871 )
1872 )
1874 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
1875 assert res.need_host_request_feedback
1877 # surfer can't leave feedback
1878 with requests_session(surfer_token) as api:
1879 with pytest.raises(grpc.RpcError) as e:
1880 api.SendHostRequestFeedback(
1881 requests_pb2.SendHostRequestFeedbackReq(
1882 host_request_id=hr_id,
1883 )
1884 )
1885 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1886 assert e.value.details() == "Couldn't find that host request."
1888 with requests_session(host_token) as api:
1889 api.SendHostRequestFeedback(
1890 requests_pb2.SendHostRequestFeedbackReq(
1891 host_request_id=hr_id,
1892 host_request_quality=requests_pb2.HOST_REQUEST_QUALITY_LOW,
1893 )
1894 )
1895 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
1896 assert not res.need_host_request_feedback
1898 # can't leave it twice
1899 with requests_session(host_token) as api:
1900 with pytest.raises(grpc.RpcError) as e:
1901 api.SendHostRequestFeedback(
1902 requests_pb2.SendHostRequestFeedbackReq(
1903 host_request_id=hr_id,
1904 )
1905 )
1906 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1907 assert e.value.details() == "You have already left feedback for this host request!"
1909 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
1910 assert not res.need_host_request_feedback
1912 with requests_session(host2_token) as api:
1913 api.RespondHostRequest(
1914 requests_pb2.RespondHostRequestReq(host_request_id=hr2_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED)
1915 )
1916 # can't leave feedback on the wrong one
1917 with pytest.raises(grpc.RpcError) as e:
1918 api.SendHostRequestFeedback(
1919 requests_pb2.SendHostRequestFeedbackReq(
1920 host_request_id=hr_id,
1921 )
1922 )
1923 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1924 assert e.value.details() == "Couldn't find that host request."
1926 # null feedback is still feedback
1927 api.SendHostRequestFeedback(requests_pb2.SendHostRequestFeedbackReq(host_request_id=hr2_id))
1929 with requests_session(host3_token) as api:
1930 api.RespondHostRequest(
1931 requests_pb2.RespondHostRequestReq(host_request_id=hr3_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED)
1932 )
1934 api.SendHostRequestFeedback(
1935 requests_pb2.SendHostRequestFeedbackReq(host_request_id=hr3_id, decline_reason="bad req")
1936 )
1939def _make_trip_node_admin(user_id: int, trip_id: int):
1940 with session_scope() as session:
1941 node_id = session.execute(select(PublicTrip.node_id).where(PublicTrip.id == trip_id)).scalar_one()
1942 cluster = session.execute(
1943 select(Cluster).where(Cluster.parent_node_id == node_id).where(Cluster.is_official_cluster)
1944 ).scalar_one_or_none()
1945 if cluster is None: 1945 ↛ 1954line 1945 didn't jump to line 1954 because the condition on line 1945 was always true
1946 cluster = Cluster(
1947 name="Test community",
1948 description="Test",
1949 parent_node_id=node_id,
1950 is_official_cluster=True,
1951 )
1952 session.add(cluster)
1953 session.flush()
1954 session.add(ClusterSubscription(cluster_id=cluster.id, user_id=user_id, role=ClusterRole.admin))
1957def _create_public_trip(user_id: int, from_date, to_date, *, status=None, same_gender_only: bool = False):
1958 with session_scope() as session:
1959 node = session.execute(select(Node).limit(1)).scalar_one_or_none()
1960 if node is None: 1960 ↛ 1967line 1960 didn't jump to line 1967 because the condition on line 1960 was always true
1961 node = Node(
1962 geom=to_multi(create_polygon_lat_lng([[0, 0], [0, 2], [2, 2], [2, 0], [0, 0]])),
1963 node_type=NodeType.locality,
1964 )
1965 session.add(node)
1966 session.flush()
1967 trip = PublicTrip(
1968 user_id=user_id,
1969 node_id=node.id,
1970 from_date=from_date,
1971 to_date=to_date,
1972 description="Looking for a host!",
1973 status=status or PublicTripStatus.searching_for_host,
1974 same_gender_only=same_gender_only,
1975 )
1976 session.add(trip)
1977 session.flush()
1978 return trip.id
1981def test_create_request_with_public_trip(db, moderator):
1982 """Hosts can offer to host a public trip; offered dates must be within trip dates."""
1983 surfer, surfer_token = generate_user()
1984 host, host_token = generate_user()
1986 trip_from = today() + timedelta(days=10)
1987 trip_to = today() + timedelta(days=20)
1988 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
1990 with requests_session(host_token) as api:
1991 # Happy path: dates within trip window
1992 res = api.CreateHostRequest(
1993 requests_pb2.CreateHostRequestReq(
1994 host_user_id=surfer.id,
1995 from_date=(trip_from + timedelta(days=1)).isoformat(),
1996 to_date=(trip_to - timedelta(days=1)).isoformat(),
1997 text=valid_request_text(),
1998 public_trip_id=trip_id,
1999 )
2000 )
2001 host_request_id = res.host_request_id
2003 moderator.approve_host_request(host_request_id)
2005 with requests_session(host_token) as api:
2006 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2007 assert hr.public_trip_id == trip_id
2010def test_create_request_with_public_trip_dates_out_of_range(db):
2011 """Offered dates outside the trip window are rejected."""
2012 surfer, _ = generate_user()
2013 host, host_token = generate_user()
2015 trip_from = today() + timedelta(days=10)
2016 trip_to = today() + timedelta(days=20)
2017 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2019 with requests_session(host_token) as api:
2020 # from_date before trip starts
2021 with pytest.raises(grpc.RpcError) as e:
2022 api.CreateHostRequest(
2023 requests_pb2.CreateHostRequestReq(
2024 host_user_id=surfer.id,
2025 from_date=(trip_from - timedelta(days=1)).isoformat(),
2026 to_date=(trip_from + timedelta(days=1)).isoformat(),
2027 text=valid_request_text(),
2028 public_trip_id=trip_id,
2029 )
2030 )
2031 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2033 # to_date after trip ends
2034 with pytest.raises(grpc.RpcError) as e:
2035 api.CreateHostRequest(
2036 requests_pb2.CreateHostRequestReq(
2037 host_user_id=surfer.id,
2038 from_date=(trip_to - timedelta(days=1)).isoformat(),
2039 to_date=(trip_to + timedelta(days=1)).isoformat(),
2040 text=valid_request_text(),
2041 public_trip_id=trip_id,
2042 )
2043 )
2044 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2047def test_create_request_with_public_trip_user_mismatch(db):
2048 """The host_user_id must match the public trip's traveler."""
2049 trip_owner, _ = generate_user()
2050 other_user, _ = generate_user()
2051 host, host_token = generate_user()
2053 trip_from = today() + timedelta(days=10)
2054 trip_to = today() + timedelta(days=20)
2055 trip_id = _create_public_trip(trip_owner.id, trip_from, trip_to)
2057 with requests_session(host_token) as api:
2058 with pytest.raises(grpc.RpcError) as e:
2059 api.CreateHostRequest(
2060 requests_pb2.CreateHostRequestReq(
2061 host_user_id=other_user.id, # not the trip owner
2062 from_date=(trip_from + timedelta(days=1)).isoformat(),
2063 to_date=(trip_to - timedelta(days=1)).isoformat(),
2064 text=valid_request_text(),
2065 public_trip_id=trip_id,
2066 )
2067 )
2068 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2071def test_create_request_with_closed_public_trip(db):
2072 """Cannot offer to host a trip that's been closed."""
2073 surfer, _ = generate_user()
2074 host, host_token = generate_user()
2076 trip_from = today() + timedelta(days=10)
2077 trip_to = today() + timedelta(days=20)
2078 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, status=PublicTripStatus.closed)
2080 with requests_session(host_token) as api:
2081 with pytest.raises(grpc.RpcError) as e:
2082 api.CreateHostRequest(
2083 requests_pb2.CreateHostRequestReq(
2084 host_user_id=surfer.id,
2085 from_date=(trip_from + timedelta(days=1)).isoformat(),
2086 to_date=(trip_to - timedelta(days=1)).isoformat(),
2087 text=valid_request_text(),
2088 public_trip_id=trip_id,
2089 )
2090 )
2091 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2094def test_create_request_with_nonexistent_public_trip(db):
2095 """Nonexistent public trip ID returns NOT_FOUND."""
2096 surfer, _ = generate_user()
2097 host, host_token = generate_user()
2099 with requests_session(host_token) as api:
2100 with pytest.raises(grpc.RpcError) as e:
2101 api.CreateHostRequest(
2102 requests_pb2.CreateHostRequestReq(
2103 host_user_id=surfer.id,
2104 from_date=(today() + timedelta(days=2)).isoformat(),
2105 to_date=(today() + timedelta(days=3)).isoformat(),
2106 text=valid_request_text(),
2107 public_trip_id=999999,
2108 )
2109 )
2110 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2113def test_create_request_without_public_trip_id_unchanged(db, moderator):
2114 """Existing flow without public_trip_id still works (backwards compatibility)."""
2115 surfer, _ = generate_user()
2116 host, host_token = generate_user()
2118 with requests_session(host_token) as api:
2119 res = api.CreateHostRequest(
2120 requests_pb2.CreateHostRequestReq(
2121 host_user_id=surfer.id,
2122 from_date=(today() + timedelta(days=2)).isoformat(),
2123 to_date=(today() + timedelta(days=3)).isoformat(),
2124 text=valid_request_text(),
2125 )
2126 )
2127 host_request_id = res.host_request_id
2129 moderator.approve_host_request(host_request_id)
2131 with requests_session(host_token) as api:
2132 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2133 assert not hr.HasField("public_trip_id")
2136def test_create_request_same_gender_only_wrong_gender_rejected(db):
2137 surfer, _ = generate_user(gender="Woman")
2138 _, host_token = generate_user(gender="Man")
2140 trip_from = today() + timedelta(days=10)
2141 trip_to = today() + timedelta(days=20)
2142 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2144 with requests_session(host_token) as api:
2145 with pytest.raises(grpc.RpcError) as e:
2146 api.CreateHostRequest(
2147 requests_pb2.CreateHostRequestReq(
2148 host_user_id=surfer.id,
2149 from_date=trip_from.isoformat(),
2150 to_date=trip_to.isoformat(),
2151 text=valid_request_text(),
2152 public_trip_id=trip_id,
2153 )
2154 )
2155 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2158def test_create_request_same_gender_only_same_gender_allowed(db, moderator):
2159 surfer, _ = generate_user(gender="Woman")
2160 _, host_token = generate_user(gender="Woman")
2162 trip_from = today() + timedelta(days=10)
2163 trip_to = today() + timedelta(days=20)
2164 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2166 with requests_session(host_token) as api:
2167 res = api.CreateHostRequest(
2168 requests_pb2.CreateHostRequestReq(
2169 host_user_id=surfer.id,
2170 from_date=trip_from.isoformat(),
2171 to_date=trip_to.isoformat(),
2172 text=valid_request_text(),
2173 public_trip_id=trip_id,
2174 )
2175 )
2176 assert res.host_request_id > 0
2179def test_create_request_same_gender_only_moderator_bypass(db, moderator):
2180 surfer, _ = generate_user(gender="Woman")
2181 host, host_token = generate_user(gender="Man")
2183 trip_from = today() + timedelta(days=10)
2184 trip_to = today() + timedelta(days=20)
2185 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2186 _make_trip_node_admin(host.id, trip_id)
2188 with requests_session(host_token) as api:
2189 res = api.CreateHostRequest(
2190 requests_pb2.CreateHostRequestReq(
2191 host_user_id=surfer.id,
2192 from_date=trip_from.isoformat(),
2193 to_date=trip_to.isoformat(),
2194 text=valid_request_text(),
2195 public_trip_id=trip_id,
2196 )
2197 )
2198 assert res.host_request_id > 0
2201def test_create_request_duplicate_offer_rejected(db):
2202 surfer, _ = generate_user()
2203 _, host_token = generate_user()
2205 trip_from = today() + timedelta(days=10)
2206 trip_to = today() + timedelta(days=20)
2207 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2209 with requests_session(host_token) as api:
2210 api.CreateHostRequest(
2211 requests_pb2.CreateHostRequestReq(
2212 host_user_id=surfer.id,
2213 from_date=trip_from.isoformat(),
2214 to_date=trip_to.isoformat(),
2215 text=valid_request_text(),
2216 public_trip_id=trip_id,
2217 )
2218 )
2219 with pytest.raises(grpc.RpcError) as e:
2220 api.CreateHostRequest(
2221 requests_pb2.CreateHostRequestReq(
2222 host_user_id=surfer.id,
2223 from_date=trip_from.isoformat(),
2224 to_date=trip_to.isoformat(),
2225 text=valid_request_text(),
2226 public_trip_id=trip_id,
2227 )
2228 )
2229 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION