Coverage for app/backend/src/tests/test_requests.py: 99%
1171 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +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 ModerationObjectType,
24 ModerationState,
25 ModerationVisibility,
26 Node,
27 NodeType,
28 Notification,
29 RateLimitAction,
30)
31from couchers.models.public_trips import PublicTrip, PublicTripStatus
32from couchers.proto import (
33 api_pb2,
34 auth_pb2,
35 messages_pb2,
36 requests_pb2,
37)
38from couchers.proto.internal import unsubscribe_pb2
39from couchers.rate_limits.definitions import RATE_LIMIT_DEFINITIONS, RATE_LIMIT_HOURS
40from couchers.utils import create_coordinate, create_polygon_lat_lng, now, to_multi, today
41from tests.fixtures.db import backdate_conversations, generate_user
42from tests.fixtures.misc import EmailCollector, PushCollector
43from tests.fixtures.sessions import api_session, auth_api_session, requests_session
44from tests.test_public_trips import _create_trip_directly, _make_node
47def valid_request_text(text: str = "Test request") -> str:
48 """Pads a request text to a valid length."""
49 # Request lengths are measured in utf-16 code units to match the frontend.
50 utf16_length = len(text.encode("utf-16-le")) // 2
51 if utf16_length >= HOST_REQUEST_MIN_LENGTH_UTF16: 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true
52 return text
53 padding_length = HOST_REQUEST_MIN_LENGTH_UTF16 - utf16_length
54 return text + ("_" * padding_length) # Each "_" adds one utf16 code unit.
57def test_create_request(db, moderator):
58 user1, token1 = generate_user()
59 hosting_city = "Morningside Heights, New York City"
60 hosting_lat = 40.8086
61 hosting_lng = -73.9616
62 hosting_radius = 500
63 user2, token2 = generate_user(
64 city=hosting_city,
65 geom=create_coordinate(hosting_lat, hosting_lng),
66 geom_radius=hosting_radius,
67 )
69 today_plus_2 = today() + timedelta(days=2)
70 today_plus_3 = today() + timedelta(days=3)
71 today_minus_2 = today() - timedelta(days=2)
72 today_minus_3 = today() - timedelta(days=3)
74 with requests_session(token1) as api:
75 with pytest.raises(grpc.RpcError) as e:
76 api.CreateHostRequest(
77 requests_pb2.CreateHostRequestReq(
78 host_user_id=user1.id,
79 from_date=today_plus_2.isoformat(),
80 to_date=today_plus_3.isoformat(),
81 text=valid_request_text(),
82 )
83 )
84 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
85 assert e.value.details() == "Can't request hosting from yourself."
87 with pytest.raises(grpc.RpcError) as e:
88 api.CreateHostRequest(
89 requests_pb2.CreateHostRequestReq(
90 host_user_id=999,
91 from_date=today_plus_2.isoformat(),
92 to_date=today_plus_3.isoformat(),
93 text=valid_request_text(),
94 )
95 )
96 assert e.value.code() == grpc.StatusCode.NOT_FOUND
97 assert e.value.details() == "Couldn't find that user."
99 with pytest.raises(grpc.RpcError) as e:
100 api.CreateHostRequest(
101 requests_pb2.CreateHostRequestReq(
102 host_user_id=user2.id,
103 from_date=today_plus_3.isoformat(),
104 to_date=today_plus_2.isoformat(),
105 text=valid_request_text(),
106 )
107 )
108 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
109 assert e.value.details() == "From date can't be after to date."
111 with pytest.raises(grpc.RpcError) as e:
112 api.CreateHostRequest(
113 requests_pb2.CreateHostRequestReq(
114 host_user_id=user2.id,
115 from_date=today_minus_3.isoformat(),
116 to_date=today_plus_2.isoformat(),
117 text=valid_request_text(),
118 )
119 )
120 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
121 assert e.value.details() == "From date must be today or later."
123 with pytest.raises(grpc.RpcError) as e:
124 api.CreateHostRequest(
125 requests_pb2.CreateHostRequestReq(
126 host_user_id=user2.id,
127 from_date=today_plus_2.isoformat(),
128 to_date=today_minus_2.isoformat(),
129 text=valid_request_text(),
130 )
131 )
132 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
133 assert e.value.details() == "From date can't be after to date."
135 with pytest.raises(grpc.RpcError) as e:
136 api.CreateHostRequest(
137 requests_pb2.CreateHostRequestReq(
138 host_user_id=user2.id,
139 from_date="2020-00-06",
140 to_date=today_minus_2.isoformat(),
141 text=valid_request_text(),
142 )
143 )
144 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
145 assert e.value.details() == "Invalid date."
147 with pytest.raises(grpc.RpcError) as e:
148 api.CreateHostRequest(
149 requests_pb2.CreateHostRequestReq(
150 host_user_id=user2.id,
151 from_date=today_plus_2.isoformat(),
152 to_date=today_plus_3.isoformat(),
153 text="Too short.",
154 )
155 )
156 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
157 assert e.value.details() == "Host request cannot be shorter than 250 characters."
159 res = api.CreateHostRequest(
160 requests_pb2.CreateHostRequestReq(
161 host_user_id=user2.id,
162 from_date=today_plus_2.isoformat(),
163 to_date=today_plus_3.isoformat(),
164 text=valid_request_text(),
165 )
166 )
167 host_request_id = res.host_request_id
169 moderator.approve_host_request(host_request_id)
171 with requests_session(token1) as api:
172 host_requests = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)).host_requests
174 assert len(host_requests) == 1
175 hr = host_requests[0]
177 assert hr.latest_message.text.text == valid_request_text()
179 assert hr.hosting_city == hosting_city
180 assert round(hr.hosting_lat, 4) == hosting_lat
181 assert round(hr.hosting_lng, 4) == hosting_lng
182 assert hr.hosting_radius == hosting_radius
184 today_ = today()
185 today_plus_one_year = today_ + timedelta(days=365)
186 today_plus_one_year_plus_2 = today_plus_one_year + timedelta(days=2)
187 today_plus_one_year_plus_3 = today_plus_one_year + timedelta(days=3)
188 with pytest.raises(grpc.RpcError) as e:
189 api.CreateHostRequest(
190 requests_pb2.CreateHostRequestReq(
191 host_user_id=user2.id,
192 from_date=today_plus_one_year_plus_2.isoformat(),
193 to_date=today_plus_one_year_plus_3.isoformat(),
194 text=valid_request_text("Test from date after one year"),
195 )
196 )
197 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
198 assert e.value.details() == "The start date must be within one year from today."
200 with pytest.raises(grpc.RpcError) as e:
201 api.CreateHostRequest(
202 requests_pb2.CreateHostRequestReq(
203 host_user_id=user2.id,
204 from_date=today_plus_2.isoformat(),
205 to_date=today_plus_one_year_plus_3.isoformat(),
206 text=valid_request_text("Test to date one year after from date"),
207 )
208 )
209 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
210 assert e.value.details() == "You cannot request to stay with someone for longer than one year."
213def test_create_host_request_rejects_date_past_in_host_timezone(db):
214 # When the host's timezone has already rolled over to the next day, a
215 # from_date of "today in UTC" is in the past from the host's perspective and
216 # must be rejected. The frontend blocks this date before submission; the
217 # backend enforces the same rule for consistency.
218 user1, token1 = generate_user()
219 # geom inside the fake Europe/Helsinki timezone polygon used in tests
220 user2, _ = generate_user(geom=create_coordinate(61, 25))
222 # Helsinki is already on 2026-01-16; requester submits 2026-01-15.
223 fake_today_by_tz = {"Europe/Helsinki": date(2026, 1, 16)}
225 with patch(
226 "couchers.servicers.requests.today_in_timezone",
227 side_effect=lambda tz: fake_today_by_tz.get(tz, date(2026, 1, 15)),
228 ):
229 with requests_session(token1) as api:
230 with pytest.raises(grpc.RpcError) as e:
231 api.CreateHostRequest(
232 requests_pb2.CreateHostRequestReq(
233 host_user_id=user2.id,
234 from_date="2026-01-15",
235 to_date="2026-01-18",
236 text=valid_request_text(),
237 )
238 )
239 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
242def test_create_host_request_date_valid_when_host_behind_requester(db):
243 # Simulate the opposite timezone direction: the host (America/New_York) is
244 # still on 2026-01-15 while the requester has already rolled into 2026-01-16.
245 # A from_date of 2026-01-16 is "today" for the requester and "tomorrow" for
246 # the host — must be accepted without issue.
247 user1, token1 = generate_user()
248 user2, _ = generate_user() # default geom resolves to America/New_York
250 fake_today_by_tz = {"America/New_York": date(2026, 1, 15)}
252 with patch(
253 "couchers.servicers.requests.today_in_timezone",
254 side_effect=lambda tz: fake_today_by_tz.get(tz, date(2026, 1, 15)),
255 ):
256 with requests_session(token1) as api:
257 res = api.CreateHostRequest(
258 requests_pb2.CreateHostRequestReq(
259 host_user_id=user2.id,
260 from_date="2026-01-16",
261 to_date="2026-01-20",
262 text=valid_request_text(),
263 )
264 )
265 assert res.host_request_id
268def test_create_request_duplicate_within_window(db):
269 """A second request to the same host for overlapping dates inside the window is rejected, so a
270 client that resends (or a user who re-taps Send) doesn't create a pile of duplicates."""
271 user1, token1 = generate_user()
272 user2, _ = generate_user()
273 user3, _ = generate_user()
274 from_date = today() + timedelta(days=10)
275 to_date = today() + timedelta(days=15)
277 with requests_session(token1) as api:
278 assert api.CreateHostRequest(
279 requests_pb2.CreateHostRequestReq(
280 host_user_id=user2.id,
281 from_date=from_date.isoformat(),
282 to_date=to_date.isoformat(),
283 text=valid_request_text(),
284 )
285 ).host_request_id
287 with pytest.raises(grpc.RpcError) as e:
288 api.CreateHostRequest(
289 requests_pb2.CreateHostRequestReq(
290 host_user_id=user2.id,
291 from_date=from_date.isoformat(),
292 to_date=to_date.isoformat(),
293 text=valid_request_text(),
294 )
295 )
296 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
297 assert "past 3 hours" in (e.value.details() or "")
299 # partly overlapping dates are blocked too
300 with pytest.raises(grpc.RpcError) as e:
301 api.CreateHostRequest(
302 requests_pb2.CreateHostRequestReq(
303 host_user_id=user2.id,
304 from_date=(to_date - timedelta(days=1)).isoformat(),
305 to_date=(to_date + timedelta(days=5)).isoformat(),
306 text=valid_request_text(),
307 )
308 )
309 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
311 # a different host is unaffected
312 assert api.CreateHostRequest(
313 requests_pb2.CreateHostRequestReq(
314 host_user_id=user3.id,
315 from_date=from_date.isoformat(),
316 to_date=to_date.isoformat(),
317 text=valid_request_text(),
318 )
319 ).host_request_id
322def test_create_request_duplicate_window_allows_other_dates(db):
323 """Only overlapping stays count as duplicates, including a stay starting the day another one
324 ends."""
325 user1, token1 = generate_user()
326 user2, _ = generate_user()
327 from_date = today() + timedelta(days=10)
328 to_date = today() + timedelta(days=15)
330 with requests_session(token1) as api:
331 assert api.CreateHostRequest(
332 requests_pb2.CreateHostRequestReq(
333 host_user_id=user2.id,
334 from_date=from_date.isoformat(),
335 to_date=to_date.isoformat(),
336 text=valid_request_text(),
337 )
338 ).host_request_id
340 assert api.CreateHostRequest(
341 requests_pb2.CreateHostRequestReq(
342 host_user_id=user2.id,
343 from_date=to_date.isoformat(),
344 to_date=(to_date + timedelta(days=5)).isoformat(),
345 text=valid_request_text(),
346 )
347 ).host_request_id
349 assert api.CreateHostRequest(
350 requests_pb2.CreateHostRequestReq(
351 host_user_id=user2.id,
352 from_date=(from_date + timedelta(days=90)).isoformat(),
353 to_date=(to_date + timedelta(days=90)).isoformat(),
354 text=valid_request_text(),
355 )
356 ).host_request_id
359def test_create_request_duplicate_allowed_after_window(db):
360 user1, token1 = generate_user()
361 user2, _ = generate_user()
362 from_date = today() + timedelta(days=10)
363 to_date = today() + timedelta(days=15)
365 with requests_session(token1) as api:
366 api.CreateHostRequest(
367 requests_pb2.CreateHostRequestReq(
368 host_user_id=user2.id,
369 from_date=from_date.isoformat(),
370 to_date=to_date.isoformat(),
371 text=valid_request_text(),
372 )
373 )
374 backdate_conversations()
376 assert api.CreateHostRequest(
377 requests_pb2.CreateHostRequestReq(
378 host_user_id=user2.id,
379 from_date=from_date.isoformat(),
380 to_date=to_date.isoformat(),
381 text=valid_request_text(),
382 )
383 ).host_request_id
386def test_create_request_duplicate_window_ignores_public_trip_offers(db):
387 """Offers on public trips are deduplicated per trip, so two offers to the same traveler for
388 two different trips must both go through."""
389 surfer, _ = generate_user()
390 host, host_token = generate_user()
392 trip_1_from = today() + timedelta(days=10)
393 trip_1_to = today() + timedelta(days=20)
394 trip_2_from = today() + timedelta(days=40)
395 trip_2_to = today() + timedelta(days=50)
396 trip_1_id = _create_public_trip(surfer.id, trip_1_from, trip_1_to)
397 trip_2_id = _create_public_trip(surfer.id, trip_2_from, trip_2_to)
399 with requests_session(host_token) as api:
400 assert api.CreateHostRequest(
401 requests_pb2.CreateHostRequestReq(
402 host_user_id=surfer.id,
403 from_date=trip_1_from.isoformat(),
404 to_date=trip_1_to.isoformat(),
405 text=valid_request_text(),
406 public_trip_id=trip_1_id,
407 )
408 ).host_request_id
410 assert api.CreateHostRequest(
411 requests_pb2.CreateHostRequestReq(
412 host_user_id=surfer.id,
413 from_date=trip_2_from.isoformat(),
414 to_date=trip_2_to.isoformat(),
415 text=valid_request_text(),
416 public_trip_id=trip_2_id,
417 )
418 ).host_request_id
420 # the window only counts non-trip requests, so trip offers don't block a plain request
421 # even for dates they cover
422 assert api.CreateHostRequest(
423 requests_pb2.CreateHostRequestReq(
424 host_user_id=surfer.id,
425 from_date=(trip_1_from + timedelta(days=2)).isoformat(),
426 to_date=(trip_1_to - timedelta(days=2)).isoformat(),
427 text=valid_request_text(),
428 )
429 ).host_request_id
432def test_create_request_incomplete_profile(db):
433 user1, token1 = generate_user(complete_profile=False)
434 user2, _ = generate_user()
435 today_plus_2 = today() + timedelta(days=2)
436 today_plus_3 = today() + timedelta(days=3)
437 with requests_session(token1) as api:
438 with pytest.raises(grpc.RpcError) as e:
439 api.CreateHostRequest(
440 requests_pb2.CreateHostRequestReq(
441 host_user_id=user2.id,
442 from_date=today_plus_2.isoformat(),
443 to_date=today_plus_3.isoformat(),
444 text=valid_request_text(),
445 )
446 )
447 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
448 assert e.value.details() == "You have to complete your profile before you can send a request."
451def test_excessive_requests_are_reported(db, low_rate_limits, email_collector: EmailCollector):
452 """Test that excessive host requests are first reported in a warning email and finally lead blocking of further requests."""
453 user, token = generate_user()
454 today_plus_2 = today() + timedelta(days=2)
455 today_plus_3 = today() + timedelta(days=3)
456 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.host_request]
457 with requests_session(token) as api:
458 # Test warning email
459 for _ in range(rate_limit_definition.warning_limit):
460 host_user, _ = generate_user()
461 _ = api.CreateHostRequest(
462 requests_pb2.CreateHostRequestReq(
463 host_user_id=host_user.id,
464 from_date=today_plus_2.isoformat(),
465 to_date=today_plus_3.isoformat(),
466 text=valid_request_text(),
467 )
468 )
470 assert email_collector.count_for_reports() == 0
471 host_user, _ = generate_user()
472 _ = api.CreateHostRequest(
473 requests_pb2.CreateHostRequestReq(
474 host_user_id=host_user.id,
475 from_date=today_plus_2.isoformat(),
476 to_date=today_plus_3.isoformat(),
477 text=valid_request_text("Excessive test request"),
478 )
479 )
481 email = email_collector.pop_for_reports(last=True)
482 assert email.plain.startswith(
483 f"User {user.username} has sent {rate_limit_definition.warning_limit} host requests in the past {RATE_LIMIT_HOURS} hours."
484 )
486 # Test ban after exceeding HOST_REQUEST_HARD_LIMIT
487 for _ in range(rate_limit_definition.hard_limit - rate_limit_definition.warning_limit - 1):
488 host_user, _ = generate_user()
489 _ = api.CreateHostRequest(
490 requests_pb2.CreateHostRequestReq(
491 host_user_id=host_user.id,
492 from_date=today_plus_2.isoformat(),
493 to_date=today_plus_3.isoformat(),
494 text=valid_request_text(),
495 )
496 )
498 assert email_collector.count_for_reports() == 0
500 host_user, _ = generate_user()
501 with pytest.raises(grpc.RpcError) as exc_info:
502 _ = api.CreateHostRequest(
503 requests_pb2.CreateHostRequestReq(
504 host_user_id=host_user.id,
505 from_date=today_plus_2.isoformat(),
506 to_date=today_plus_3.isoformat(),
507 text=valid_request_text("Excessive test request"),
508 )
509 )
510 assert exc_info.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
511 assert (
512 exc_info.value.details()
513 == "You have sent a lot of host requests in the past 24 hours. To avoid spam, you can't send any more for now."
514 )
516 email = email_collector.pop_for_reports(last=True)
517 assert email.plain.startswith(
518 f"User {user.username} has sent {rate_limit_definition.hard_limit} host requests in the past {RATE_LIMIT_HOURS} hours."
519 )
520 assert "The user has been blocked from sending further host requests for now." in email.plain
523def add_message(db, text, author_id, conversation_id):
524 with session_scope() as session:
525 message = Message(
526 conversation_id=conversation_id, author_id=author_id, text=text, message_type=MessageType.text
527 )
529 session.add(message)
532def test_GetHostRequest(db):
533 user1, token1 = generate_user()
534 user2, token2 = generate_user()
535 user3, token3 = generate_user()
536 today_plus_2 = today() + timedelta(days=2)
537 today_plus_3 = today() + timedelta(days=3)
538 with requests_session(token1) as api:
539 host_request_id = api.CreateHostRequest(
540 requests_pb2.CreateHostRequestReq(
541 host_user_id=user2.id,
542 from_date=today_plus_2.isoformat(),
543 to_date=today_plus_3.isoformat(),
544 text=valid_request_text("Test request 1"),
545 )
546 ).host_request_id
548 with pytest.raises(grpc.RpcError) as e:
549 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=999))
550 assert e.value.code() == grpc.StatusCode.NOT_FOUND
551 assert e.value.details() == "Couldn't find that host request."
553 api.SendHostRequestMessage(
554 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
555 )
557 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
558 assert res.latest_message.text.text == "Test message 1"
561def test_ListHostRequests(db, moderator):
562 user1, token1 = generate_user()
563 user2, token2 = generate_user()
564 user3, token3 = generate_user()
565 today_plus_2 = today() + timedelta(days=2)
566 today_plus_3 = today() + timedelta(days=3)
567 with requests_session(token1) as api:
568 host_request_1 = api.CreateHostRequest(
569 requests_pb2.CreateHostRequestReq(
570 host_user_id=user2.id,
571 from_date=today_plus_2.isoformat(),
572 to_date=today_plus_3.isoformat(),
573 text=valid_request_text("Test request 1"),
574 )
575 ).host_request_id
577 host_request_2 = api.CreateHostRequest(
578 requests_pb2.CreateHostRequestReq(
579 host_user_id=user3.id,
580 from_date=today_plus_2.isoformat(),
581 to_date=today_plus_3.isoformat(),
582 text=valid_request_text("Test request 2"),
583 )
584 ).host_request_id
586 moderator.approve_host_request(host_request_1)
587 moderator.approve_host_request(host_request_2)
589 with requests_session(token1) as api:
590 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
591 assert res.no_more
592 assert len(res.host_requests) == 2
594 with requests_session(token2) as api:
595 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
596 assert res.no_more
597 assert len(res.host_requests) == 1
598 assert res.host_requests[0].latest_message.text.text == valid_request_text("Test request 1")
599 assert res.host_requests[0].surfer_user_id == user1.id
600 assert res.host_requests[0].host_user_id == user2.id
601 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_PENDING
603 add_message(db, "Test request 1 message 1", user2.id, host_request_1)
604 add_message(db, "Test request 1 message 2", user2.id, host_request_1)
605 add_message(db, "Test request 1 message 3", user2.id, host_request_1)
607 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
608 assert res.host_requests[0].latest_message.text.text == "Test request 1 message 3"
610 host_request_3 = api.CreateHostRequest(
611 requests_pb2.CreateHostRequestReq(
612 host_user_id=user1.id,
613 from_date=today_plus_2.isoformat(),
614 to_date=today_plus_3.isoformat(),
615 text=valid_request_text("Test request 3"),
616 )
617 ).host_request_id
619 moderator.approve_host_request(host_request_3)
621 add_message(db, "Test request 2 message 1", user1.id, host_request_2)
622 add_message(db, "Test request 2 message 2", user3.id, host_request_2)
624 with requests_session(token3) as api:
625 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
626 assert res.no_more
627 assert len(res.host_requests) == 1
628 assert res.host_requests[0].latest_message.text.text == "Test request 2 message 2"
630 with requests_session(token1) as api:
631 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
632 assert len(res.host_requests) == 1
634 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq())
635 assert len(res.host_requests) == 3
638def test_ListHostRequests_pagination_regression(db, moderator):
639 """
640 ListHostRequests was skipping a request when getting multiple pages
641 """
642 user1, token1 = generate_user()
643 user2, token2 = generate_user()
644 today_plus_2 = today() + timedelta(days=2)
645 today_plus_3 = today() + timedelta(days=3)
646 with requests_session(token1) as api:
647 host_request_1 = api.CreateHostRequest(
648 requests_pb2.CreateHostRequestReq(
649 host_user_id=user2.id,
650 from_date=today_plus_2.isoformat(),
651 to_date=today_plus_3.isoformat(),
652 text=valid_request_text("Test request 1"),
653 )
654 ).host_request_id
655 backdate_conversations()
657 host_request_2 = api.CreateHostRequest(
658 requests_pb2.CreateHostRequestReq(
659 host_user_id=user2.id,
660 from_date=today_plus_2.isoformat(),
661 to_date=today_plus_3.isoformat(),
662 text=valid_request_text("Test request 2"),
663 )
664 ).host_request_id
665 backdate_conversations()
667 host_request_3 = api.CreateHostRequest(
668 requests_pb2.CreateHostRequestReq(
669 host_user_id=user2.id,
670 from_date=today_plus_2.isoformat(),
671 to_date=today_plus_3.isoformat(),
672 text=valid_request_text("Test request 3"),
673 )
674 ).host_request_id
676 moderator.approve_host_request(host_request_1)
677 moderator.approve_host_request(host_request_2)
678 moderator.approve_host_request(host_request_3)
680 with requests_session(token2) as api:
681 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
682 assert res.no_more
683 assert len(res.host_requests) == 3
684 assert res.host_requests[0].latest_message.text.text == valid_request_text("Test request 3")
685 assert res.host_requests[1].latest_message.text.text == valid_request_text("Test request 2")
686 assert res.host_requests[2].latest_message.text.text == valid_request_text("Test request 1")
688 with requests_session(token2) as api:
689 api.RespondHostRequest(
690 requests_pb2.RespondHostRequestReq(
691 host_request_id=host_request_2,
692 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
693 text="Accepting host request 2",
694 )
695 )
696 api.RespondHostRequest(
697 requests_pb2.RespondHostRequestReq(
698 host_request_id=host_request_1,
699 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
700 text="Accepting host request 1",
701 )
702 )
703 api.RespondHostRequest(
704 requests_pb2.RespondHostRequestReq(
705 host_request_id=host_request_3,
706 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
707 text="Accepting host request 3",
708 )
709 )
711 with requests_session(token2) as api:
712 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
713 assert res.no_more
714 assert len(res.host_requests) == 3
715 assert res.host_requests[0].latest_message.text.text == "Accepting host request 3"
716 assert res.host_requests[1].latest_message.text.text == "Accepting host request 1"
717 assert res.host_requests[2].latest_message.text.text == "Accepting host request 2"
719 with requests_session(token2) as api:
720 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True, number=1))
721 assert not res.no_more
722 assert len(res.host_requests) == 1
723 assert res.host_requests[0].latest_message.text.text == "Accepting host request 3"
724 res = api.ListHostRequests(
725 requests_pb2.ListHostRequestsReq(only_received=True, number=1, page_token=res.next_page_token)
726 )
727 assert not res.no_more
728 assert len(res.host_requests) == 1
729 assert res.host_requests[0].latest_message.text.text == "Accepting host request 1"
730 res = api.ListHostRequests(
731 requests_pb2.ListHostRequestsReq(only_received=True, number=1, page_token=res.next_page_token)
732 )
733 assert res.no_more
734 assert len(res.host_requests) == 1
735 assert res.host_requests[0].latest_message.text.text == "Accepting host request 2"
738def test_ListHostRequests_sort_by_from_date(db, moderator):
739 user1, token1 = generate_user()
740 user2, token2 = generate_user()
741 today_plus_2 = today() + timedelta(days=2)
742 today_plus_3 = today() + timedelta(days=3)
743 today_plus_5 = today() + timedelta(days=5)
744 today_plus_7 = today() + timedelta(days=7)
745 today_plus_10 = today() + timedelta(days=10)
747 with requests_session(token1) as api:
748 hr_late = api.CreateHostRequest(
749 requests_pb2.CreateHostRequestReq(
750 host_user_id=user2.id,
751 from_date=today_plus_7.isoformat(),
752 to_date=today_plus_10.isoformat(),
753 text=valid_request_text("Late request"),
754 )
755 ).host_request_id
757 hr_early = api.CreateHostRequest(
758 requests_pb2.CreateHostRequestReq(
759 host_user_id=user2.id,
760 from_date=today_plus_2.isoformat(),
761 to_date=today_plus_3.isoformat(),
762 text=valid_request_text("Early request"),
763 )
764 ).host_request_id
766 hr_mid = api.CreateHostRequest(
767 requests_pb2.CreateHostRequestReq(
768 host_user_id=user2.id,
769 from_date=today_plus_5.isoformat(),
770 to_date=today_plus_7.isoformat(),
771 text=valid_request_text("Mid request"),
772 )
773 ).host_request_id
775 moderator.approve_host_request(hr_late)
776 moderator.approve_host_request(hr_early)
777 moderator.approve_host_request(hr_mid)
779 with requests_session(token2) as api:
780 # default sort: latest message first (creation order reversed)
781 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
782 assert res.no_more
783 assert [r.host_request_id for r in res.host_requests] == [hr_mid, hr_early, hr_late]
785 # from_date sort: ascending by travel date
786 res = api.ListHostRequests(
787 requests_pb2.ListHostRequestsReq(
788 only_received=True,
789 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
790 )
791 )
792 assert res.no_more
793 assert [r.host_request_id for r in res.host_requests] == [hr_early, hr_mid, hr_late]
796def test_ListHostRequests_sort_by_from_date_pagination(db, moderator):
797 """Pagination cursor correctly handles both different and identical from_dates."""
798 user1, token1 = generate_user()
799 user2, token2 = generate_user()
800 today_plus_2 = today() + timedelta(days=2)
801 today_plus_3 = today() + timedelta(days=3)
802 today_plus_5 = today() + timedelta(days=5)
804 with requests_session(token1) as api:
805 hr_a = api.CreateHostRequest(
806 requests_pb2.CreateHostRequestReq(
807 host_user_id=user2.id,
808 from_date=today_plus_2.isoformat(),
809 to_date=today_plus_3.isoformat(),
810 text=valid_request_text("Request A"),
811 )
812 ).host_request_id
813 backdate_conversations()
815 # Same from_date as A — tiebreaker by conversation_id
816 hr_b = api.CreateHostRequest(
817 requests_pb2.CreateHostRequestReq(
818 host_user_id=user2.id,
819 from_date=today_plus_2.isoformat(),
820 to_date=today_plus_3.isoformat(),
821 text=valid_request_text("Request B"),
822 )
823 ).host_request_id
824 backdate_conversations()
826 hr_c = api.CreateHostRequest(
827 requests_pb2.CreateHostRequestReq(
828 host_user_id=user2.id,
829 from_date=today_plus_5.isoformat(),
830 to_date=(today_plus_5 + timedelta(days=2)).isoformat(),
831 text=valid_request_text("Request C"),
832 )
833 ).host_request_id
835 moderator.approve_host_request(hr_a)
836 moderator.approve_host_request(hr_b)
837 moderator.approve_host_request(hr_c)
839 with requests_session(token2) as api:
840 res = api.ListHostRequests(
841 requests_pb2.ListHostRequestsReq(
842 only_received=True,
843 number=1,
844 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
845 )
846 )
847 assert not res.no_more
848 assert len(res.host_requests) == 1
849 assert res.host_requests[0].host_request_id == hr_a
851 res = api.ListHostRequests(
852 requests_pb2.ListHostRequestsReq(
853 only_received=True,
854 number=1,
855 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
856 page_token=res.next_page_token,
857 )
858 )
859 assert not res.no_more
860 assert len(res.host_requests) == 1
861 assert res.host_requests[0].host_request_id == hr_b
863 res = api.ListHostRequests(
864 requests_pb2.ListHostRequestsReq(
865 only_received=True,
866 number=1,
867 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
868 page_token=res.next_page_token,
869 )
870 )
871 assert res.no_more
872 assert len(res.host_requests) == 1
873 assert res.host_requests[0].host_request_id == hr_c
876def test_ListHostRequests_active_filter(db, moderator):
877 user1, token1 = generate_user()
878 user2, token2 = generate_user()
879 today_plus_2 = today() + timedelta(days=2)
880 today_plus_3 = today() + timedelta(days=3)
882 with requests_session(token1) as api:
883 request_id = api.CreateHostRequest(
884 requests_pb2.CreateHostRequestReq(
885 host_user_id=user2.id,
886 from_date=today_plus_2.isoformat(),
887 to_date=today_plus_3.isoformat(),
888 text=valid_request_text("Test request 1"),
889 )
890 ).host_request_id
892 moderator.approve_host_request(request_id)
894 with requests_session(token1) as api:
895 api.RespondHostRequest(
896 requests_pb2.RespondHostRequestReq(
897 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
898 )
899 )
901 with requests_session(token2) as api:
902 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
903 assert len(res.host_requests) == 1
904 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True))
905 assert len(res.host_requests) == 0
908def test_ListHostRequests_active_filter_excludes_past(db, moderator):
909 """only_active must exclude requests whose end date has passed (regression test for <= bug)."""
910 user1, token1 = generate_user()
911 user2, token2 = generate_user()
912 today_plus_2 = today() + timedelta(days=2)
913 today_plus_3 = today() + timedelta(days=3)
915 with requests_session(token1) as api:
916 request_id = api.CreateHostRequest(
917 requests_pb2.CreateHostRequestReq(
918 host_user_id=user2.id,
919 from_date=today_plus_2.isoformat(),
920 to_date=today_plus_3.isoformat(),
921 text=valid_request_text("Past stay regression"),
922 )
923 ).host_request_id
925 moderator.approve_host_request(request_id)
927 with requests_session(token2) as api:
928 api.RespondHostRequest(
929 requests_pb2.RespondHostRequestReq(
930 host_request_id=request_id,
931 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
932 )
933 )
935 # Future request is visible with only_active
936 with requests_session(token2) as api:
937 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True))
938 assert len(res.host_requests) == 1
940 # Move dates into the past
941 with session_scope() as session:
942 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == request_id)).scalar_one()
943 hr.from_date = today() - timedelta(days=3)
944 hr.to_date = today() - timedelta(days=2)
946 # Past request must be excluded by only_active
947 with requests_session(token2) as api:
948 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True))
949 assert len(res.host_requests) == 0
951 # Still visible without the filter
952 with requests_session(token2) as api:
953 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
954 assert len(res.host_requests) == 1
957def test_ListHostRequests_status_in_filter(db, moderator):
958 """status_in must return only requests with the specified statuses."""
959 user1, token1 = generate_user()
960 user2, token2 = generate_user()
961 today_plus_2 = today() + timedelta(days=2)
962 today_plus_3 = today() + timedelta(days=3)
963 today_plus_4 = today() + timedelta(days=4)
964 today_plus_5 = today() + timedelta(days=5)
966 # Create a pending request
967 with requests_session(token1) as api:
968 pending_id = api.CreateHostRequest(
969 requests_pb2.CreateHostRequestReq(
970 host_user_id=user2.id,
971 from_date=today_plus_2.isoformat(),
972 to_date=today_plus_3.isoformat(),
973 text=valid_request_text("Pending"),
974 )
975 ).host_request_id
977 moderator.approve_host_request(pending_id)
979 # Create an accepted request
980 with requests_session(token1) as api:
981 accepted_id = api.CreateHostRequest(
982 requests_pb2.CreateHostRequestReq(
983 host_user_id=user2.id,
984 from_date=today_plus_4.isoformat(),
985 to_date=today_plus_5.isoformat(),
986 text=valid_request_text("Accepted"),
987 )
988 ).host_request_id
990 moderator.approve_host_request(accepted_id)
992 with requests_session(token2) as api:
993 api.RespondHostRequest(
994 requests_pb2.RespondHostRequestReq(
995 host_request_id=accepted_id,
996 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
997 )
998 )
1000 # Filter to accepted only
1001 with requests_session(token2) as api:
1002 res = api.ListHostRequests(
1003 requests_pb2.ListHostRequestsReq(
1004 status_in=[messages_pb2.HOST_REQUEST_STATUS_ACCEPTED],
1005 )
1006 )
1007 assert len(res.host_requests) == 1
1008 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1010 # Filter to pending only
1011 with requests_session(token2) as api:
1012 res = api.ListHostRequests(
1013 requests_pb2.ListHostRequestsReq(
1014 status_in=[messages_pb2.HOST_REQUEST_STATUS_PENDING],
1015 )
1016 )
1017 assert len(res.host_requests) == 1
1018 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_PENDING
1020 # Filter to accepted + pending — both appear
1021 with requests_session(token2) as api:
1022 res = api.ListHostRequests(
1023 requests_pb2.ListHostRequestsReq(
1024 status_in=[
1025 messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1026 messages_pb2.HOST_REQUEST_STATUS_PENDING,
1027 ],
1028 )
1029 )
1030 assert len(res.host_requests) == 2
1032 # Filter to confirmed — none appear
1033 with requests_session(token2) as api:
1034 res = api.ListHostRequests(
1035 requests_pb2.ListHostRequestsReq(
1036 status_in=[messages_pb2.HOST_REQUEST_STATUS_CONFIRMED],
1037 )
1038 )
1039 assert len(res.host_requests) == 0
1041 # Empty status_in — all requests returned (no filter applied)
1042 with requests_session(token2) as api:
1043 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
1044 assert len(res.host_requests) == 2
1047def test_RespondHostRequests(db, moderator):
1048 user1, token1 = generate_user()
1049 user2, token2 = generate_user()
1050 user3, token3 = generate_user()
1051 today_plus_2 = today() + timedelta(days=2)
1052 today_plus_3 = today() + timedelta(days=3)
1054 with requests_session(token1) as api:
1055 request_id = api.CreateHostRequest(
1056 requests_pb2.CreateHostRequestReq(
1057 host_user_id=user2.id,
1058 from_date=today_plus_2.isoformat(),
1059 to_date=today_plus_3.isoformat(),
1060 text=valid_request_text("Test request 1"),
1061 )
1062 ).host_request_id
1064 moderator.approve_host_request(request_id)
1066 # another user can't access
1067 with requests_session(token3) as api:
1068 with pytest.raises(grpc.RpcError) as e:
1069 api.RespondHostRequest(
1070 requests_pb2.RespondHostRequestReq(
1071 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1072 )
1073 )
1074 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1075 assert e.value.details() == "Couldn't find that host request."
1077 with requests_session(token1) as api:
1078 with pytest.raises(grpc.RpcError) as e:
1079 api.RespondHostRequest(
1080 requests_pb2.RespondHostRequestReq(
1081 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1082 )
1083 )
1084 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1085 assert e.value.details() == "You are not the host of this request."
1087 with requests_session(token2) as api:
1088 # non existing id
1089 with pytest.raises(grpc.RpcError) as e:
1090 api.RespondHostRequest(
1091 requests_pb2.RespondHostRequestReq(
1092 host_request_id=9999, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1093 )
1094 )
1095 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1097 # host can't confirm or cancel (host should accept/reject)
1098 with pytest.raises(grpc.RpcError) as e:
1099 api.RespondHostRequest(
1100 requests_pb2.RespondHostRequestReq(
1101 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1102 )
1103 )
1104 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1105 assert e.value.details() == "You can't set the host request status to that."
1106 with pytest.raises(grpc.RpcError) as e:
1107 api.RespondHostRequest(
1108 requests_pb2.RespondHostRequestReq(
1109 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1110 )
1111 )
1112 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1113 assert e.value.details() == "You can't set the host request status to that."
1115 api.RespondHostRequest(
1116 requests_pb2.RespondHostRequestReq(
1117 host_request_id=request_id,
1118 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED,
1119 text="Test rejection message",
1120 )
1121 )
1122 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=request_id))
1123 assert res.messages[0].text.text == "Test rejection message"
1124 assert res.messages[1].WhichOneof("content") == "host_request_status_changed"
1125 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
1126 # should be able to move from rejected -> accepted
1127 api.RespondHostRequest(
1128 requests_pb2.RespondHostRequestReq(
1129 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1130 )
1131 )
1133 with requests_session(token1) as api:
1134 # can't make pending
1135 with pytest.raises(grpc.RpcError) as e:
1136 api.RespondHostRequest(
1137 requests_pb2.RespondHostRequestReq(
1138 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_PENDING
1139 )
1140 )
1141 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1142 assert e.value.details() == "You can't set the host request status to that."
1144 # can confirm then cancel
1145 api.RespondHostRequest(
1146 requests_pb2.RespondHostRequestReq(
1147 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1148 )
1149 )
1151 api.RespondHostRequest(
1152 requests_pb2.RespondHostRequestReq(
1153 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1154 )
1155 )
1157 # can't confirm after having cancelled
1158 with pytest.raises(grpc.RpcError) as e:
1159 api.RespondHostRequest(
1160 requests_pb2.RespondHostRequestReq(
1161 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1162 )
1163 )
1164 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1165 assert e.value.details() == "You can't set the host request status to that."
1167 # at this point there should be 7 messages
1168 # 2 for creation, 2 for the status change with message, 3 for the other status changed
1169 with requests_session(token1) as api:
1170 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=request_id))
1171 assert len(res.messages) == 7
1172 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1173 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1174 assert res.messages[2].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1175 assert res.messages[4].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
1176 assert res.messages[6].WhichOneof("content") == "chat_created"
1179def test_get_host_request_messages(db, moderator):
1180 user1, token1 = generate_user()
1181 user2, token2 = generate_user()
1182 today_plus_2 = today() + timedelta(days=2)
1183 today_plus_3 = today() + timedelta(days=3)
1184 with requests_session(token1) as api:
1185 res = api.CreateHostRequest(
1186 requests_pb2.CreateHostRequestReq(
1187 host_user_id=user2.id,
1188 from_date=today_plus_2.isoformat(),
1189 to_date=today_plus_3.isoformat(),
1190 text=valid_request_text("Test request 1"),
1191 )
1192 )
1193 conversation_id = res.host_request_id
1195 moderator.approve_host_request(conversation_id)
1197 add_message(db, "Test request 1 message 1", user1.id, conversation_id)
1198 add_message(db, "Test request 1 message 2", user1.id, conversation_id)
1199 add_message(db, "Test request 1 message 3", user1.id, conversation_id)
1201 with requests_session(token2) as api:
1202 api.RespondHostRequest(
1203 requests_pb2.RespondHostRequestReq(
1204 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1205 )
1206 )
1208 add_message(db, "Test request 1 message 4", user2.id, conversation_id)
1209 add_message(db, "Test request 1 message 5", user2.id, conversation_id)
1211 api.RespondHostRequest(
1212 requests_pb2.RespondHostRequestReq(
1213 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED
1214 )
1215 )
1217 with requests_session(token1) as api:
1218 # 9 including initial message
1219 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=conversation_id))
1220 assert len(res.messages) == 9
1221 assert res.no_more
1223 res = api.GetHostRequestMessages(
1224 requests_pb2.GetHostRequestMessagesReq(host_request_id=conversation_id, number=3)
1225 )
1226 assert not res.no_more
1227 assert len(res.messages) == 3
1228 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
1229 assert res.messages[0].WhichOneof("content") == "host_request_status_changed"
1230 assert res.messages[1].text.text == "Test request 1 message 5"
1231 assert res.messages[2].text.text == "Test request 1 message 4"
1233 res = api.GetHostRequestMessages(
1234 requests_pb2.GetHostRequestMessagesReq(
1235 host_request_id=conversation_id,
1236 last_message_id=res.messages[2].message_id,
1237 number=6,
1238 )
1239 )
1240 assert res.no_more
1241 assert len(res.messages) == 6
1242 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1243 assert res.messages[0].WhichOneof("content") == "host_request_status_changed"
1244 assert res.messages[1].text.text == "Test request 1 message 3"
1245 assert res.messages[2].text.text == "Test request 1 message 2"
1246 assert res.messages[3].text.text == "Test request 1 message 1"
1247 assert res.messages[4].text.text == valid_request_text("Test request 1")
1248 assert res.messages[5].WhichOneof("content") == "chat_created"
1251def test_SendHostRequestMessage(db, moderator):
1252 user1, token1 = generate_user()
1253 user2, token2 = generate_user()
1254 user3, token3 = generate_user()
1255 today_plus_2 = today() + timedelta(days=2)
1256 today_plus_3 = today() + timedelta(days=3)
1257 with requests_session(token1) as api:
1258 host_request_id = api.CreateHostRequest(
1259 requests_pb2.CreateHostRequestReq(
1260 host_user_id=user2.id,
1261 from_date=today_plus_2.isoformat(),
1262 to_date=today_plus_3.isoformat(),
1263 text=valid_request_text("Test request 1"),
1264 )
1265 ).host_request_id
1267 moderator.approve_host_request(host_request_id)
1269 with requests_session(token1) as api:
1270 with pytest.raises(grpc.RpcError) as e:
1271 api.SendHostRequestMessage(
1272 requests_pb2.SendHostRequestMessageReq(host_request_id=999, text="Test message 1")
1273 )
1274 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1276 with pytest.raises(grpc.RpcError) as e:
1277 api.SendHostRequestMessage(requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text=""))
1278 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1279 assert e.value.details() == "Invalid message."
1281 api.SendHostRequestMessage(
1282 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1283 )
1284 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id))
1285 assert res.messages[0].text.text == "Test message 1"
1286 assert res.messages[0].author_user_id == user1.id
1288 with requests_session(token3) as api:
1289 # other user can't send
1290 with pytest.raises(grpc.RpcError) as e:
1291 api.SendHostRequestMessage(
1292 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1293 )
1294 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1295 assert e.value.details() == "Couldn't find that host request."
1297 with requests_session(token2) as api:
1298 api.SendHostRequestMessage(
1299 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1300 )
1301 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id))
1302 # including 2 for creation control message and message
1303 assert len(res.messages) == 4
1304 assert res.messages[0].text.text == "Test message 2"
1305 assert res.messages[0].author_user_id == user2.id
1307 # CAN send messages to a rejected, confirmed or cancelled request, and for accepted
1308 api.RespondHostRequest(
1309 requests_pb2.RespondHostRequestReq(
1310 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED
1311 )
1312 )
1313 api.SendHostRequestMessage(
1314 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3")
1315 )
1317 api.RespondHostRequest(
1318 requests_pb2.RespondHostRequestReq(
1319 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1320 )
1321 )
1323 with requests_session(token1) as api:
1324 api.RespondHostRequest(
1325 requests_pb2.RespondHostRequestReq(
1326 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1327 )
1328 )
1329 api.SendHostRequestMessage(
1330 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3")
1331 )
1333 api.RespondHostRequest(
1334 requests_pb2.RespondHostRequestReq(
1335 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1336 )
1337 )
1338 api.SendHostRequestMessage(
1339 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3")
1340 )
1343def test_get_updates(db, moderator):
1344 user1, token1 = generate_user()
1345 user2, token2 = generate_user()
1346 user3, token3 = generate_user()
1347 today_plus_2 = today() + timedelta(days=2)
1348 today_plus_3 = today() + timedelta(days=3)
1349 with requests_session(token1) as api:
1350 host_request_id = api.CreateHostRequest(
1351 requests_pb2.CreateHostRequestReq(
1352 host_user_id=user2.id,
1353 from_date=today_plus_2.isoformat(),
1354 to_date=today_plus_3.isoformat(),
1355 text=valid_request_text("Test message 0"),
1356 )
1357 ).host_request_id
1359 moderator.approve_host_request(host_request_id)
1361 with requests_session(token1) as api:
1362 api.SendHostRequestMessage(
1363 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1364 )
1365 api.SendHostRequestMessage(
1366 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1367 )
1368 api.RespondHostRequest(
1369 requests_pb2.RespondHostRequestReq(
1370 host_request_id=host_request_id,
1371 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED,
1372 text="Test message 3",
1373 )
1374 )
1376 backdate_conversations()
1377 api.CreateHostRequest(
1378 requests_pb2.CreateHostRequestReq(
1379 host_user_id=user2.id,
1380 from_date=today_plus_2.isoformat(),
1381 to_date=today_plus_3.isoformat(),
1382 text=valid_request_text("Test message 4"),
1383 )
1384 )
1386 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id))
1387 assert len(res.messages) == 6
1388 assert res.messages[0].text.text == "Test message 3"
1389 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1390 assert res.messages[2].text.text == "Test message 2"
1391 assert res.messages[3].text.text == "Test message 1"
1392 assert res.messages[4].text.text == valid_request_text("Test message 0")
1393 message_id_3 = res.messages[0].message_id
1394 message_id_cancel = res.messages[1].message_id
1395 message_id_2 = res.messages[2].message_id
1396 message_id_1 = res.messages[3].message_id
1397 message_id_0 = res.messages[4].message_id
1399 with pytest.raises(grpc.RpcError) as e:
1400 api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=0))
1401 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1403 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1))
1404 assert res.no_more
1405 assert len(res.updates) == 5
1406 assert res.updates[0].message.text.text == "Test message 2"
1407 assert res.updates[1].message.host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1408 assert res.updates[1].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1409 assert res.updates[2].message.text.text == "Test message 3"
1410 assert res.updates[3].message.WhichOneof("content") == "chat_created"
1411 assert res.updates[3].status == messages_pb2.HOST_REQUEST_STATUS_PENDING
1412 assert res.updates[4].message.text.text == valid_request_text("Test message 4")
1414 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1, number=1))
1415 assert not res.no_more
1416 assert len(res.updates) == 1
1417 assert res.updates[0].message.text.text == "Test message 2"
1418 assert res.updates[0].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1420 with requests_session(token3) as api:
1421 # other user can't access
1422 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1))
1423 assert len(res.updates) == 0
1426def test_archive_host_request(db, moderator):
1427 user1, token1 = generate_user()
1428 user2, token2 = generate_user()
1430 today_plus_2 = today() + timedelta(days=2)
1431 today_plus_3 = today() + timedelta(days=3)
1433 with requests_session(token1) as api:
1434 host_request_id = api.CreateHostRequest(
1435 requests_pb2.CreateHostRequestReq(
1436 host_user_id=user2.id,
1437 from_date=today_plus_2.isoformat(),
1438 to_date=today_plus_3.isoformat(),
1439 text=valid_request_text("Test message 0"),
1440 )
1441 ).host_request_id
1443 api.SendHostRequestMessage(
1444 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1445 )
1446 api.SendHostRequestMessage(
1447 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1448 )
1450 moderator.approve_host_request(host_request_id)
1452 # happy path archiving host request
1453 with requests_session(token1) as api:
1454 api.RespondHostRequest(
1455 requests_pb2.RespondHostRequestReq(
1456 host_request_id=host_request_id,
1457 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED,
1458 text="Test message 3",
1459 )
1460 )
1461 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
1462 assert len(res.host_requests) == 1
1463 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1465 # Verify is_archived is False before archiving
1466 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1467 assert not res.is_archived
1469 api.SetHostRequestArchiveStatus(
1470 requests_pb2.SetHostRequestArchiveStatusReq(host_request_id=host_request_id, is_archived=True)
1471 )
1472 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_archived=True))
1473 assert len(res.host_requests) == 1
1475 # Verify is_archived is True after archiving
1476 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1477 assert res.is_archived
1480def test_host_request_unseen_message_count(db, moderator):
1481 user1, token1 = generate_user()
1482 user2, token2 = generate_user()
1484 with requests_session(token1) as api:
1485 host_request_id = api.CreateHostRequest(
1486 requests_pb2.CreateHostRequestReq(
1487 host_user_id=user2.id,
1488 from_date=(today() + timedelta(days=2)).isoformat(),
1489 to_date=(today() + timedelta(days=3)).isoformat(),
1490 text=valid_request_text(),
1491 )
1492 ).host_request_id
1493 moderator.approve_host_request(host_request_id)
1495 with requests_session(token1) as api:
1496 api.SendHostRequestMessage(
1497 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="and one more thing")
1498 )
1499 # sending marks your own messages seen
1500 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1501 assert res.unseen_message_count == 0
1503 with requests_session(token2) as api:
1504 listed = api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0]
1505 # the request's control message, its text, and the follow-up
1506 assert listed.unseen_message_count == 3
1507 assert (
1508 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)).unseen_message_count
1509 == 3
1510 )
1512 api.MarkLastSeenHostRequest(
1513 requests_pb2.MarkLastSeenHostRequestReq(
1514 host_request_id=host_request_id, last_seen_message_id=listed.latest_message.message_id
1515 )
1516 )
1517 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].unseen_message_count == 0
1518 assert (
1519 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)).unseen_message_count
1520 == 0
1521 )
1524def test_mark_last_seen(db, moderator):
1525 user1, token1 = generate_user()
1526 user2, token2 = generate_user()
1527 user3, token3 = generate_user()
1528 today_plus_2 = today() + timedelta(days=2)
1529 today_plus_3 = today() + timedelta(days=3)
1530 with requests_session(token1) as api:
1531 host_request_id = api.CreateHostRequest(
1532 requests_pb2.CreateHostRequestReq(
1533 host_user_id=user2.id,
1534 from_date=today_plus_2.isoformat(),
1535 to_date=today_plus_3.isoformat(),
1536 text=valid_request_text("Test message 0"),
1537 )
1538 ).host_request_id
1539 backdate_conversations()
1541 host_request_id_2 = api.CreateHostRequest(
1542 requests_pb2.CreateHostRequestReq(
1543 host_user_id=user2.id,
1544 from_date=today_plus_2.isoformat(),
1545 to_date=today_plus_3.isoformat(),
1546 text=valid_request_text("Test message 0a"),
1547 )
1548 ).host_request_id
1550 moderator.approve_host_request(host_request_id)
1551 moderator.approve_host_request(host_request_id_2)
1553 with requests_session(token1) as api:
1554 api.SendHostRequestMessage(
1555 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1556 )
1557 api.SendHostRequestMessage(
1558 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1559 )
1560 api.RespondHostRequest(
1561 requests_pb2.RespondHostRequestReq(
1562 host_request_id=host_request_id,
1563 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED,
1564 text="Test message 3",
1565 )
1566 )
1568 moderator.approve_host_request(host_request_id)
1569 moderator.approve_host_request(host_request_id_2)
1571 # test Ping unseen host request count, should be automarked after sending
1572 with api_session(token1) as api:
1573 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 0
1574 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1576 with api_session(token2) as api:
1577 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 2
1578 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1580 with requests_session(token2) as api:
1581 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].last_seen_message_id == 0
1583 api.MarkLastSeenHostRequest(
1584 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=3)
1585 )
1587 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].last_seen_message_id == 3
1589 with pytest.raises(grpc.RpcError) as e:
1590 api.MarkLastSeenHostRequest(
1591 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=1)
1592 )
1593 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1594 assert e.value.details() == "You can't unsee messages."
1596 # this will be used to test sent request notifications
1597 host_request_id_3 = api.CreateHostRequest(
1598 requests_pb2.CreateHostRequestReq(
1599 host_user_id=user1.id,
1600 from_date=today_plus_2.isoformat(),
1601 to_date=today_plus_3.isoformat(),
1602 text=valid_request_text("Another test request"),
1603 )
1604 ).host_request_id
1606 moderator.approve_host_request(host_request_id_3)
1608 with requests_session(token2) as api:
1609 # this should make id_2 all read
1610 api.SendHostRequestMessage(
1611 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id_2, text="Test")
1612 )
1614 with api_session(token2) as api:
1615 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 1
1616 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1618 # make sure sent and received count for unseen notifications
1619 with requests_session(token1) as api:
1620 api.SendHostRequestMessage(
1621 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id_3, text="Test message")
1622 )
1624 with api_session(token2) as api:
1625 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 1
1626 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 1
1629def _create_host_request_via_api(surfer_token: str, host_id: int, moderator, public_trip_id: int | None = None) -> int:
1630 with requests_session(surfer_token) as api:
1631 res = api.CreateHostRequest(
1632 requests_pb2.CreateHostRequestReq(
1633 host_user_id=host_id,
1634 from_date=(today() + timedelta(days=5)).isoformat(),
1635 to_date=(today() + timedelta(days=10)).isoformat(),
1636 text=valid_request_text(),
1637 public_trip_id=public_trip_id,
1638 )
1639 )
1640 moderator.approve_host_request(res.host_request_id)
1641 return int(res.host_request_id)
1644def test_ping_role_based_counts_match_direction_without_offers(db, moderator):
1645 user1, token1 = generate_user()
1646 user2, token2 = generate_user()
1648 # user2 surfs with user1 -> user1 is the host of the stay
1649 _create_host_request_via_api(token2, user1.id, moderator)
1651 with api_session(token1) as api:
1652 res = api.Ping(api_pb2.PingReq())
1653 # with no public-trip offers, role-based counts equal the direction-based ones
1654 assert res.unseen_received_host_request_count == 1
1655 assert res.unseen_hosting_host_request_count == 1
1656 assert res.unseen_sent_host_request_count == 0
1657 assert res.unseen_surfing_host_request_count == 0
1658 assert res.unseen_public_trip_offer_count == 0
1661def test_ping_counts_public_trip_offer_by_role(db, moderator):
1662 traveler, traveler_token = generate_user()
1663 host, host_token = generate_user()
1664 node_id = _make_node()
1665 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1667 request_id = _create_host_request_via_api(host_token, traveler.id, moderator, public_trip_id=trip_id)
1669 # the traveller (recipient) has the offer's create message unseen: counts as surfing + public-trip offer
1670 with api_session(traveler_token) as api:
1671 res = api.Ping(api_pb2.PingReq())
1672 assert res.unseen_surfing_host_request_count == 1
1673 assert res.unseen_public_trip_offer_count == 1
1674 assert res.unseen_hosting_host_request_count == 0
1676 # the traveller replies, so now the offering host has an unseen message under hosting
1677 with requests_session(traveler_token) as api:
1678 api.SendHostRequestMessage(
1679 requests_pb2.SendHostRequestMessageReq(host_request_id=request_id, text="thanks for the offer")
1680 )
1682 with api_session(host_token) as api:
1683 res = api.Ping(api_pb2.PingReq())
1684 assert res.unseen_hosting_host_request_count == 1
1685 assert res.unseen_surfing_host_request_count == 0
1686 assert res.unseen_public_trip_offer_count == 0
1689def test_ping_public_trip_offer_count_gated_by_flag(db, moderator, feature_flags):
1690 feature_flags.set("public_trips_enabled", False)
1692 traveler, traveler_token = generate_user()
1693 host, host_token = generate_user()
1694 node_id = _make_node()
1695 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1697 _create_host_request_via_api(host_token, traveler.id, moderator, public_trip_id=trip_id)
1699 with api_session(traveler_token) as api:
1700 res = api.Ping(api_pb2.PingReq())
1701 # the dedicated offer count is gated off...
1702 assert res.unseen_public_trip_offer_count == 0
1703 # ...but the offer is a real conversation and still surfaces under surfing
1704 assert res.unseen_surfing_host_request_count == 1
1707def test_mark_last_seen_clears_notifications(db, moderator):
1708 user1, token1 = generate_user()
1709 user2, token2 = generate_user()
1710 today_plus_2 = today() + timedelta(days=2)
1711 today_plus_3 = today() + timedelta(days=3)
1713 with requests_session(token1) as api:
1714 host_request_id = api.CreateHostRequest(
1715 requests_pb2.CreateHostRequestReq(
1716 host_user_id=user2.id,
1717 from_date=today_plus_2.isoformat(),
1718 to_date=today_plus_3.isoformat(),
1719 text=valid_request_text("Test message"),
1720 )
1721 ).host_request_id
1723 moderator.approve_host_request(host_request_id)
1725 def unseen_notification_count(user_id):
1726 with session_scope() as session:
1727 return session.execute(
1728 select(func.count())
1729 .select_from(Notification)
1730 .where(Notification.user_id == user_id)
1731 .where(Notification.key == str(host_request_id))
1732 .where(Notification.is_seen == False)
1733 ).scalar_one()
1735 assert unseen_notification_count(user2.id) > 0
1737 with requests_session(token2) as api:
1738 api.MarkLastSeenHostRequest(
1739 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=1)
1740 )
1742 assert unseen_notification_count(user2.id) == 0
1745def test_response_rate(db, moderator):
1746 user1, token1 = generate_user()
1747 user2, token2 = generate_user()
1748 user3, token3 = generate_user(delete_user=True)
1750 today_plus_2 = today() + timedelta(days=2)
1751 today_plus_3 = today() + timedelta(days=3)
1753 with session_scope() as session:
1754 refresh_materialized_view(session, "user_response_rates")
1756 with requests_session(token1) as api:
1757 # deleted: not found
1758 with pytest.raises(grpc.RpcError) as e:
1759 api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user3.id))
1760 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1761 assert e.value.details() == "Couldn't find that user."
1763 # no requests: insufficient
1764 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1765 assert res.HasField("insufficient_data")
1767 # send a request and back date it by 36 hours
1768 host_request_1 = api.CreateHostRequest(
1769 requests_pb2.CreateHostRequestReq(
1770 host_user_id=user2.id,
1771 from_date=today_plus_2.isoformat(),
1772 to_date=today_plus_3.isoformat(),
1773 text=valid_request_text("Test request"),
1774 )
1775 ).host_request_id
1776 moderator.approve_host_request(host_request_1)
1777 backdate_conversations()
1778 with session_scope() as session:
1779 session.execute(
1780 select(Message)
1781 .where(Message.conversation_id == host_request_1)
1782 .where(Message.message_type == MessageType.chat_created)
1783 ).scalar_one().time = now() - timedelta(hours=36)
1784 refresh_materialized_view(session, "user_response_rates")
1786 # still insufficient
1787 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1788 assert res.HasField("insufficient_data")
1790 # send a request and back date it by 35 hours
1791 host_request_2 = api.CreateHostRequest(
1792 requests_pb2.CreateHostRequestReq(
1793 host_user_id=user2.id,
1794 from_date=today_plus_2.isoformat(),
1795 to_date=today_plus_3.isoformat(),
1796 text=valid_request_text("Test request"),
1797 )
1798 ).host_request_id
1799 moderator.approve_host_request(host_request_2)
1800 backdate_conversations()
1801 with session_scope() as session:
1802 session.execute(
1803 select(Message)
1804 .where(Message.conversation_id == host_request_2)
1805 .where(Message.message_type == MessageType.chat_created)
1806 ).scalar_one().time = now() - timedelta(hours=35)
1807 refresh_materialized_view(session, "user_response_rates")
1809 # still insufficient
1810 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1811 assert res.HasField("insufficient_data")
1813 # send a request and back date it by 34 hours
1814 host_request_3 = api.CreateHostRequest(
1815 requests_pb2.CreateHostRequestReq(
1816 host_user_id=user2.id,
1817 from_date=today_plus_2.isoformat(),
1818 to_date=today_plus_3.isoformat(),
1819 text=valid_request_text("Test request"),
1820 )
1821 ).host_request_id
1822 moderator.approve_host_request(host_request_3)
1823 backdate_conversations()
1824 with session_scope() as session:
1825 session.execute(
1826 select(Message)
1827 .where(Message.conversation_id == host_request_3)
1828 .where(Message.message_type == MessageType.chat_created)
1829 ).scalar_one().time = now() - timedelta(hours=34)
1830 refresh_materialized_view(session, "user_response_rates")
1832 # now low
1833 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1834 assert res.HasField("low")
1836 with requests_session(token2) as api:
1837 # accept a host req
1838 api.RespondHostRequest(
1839 requests_pb2.RespondHostRequestReq(
1840 host_request_id=host_request_2,
1841 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1842 text="Accepting host request",
1843 )
1844 )
1846 with session_scope() as session:
1847 refresh_materialized_view(session, "user_response_rates")
1849 with requests_session(token1) as api:
1850 # now some w p33 = 35h
1851 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1852 assert res.HasField("some")
1853 assert res.some.response_time_p33.ToTimedelta() == timedelta(hours=35)
1855 with requests_session(token2) as api:
1856 # accept another host req
1857 api.RespondHostRequest(
1858 requests_pb2.RespondHostRequestReq(
1859 host_request_id=host_request_3,
1860 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1861 text="Accepting host request",
1862 )
1863 )
1865 with session_scope() as session:
1866 refresh_materialized_view(session, "user_response_rates")
1868 with requests_session(token1) as api:
1869 # now most w p33 = 34h, p66 = 35h
1870 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1871 assert res.HasField("most")
1872 assert res.most.response_time_p33.ToTimedelta() == timedelta(hours=34)
1873 assert res.most.response_time_p66.ToTimedelta() == timedelta(hours=35)
1875 with requests_session(token2) as api:
1876 # accept last host req
1877 api.RespondHostRequest(
1878 requests_pb2.RespondHostRequestReq(
1879 host_request_id=host_request_1,
1880 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1881 text="Accepting host request",
1882 )
1883 )
1885 with session_scope() as session:
1886 refresh_materialized_view(session, "user_response_rates")
1888 with requests_session(token1) as api:
1889 # now all w p33 = 34h, p66 = 35h
1890 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1891 assert res.HasField("almost_all")
1892 assert res.almost_all.response_time_p33.ToTimedelta() == timedelta(hours=34)
1893 assert res.almost_all.response_time_p66.ToTimedelta() == timedelta(hours=35)
1895 # send a request and back date it by 2 hours
1896 host_request_4 = api.CreateHostRequest(
1897 requests_pb2.CreateHostRequestReq(
1898 host_user_id=user2.id,
1899 from_date=today_plus_2.isoformat(),
1900 to_date=today_plus_3.isoformat(),
1901 text=valid_request_text("Test request"),
1902 )
1903 ).host_request_id
1904 moderator.approve_host_request(host_request_4)
1905 backdate_conversations()
1906 with session_scope() as session:
1907 session.execute(
1908 select(Message)
1909 .where(Message.conversation_id == host_request_4)
1910 .where(Message.message_type == MessageType.chat_created)
1911 ).scalar_one().time = now() - timedelta(hours=2)
1912 refresh_materialized_view(session, "user_response_rates")
1914 # send a request and back date it by 4 hours
1915 host_request_5 = api.CreateHostRequest(
1916 requests_pb2.CreateHostRequestReq(
1917 host_user_id=user2.id,
1918 from_date=today_plus_2.isoformat(),
1919 to_date=today_plus_3.isoformat(),
1920 text=valid_request_text("Test request"),
1921 )
1922 ).host_request_id
1923 moderator.approve_host_request(host_request_5)
1924 backdate_conversations()
1925 with session_scope() as session:
1926 session.execute(
1927 select(Message)
1928 .where(Message.conversation_id == host_request_5)
1929 .where(Message.message_type == MessageType.chat_created)
1930 ).scalar_one().time = now() - timedelta(hours=4)
1931 refresh_materialized_view(session, "user_response_rates")
1933 # now some w p33 = 35h
1934 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1935 assert res.HasField("some")
1936 assert res.some.response_time_p33.ToTimedelta() == timedelta(hours=35)
1938 with requests_session(token2) as api:
1939 # accept host req
1940 api.RespondHostRequest(
1941 requests_pb2.RespondHostRequestReq(
1942 host_request_id=host_request_5,
1943 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1944 text="Accepting host request",
1945 )
1946 )
1948 with session_scope() as session:
1949 refresh_materialized_view(session, "user_response_rates")
1951 with requests_session(token1) as api:
1952 # now most w p33 = 34h, p66 = 36h
1953 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1954 assert res.HasField("most")
1955 assert res.most.response_time_p33.ToTimedelta() == timedelta(hours=34)
1956 assert res.most.response_time_p66.ToTimedelta() == timedelta(hours=36)
1958 with requests_session(token2) as api:
1959 # accept host req
1960 api.RespondHostRequest(
1961 requests_pb2.RespondHostRequestReq(
1962 host_request_id=host_request_4,
1963 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1964 text="Accepting host request",
1965 )
1966 )
1968 with session_scope() as session:
1969 refresh_materialized_view(session, "user_response_rates")
1971 with requests_session(token1) as api:
1972 # now most w p33 = 4h, p66 = 35h
1973 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1974 assert res.HasField("almost_all")
1975 assert res.almost_all.response_time_p33.ToTimedelta() == timedelta(hours=4)
1976 assert res.almost_all.response_time_p66.ToTimedelta() == timedelta(hours=35)
1979def test_request_notifications(db, email_collector: EmailCollector, push_collector: PushCollector, moderator):
1980 host, host_token = generate_user(complete_profile=True)
1981 surfer, surfer_token = generate_user(complete_profile=True)
1983 host_loc_context = LocalizationContext.from_user(host)
1984 surfer_loc_context = LocalizationContext.from_user(surfer)
1986 today_plus_2 = today() + timedelta(days=2)
1987 today_plus_3 = today() + timedelta(days=3)
1989 with requests_session(surfer_token) as api:
1990 hr_id = api.CreateHostRequest(
1991 requests_pb2.CreateHostRequestReq(
1992 host_user_id=host.id,
1993 from_date=today_plus_2.isoformat(),
1994 to_date=today_plus_3.isoformat(),
1995 text=valid_request_text("can i stay plz"),
1996 )
1997 ).host_request_id
1999 moderator.approve_host_request(hr_id)
2001 email = email_collector.pop_for_recipient(host.email, last=True)
2002 assert email.recipient == host.email
2003 assert "host request" in email.subject.lower()
2004 assert host.name in email.plain
2005 assert host.name in email.html
2006 assert "quick decline" in email.plain.lower(), email.plain
2007 assert "quick decline" in email.html.lower()
2008 assert surfer.name in email.plain
2009 assert surfer.name in email.html
2010 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
2011 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.html
2012 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
2013 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.html
2014 assert "http://localhost:5001/img/thumbnail/" not in email.plain
2015 assert "http://localhost:5001/img/thumbnail/" in email.html
2016 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
2017 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
2018 assert not email.attachments
2020 assert push_collector.pop_for_user(host.id, last=True).content.title == f"New host request from {surfer.name}"
2022 with requests_session(host_token) as api:
2023 api.RespondHostRequest(
2024 requests_pb2.RespondHostRequestReq(
2025 host_request_id=hr_id,
2026 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
2027 text="Accepting host request",
2028 )
2029 )
2031 email = email_collector.pop_for_recipient(surfer.email, last=True)
2032 assert email.recipient == surfer.email
2033 assert "host request" in email.subject.lower()
2034 assert host.name in email.plain
2035 assert host.name in email.html
2036 assert surfer.name in email.plain
2037 assert surfer.name in email.html
2038 assert surfer_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
2039 assert surfer_loc_context.localize_date(today_plus_2, with_year=False) in email.html
2040 assert surfer_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
2041 assert surfer_loc_context.localize_date(today_plus_3, with_year=False) in email.html
2042 assert "http://localhost:5001/img/thumbnail/" not in email.plain
2043 assert "http://localhost:5001/img/thumbnail/" in email.html
2044 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
2045 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
2046 assert len(email.attachments or []) == 1
2048 assert push_collector.pop_for_user(surfer.id, last=True).content.title == f"{host.name} accepted your host request"
2051def test_quick_decline(db, email_collector: EmailCollector, push_collector: PushCollector, moderator):
2052 host, host_token = generate_user(complete_profile=True)
2053 surfer, surfer_token = generate_user(complete_profile=True)
2055 host_loc_context = LocalizationContext.from_user(host)
2057 today_plus_2 = today() + timedelta(days=2)
2058 today_plus_3 = today() + timedelta(days=3)
2060 with requests_session(surfer_token) as api:
2061 hr_id = api.CreateHostRequest(
2062 requests_pb2.CreateHostRequestReq(
2063 host_user_id=host.id,
2064 from_date=today_plus_2.isoformat(),
2065 to_date=today_plus_3.isoformat(),
2066 text=valid_request_text("can i stay plz"),
2067 )
2068 ).host_request_id
2070 moderator.approve_host_request(hr_id)
2072 email = email_collector.pop_for_recipient(host.email, last=True)
2073 assert email.recipient == host.email
2074 assert "host request" in email.subject.lower()
2075 assert host.name in email.plain
2076 assert host.name in email.html
2077 assert "quick decline" in email.plain.lower(), email.plain
2078 assert "quick decline" in email.html.lower()
2079 assert surfer.name in email.plain
2080 assert surfer.name in email.html
2081 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
2082 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.html
2083 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
2084 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.html
2085 assert "http://localhost:5001/img/thumbnail/" not in email.plain
2086 assert "http://localhost:5001/img/thumbnail/" in email.html
2087 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
2088 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
2090 assert push_collector.pop_for_user(host.id, last=True).content.title == f"New host request from {surfer.name}"
2092 # very ugly
2093 # http://localhost:3000/quick-link?payload=CAEiGAoOZnJpZW5kX3JlcXVlc3QSBmFjY2VwdA==&sig=BQdk024NTATm8zlR0krSXTBhP5U9TlFv7VhJeIHZtUg=
2094 for link in re.findall(r'<a href="(.*?)"', email.html): 2094 ↛ 2113line 2094 didn't jump to line 2113 because the loop on line 2094 didn't complete
2095 if "payload" not in link:
2096 continue
2097 print(link)
2098 url_parts = urlparse(html.unescape(link))
2099 params = parse_qs(url_parts.query)
2100 print(params["payload"][0])
2101 payload = unsubscribe_pb2.UnsubscribePayload.FromString(b64decode(params["payload"][0]))
2102 if payload.HasField("host_request_quick_decline"): 2102 ↛ 2094line 2102 didn't jump to line 2094 because the condition on line 2102 was always true
2103 with auth_api_session() as (auth_api, metadata_interceptor):
2104 res = auth_api.Unsubscribe(
2105 auth_pb2.UnsubscribeReq(
2106 payload=b64decode(params["payload"][0]),
2107 sig=b64decode(params["sig"][0]),
2108 )
2109 )
2110 assert res.response == "Thank you for responding to the host request!"
2111 break
2112 else:
2113 raise Exception("Didn't find link")
2115 with requests_session(surfer_token) as api:
2116 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
2117 assert res.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
2120def test_host_req_feedback(db, moderator):
2121 host, host_token = generate_user(complete_profile=True)
2122 host2, host2_token = generate_user(complete_profile=True)
2123 host3, host3_token = generate_user(complete_profile=True)
2124 surfer, surfer_token = generate_user(complete_profile=True)
2126 today_plus_2 = today() + timedelta(days=2)
2127 today_plus_3 = today() + timedelta(days=3)
2129 with requests_session(surfer_token) as api:
2130 hr_id = api.CreateHostRequest(
2131 requests_pb2.CreateHostRequestReq(
2132 host_user_id=host.id,
2133 from_date=today_plus_2.isoformat(),
2134 to_date=today_plus_3.isoformat(),
2135 text=valid_request_text("can i stay plz"),
2136 )
2137 ).host_request_id
2138 hr2_id = api.CreateHostRequest(
2139 requests_pb2.CreateHostRequestReq(
2140 host_user_id=host2.id,
2141 from_date=today_plus_2.isoformat(),
2142 to_date=today_plus_3.isoformat(),
2143 text=valid_request_text("can i stay plz"),
2144 )
2145 ).host_request_id
2146 hr3_id = api.CreateHostRequest(
2147 requests_pb2.CreateHostRequestReq(
2148 host_user_id=host3.id,
2149 from_date=today_plus_2.isoformat(),
2150 to_date=today_plus_3.isoformat(),
2151 text=valid_request_text("can i stay plz"),
2152 )
2153 ).host_request_id
2155 moderator.approve_host_request(hr_id)
2156 moderator.approve_host_request(hr2_id)
2157 moderator.approve_host_request(hr3_id)
2159 with requests_session(host_token) as api:
2160 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
2161 assert not res.need_host_request_feedback
2163 api.RespondHostRequest(
2164 requests_pb2.RespondHostRequestReq(
2165 host_request_id=hr_id,
2166 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED,
2167 )
2168 )
2170 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
2171 assert res.need_host_request_feedback
2173 # surfer can't leave feedback
2174 with requests_session(surfer_token) as api:
2175 with pytest.raises(grpc.RpcError) as e:
2176 api.SendHostRequestFeedback(
2177 requests_pb2.SendHostRequestFeedbackReq(
2178 host_request_id=hr_id,
2179 )
2180 )
2181 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2182 assert e.value.details() == "Couldn't find that host request."
2184 with requests_session(host_token) as api:
2185 api.SendHostRequestFeedback(
2186 requests_pb2.SendHostRequestFeedbackReq(
2187 host_request_id=hr_id,
2188 host_request_quality=requests_pb2.HOST_REQUEST_QUALITY_LOW,
2189 )
2190 )
2191 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
2192 assert not res.need_host_request_feedback
2194 # can't leave it twice
2195 with requests_session(host_token) as api:
2196 with pytest.raises(grpc.RpcError) as e:
2197 api.SendHostRequestFeedback(
2198 requests_pb2.SendHostRequestFeedbackReq(
2199 host_request_id=hr_id,
2200 )
2201 )
2202 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2203 assert e.value.details() == "You have already left feedback for this host request!"
2205 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
2206 assert not res.need_host_request_feedback
2208 with requests_session(host2_token) as api:
2209 api.RespondHostRequest(
2210 requests_pb2.RespondHostRequestReq(host_request_id=hr2_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED)
2211 )
2212 # can't leave feedback on the wrong one
2213 with pytest.raises(grpc.RpcError) as e:
2214 api.SendHostRequestFeedback(
2215 requests_pb2.SendHostRequestFeedbackReq(
2216 host_request_id=hr_id,
2217 )
2218 )
2219 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2220 assert e.value.details() == "Couldn't find that host request."
2222 # null feedback is still feedback
2223 api.SendHostRequestFeedback(requests_pb2.SendHostRequestFeedbackReq(host_request_id=hr2_id))
2225 with requests_session(host3_token) as api:
2226 api.RespondHostRequest(
2227 requests_pb2.RespondHostRequestReq(host_request_id=hr3_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED)
2228 )
2230 api.SendHostRequestFeedback(
2231 requests_pb2.SendHostRequestFeedbackReq(host_request_id=hr3_id, decline_reason="bad req")
2232 )
2235def _make_trip_node_admin(user_id: int, trip_id: int):
2236 with session_scope() as session:
2237 node_id = session.execute(select(PublicTrip.node_id).where(PublicTrip.id == trip_id)).scalar_one()
2238 cluster = session.execute(
2239 select(Cluster).where(Cluster.parent_node_id == node_id).where(Cluster.is_official_cluster)
2240 ).scalar_one_or_none()
2241 if cluster is None: 2241 ↛ 2250line 2241 didn't jump to line 2250 because the condition on line 2241 was always true
2242 cluster = Cluster(
2243 name="Test community",
2244 description="Test",
2245 parent_node_id=node_id,
2246 is_official_cluster=True,
2247 )
2248 session.add(cluster)
2249 session.flush()
2250 session.add(ClusterSubscription(cluster_id=cluster.id, user_id=user_id, role=ClusterRole.admin))
2253def _create_public_trip(
2254 user_id: int,
2255 from_date,
2256 to_date,
2257 *,
2258 status=None,
2259 same_gender_only: bool = False,
2260 visibility: ModerationVisibility = ModerationVisibility.visible,
2261):
2262 with session_scope() as session:
2263 node = session.execute(select(Node).limit(1)).scalar_one_or_none()
2264 if node is None:
2265 node = Node(
2266 geom=to_multi(create_polygon_lat_lng([[0, 0], [0, 2], [2, 2], [2, 0], [0, 0]])),
2267 node_type=NodeType.locality,
2268 )
2269 session.add(node)
2270 session.flush()
2271 moderation_state = ModerationState(
2272 object_type=ModerationObjectType.public_trip,
2273 object_id=0, # placeholder, set after PublicTrip flush
2274 visibility=visibility,
2275 )
2276 session.add(moderation_state)
2277 session.flush()
2278 trip = PublicTrip(
2279 user_id=user_id,
2280 node_id=node.id,
2281 from_date=from_date,
2282 to_date=to_date,
2283 description="Looking for a host!",
2284 status=status or PublicTripStatus.searching_for_host,
2285 same_gender_only=same_gender_only,
2286 moderation_state_id=moderation_state.id,
2287 )
2288 session.add(trip)
2289 session.flush()
2290 moderation_state.object_id = trip.id
2291 return trip.id
2294def test_create_request_with_public_trip(db, moderator):
2295 """Hosts can offer to host a public trip; offered dates must be within trip dates."""
2296 surfer, surfer_token = generate_user()
2297 host, host_token = generate_user()
2299 trip_from = today() + timedelta(days=10)
2300 trip_to = today() + timedelta(days=20)
2301 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2303 with requests_session(host_token) as api:
2304 # Happy path: dates within trip window
2305 res = api.CreateHostRequest(
2306 requests_pb2.CreateHostRequestReq(
2307 host_user_id=surfer.id,
2308 from_date=(trip_from + timedelta(days=1)).isoformat(),
2309 to_date=(trip_to - timedelta(days=1)).isoformat(),
2310 text=valid_request_text(),
2311 public_trip_id=trip_id,
2312 )
2313 )
2314 host_request_id = res.host_request_id
2316 moderator.approve_host_request(host_request_id)
2318 with requests_session(host_token) as api:
2319 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2320 assert hr.public_trip_id == trip_id
2323def test_create_request_with_public_trip_hosting_snapshot(db, moderator):
2324 """An offer snapshots the offering host's place, not the traveller's."""
2325 surfer, _ = generate_user(city="Surfer city", geom=create_coordinate(1, 1), geom_radius=11)
2326 host, host_token = generate_user(city="Host city", geom=create_coordinate(2, 2), geom_radius=22)
2328 trip_from = today() + timedelta(days=10)
2329 trip_to = today() + timedelta(days=20)
2330 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2332 with requests_session(host_token) as api:
2333 host_request_id = api.CreateHostRequest(
2334 requests_pb2.CreateHostRequestReq(
2335 host_user_id=surfer.id,
2336 from_date=trip_from.isoformat(),
2337 to_date=trip_to.isoformat(),
2338 text=valid_request_text(),
2339 public_trip_id=trip_id,
2340 )
2341 ).host_request_id
2343 moderator.approve_host_request(host_request_id)
2345 with requests_session(host_token) as api:
2346 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2347 assert hr.hosting_city == "Host city"
2348 assert round(hr.hosting_lat, 4) == 2
2349 assert round(hr.hosting_lng, 4) == 2
2350 assert hr.hosting_radius == 22
2353def test_public_trip_offer_stay_roles(db, moderator):
2354 """An offer reverses initiator/recipient, but the surfer and host are still the traveller and the host."""
2355 surfer, surfer_token = generate_user()
2356 host, host_token = generate_user()
2358 trip_from = today() + timedelta(days=10)
2359 trip_to = today() + timedelta(days=20)
2360 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2362 with requests_session(host_token) as api:
2363 host_request_id = api.CreateHostRequest(
2364 requests_pb2.CreateHostRequestReq(
2365 host_user_id=surfer.id,
2366 from_date=trip_from.isoformat(),
2367 to_date=trip_to.isoformat(),
2368 text=valid_request_text(),
2369 public_trip_id=trip_id,
2370 )
2371 ).host_request_id
2373 moderator.approve_host_request(host_request_id)
2375 with session_scope() as session:
2376 host_request = session.execute(
2377 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
2378 ).scalar_one()
2379 assert host_request.initiator_user_id == host.id
2380 assert host_request.recipient_user_id == surfer.id
2381 assert host_request.surfer_user_id == surfer.id
2382 assert host_request.host_user_id == host.id
2384 for token in (surfer_token, host_token):
2385 with requests_session(token) as api:
2386 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2387 assert hr.surfer_user_id == surfer.id
2388 assert hr.host_user_id == host.id
2390 listed = api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests
2391 assert [(r.surfer_user_id, r.host_user_id) for r in listed] == [(surfer.id, host.id)]
2394def test_create_request_with_public_trip_dates_out_of_range(db):
2395 """Offered dates outside the trip window are rejected."""
2396 surfer, _ = generate_user()
2397 host, host_token = generate_user()
2399 trip_from = today() + timedelta(days=10)
2400 trip_to = today() + timedelta(days=20)
2401 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2403 with requests_session(host_token) as api:
2404 # from_date before trip starts
2405 with pytest.raises(grpc.RpcError) as e:
2406 api.CreateHostRequest(
2407 requests_pb2.CreateHostRequestReq(
2408 host_user_id=surfer.id,
2409 from_date=(trip_from - timedelta(days=1)).isoformat(),
2410 to_date=(trip_from + timedelta(days=1)).isoformat(),
2411 text=valid_request_text(),
2412 public_trip_id=trip_id,
2413 )
2414 )
2415 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2417 # to_date after trip ends
2418 with pytest.raises(grpc.RpcError) as e:
2419 api.CreateHostRequest(
2420 requests_pb2.CreateHostRequestReq(
2421 host_user_id=surfer.id,
2422 from_date=(trip_to - timedelta(days=1)).isoformat(),
2423 to_date=(trip_to + timedelta(days=1)).isoformat(),
2424 text=valid_request_text(),
2425 public_trip_id=trip_id,
2426 )
2427 )
2428 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2431def test_create_request_with_public_trip_user_mismatch(db):
2432 """The host_user_id must match the public trip's traveler."""
2433 trip_owner, _ = generate_user()
2434 other_user, _ = generate_user()
2435 host, host_token = generate_user()
2437 trip_from = today() + timedelta(days=10)
2438 trip_to = today() + timedelta(days=20)
2439 trip_id = _create_public_trip(trip_owner.id, trip_from, trip_to)
2441 with requests_session(host_token) as api:
2442 with pytest.raises(grpc.RpcError) as e:
2443 api.CreateHostRequest(
2444 requests_pb2.CreateHostRequestReq(
2445 host_user_id=other_user.id, # not the trip owner
2446 from_date=(trip_from + timedelta(days=1)).isoformat(),
2447 to_date=(trip_to - timedelta(days=1)).isoformat(),
2448 text=valid_request_text(),
2449 public_trip_id=trip_id,
2450 )
2451 )
2452 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2455def test_create_request_with_closed_public_trip(db):
2456 """Cannot offer to host a trip that's been closed."""
2457 surfer, _ = generate_user()
2458 host, host_token = generate_user()
2460 trip_from = today() + timedelta(days=10)
2461 trip_to = today() + timedelta(days=20)
2462 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, status=PublicTripStatus.closed)
2464 with requests_session(host_token) as api:
2465 with pytest.raises(grpc.RpcError) as e:
2466 api.CreateHostRequest(
2467 requests_pb2.CreateHostRequestReq(
2468 host_user_id=surfer.id,
2469 from_date=(trip_from + timedelta(days=1)).isoformat(),
2470 to_date=(trip_to - timedelta(days=1)).isoformat(),
2471 text=valid_request_text(),
2472 public_trip_id=trip_id,
2473 )
2474 )
2475 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2478def test_create_request_with_nonexistent_public_trip(db):
2479 """Nonexistent public trip ID returns NOT_FOUND."""
2480 surfer, _ = generate_user()
2481 host, host_token = generate_user()
2483 with requests_session(host_token) as api:
2484 with pytest.raises(grpc.RpcError) as e:
2485 api.CreateHostRequest(
2486 requests_pb2.CreateHostRequestReq(
2487 host_user_id=surfer.id,
2488 from_date=(today() + timedelta(days=2)).isoformat(),
2489 to_date=(today() + timedelta(days=3)).isoformat(),
2490 text=valid_request_text(),
2491 public_trip_id=999999,
2492 )
2493 )
2494 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2497def test_create_request_with_shadowed_public_trip(db, moderator):
2498 """A trip awaiting moderation can't be offered on until it's approved."""
2499 surfer, _ = generate_user()
2500 host, host_token = generate_user()
2502 trip_from = today() + timedelta(days=10)
2503 trip_to = today() + timedelta(days=20)
2504 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, visibility=ModerationVisibility.shadowed)
2506 with requests_session(host_token) as api:
2507 with pytest.raises(grpc.RpcError) as e:
2508 api.CreateHostRequest(
2509 requests_pb2.CreateHostRequestReq(
2510 host_user_id=surfer.id,
2511 from_date=(trip_from + timedelta(days=1)).isoformat(),
2512 to_date=(trip_to - timedelta(days=1)).isoformat(),
2513 text=valid_request_text(),
2514 public_trip_id=trip_id,
2515 )
2516 )
2517 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2518 assert e.value.details() == "Couldn't find that public trip."
2520 moderator.approve_public_trip(trip_id)
2522 with requests_session(host_token) as api:
2523 res = api.CreateHostRequest(
2524 requests_pb2.CreateHostRequestReq(
2525 host_user_id=surfer.id,
2526 from_date=(trip_from + timedelta(days=1)).isoformat(),
2527 to_date=(trip_to - timedelta(days=1)).isoformat(),
2528 text=valid_request_text(),
2529 public_trip_id=trip_id,
2530 )
2531 )
2532 assert res.host_request_id
2535def test_create_request_with_hidden_public_trip(db):
2536 """A hidden trip can't be offered on."""
2537 surfer, _ = generate_user()
2538 host, host_token = generate_user()
2540 trip_from = today() + timedelta(days=10)
2541 trip_to = today() + timedelta(days=20)
2542 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, visibility=ModerationVisibility.hidden)
2544 with requests_session(host_token) as api:
2545 with pytest.raises(grpc.RpcError) as e:
2546 api.CreateHostRequest(
2547 requests_pb2.CreateHostRequestReq(
2548 host_user_id=surfer.id,
2549 from_date=(trip_from + timedelta(days=1)).isoformat(),
2550 to_date=(trip_to - timedelta(days=1)).isoformat(),
2551 text=valid_request_text(),
2552 public_trip_id=trip_id,
2553 )
2554 )
2555 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2556 assert e.value.details() == "Couldn't find that public trip."
2559def test_create_request_without_public_trip_id_unchanged(db, moderator):
2560 """Existing flow without public_trip_id still works (backwards compatibility)."""
2561 surfer, _ = generate_user()
2562 host, host_token = generate_user()
2564 with requests_session(host_token) as api:
2565 res = api.CreateHostRequest(
2566 requests_pb2.CreateHostRequestReq(
2567 host_user_id=surfer.id,
2568 from_date=(today() + timedelta(days=2)).isoformat(),
2569 to_date=(today() + timedelta(days=3)).isoformat(),
2570 text=valid_request_text(),
2571 )
2572 )
2573 host_request_id = res.host_request_id
2575 moderator.approve_host_request(host_request_id)
2577 with requests_session(host_token) as api:
2578 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2579 assert not hr.HasField("public_trip_id")
2582def test_create_request_same_gender_only_wrong_gender_rejected(db):
2583 surfer, _ = generate_user(gender="Woman")
2584 _, host_token = generate_user(gender="Man")
2586 trip_from = today() + timedelta(days=10)
2587 trip_to = today() + timedelta(days=20)
2588 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2590 with requests_session(host_token) as api:
2591 with pytest.raises(grpc.RpcError) as e:
2592 api.CreateHostRequest(
2593 requests_pb2.CreateHostRequestReq(
2594 host_user_id=surfer.id,
2595 from_date=trip_from.isoformat(),
2596 to_date=trip_to.isoformat(),
2597 text=valid_request_text(),
2598 public_trip_id=trip_id,
2599 )
2600 )
2601 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2604def test_create_request_same_gender_only_same_gender_allowed(db, moderator):
2605 surfer, _ = generate_user(gender="Woman")
2606 _, host_token = generate_user(gender="Woman")
2608 trip_from = today() + timedelta(days=10)
2609 trip_to = today() + timedelta(days=20)
2610 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2612 with requests_session(host_token) as api:
2613 res = api.CreateHostRequest(
2614 requests_pb2.CreateHostRequestReq(
2615 host_user_id=surfer.id,
2616 from_date=trip_from.isoformat(),
2617 to_date=trip_to.isoformat(),
2618 text=valid_request_text(),
2619 public_trip_id=trip_id,
2620 )
2621 )
2622 assert res.host_request_id > 0
2625def test_create_request_same_gender_only_moderator_bypass(db, moderator):
2626 surfer, _ = generate_user(gender="Woman")
2627 host, host_token = generate_user(gender="Man")
2629 trip_from = today() + timedelta(days=10)
2630 trip_to = today() + timedelta(days=20)
2631 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2632 _make_trip_node_admin(host.id, trip_id)
2634 with requests_session(host_token) as api:
2635 res = api.CreateHostRequest(
2636 requests_pb2.CreateHostRequestReq(
2637 host_user_id=surfer.id,
2638 from_date=trip_from.isoformat(),
2639 to_date=trip_to.isoformat(),
2640 text=valid_request_text(),
2641 public_trip_id=trip_id,
2642 )
2643 )
2644 assert res.host_request_id > 0
2647def test_create_request_duplicate_offer_rejected(db):
2648 surfer, _ = generate_user()
2649 _, host_token = generate_user()
2651 trip_from = today() + timedelta(days=10)
2652 trip_to = today() + timedelta(days=20)
2653 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2655 with requests_session(host_token) as api:
2656 api.CreateHostRequest(
2657 requests_pb2.CreateHostRequestReq(
2658 host_user_id=surfer.id,
2659 from_date=trip_from.isoformat(),
2660 to_date=trip_to.isoformat(),
2661 text=valid_request_text(),
2662 public_trip_id=trip_id,
2663 )
2664 )
2665 with pytest.raises(grpc.RpcError) as e:
2666 api.CreateHostRequest(
2667 requests_pb2.CreateHostRequestReq(
2668 host_user_id=surfer.id,
2669 from_date=trip_from.isoformat(),
2670 to_date=trip_to.isoformat(),
2671 text=valid_request_text(),
2672 public_trip_id=trip_id,
2673 )
2674 )
2675 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION