Coverage for app/backend/src/tests/test_requests.py: 99%
986 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +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 conversations_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 == conversations_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=conversations_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=conversations_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=conversations_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=conversations_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=conversations_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=conversations_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=[conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED],
838 )
839 )
840 assert len(res.host_requests) == 1
841 assert res.host_requests[0].status == conversations_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=[conversations_pb2.HOST_REQUEST_STATUS_PENDING],
848 )
849 )
850 assert len(res.host_requests) == 1
851 assert res.host_requests[0].status == conversations_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 conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
859 conversations_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=[conversations_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=conversations_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=conversations_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=conversations_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=conversations_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=conversations_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=conversations_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 == conversations_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=conversations_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=conversations_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=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED
981 )
982 )
984 api.RespondHostRequest(
985 requests_pb2.RespondHostRequestReq(
986 host_request_id=request_id, status=conversations_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=conversations_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 == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED
1006 assert res.messages[1].host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED
1007 assert res.messages[2].host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED
1008 assert res.messages[4].host_request_status_changed.status == conversations_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=conversations_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=conversations_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 == conversations_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 == conversations_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=conversations_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=conversations_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=conversations_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=conversations_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=conversations_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 == conversations_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 (
1240 res.updates[1].message.host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED
1241 )
1242 assert res.updates[1].status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED
1243 assert res.updates[2].message.text.text == "Test message 3"
1244 assert res.updates[3].message.WhichOneof("content") == "chat_created"
1245 assert res.updates[3].status == conversations_pb2.HOST_REQUEST_STATUS_PENDING
1246 assert res.updates[4].message.text.text == valid_request_text("Test message 4")
1248 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1, number=1))
1249 assert not res.no_more
1250 assert len(res.updates) == 1
1251 assert res.updates[0].message.text.text == "Test message 2"
1252 assert res.updates[0].status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED
1254 with requests_session(token3) as api:
1255 # other user can't access
1256 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1))
1257 assert len(res.updates) == 0
1260def test_archive_host_request(db, moderator):
1261 user1, token1 = generate_user()
1262 user2, token2 = generate_user()
1264 today_plus_2 = today() + timedelta(days=2)
1265 today_plus_3 = today() + timedelta(days=3)
1267 with requests_session(token1) as api:
1268 host_request_id = api.CreateHostRequest(
1269 requests_pb2.CreateHostRequestReq(
1270 host_user_id=user2.id,
1271 from_date=today_plus_2.isoformat(),
1272 to_date=today_plus_3.isoformat(),
1273 text=valid_request_text("Test message 0"),
1274 )
1275 ).host_request_id
1277 api.SendHostRequestMessage(
1278 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1279 )
1280 api.SendHostRequestMessage(
1281 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1282 )
1284 moderator.approve_host_request(host_request_id)
1286 # happy path archiving host request
1287 with requests_session(token1) as api:
1288 api.RespondHostRequest(
1289 requests_pb2.RespondHostRequestReq(
1290 host_request_id=host_request_id,
1291 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED,
1292 text="Test message 3",
1293 )
1294 )
1295 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
1296 assert len(res.host_requests) == 1
1297 assert res.host_requests[0].status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED
1299 # Verify is_archived is False before archiving
1300 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1301 assert not res.is_archived
1303 api.SetHostRequestArchiveStatus(
1304 requests_pb2.SetHostRequestArchiveStatusReq(host_request_id=host_request_id, is_archived=True)
1305 )
1306 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_archived=True))
1307 assert len(res.host_requests) == 1
1309 # Verify is_archived is True after archiving
1310 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1311 assert res.is_archived
1314def test_mark_last_seen(db, moderator):
1315 user1, token1 = generate_user()
1316 user2, token2 = generate_user()
1317 user3, token3 = generate_user()
1318 today_plus_2 = today() + timedelta(days=2)
1319 today_plus_3 = today() + timedelta(days=3)
1320 with requests_session(token1) as api:
1321 host_request_id = api.CreateHostRequest(
1322 requests_pb2.CreateHostRequestReq(
1323 host_user_id=user2.id,
1324 from_date=today_plus_2.isoformat(),
1325 to_date=today_plus_3.isoformat(),
1326 text=valid_request_text("Test message 0"),
1327 )
1328 ).host_request_id
1330 host_request_id_2 = api.CreateHostRequest(
1331 requests_pb2.CreateHostRequestReq(
1332 host_user_id=user2.id,
1333 from_date=today_plus_2.isoformat(),
1334 to_date=today_plus_3.isoformat(),
1335 text=valid_request_text("Test message 0a"),
1336 )
1337 ).host_request_id
1339 moderator.approve_host_request(host_request_id)
1340 moderator.approve_host_request(host_request_id_2)
1342 with requests_session(token1) as api:
1343 api.SendHostRequestMessage(
1344 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1345 )
1346 api.SendHostRequestMessage(
1347 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1348 )
1349 api.RespondHostRequest(
1350 requests_pb2.RespondHostRequestReq(
1351 host_request_id=host_request_id,
1352 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED,
1353 text="Test message 3",
1354 )
1355 )
1357 moderator.approve_host_request(host_request_id)
1358 moderator.approve_host_request(host_request_id_2)
1360 # test Ping unseen host request count, should be automarked after sending
1361 with api_session(token1) as api:
1362 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 0
1363 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1365 with api_session(token2) as api:
1366 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 2
1367 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1369 with requests_session(token2) as api:
1370 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].last_seen_message_id == 0
1372 api.MarkLastSeenHostRequest(
1373 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=3)
1374 )
1376 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].last_seen_message_id == 3
1378 with pytest.raises(grpc.RpcError) as e:
1379 api.MarkLastSeenHostRequest(
1380 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=1)
1381 )
1382 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1383 assert e.value.details() == "You can't unsee messages."
1385 # this will be used to test sent request notifications
1386 host_request_id_3 = api.CreateHostRequest(
1387 requests_pb2.CreateHostRequestReq(
1388 host_user_id=user1.id,
1389 from_date=today_plus_2.isoformat(),
1390 to_date=today_plus_3.isoformat(),
1391 text=valid_request_text("Another test request"),
1392 )
1393 ).host_request_id
1395 moderator.approve_host_request(host_request_id_3)
1397 with requests_session(token2) as api:
1398 # this should make id_2 all read
1399 api.SendHostRequestMessage(
1400 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id_2, text="Test")
1401 )
1403 with api_session(token2) as api:
1404 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 1
1405 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1407 # make sure sent and received count for unseen notifications
1408 with requests_session(token1) as api:
1409 api.SendHostRequestMessage(
1410 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id_3, text="Test message")
1411 )
1413 with api_session(token2) as api:
1414 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 1
1415 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 1
1418def test_mark_last_seen_clears_notifications(db, moderator):
1419 user1, token1 = generate_user()
1420 user2, token2 = generate_user()
1421 today_plus_2 = today() + timedelta(days=2)
1422 today_plus_3 = today() + timedelta(days=3)
1424 with requests_session(token1) as api:
1425 host_request_id = api.CreateHostRequest(
1426 requests_pb2.CreateHostRequestReq(
1427 host_user_id=user2.id,
1428 from_date=today_plus_2.isoformat(),
1429 to_date=today_plus_3.isoformat(),
1430 text=valid_request_text("Test message"),
1431 )
1432 ).host_request_id
1434 moderator.approve_host_request(host_request_id)
1436 def unseen_notification_count(user_id):
1437 with session_scope() as session:
1438 return session.execute(
1439 select(func.count())
1440 .select_from(Notification)
1441 .where(Notification.user_id == user_id)
1442 .where(Notification.key == str(host_request_id))
1443 .where(Notification.is_seen == False)
1444 ).scalar_one()
1446 assert unseen_notification_count(user2.id) > 0
1448 with requests_session(token2) as api:
1449 api.MarkLastSeenHostRequest(
1450 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=1)
1451 )
1453 assert unseen_notification_count(user2.id) == 0
1456def test_response_rate(db, moderator):
1457 user1, token1 = generate_user()
1458 user2, token2 = generate_user()
1459 user3, token3 = generate_user(delete_user=True)
1461 today_plus_2 = today() + timedelta(days=2)
1462 today_plus_3 = today() + timedelta(days=3)
1464 with session_scope() as session:
1465 refresh_materialized_view(session, "user_response_rates")
1467 with requests_session(token1) as api:
1468 # deleted: not found
1469 with pytest.raises(grpc.RpcError) as e:
1470 api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user3.id))
1471 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1472 assert e.value.details() == "Couldn't find that user."
1474 # no requests: insufficient
1475 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1476 assert res.HasField("insufficient_data")
1478 # send a request and back date it by 36 hours
1479 host_request_1 = api.CreateHostRequest(
1480 requests_pb2.CreateHostRequestReq(
1481 host_user_id=user2.id,
1482 from_date=today_plus_2.isoformat(),
1483 to_date=today_plus_3.isoformat(),
1484 text=valid_request_text("Test request"),
1485 )
1486 ).host_request_id
1487 moderator.approve_host_request(host_request_1)
1488 with session_scope() as session:
1489 session.execute(
1490 select(Message)
1491 .where(Message.conversation_id == host_request_1)
1492 .where(Message.message_type == MessageType.chat_created)
1493 ).scalar_one().time = now() - timedelta(hours=36)
1494 refresh_materialized_view(session, "user_response_rates")
1496 # still insufficient
1497 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1498 assert res.HasField("insufficient_data")
1500 # send a request and back date it by 35 hours
1501 host_request_2 = api.CreateHostRequest(
1502 requests_pb2.CreateHostRequestReq(
1503 host_user_id=user2.id,
1504 from_date=today_plus_2.isoformat(),
1505 to_date=today_plus_3.isoformat(),
1506 text=valid_request_text("Test request"),
1507 )
1508 ).host_request_id
1509 moderator.approve_host_request(host_request_2)
1510 with session_scope() as session:
1511 session.execute(
1512 select(Message)
1513 .where(Message.conversation_id == host_request_2)
1514 .where(Message.message_type == MessageType.chat_created)
1515 ).scalar_one().time = now() - timedelta(hours=35)
1516 refresh_materialized_view(session, "user_response_rates")
1518 # still insufficient
1519 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1520 assert res.HasField("insufficient_data")
1522 # send a request and back date it by 34 hours
1523 host_request_3 = api.CreateHostRequest(
1524 requests_pb2.CreateHostRequestReq(
1525 host_user_id=user2.id,
1526 from_date=today_plus_2.isoformat(),
1527 to_date=today_plus_3.isoformat(),
1528 text=valid_request_text("Test request"),
1529 )
1530 ).host_request_id
1531 moderator.approve_host_request(host_request_3)
1532 with session_scope() as session:
1533 session.execute(
1534 select(Message)
1535 .where(Message.conversation_id == host_request_3)
1536 .where(Message.message_type == MessageType.chat_created)
1537 ).scalar_one().time = now() - timedelta(hours=34)
1538 refresh_materialized_view(session, "user_response_rates")
1540 # now low
1541 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1542 assert res.HasField("low")
1544 with requests_session(token2) as api:
1545 # accept a host req
1546 api.RespondHostRequest(
1547 requests_pb2.RespondHostRequestReq(
1548 host_request_id=host_request_2,
1549 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1550 text="Accepting host request",
1551 )
1552 )
1554 with session_scope() as session:
1555 refresh_materialized_view(session, "user_response_rates")
1557 with requests_session(token1) as api:
1558 # now some w p33 = 35h
1559 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1560 assert res.HasField("some")
1561 assert res.some.response_time_p33.ToTimedelta() == timedelta(hours=35)
1563 with requests_session(token2) as api:
1564 # accept another host req
1565 api.RespondHostRequest(
1566 requests_pb2.RespondHostRequestReq(
1567 host_request_id=host_request_3,
1568 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1569 text="Accepting host request",
1570 )
1571 )
1573 with session_scope() as session:
1574 refresh_materialized_view(session, "user_response_rates")
1576 with requests_session(token1) as api:
1577 # now most w p33 = 34h, p66 = 35h
1578 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1579 assert res.HasField("most")
1580 assert res.most.response_time_p33.ToTimedelta() == timedelta(hours=34)
1581 assert res.most.response_time_p66.ToTimedelta() == timedelta(hours=35)
1583 with requests_session(token2) as api:
1584 # accept last host req
1585 api.RespondHostRequest(
1586 requests_pb2.RespondHostRequestReq(
1587 host_request_id=host_request_1,
1588 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1589 text="Accepting host request",
1590 )
1591 )
1593 with session_scope() as session:
1594 refresh_materialized_view(session, "user_response_rates")
1596 with requests_session(token1) as api:
1597 # now all w p33 = 34h, p66 = 35h
1598 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1599 assert res.HasField("almost_all")
1600 assert res.almost_all.response_time_p33.ToTimedelta() == timedelta(hours=34)
1601 assert res.almost_all.response_time_p66.ToTimedelta() == timedelta(hours=35)
1603 # send a request and back date it by 2 hours
1604 host_request_4 = api.CreateHostRequest(
1605 requests_pb2.CreateHostRequestReq(
1606 host_user_id=user2.id,
1607 from_date=today_plus_2.isoformat(),
1608 to_date=today_plus_3.isoformat(),
1609 text=valid_request_text("Test request"),
1610 )
1611 ).host_request_id
1612 moderator.approve_host_request(host_request_4)
1613 with session_scope() as session:
1614 session.execute(
1615 select(Message)
1616 .where(Message.conversation_id == host_request_4)
1617 .where(Message.message_type == MessageType.chat_created)
1618 ).scalar_one().time = now() - timedelta(hours=2)
1619 refresh_materialized_view(session, "user_response_rates")
1621 # send a request and back date it by 4 hours
1622 host_request_5 = api.CreateHostRequest(
1623 requests_pb2.CreateHostRequestReq(
1624 host_user_id=user2.id,
1625 from_date=today_plus_2.isoformat(),
1626 to_date=today_plus_3.isoformat(),
1627 text=valid_request_text("Test request"),
1628 )
1629 ).host_request_id
1630 moderator.approve_host_request(host_request_5)
1631 with session_scope() as session:
1632 session.execute(
1633 select(Message)
1634 .where(Message.conversation_id == host_request_5)
1635 .where(Message.message_type == MessageType.chat_created)
1636 ).scalar_one().time = now() - timedelta(hours=4)
1637 refresh_materialized_view(session, "user_response_rates")
1639 # now some w p33 = 35h
1640 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1641 assert res.HasField("some")
1642 assert res.some.response_time_p33.ToTimedelta() == timedelta(hours=35)
1644 with requests_session(token2) as api:
1645 # accept host req
1646 api.RespondHostRequest(
1647 requests_pb2.RespondHostRequestReq(
1648 host_request_id=host_request_5,
1649 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1650 text="Accepting host request",
1651 )
1652 )
1654 with session_scope() as session:
1655 refresh_materialized_view(session, "user_response_rates")
1657 with requests_session(token1) as api:
1658 # now most w p33 = 34h, p66 = 36h
1659 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1660 assert res.HasField("most")
1661 assert res.most.response_time_p33.ToTimedelta() == timedelta(hours=34)
1662 assert res.most.response_time_p66.ToTimedelta() == timedelta(hours=36)
1664 with requests_session(token2) as api:
1665 # accept host req
1666 api.RespondHostRequest(
1667 requests_pb2.RespondHostRequestReq(
1668 host_request_id=host_request_4,
1669 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1670 text="Accepting host request",
1671 )
1672 )
1674 with session_scope() as session:
1675 refresh_materialized_view(session, "user_response_rates")
1677 with requests_session(token1) as api:
1678 # now most w p33 = 4h, p66 = 35h
1679 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1680 assert res.HasField("almost_all")
1681 assert res.almost_all.response_time_p33.ToTimedelta() == timedelta(hours=4)
1682 assert res.almost_all.response_time_p66.ToTimedelta() == timedelta(hours=35)
1685def test_request_notifications(db, email_collector: EmailCollector, push_collector: PushCollector, moderator):
1686 host, host_token = generate_user(complete_profile=True)
1687 surfer, surfer_token = generate_user(complete_profile=True)
1689 host_loc_context = LocalizationContext.from_user(host)
1690 surfer_loc_context = LocalizationContext.from_user(surfer)
1692 today_plus_2 = today() + timedelta(days=2)
1693 today_plus_3 = today() + timedelta(days=3)
1695 with requests_session(surfer_token) as api:
1696 hr_id = api.CreateHostRequest(
1697 requests_pb2.CreateHostRequestReq(
1698 host_user_id=host.id,
1699 from_date=today_plus_2.isoformat(),
1700 to_date=today_plus_3.isoformat(),
1701 text=valid_request_text("can i stay plz"),
1702 )
1703 ).host_request_id
1705 moderator.approve_host_request(hr_id)
1707 email = email_collector.pop_for_recipient(host.email, last=True)
1708 assert email.recipient == host.email
1709 assert "host request" in email.subject.lower()
1710 assert host.name in email.plain
1711 assert host.name in email.html
1712 assert "quick decline" in email.plain.lower(), email.plain
1713 assert "quick decline" in email.html.lower()
1714 assert surfer.name in email.plain
1715 assert surfer.name in email.html
1716 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
1717 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.html
1718 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
1719 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.html
1720 assert "http://localhost:5001/img/thumbnail/" not in email.plain
1721 assert "http://localhost:5001/img/thumbnail/" in email.html
1722 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
1723 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
1724 assert not email.attachments
1726 assert push_collector.pop_for_user(host.id, last=True).content.title == f"New host request from {surfer.name}"
1728 with requests_session(host_token) as api:
1729 api.RespondHostRequest(
1730 requests_pb2.RespondHostRequestReq(
1731 host_request_id=hr_id,
1732 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1733 text="Accepting host request",
1734 )
1735 )
1737 email = email_collector.pop_for_recipient(surfer.email, last=True)
1738 assert email.recipient == surfer.email
1739 assert "host request" in email.subject.lower()
1740 assert host.name in email.plain
1741 assert host.name in email.html
1742 assert surfer.name in email.plain
1743 assert surfer.name in email.html
1744 assert surfer_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
1745 assert surfer_loc_context.localize_date(today_plus_2, with_year=False) in email.html
1746 assert surfer_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
1747 assert surfer_loc_context.localize_date(today_plus_3, with_year=False) in email.html
1748 assert "http://localhost:5001/img/thumbnail/" not in email.plain
1749 assert "http://localhost:5001/img/thumbnail/" in email.html
1750 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
1751 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
1752 assert len(email.attachments or []) == 1
1754 assert push_collector.pop_for_user(surfer.id, last=True).content.title == f"{host.name} accepted your host request"
1757def test_quick_decline(db, email_collector: EmailCollector, push_collector: PushCollector, moderator):
1758 host, host_token = generate_user(complete_profile=True)
1759 surfer, surfer_token = generate_user(complete_profile=True)
1761 host_loc_context = LocalizationContext.from_user(host)
1763 today_plus_2 = today() + timedelta(days=2)
1764 today_plus_3 = today() + timedelta(days=3)
1766 with requests_session(surfer_token) as api:
1767 hr_id = api.CreateHostRequest(
1768 requests_pb2.CreateHostRequestReq(
1769 host_user_id=host.id,
1770 from_date=today_plus_2.isoformat(),
1771 to_date=today_plus_3.isoformat(),
1772 text=valid_request_text("can i stay plz"),
1773 )
1774 ).host_request_id
1776 moderator.approve_host_request(hr_id)
1778 email = email_collector.pop_for_recipient(host.email, last=True)
1779 assert email.recipient == host.email
1780 assert "host request" in email.subject.lower()
1781 assert host.name in email.plain
1782 assert host.name in email.html
1783 assert "quick decline" in email.plain.lower(), email.plain
1784 assert "quick decline" in email.html.lower()
1785 assert surfer.name in email.plain
1786 assert surfer.name in email.html
1787 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
1788 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.html
1789 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
1790 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.html
1791 assert "http://localhost:5001/img/thumbnail/" not in email.plain
1792 assert "http://localhost:5001/img/thumbnail/" in email.html
1793 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
1794 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
1796 assert push_collector.pop_for_user(host.id, last=True).content.title == f"New host request from {surfer.name}"
1798 # very ugly
1799 # http://localhost:3000/quick-link?payload=CAEiGAoOZnJpZW5kX3JlcXVlc3QSBmFjY2VwdA==&sig=BQdk024NTATm8zlR0krSXTBhP5U9TlFv7VhJeIHZtUg=
1800 for link in re.findall(r'<a href="(.*?)"', email.html): 1800 ↛ 1819line 1800 didn't jump to line 1819 because the loop on line 1800 didn't complete
1801 if "payload" not in link:
1802 continue
1803 print(link)
1804 url_parts = urlparse(html.unescape(link))
1805 params = parse_qs(url_parts.query)
1806 print(params["payload"][0])
1807 payload = unsubscribe_pb2.UnsubscribePayload.FromString(b64decode(params["payload"][0]))
1808 if payload.HasField("host_request_quick_decline"): 1808 ↛ 1800line 1808 didn't jump to line 1800 because the condition on line 1808 was always true
1809 with auth_api_session() as (auth_api, metadata_interceptor):
1810 res = auth_api.Unsubscribe(
1811 auth_pb2.UnsubscribeReq(
1812 payload=b64decode(params["payload"][0]),
1813 sig=b64decode(params["sig"][0]),
1814 )
1815 )
1816 assert res.response == "Thank you for responding to the host request!"
1817 break
1818 else:
1819 raise Exception("Didn't find link")
1821 with requests_session(surfer_token) as api:
1822 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
1823 assert res.status == conversations_pb2.HOST_REQUEST_STATUS_REJECTED
1826def test_host_req_feedback(db, moderator):
1827 host, host_token = generate_user(complete_profile=True)
1828 host2, host2_token = generate_user(complete_profile=True)
1829 host3, host3_token = generate_user(complete_profile=True)
1830 surfer, surfer_token = generate_user(complete_profile=True)
1832 today_plus_2 = today() + timedelta(days=2)
1833 today_plus_3 = today() + timedelta(days=3)
1835 with requests_session(surfer_token) as api:
1836 hr_id = api.CreateHostRequest(
1837 requests_pb2.CreateHostRequestReq(
1838 host_user_id=host.id,
1839 from_date=today_plus_2.isoformat(),
1840 to_date=today_plus_3.isoformat(),
1841 text=valid_request_text("can i stay plz"),
1842 )
1843 ).host_request_id
1844 hr2_id = api.CreateHostRequest(
1845 requests_pb2.CreateHostRequestReq(
1846 host_user_id=host2.id,
1847 from_date=today_plus_2.isoformat(),
1848 to_date=today_plus_3.isoformat(),
1849 text=valid_request_text("can i stay plz"),
1850 )
1851 ).host_request_id
1852 hr3_id = api.CreateHostRequest(
1853 requests_pb2.CreateHostRequestReq(
1854 host_user_id=host3.id,
1855 from_date=today_plus_2.isoformat(),
1856 to_date=today_plus_3.isoformat(),
1857 text=valid_request_text("can i stay plz"),
1858 )
1859 ).host_request_id
1861 moderator.approve_host_request(hr_id)
1862 moderator.approve_host_request(hr2_id)
1863 moderator.approve_host_request(hr3_id)
1865 with requests_session(host_token) as api:
1866 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
1867 assert not res.need_host_request_feedback
1869 api.RespondHostRequest(
1870 requests_pb2.RespondHostRequestReq(
1871 host_request_id=hr_id,
1872 status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED,
1873 )
1874 )
1876 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
1877 assert res.need_host_request_feedback
1879 # surfer can't leave feedback
1880 with requests_session(surfer_token) as api:
1881 with pytest.raises(grpc.RpcError) as e:
1882 api.SendHostRequestFeedback(
1883 requests_pb2.SendHostRequestFeedbackReq(
1884 host_request_id=hr_id,
1885 )
1886 )
1887 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1888 assert e.value.details() == "Couldn't find that host request."
1890 with requests_session(host_token) as api:
1891 api.SendHostRequestFeedback(
1892 requests_pb2.SendHostRequestFeedbackReq(
1893 host_request_id=hr_id,
1894 host_request_quality=requests_pb2.HOST_REQUEST_QUALITY_LOW,
1895 )
1896 )
1897 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
1898 assert not res.need_host_request_feedback
1900 # can't leave it twice
1901 with requests_session(host_token) as api:
1902 with pytest.raises(grpc.RpcError) as e:
1903 api.SendHostRequestFeedback(
1904 requests_pb2.SendHostRequestFeedbackReq(
1905 host_request_id=hr_id,
1906 )
1907 )
1908 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1909 assert e.value.details() == "You have already left feedback for this host request!"
1911 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
1912 assert not res.need_host_request_feedback
1914 with requests_session(host2_token) as api:
1915 api.RespondHostRequest(
1916 requests_pb2.RespondHostRequestReq(
1917 host_request_id=hr2_id, status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED
1918 )
1919 )
1920 # can't leave feedback on the wrong one
1921 with pytest.raises(grpc.RpcError) as e:
1922 api.SendHostRequestFeedback(
1923 requests_pb2.SendHostRequestFeedbackReq(
1924 host_request_id=hr_id,
1925 )
1926 )
1927 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1928 assert e.value.details() == "Couldn't find that host request."
1930 # null feedback is still feedback
1931 api.SendHostRequestFeedback(requests_pb2.SendHostRequestFeedbackReq(host_request_id=hr2_id))
1933 with requests_session(host3_token) as api:
1934 api.RespondHostRequest(
1935 requests_pb2.RespondHostRequestReq(
1936 host_request_id=hr3_id, status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED
1937 )
1938 )
1940 api.SendHostRequestFeedback(
1941 requests_pb2.SendHostRequestFeedbackReq(host_request_id=hr3_id, decline_reason="bad req")
1942 )
1945def _make_trip_node_admin(user_id: int, trip_id: int):
1946 with session_scope() as session:
1947 node_id = session.execute(select(PublicTrip.node_id).where(PublicTrip.id == trip_id)).scalar_one()
1948 cluster = session.execute(
1949 select(Cluster).where(Cluster.parent_node_id == node_id).where(Cluster.is_official_cluster)
1950 ).scalar_one_or_none()
1951 if cluster is None: 1951 ↛ 1960line 1951 didn't jump to line 1960 because the condition on line 1951 was always true
1952 cluster = Cluster(
1953 name="Test community",
1954 description="Test",
1955 parent_node_id=node_id,
1956 is_official_cluster=True,
1957 )
1958 session.add(cluster)
1959 session.flush()
1960 session.add(ClusterSubscription(cluster_id=cluster.id, user_id=user_id, role=ClusterRole.admin))
1963def _create_public_trip(user_id: int, from_date, to_date, *, status=None, same_gender_only: bool = False):
1964 with session_scope() as session:
1965 node = session.execute(select(Node).limit(1)).scalar_one_or_none()
1966 if node is None: 1966 ↛ 1973line 1966 didn't jump to line 1973 because the condition on line 1966 was always true
1967 node = Node(
1968 geom=to_multi(create_polygon_lat_lng([[0, 0], [0, 2], [2, 2], [2, 0], [0, 0]])),
1969 node_type=NodeType.locality,
1970 )
1971 session.add(node)
1972 session.flush()
1973 trip = PublicTrip(
1974 user_id=user_id,
1975 node_id=node.id,
1976 from_date=from_date,
1977 to_date=to_date,
1978 description="Looking for a host!",
1979 status=status or PublicTripStatus.searching_for_host,
1980 same_gender_only=same_gender_only,
1981 )
1982 session.add(trip)
1983 session.flush()
1984 return trip.id
1987def test_create_request_with_public_trip(db, moderator):
1988 """Hosts can offer to host a public trip; offered dates must be within trip dates."""
1989 surfer, surfer_token = generate_user()
1990 host, host_token = generate_user()
1992 trip_from = today() + timedelta(days=10)
1993 trip_to = today() + timedelta(days=20)
1994 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
1996 with requests_session(host_token) as api:
1997 # Happy path: dates within trip window
1998 res = api.CreateHostRequest(
1999 requests_pb2.CreateHostRequestReq(
2000 host_user_id=surfer.id,
2001 from_date=(trip_from + timedelta(days=1)).isoformat(),
2002 to_date=(trip_to - timedelta(days=1)).isoformat(),
2003 text=valid_request_text(),
2004 public_trip_id=trip_id,
2005 )
2006 )
2007 host_request_id = res.host_request_id
2009 moderator.approve_host_request(host_request_id)
2011 with requests_session(host_token) as api:
2012 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2013 assert hr.public_trip_id == trip_id
2016def test_create_request_with_public_trip_dates_out_of_range(db):
2017 """Offered dates outside the trip window are rejected."""
2018 surfer, _ = generate_user()
2019 host, host_token = generate_user()
2021 trip_from = today() + timedelta(days=10)
2022 trip_to = today() + timedelta(days=20)
2023 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2025 with requests_session(host_token) as api:
2026 # from_date before trip starts
2027 with pytest.raises(grpc.RpcError) as e:
2028 api.CreateHostRequest(
2029 requests_pb2.CreateHostRequestReq(
2030 host_user_id=surfer.id,
2031 from_date=(trip_from - timedelta(days=1)).isoformat(),
2032 to_date=(trip_from + timedelta(days=1)).isoformat(),
2033 text=valid_request_text(),
2034 public_trip_id=trip_id,
2035 )
2036 )
2037 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2039 # to_date after trip ends
2040 with pytest.raises(grpc.RpcError) as e:
2041 api.CreateHostRequest(
2042 requests_pb2.CreateHostRequestReq(
2043 host_user_id=surfer.id,
2044 from_date=(trip_to - timedelta(days=1)).isoformat(),
2045 to_date=(trip_to + timedelta(days=1)).isoformat(),
2046 text=valid_request_text(),
2047 public_trip_id=trip_id,
2048 )
2049 )
2050 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2053def test_create_request_with_public_trip_user_mismatch(db):
2054 """The host_user_id must match the public trip's traveler."""
2055 trip_owner, _ = generate_user()
2056 other_user, _ = generate_user()
2057 host, host_token = generate_user()
2059 trip_from = today() + timedelta(days=10)
2060 trip_to = today() + timedelta(days=20)
2061 trip_id = _create_public_trip(trip_owner.id, trip_from, trip_to)
2063 with requests_session(host_token) as api:
2064 with pytest.raises(grpc.RpcError) as e:
2065 api.CreateHostRequest(
2066 requests_pb2.CreateHostRequestReq(
2067 host_user_id=other_user.id, # not the trip owner
2068 from_date=(trip_from + timedelta(days=1)).isoformat(),
2069 to_date=(trip_to - timedelta(days=1)).isoformat(),
2070 text=valid_request_text(),
2071 public_trip_id=trip_id,
2072 )
2073 )
2074 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2077def test_create_request_with_closed_public_trip(db):
2078 """Cannot offer to host a trip that's been closed."""
2079 surfer, _ = generate_user()
2080 host, host_token = generate_user()
2082 trip_from = today() + timedelta(days=10)
2083 trip_to = today() + timedelta(days=20)
2084 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, status=PublicTripStatus.closed)
2086 with requests_session(host_token) as api:
2087 with pytest.raises(grpc.RpcError) as e:
2088 api.CreateHostRequest(
2089 requests_pb2.CreateHostRequestReq(
2090 host_user_id=surfer.id,
2091 from_date=(trip_from + timedelta(days=1)).isoformat(),
2092 to_date=(trip_to - timedelta(days=1)).isoformat(),
2093 text=valid_request_text(),
2094 public_trip_id=trip_id,
2095 )
2096 )
2097 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2100def test_create_request_with_nonexistent_public_trip(db):
2101 """Nonexistent public trip ID returns NOT_FOUND."""
2102 surfer, _ = generate_user()
2103 host, host_token = generate_user()
2105 with requests_session(host_token) as api:
2106 with pytest.raises(grpc.RpcError) as e:
2107 api.CreateHostRequest(
2108 requests_pb2.CreateHostRequestReq(
2109 host_user_id=surfer.id,
2110 from_date=(today() + timedelta(days=2)).isoformat(),
2111 to_date=(today() + timedelta(days=3)).isoformat(),
2112 text=valid_request_text(),
2113 public_trip_id=999999,
2114 )
2115 )
2116 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2119def test_create_request_without_public_trip_id_unchanged(db, moderator):
2120 """Existing flow without public_trip_id still works (backwards compatibility)."""
2121 surfer, _ = generate_user()
2122 host, host_token = generate_user()
2124 with requests_session(host_token) as api:
2125 res = api.CreateHostRequest(
2126 requests_pb2.CreateHostRequestReq(
2127 host_user_id=surfer.id,
2128 from_date=(today() + timedelta(days=2)).isoformat(),
2129 to_date=(today() + timedelta(days=3)).isoformat(),
2130 text=valid_request_text(),
2131 )
2132 )
2133 host_request_id = res.host_request_id
2135 moderator.approve_host_request(host_request_id)
2137 with requests_session(host_token) as api:
2138 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2139 assert not hr.HasField("public_trip_id")
2142def test_create_request_same_gender_only_wrong_gender_rejected(db):
2143 surfer, _ = generate_user(gender="Woman")
2144 _, host_token = generate_user(gender="Man")
2146 trip_from = today() + timedelta(days=10)
2147 trip_to = today() + timedelta(days=20)
2148 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2150 with requests_session(host_token) as api:
2151 with pytest.raises(grpc.RpcError) as e:
2152 api.CreateHostRequest(
2153 requests_pb2.CreateHostRequestReq(
2154 host_user_id=surfer.id,
2155 from_date=trip_from.isoformat(),
2156 to_date=trip_to.isoformat(),
2157 text=valid_request_text(),
2158 public_trip_id=trip_id,
2159 )
2160 )
2161 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2164def test_create_request_same_gender_only_same_gender_allowed(db, moderator):
2165 surfer, _ = generate_user(gender="Woman")
2166 _, host_token = generate_user(gender="Woman")
2168 trip_from = today() + timedelta(days=10)
2169 trip_to = today() + timedelta(days=20)
2170 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2172 with requests_session(host_token) as api:
2173 res = api.CreateHostRequest(
2174 requests_pb2.CreateHostRequestReq(
2175 host_user_id=surfer.id,
2176 from_date=trip_from.isoformat(),
2177 to_date=trip_to.isoformat(),
2178 text=valid_request_text(),
2179 public_trip_id=trip_id,
2180 )
2181 )
2182 assert res.host_request_id > 0
2185def test_create_request_same_gender_only_moderator_bypass(db, moderator):
2186 surfer, _ = generate_user(gender="Woman")
2187 host, host_token = generate_user(gender="Man")
2189 trip_from = today() + timedelta(days=10)
2190 trip_to = today() + timedelta(days=20)
2191 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2192 _make_trip_node_admin(host.id, trip_id)
2194 with requests_session(host_token) as api:
2195 res = api.CreateHostRequest(
2196 requests_pb2.CreateHostRequestReq(
2197 host_user_id=surfer.id,
2198 from_date=trip_from.isoformat(),
2199 to_date=trip_to.isoformat(),
2200 text=valid_request_text(),
2201 public_trip_id=trip_id,
2202 )
2203 )
2204 assert res.host_request_id > 0
2207def test_create_request_duplicate_offer_rejected(db):
2208 surfer, _ = generate_user()
2209 _, host_token = generate_user()
2211 trip_from = today() + timedelta(days=10)
2212 trip_to = today() + timedelta(days=20)
2213 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2215 with requests_session(host_token) as api:
2216 api.CreateHostRequest(
2217 requests_pb2.CreateHostRequestReq(
2218 host_user_id=surfer.id,
2219 from_date=trip_from.isoformat(),
2220 to_date=trip_to.isoformat(),
2221 text=valid_request_text(),
2222 public_trip_id=trip_id,
2223 )
2224 )
2225 with pytest.raises(grpc.RpcError) as e:
2226 api.CreateHostRequest(
2227 requests_pb2.CreateHostRequestReq(
2228 host_user_id=surfer.id,
2229 from_date=trip_from.isoformat(),
2230 to_date=trip_to.isoformat(),
2231 text=valid_request_text(),
2232 public_trip_id=trip_id,
2233 )
2234 )
2235 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION