Coverage for app/backend/src/tests/test_requests.py: 99%
1453 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-03 11:56 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-03 11:56 +0000
1import html
2import re
3from datetime import date, timedelta
4from unittest.mock import DEFAULT, patch
5from urllib.parse import parse_qs, urlparse
7import grpc
8import pytest
9from google.protobuf import wrappers_pb2
10from sqlalchemy import func, select, update
11from sqlalchemy.exc import IntegrityError
12from sqlalchemy_utils import refresh_materialized_view
14from couchers.constants import HOST_REQUEST_MIN_LENGTH_UTF16, PUBLIC_TRIP_OFFER_MIN_LENGTH_UTF16
15from couchers.crypto import b64decode
16from couchers.db import session_scope
17from couchers.i18n import LocalizationContext
18from couchers.materialized_views import UserResponseRate
19from couchers.models import (
20 Cluster,
21 ClusterRole,
22 ClusterSubscription,
23 HostRequest,
24 HostRequestStatus,
25 Message,
26 MessageType,
27 ModerationObjectType,
28 ModerationState,
29 ModerationVisibility,
30 Node,
31 NodeType,
32 Notification,
33 RateLimitAction,
34 RateLimitViolation,
35)
36from couchers.models.public_trips import PublicTrip, PublicTripStatus
37from couchers.proto import (
38 api_pb2,
39 auth_pb2,
40 messages_pb2,
41 requests_pb2,
42)
43from couchers.proto.internal import unsubscribe_pb2
44from couchers.rate_limits.definitions import RATE_LIMIT_DEFINITIONS, RATE_LIMIT_HOURS
45from couchers.utils import create_coordinate, create_polygon_lat_lng, now, to_multi, today
46from tests.fixtures.db import backdate_conversations, generate_user, make_user_block
47from tests.fixtures.misc import EmailCollector, PushCollector
48from tests.fixtures.sessions import api_session, auth_api_session, requests_session
49from tests.test_public_trips import _create_trip_directly, _make_node
52def valid_request_text(text: str = "Test request") -> str:
53 """Pads a request text to a valid length."""
54 # Request lengths are measured in utf-16 code units to match the frontend.
55 utf16_length = len(text.encode("utf-16-le")) // 2
56 if utf16_length >= HOST_REQUEST_MIN_LENGTH_UTF16: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 return text
58 padding_length = HOST_REQUEST_MIN_LENGTH_UTF16 - utf16_length
59 return text + ("_" * padding_length) # Each "_" adds one utf16 code unit.
62def test_create_request(db, moderator):
63 user1, token1 = generate_user()
64 hosting_city = "Morningside Heights, New York City"
65 hosting_lat = 40.8086
66 hosting_lng = -73.9616
67 hosting_radius = 500
68 user2, token2 = generate_user(
69 city=hosting_city,
70 geom=create_coordinate(hosting_lat, hosting_lng),
71 geom_radius=hosting_radius,
72 )
74 today_plus_2 = today() + timedelta(days=2)
75 today_plus_3 = today() + timedelta(days=3)
76 today_minus_2 = today() - timedelta(days=2)
77 today_minus_3 = today() - timedelta(days=3)
79 with requests_session(token1) as api:
80 with pytest.raises(grpc.RpcError) as e:
81 api.CreateHostRequest(
82 requests_pb2.CreateHostRequestReq(
83 host_user_id=user1.id,
84 from_date=today_plus_2.isoformat(),
85 to_date=today_plus_3.isoformat(),
86 text=valid_request_text(),
87 )
88 )
89 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
90 assert e.value.details() == "Can't request hosting from yourself."
92 with pytest.raises(grpc.RpcError) as e:
93 api.CreateHostRequest(
94 requests_pb2.CreateHostRequestReq(
95 host_user_id=999,
96 from_date=today_plus_2.isoformat(),
97 to_date=today_plus_3.isoformat(),
98 text=valid_request_text(),
99 )
100 )
101 assert e.value.code() == grpc.StatusCode.NOT_FOUND
102 assert e.value.details() == "Couldn't find that user."
104 with pytest.raises(grpc.RpcError) as e:
105 api.CreateHostRequest(
106 requests_pb2.CreateHostRequestReq(
107 host_user_id=user2.id,
108 from_date=today_plus_3.isoformat(),
109 to_date=today_plus_2.isoformat(),
110 text=valid_request_text(),
111 )
112 )
113 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
114 assert e.value.details() == "From date can't be after to date."
116 with pytest.raises(grpc.RpcError) as e:
117 api.CreateHostRequest(
118 requests_pb2.CreateHostRequestReq(
119 host_user_id=user2.id,
120 from_date=today_minus_3.isoformat(),
121 to_date=today_plus_2.isoformat(),
122 text=valid_request_text(),
123 )
124 )
125 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
126 assert e.value.details() == "From date must be today or later."
128 with pytest.raises(grpc.RpcError) as e:
129 api.CreateHostRequest(
130 requests_pb2.CreateHostRequestReq(
131 host_user_id=user2.id,
132 from_date=today_plus_2.isoformat(),
133 to_date=today_minus_2.isoformat(),
134 text=valid_request_text(),
135 )
136 )
137 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
138 assert e.value.details() == "From date can't be after to date."
140 with pytest.raises(grpc.RpcError) as e:
141 api.CreateHostRequest(
142 requests_pb2.CreateHostRequestReq(
143 host_user_id=user2.id,
144 from_date="2020-00-06",
145 to_date=today_minus_2.isoformat(),
146 text=valid_request_text(),
147 )
148 )
149 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
150 assert e.value.details() == "Invalid date."
152 with pytest.raises(grpc.RpcError) as e:
153 api.CreateHostRequest(
154 requests_pb2.CreateHostRequestReq(
155 host_user_id=user2.id,
156 from_date=today_plus_2.isoformat(),
157 to_date=today_plus_3.isoformat(),
158 text="Too short.",
159 )
160 )
161 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
162 assert e.value.details() == "Host request cannot be shorter than 250 characters."
164 res = api.CreateHostRequest(
165 requests_pb2.CreateHostRequestReq(
166 host_user_id=user2.id,
167 from_date=today_plus_2.isoformat(),
168 to_date=today_plus_3.isoformat(),
169 text=valid_request_text(),
170 )
171 )
172 host_request_id = res.host_request_id
174 moderator.approve_host_request(host_request_id)
176 with requests_session(token1) as api:
177 host_requests = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)).host_requests
179 assert len(host_requests) == 1
180 hr = host_requests[0]
182 assert hr.latest_message.text.text == valid_request_text()
184 assert hr.hosting_city == hosting_city
185 assert round(hr.hosting_lat, 4) == hosting_lat
186 assert round(hr.hosting_lng, 4) == hosting_lng
187 assert hr.hosting_radius == hosting_radius
189 today_ = today()
190 today_plus_one_year = today_ + timedelta(days=365)
191 today_plus_one_year_plus_2 = today_plus_one_year + timedelta(days=2)
192 today_plus_one_year_plus_3 = today_plus_one_year + timedelta(days=3)
193 with pytest.raises(grpc.RpcError) as e:
194 api.CreateHostRequest(
195 requests_pb2.CreateHostRequestReq(
196 host_user_id=user2.id,
197 from_date=today_plus_one_year_plus_2.isoformat(),
198 to_date=today_plus_one_year_plus_3.isoformat(),
199 text=valid_request_text("Test from date after one year"),
200 )
201 )
202 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
203 assert e.value.details() == "The start date must be within one year from today."
205 with pytest.raises(grpc.RpcError) as e:
206 api.CreateHostRequest(
207 requests_pb2.CreateHostRequestReq(
208 host_user_id=user2.id,
209 from_date=today_plus_2.isoformat(),
210 to_date=today_plus_one_year_plus_3.isoformat(),
211 text=valid_request_text("Test to date one year after from date"),
212 )
213 )
214 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
215 assert e.value.details() == "You cannot request to stay with someone for longer than one year."
218def test_create_host_request_rejects_date_past_in_host_timezone(db):
219 # When the host's timezone has already rolled over to the next day, a
220 # from_date of "today in UTC" is in the past from the host's perspective and
221 # must be rejected. The frontend blocks this date before submission; the
222 # backend enforces the same rule for consistency.
223 user1, token1 = generate_user()
224 # geom inside the fake Europe/Helsinki timezone polygon used in tests
225 user2, _ = generate_user(geom=create_coordinate(61, 25))
227 # Helsinki is already on 2026-01-16; requester submits 2026-01-15.
228 fake_today_by_tz = {"Europe/Helsinki": date(2026, 1, 16)}
230 with patch(
231 "couchers.servicers.requests.today_in_timezone",
232 side_effect=lambda tz: fake_today_by_tz.get(tz, date(2026, 1, 15)),
233 ):
234 with requests_session(token1) as api:
235 with pytest.raises(grpc.RpcError) as e:
236 api.CreateHostRequest(
237 requests_pb2.CreateHostRequestReq(
238 host_user_id=user2.id,
239 from_date="2026-01-15",
240 to_date="2026-01-18",
241 text=valid_request_text(),
242 )
243 )
244 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
247def test_create_host_request_date_valid_when_host_behind_requester(db):
248 # Simulate the opposite timezone direction: the host (America/New_York) is
249 # still on 2026-01-15 while the requester has already rolled into 2026-01-16.
250 # A from_date of 2026-01-16 is "today" for the requester and "tomorrow" for
251 # the host — must be accepted without issue.
252 user1, token1 = generate_user()
253 user2, _ = generate_user() # default geom resolves to America/New_York
255 fake_today_by_tz = {"America/New_York": date(2026, 1, 15)}
257 with patch(
258 "couchers.servicers.requests.today_in_timezone",
259 side_effect=lambda tz: fake_today_by_tz.get(tz, date(2026, 1, 15)),
260 ):
261 with requests_session(token1) as api:
262 res = api.CreateHostRequest(
263 requests_pb2.CreateHostRequestReq(
264 host_user_id=user2.id,
265 from_date="2026-01-16",
266 to_date="2026-01-20",
267 text=valid_request_text(),
268 )
269 )
270 assert res.host_request_id
273def test_create_request_records_host_timezone(db, frozen_timewarp, moderator):
274 surfer, surfer_token = generate_user() # default geom resolves to America/New_York
275 # geom inside the fake Europe/Helsinki timezone polygon used in tests
276 host, host_token = generate_user(geom=create_coordinate(61, 25))
278 with requests_session(surfer_token) as api:
279 host_request_id = api.CreateHostRequest(
280 requests_pb2.CreateHostRequestReq(
281 host_user_id=host.id,
282 from_date=(today() + timedelta(days=2)).isoformat(),
283 to_date=(today() + timedelta(days=3)).isoformat(),
284 text=valid_request_text(),
285 )
286 ).host_request_id
288 moderator.approve_host_request(host_request_id)
290 with requests_session(surfer_token) as api:
291 get_res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
292 assert get_res.timezone == "Europe/Helsinki"
294 # The host request timezone shouldn't change if the host then moves to a different timezone
295 # Otherwise this would affect all historical host requests
296 with api_session(host_token) as api:
297 api.UpdateProfile(
298 api_pb2.UpdateProfileReq(lat=wrappers_pb2.DoubleValue(value=0.01), lng=wrappers_pb2.DoubleValue(value=0.01))
299 )
300 assert api.GetUser(api_pb2.GetUserReq(user=host.username)).timezone != "Europe/Helsinki"
302 with requests_session(surfer_token) as api:
303 get_res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
304 assert get_res.timezone == "Europe/Helsinki"
307def test_create_request_duplicate_within_window(db):
308 """A second request to the same host for overlapping dates inside the window is rejected, so a
309 client that resends (or a user who re-taps Send) doesn't create a pile of duplicates."""
310 user1, token1 = generate_user()
311 user2, _ = generate_user()
312 user3, _ = generate_user()
313 from_date = today() + timedelta(days=10)
314 to_date = today() + timedelta(days=15)
316 with requests_session(token1) as api:
317 assert api.CreateHostRequest(
318 requests_pb2.CreateHostRequestReq(
319 host_user_id=user2.id,
320 from_date=from_date.isoformat(),
321 to_date=to_date.isoformat(),
322 text=valid_request_text(),
323 )
324 ).host_request_id
326 with pytest.raises(grpc.RpcError) as e:
327 api.CreateHostRequest(
328 requests_pb2.CreateHostRequestReq(
329 host_user_id=user2.id,
330 from_date=from_date.isoformat(),
331 to_date=to_date.isoformat(),
332 text=valid_request_text(),
333 )
334 )
335 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
336 assert "past 3 hours" in (e.value.details() or "")
338 # partly overlapping dates are blocked too
339 with pytest.raises(grpc.RpcError) as e:
340 api.CreateHostRequest(
341 requests_pb2.CreateHostRequestReq(
342 host_user_id=user2.id,
343 from_date=(to_date - timedelta(days=1)).isoformat(),
344 to_date=(to_date + timedelta(days=5)).isoformat(),
345 text=valid_request_text(),
346 )
347 )
348 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
350 # a different host is unaffected
351 assert api.CreateHostRequest(
352 requests_pb2.CreateHostRequestReq(
353 host_user_id=user3.id,
354 from_date=from_date.isoformat(),
355 to_date=to_date.isoformat(),
356 text=valid_request_text(),
357 )
358 ).host_request_id
361def test_create_request_duplicate_window_allows_other_dates(db):
362 """Only overlapping stays count as duplicates, including a stay starting the day another one
363 ends."""
364 user1, token1 = generate_user()
365 user2, _ = generate_user()
366 from_date = today() + timedelta(days=10)
367 to_date = today() + timedelta(days=15)
369 with requests_session(token1) as api:
370 assert api.CreateHostRequest(
371 requests_pb2.CreateHostRequestReq(
372 host_user_id=user2.id,
373 from_date=from_date.isoformat(),
374 to_date=to_date.isoformat(),
375 text=valid_request_text(),
376 )
377 ).host_request_id
379 assert api.CreateHostRequest(
380 requests_pb2.CreateHostRequestReq(
381 host_user_id=user2.id,
382 from_date=to_date.isoformat(),
383 to_date=(to_date + timedelta(days=5)).isoformat(),
384 text=valid_request_text(),
385 )
386 ).host_request_id
388 assert api.CreateHostRequest(
389 requests_pb2.CreateHostRequestReq(
390 host_user_id=user2.id,
391 from_date=(from_date + timedelta(days=90)).isoformat(),
392 to_date=(to_date + timedelta(days=90)).isoformat(),
393 text=valid_request_text(),
394 )
395 ).host_request_id
398def test_create_request_duplicate_allowed_after_window(db):
399 user1, token1 = generate_user()
400 user2, _ = generate_user()
401 from_date = today() + timedelta(days=10)
402 to_date = today() + timedelta(days=15)
404 with requests_session(token1) as api:
405 api.CreateHostRequest(
406 requests_pb2.CreateHostRequestReq(
407 host_user_id=user2.id,
408 from_date=from_date.isoformat(),
409 to_date=to_date.isoformat(),
410 text=valid_request_text(),
411 )
412 )
413 backdate_conversations()
415 assert api.CreateHostRequest(
416 requests_pb2.CreateHostRequestReq(
417 host_user_id=user2.id,
418 from_date=from_date.isoformat(),
419 to_date=to_date.isoformat(),
420 text=valid_request_text(),
421 )
422 ).host_request_id
425def test_create_request_duplicate_window_ignores_public_trip_offers(db):
426 """Offers on public trips are deduplicated per trip, so two offers to the same traveler for
427 two different trips must both go through."""
428 surfer, _ = generate_user()
429 host, host_token = generate_user()
431 trip_1_from = today() + timedelta(days=10)
432 trip_1_to = today() + timedelta(days=20)
433 trip_2_from = today() + timedelta(days=40)
434 trip_2_to = today() + timedelta(days=50)
435 trip_1_id = _create_public_trip(surfer.id, trip_1_from, trip_1_to)
436 trip_2_id = _create_public_trip(surfer.id, trip_2_from, trip_2_to)
438 with requests_session(host_token) as api:
439 assert api.CreateHostRequest(
440 requests_pb2.CreateHostRequestReq(
441 host_user_id=surfer.id,
442 from_date=trip_1_from.isoformat(),
443 to_date=trip_1_to.isoformat(),
444 text=valid_request_text(),
445 public_trip_id=trip_1_id,
446 )
447 ).host_request_id
449 assert api.CreateHostRequest(
450 requests_pb2.CreateHostRequestReq(
451 host_user_id=surfer.id,
452 from_date=trip_2_from.isoformat(),
453 to_date=trip_2_to.isoformat(),
454 text=valid_request_text(),
455 public_trip_id=trip_2_id,
456 )
457 ).host_request_id
459 # the window only counts non-trip requests, so trip offers don't block a plain request
460 # even for dates they cover
461 assert api.CreateHostRequest(
462 requests_pb2.CreateHostRequestReq(
463 host_user_id=surfer.id,
464 from_date=(trip_1_from + timedelta(days=2)).isoformat(),
465 to_date=(trip_1_to - timedelta(days=2)).isoformat(),
466 text=valid_request_text(),
467 )
468 ).host_request_id
471def test_create_request_incomplete_profile(db):
472 user1, token1 = generate_user(complete_profile=False)
473 user2, _ = generate_user()
474 today_plus_2 = today() + timedelta(days=2)
475 today_plus_3 = today() + timedelta(days=3)
476 with requests_session(token1) as api:
477 with pytest.raises(grpc.RpcError) as e:
478 api.CreateHostRequest(
479 requests_pb2.CreateHostRequestReq(
480 host_user_id=user2.id,
481 from_date=today_plus_2.isoformat(),
482 to_date=today_plus_3.isoformat(),
483 text=valid_request_text(),
484 )
485 )
486 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
487 assert e.value.details() == "You have to complete your profile before you can send a request."
490def test_excessive_requests_are_reported(db, low_rate_limits, email_collector: EmailCollector):
491 """Test that excessive host requests are first reported in a warning email and finally lead blocking of further requests."""
492 user, token = generate_user()
493 today_plus_2 = today() + timedelta(days=2)
494 today_plus_3 = today() + timedelta(days=3)
495 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.host_request]
496 with requests_session(token) as api:
497 # Test warning email
498 for _ in range(rate_limit_definition.warning_limit):
499 host_user, _ = generate_user()
500 _ = api.CreateHostRequest(
501 requests_pb2.CreateHostRequestReq(
502 host_user_id=host_user.id,
503 from_date=today_plus_2.isoformat(),
504 to_date=today_plus_3.isoformat(),
505 text=valid_request_text(),
506 )
507 )
509 assert email_collector.count_for_reports() == 0
510 host_user, _ = generate_user()
511 _ = api.CreateHostRequest(
512 requests_pb2.CreateHostRequestReq(
513 host_user_id=host_user.id,
514 from_date=today_plus_2.isoformat(),
515 to_date=today_plus_3.isoformat(),
516 text=valid_request_text("Excessive test request"),
517 )
518 )
520 email = email_collector.pop_for_reports(last=True)
521 assert email.plain.startswith(
522 f"User {user.username} has sent {rate_limit_definition.warning_limit} host requests in the past {RATE_LIMIT_HOURS} hours."
523 )
525 # Test ban after exceeding HOST_REQUEST_HARD_LIMIT
526 for _ in range(rate_limit_definition.hard_limit - rate_limit_definition.warning_limit - 1):
527 host_user, _ = generate_user()
528 _ = api.CreateHostRequest(
529 requests_pb2.CreateHostRequestReq(
530 host_user_id=host_user.id,
531 from_date=today_plus_2.isoformat(),
532 to_date=today_plus_3.isoformat(),
533 text=valid_request_text(),
534 )
535 )
537 assert email_collector.count_for_reports() == 0
539 host_user, _ = generate_user()
540 with pytest.raises(grpc.RpcError) as exc_info:
541 _ = api.CreateHostRequest(
542 requests_pb2.CreateHostRequestReq(
543 host_user_id=host_user.id,
544 from_date=today_plus_2.isoformat(),
545 to_date=today_plus_3.isoformat(),
546 text=valid_request_text("Excessive test request"),
547 )
548 )
549 assert exc_info.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
550 assert (
551 exc_info.value.details()
552 == "You have sent a lot of host requests in the past 24 hours. To avoid spam, you can't send any more for now."
553 )
555 email = email_collector.pop_for_reports(last=True)
556 assert email.plain.startswith(
557 f"User {user.username} has sent {rate_limit_definition.hard_limit} host requests in the past {RATE_LIMIT_HOURS} hours."
558 )
559 assert "The user has been blocked from sending further host requests for now." in email.plain
561 with session_scope() as session:
562 assert session.execute(
563 select(RateLimitViolation.is_hard_limit)
564 .where(RateLimitViolation.user_id == user.id)
565 .order_by(RateLimitViolation.id)
566 ).scalars().all() == [False, True]
568 # still blocked once the hard violation is on record, without reporting again
569 host_user, _ = generate_user()
570 with pytest.raises(grpc.RpcError) as exc_info:
571 _ = api.CreateHostRequest(
572 requests_pb2.CreateHostRequestReq(
573 host_user_id=host_user.id,
574 from_date=today_plus_2.isoformat(),
575 to_date=today_plus_3.isoformat(),
576 text=valid_request_text("Excessive test request"),
577 )
578 )
579 assert exc_info.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
580 assert email_collector.count_for_reports() == 0
583def add_message(db, text, author_id, conversation_id):
584 with session_scope() as session:
585 message = Message(
586 conversation_id=conversation_id, author_id=author_id, text=text, message_type=MessageType.text
587 )
589 session.add(message)
592def test_GetHostRequest(db):
593 user1, token1 = generate_user()
594 user2, token2 = generate_user()
595 user3, token3 = generate_user()
596 today_plus_2 = today() + timedelta(days=2)
597 today_plus_3 = today() + timedelta(days=3)
598 with requests_session(token1) as api:
599 host_request_id = api.CreateHostRequest(
600 requests_pb2.CreateHostRequestReq(
601 host_user_id=user2.id,
602 from_date=today_plus_2.isoformat(),
603 to_date=today_plus_3.isoformat(),
604 text=valid_request_text("Test request 1"),
605 )
606 ).host_request_id
608 with pytest.raises(grpc.RpcError) as e:
609 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=999))
610 assert e.value.code() == grpc.StatusCode.NOT_FOUND
611 assert e.value.details() == "Couldn't find that host request."
613 api.SendHostRequestMessage(
614 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
615 )
617 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
618 assert res.latest_message.text.text == "Test message 1"
621def test_ListHostRequests(db, moderator):
622 user1, token1 = generate_user()
623 user2, token2 = generate_user()
624 user3, token3 = generate_user()
625 today_plus_2 = today() + timedelta(days=2)
626 today_plus_3 = today() + timedelta(days=3)
627 with requests_session(token1) as api:
628 host_request_1 = api.CreateHostRequest(
629 requests_pb2.CreateHostRequestReq(
630 host_user_id=user2.id,
631 from_date=today_plus_2.isoformat(),
632 to_date=today_plus_3.isoformat(),
633 text=valid_request_text("Test request 1"),
634 )
635 ).host_request_id
637 host_request_2 = api.CreateHostRequest(
638 requests_pb2.CreateHostRequestReq(
639 host_user_id=user3.id,
640 from_date=today_plus_2.isoformat(),
641 to_date=today_plus_3.isoformat(),
642 text=valid_request_text("Test request 2"),
643 )
644 ).host_request_id
646 moderator.approve_host_request(host_request_1)
647 moderator.approve_host_request(host_request_2)
649 with requests_session(token1) as api:
650 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
651 assert res.no_more
652 assert len(res.host_requests) == 2
654 with requests_session(token2) as api:
655 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
656 assert res.no_more
657 assert len(res.host_requests) == 1
658 assert res.host_requests[0].latest_message.text.text == valid_request_text("Test request 1")
659 assert res.host_requests[0].surfer_user_id == user1.id
660 assert res.host_requests[0].host_user_id == user2.id
661 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_PENDING
663 add_message(db, "Test request 1 message 1", user2.id, host_request_1)
664 add_message(db, "Test request 1 message 2", user2.id, host_request_1)
665 add_message(db, "Test request 1 message 3", user2.id, host_request_1)
667 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
668 assert res.host_requests[0].latest_message.text.text == "Test request 1 message 3"
670 host_request_3 = api.CreateHostRequest(
671 requests_pb2.CreateHostRequestReq(
672 host_user_id=user1.id,
673 from_date=today_plus_2.isoformat(),
674 to_date=today_plus_3.isoformat(),
675 text=valid_request_text("Test request 3"),
676 )
677 ).host_request_id
679 moderator.approve_host_request(host_request_3)
681 add_message(db, "Test request 2 message 1", user1.id, host_request_2)
682 add_message(db, "Test request 2 message 2", user3.id, host_request_2)
684 with requests_session(token3) as api:
685 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
686 assert res.no_more
687 assert len(res.host_requests) == 1
688 assert res.host_requests[0].latest_message.text.text == "Test request 2 message 2"
690 with requests_session(token1) as api:
691 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
692 assert len(res.host_requests) == 1
694 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq())
695 assert len(res.host_requests) == 3
698def test_ListHostRequests_pagination_regression(db, moderator):
699 """
700 ListHostRequests was skipping a request when getting multiple pages
701 """
702 user1, token1 = generate_user()
703 user2, token2 = generate_user()
704 today_plus_2 = today() + timedelta(days=2)
705 today_plus_3 = today() + timedelta(days=3)
706 with requests_session(token1) as api:
707 host_request_1 = api.CreateHostRequest(
708 requests_pb2.CreateHostRequestReq(
709 host_user_id=user2.id,
710 from_date=today_plus_2.isoformat(),
711 to_date=today_plus_3.isoformat(),
712 text=valid_request_text("Test request 1"),
713 )
714 ).host_request_id
715 backdate_conversations()
717 host_request_2 = api.CreateHostRequest(
718 requests_pb2.CreateHostRequestReq(
719 host_user_id=user2.id,
720 from_date=today_plus_2.isoformat(),
721 to_date=today_plus_3.isoformat(),
722 text=valid_request_text("Test request 2"),
723 )
724 ).host_request_id
725 backdate_conversations()
727 host_request_3 = api.CreateHostRequest(
728 requests_pb2.CreateHostRequestReq(
729 host_user_id=user2.id,
730 from_date=today_plus_2.isoformat(),
731 to_date=today_plus_3.isoformat(),
732 text=valid_request_text("Test request 3"),
733 )
734 ).host_request_id
736 moderator.approve_host_request(host_request_1)
737 moderator.approve_host_request(host_request_2)
738 moderator.approve_host_request(host_request_3)
740 with requests_session(token2) as api:
741 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
742 assert res.no_more
743 assert len(res.host_requests) == 3
744 assert res.host_requests[0].latest_message.text.text == valid_request_text("Test request 3")
745 assert res.host_requests[1].latest_message.text.text == valid_request_text("Test request 2")
746 assert res.host_requests[2].latest_message.text.text == valid_request_text("Test request 1")
748 with requests_session(token2) as api:
749 api.RespondHostRequest(
750 requests_pb2.RespondHostRequestReq(
751 host_request_id=host_request_2,
752 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
753 text="Accepting host request 2",
754 )
755 )
756 api.RespondHostRequest(
757 requests_pb2.RespondHostRequestReq(
758 host_request_id=host_request_1,
759 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
760 text="Accepting host request 1",
761 )
762 )
763 api.RespondHostRequest(
764 requests_pb2.RespondHostRequestReq(
765 host_request_id=host_request_3,
766 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
767 text="Accepting host request 3",
768 )
769 )
771 with requests_session(token2) as api:
772 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
773 assert res.no_more
774 assert len(res.host_requests) == 3
775 assert res.host_requests[0].latest_message.text.text == "Accepting host request 3"
776 assert res.host_requests[1].latest_message.text.text == "Accepting host request 1"
777 assert res.host_requests[2].latest_message.text.text == "Accepting host request 2"
779 with requests_session(token2) as api:
780 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True, number=1))
781 assert not res.no_more
782 assert len(res.host_requests) == 1
783 assert res.host_requests[0].latest_message.text.text == "Accepting host request 3"
784 res = api.ListHostRequests(
785 requests_pb2.ListHostRequestsReq(only_received=True, number=1, page_token=res.next_page_token)
786 )
787 assert not res.no_more
788 assert len(res.host_requests) == 1
789 assert res.host_requests[0].latest_message.text.text == "Accepting host request 1"
790 res = api.ListHostRequests(
791 requests_pb2.ListHostRequestsReq(only_received=True, number=1, page_token=res.next_page_token)
792 )
793 assert res.no_more
794 assert len(res.host_requests) == 1
795 assert res.host_requests[0].latest_message.text.text == "Accepting host request 2"
798def test_ListHostRequests_sort_by_from_date(db, moderator):
799 user1, token1 = generate_user()
800 user2, token2 = generate_user()
801 today_plus_2 = today() + timedelta(days=2)
802 today_plus_3 = today() + timedelta(days=3)
803 today_plus_5 = today() + timedelta(days=5)
804 today_plus_7 = today() + timedelta(days=7)
805 today_plus_10 = today() + timedelta(days=10)
807 with requests_session(token1) as api:
808 hr_late = api.CreateHostRequest(
809 requests_pb2.CreateHostRequestReq(
810 host_user_id=user2.id,
811 from_date=today_plus_7.isoformat(),
812 to_date=today_plus_10.isoformat(),
813 text=valid_request_text("Late request"),
814 )
815 ).host_request_id
817 hr_early = api.CreateHostRequest(
818 requests_pb2.CreateHostRequestReq(
819 host_user_id=user2.id,
820 from_date=today_plus_2.isoformat(),
821 to_date=today_plus_3.isoformat(),
822 text=valid_request_text("Early request"),
823 )
824 ).host_request_id
826 hr_mid = api.CreateHostRequest(
827 requests_pb2.CreateHostRequestReq(
828 host_user_id=user2.id,
829 from_date=today_plus_5.isoformat(),
830 to_date=today_plus_7.isoformat(),
831 text=valid_request_text("Mid request"),
832 )
833 ).host_request_id
835 moderator.approve_host_request(hr_late)
836 moderator.approve_host_request(hr_early)
837 moderator.approve_host_request(hr_mid)
839 with requests_session(token2) as api:
840 # default sort: latest message first (creation order reversed)
841 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
842 assert res.no_more
843 assert [r.host_request_id for r in res.host_requests] == [hr_mid, hr_early, hr_late]
845 # from_date sort: ascending by travel date
846 res = api.ListHostRequests(
847 requests_pb2.ListHostRequestsReq(
848 only_received=True,
849 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
850 )
851 )
852 assert res.no_more
853 assert [r.host_request_id for r in res.host_requests] == [hr_early, hr_mid, hr_late]
856def test_ListHostRequests_sort_by_from_date_pagination(db, moderator):
857 """Pagination cursor correctly handles both different and identical from_dates."""
858 user1, token1 = generate_user()
859 user2, token2 = generate_user()
860 today_plus_2 = today() + timedelta(days=2)
861 today_plus_3 = today() + timedelta(days=3)
862 today_plus_5 = today() + timedelta(days=5)
864 with requests_session(token1) as api:
865 hr_a = api.CreateHostRequest(
866 requests_pb2.CreateHostRequestReq(
867 host_user_id=user2.id,
868 from_date=today_plus_2.isoformat(),
869 to_date=today_plus_3.isoformat(),
870 text=valid_request_text("Request A"),
871 )
872 ).host_request_id
873 backdate_conversations()
875 # Same from_date as A — tiebreaker by conversation_id
876 hr_b = api.CreateHostRequest(
877 requests_pb2.CreateHostRequestReq(
878 host_user_id=user2.id,
879 from_date=today_plus_2.isoformat(),
880 to_date=today_plus_3.isoformat(),
881 text=valid_request_text("Request B"),
882 )
883 ).host_request_id
884 backdate_conversations()
886 hr_c = api.CreateHostRequest(
887 requests_pb2.CreateHostRequestReq(
888 host_user_id=user2.id,
889 from_date=today_plus_5.isoformat(),
890 to_date=(today_plus_5 + timedelta(days=2)).isoformat(),
891 text=valid_request_text("Request C"),
892 )
893 ).host_request_id
895 moderator.approve_host_request(hr_a)
896 moderator.approve_host_request(hr_b)
897 moderator.approve_host_request(hr_c)
899 with requests_session(token2) as api:
900 res = api.ListHostRequests(
901 requests_pb2.ListHostRequestsReq(
902 only_received=True,
903 number=1,
904 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
905 )
906 )
907 assert not res.no_more
908 assert len(res.host_requests) == 1
909 assert res.host_requests[0].host_request_id == hr_a
911 res = api.ListHostRequests(
912 requests_pb2.ListHostRequestsReq(
913 only_received=True,
914 number=1,
915 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
916 page_token=res.next_page_token,
917 )
918 )
919 assert not res.no_more
920 assert len(res.host_requests) == 1
921 assert res.host_requests[0].host_request_id == hr_b
923 res = api.ListHostRequests(
924 requests_pb2.ListHostRequestsReq(
925 only_received=True,
926 number=1,
927 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE,
928 page_token=res.next_page_token,
929 )
930 )
931 assert res.no_more
932 assert len(res.host_requests) == 1
933 assert res.host_requests[0].host_request_id == hr_c
936def test_ListHostRequests_active_filter(db, moderator):
937 user1, token1 = generate_user()
938 user2, token2 = generate_user()
939 today_plus_2 = today() + timedelta(days=2)
940 today_plus_3 = today() + timedelta(days=3)
942 with requests_session(token1) as api:
943 request_id = api.CreateHostRequest(
944 requests_pb2.CreateHostRequestReq(
945 host_user_id=user2.id,
946 from_date=today_plus_2.isoformat(),
947 to_date=today_plus_3.isoformat(),
948 text=valid_request_text("Test request 1"),
949 )
950 ).host_request_id
952 moderator.approve_host_request(request_id)
954 with requests_session(token1) as api:
955 api.RespondHostRequest(
956 requests_pb2.RespondHostRequestReq(
957 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
958 )
959 )
961 with requests_session(token2) as api:
962 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
963 assert len(res.host_requests) == 1
964 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True))
965 assert len(res.host_requests) == 0
968def test_ListHostRequests_active_filter_excludes_past(db, moderator):
969 """only_active must exclude requests whose end date has passed (regression test for <= bug)."""
970 user1, token1 = generate_user()
971 user2, token2 = generate_user()
972 today_plus_2 = today() + timedelta(days=2)
973 today_plus_3 = today() + timedelta(days=3)
975 with requests_session(token1) as api:
976 request_id = api.CreateHostRequest(
977 requests_pb2.CreateHostRequestReq(
978 host_user_id=user2.id,
979 from_date=today_plus_2.isoformat(),
980 to_date=today_plus_3.isoformat(),
981 text=valid_request_text("Past stay regression"),
982 )
983 ).host_request_id
985 moderator.approve_host_request(request_id)
987 with requests_session(token2) as api:
988 api.RespondHostRequest(
989 requests_pb2.RespondHostRequestReq(
990 host_request_id=request_id,
991 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
992 )
993 )
995 # Future request is visible with only_active
996 with requests_session(token2) as api:
997 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True))
998 assert len(res.host_requests) == 1
1000 # Move dates into the past
1001 with session_scope() as session:
1002 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == request_id)).scalar_one()
1003 hr.from_date = today() - timedelta(days=3)
1004 hr.to_date = today() - timedelta(days=2)
1006 # Past request must be excluded by only_active
1007 with requests_session(token2) as api:
1008 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True))
1009 assert len(res.host_requests) == 0
1011 # Still visible without the filter
1012 with requests_session(token2) as api:
1013 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
1014 assert len(res.host_requests) == 1
1017def test_ListHostRequests_status_in_filter(db, moderator):
1018 """status_in must return only requests with the specified statuses."""
1019 user1, token1 = generate_user()
1020 user2, token2 = generate_user()
1021 today_plus_2 = today() + timedelta(days=2)
1022 today_plus_3 = today() + timedelta(days=3)
1023 today_plus_4 = today() + timedelta(days=4)
1024 today_plus_5 = today() + timedelta(days=5)
1026 # Create a pending request
1027 with requests_session(token1) as api:
1028 pending_id = api.CreateHostRequest(
1029 requests_pb2.CreateHostRequestReq(
1030 host_user_id=user2.id,
1031 from_date=today_plus_2.isoformat(),
1032 to_date=today_plus_3.isoformat(),
1033 text=valid_request_text("Pending"),
1034 )
1035 ).host_request_id
1037 moderator.approve_host_request(pending_id)
1039 # Create an accepted request
1040 with requests_session(token1) as api:
1041 accepted_id = api.CreateHostRequest(
1042 requests_pb2.CreateHostRequestReq(
1043 host_user_id=user2.id,
1044 from_date=today_plus_4.isoformat(),
1045 to_date=today_plus_5.isoformat(),
1046 text=valid_request_text("Accepted"),
1047 )
1048 ).host_request_id
1050 moderator.approve_host_request(accepted_id)
1052 with requests_session(token2) as api:
1053 api.RespondHostRequest(
1054 requests_pb2.RespondHostRequestReq(
1055 host_request_id=accepted_id,
1056 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1057 )
1058 )
1060 # Filter to accepted only
1061 with requests_session(token2) as api:
1062 res = api.ListHostRequests(
1063 requests_pb2.ListHostRequestsReq(
1064 status_in=[messages_pb2.HOST_REQUEST_STATUS_ACCEPTED],
1065 )
1066 )
1067 assert len(res.host_requests) == 1
1068 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1070 # Filter to pending only
1071 with requests_session(token2) as api:
1072 res = api.ListHostRequests(
1073 requests_pb2.ListHostRequestsReq(
1074 status_in=[messages_pb2.HOST_REQUEST_STATUS_PENDING],
1075 )
1076 )
1077 assert len(res.host_requests) == 1
1078 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_PENDING
1080 # Filter to accepted + pending — both appear
1081 with requests_session(token2) as api:
1082 res = api.ListHostRequests(
1083 requests_pb2.ListHostRequestsReq(
1084 status_in=[
1085 messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1086 messages_pb2.HOST_REQUEST_STATUS_PENDING,
1087 ],
1088 )
1089 )
1090 assert len(res.host_requests) == 2
1092 # Filter to confirmed — none appear
1093 with requests_session(token2) as api:
1094 res = api.ListHostRequests(
1095 requests_pb2.ListHostRequestsReq(
1096 status_in=[messages_pb2.HOST_REQUEST_STATUS_CONFIRMED],
1097 )
1098 )
1099 assert len(res.host_requests) == 0
1101 # Empty status_in — all requests returned (no filter applied)
1102 with requests_session(token2) as api:
1103 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True))
1104 assert len(res.host_requests) == 2
1107def test_RespondHostRequests(db, moderator):
1108 user1, token1 = generate_user()
1109 user2, token2 = generate_user()
1110 user3, token3 = generate_user()
1111 today_plus_2 = today() + timedelta(days=2)
1112 today_plus_3 = today() + timedelta(days=3)
1114 with requests_session(token1) as api:
1115 request_id = api.CreateHostRequest(
1116 requests_pb2.CreateHostRequestReq(
1117 host_user_id=user2.id,
1118 from_date=today_plus_2.isoformat(),
1119 to_date=today_plus_3.isoformat(),
1120 text=valid_request_text("Test request 1"),
1121 )
1122 ).host_request_id
1124 moderator.approve_host_request(request_id)
1126 # another user can't access
1127 with requests_session(token3) as api:
1128 with pytest.raises(grpc.RpcError) as e:
1129 api.RespondHostRequest(
1130 requests_pb2.RespondHostRequestReq(
1131 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1132 )
1133 )
1134 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1135 assert e.value.details() == "Couldn't find that host request."
1137 with requests_session(token1) as api:
1138 with pytest.raises(grpc.RpcError) as e:
1139 api.RespondHostRequest(
1140 requests_pb2.RespondHostRequestReq(
1141 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1142 )
1143 )
1144 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1145 assert e.value.details() == "You are not the host of this request."
1147 with requests_session(token2) as api:
1148 # non existing id
1149 with pytest.raises(grpc.RpcError) as e:
1150 api.RespondHostRequest(
1151 requests_pb2.RespondHostRequestReq(
1152 host_request_id=9999, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1153 )
1154 )
1155 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1157 # host can't confirm or cancel (host should accept/reject)
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."
1166 with pytest.raises(grpc.RpcError) as e:
1167 api.RespondHostRequest(
1168 requests_pb2.RespondHostRequestReq(
1169 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1170 )
1171 )
1172 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1173 assert e.value.details() == "You can't set the host request status to that."
1175 api.RespondHostRequest(
1176 requests_pb2.RespondHostRequestReq(
1177 host_request_id=request_id,
1178 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED,
1179 text="Test rejection message",
1180 )
1181 )
1182 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=request_id))
1183 assert res.messages[0].text.text == "Test rejection message"
1184 assert res.messages[1].WhichOneof("content") == "host_request_status_changed"
1185 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
1186 # should be able to move from rejected -> accepted
1187 api.RespondHostRequest(
1188 requests_pb2.RespondHostRequestReq(
1189 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1190 )
1191 )
1193 with requests_session(token1) as api:
1194 # can't make pending
1195 with pytest.raises(grpc.RpcError) as e:
1196 api.RespondHostRequest(
1197 requests_pb2.RespondHostRequestReq(
1198 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_PENDING
1199 )
1200 )
1201 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1202 assert e.value.details() == "You can't set the host request status to that."
1204 # can confirm then cancel
1205 api.RespondHostRequest(
1206 requests_pb2.RespondHostRequestReq(
1207 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1208 )
1209 )
1211 api.RespondHostRequest(
1212 requests_pb2.RespondHostRequestReq(
1213 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1214 )
1215 )
1217 # can't confirm after having cancelled
1218 with pytest.raises(grpc.RpcError) as e:
1219 api.RespondHostRequest(
1220 requests_pb2.RespondHostRequestReq(
1221 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1222 )
1223 )
1224 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1225 assert e.value.details() == "You can't set the host request status to that."
1227 # at this point there should be 7 messages
1228 # 2 for creation, 2 for the status change with message, 3 for the other status changed
1229 with requests_session(token1) as api:
1230 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=request_id))
1231 assert len(res.messages) == 7
1232 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1233 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1234 assert res.messages[2].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1235 assert res.messages[4].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
1236 assert res.messages[6].WhichOneof("content") == "chat_created"
1239def test_get_host_request_messages(db, moderator):
1240 user1, token1 = generate_user()
1241 user2, token2 = generate_user()
1242 today_plus_2 = today() + timedelta(days=2)
1243 today_plus_3 = today() + timedelta(days=3)
1244 with requests_session(token1) as api:
1245 res = api.CreateHostRequest(
1246 requests_pb2.CreateHostRequestReq(
1247 host_user_id=user2.id,
1248 from_date=today_plus_2.isoformat(),
1249 to_date=today_plus_3.isoformat(),
1250 text=valid_request_text("Test request 1"),
1251 )
1252 )
1253 conversation_id = res.host_request_id
1255 moderator.approve_host_request(conversation_id)
1257 add_message(db, "Test request 1 message 1", user1.id, conversation_id)
1258 add_message(db, "Test request 1 message 2", user1.id, conversation_id)
1259 add_message(db, "Test request 1 message 3", user1.id, conversation_id)
1261 with requests_session(token2) as api:
1262 api.RespondHostRequest(
1263 requests_pb2.RespondHostRequestReq(
1264 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1265 )
1266 )
1268 add_message(db, "Test request 1 message 4", user2.id, conversation_id)
1269 add_message(db, "Test request 1 message 5", user2.id, conversation_id)
1271 api.RespondHostRequest(
1272 requests_pb2.RespondHostRequestReq(
1273 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED
1274 )
1275 )
1277 with requests_session(token1) as api:
1278 # 9 including initial message
1279 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=conversation_id))
1280 assert len(res.messages) == 9
1281 assert res.no_more
1283 res = api.GetHostRequestMessages(
1284 requests_pb2.GetHostRequestMessagesReq(host_request_id=conversation_id, number=3)
1285 )
1286 assert not res.no_more
1287 assert len(res.messages) == 3
1288 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
1289 assert res.messages[0].WhichOneof("content") == "host_request_status_changed"
1290 assert res.messages[1].text.text == "Test request 1 message 5"
1291 assert res.messages[2].text.text == "Test request 1 message 4"
1293 res = api.GetHostRequestMessages(
1294 requests_pb2.GetHostRequestMessagesReq(
1295 host_request_id=conversation_id,
1296 last_message_id=res.messages[2].message_id,
1297 number=6,
1298 )
1299 )
1300 assert res.no_more
1301 assert len(res.messages) == 6
1302 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1303 assert res.messages[0].WhichOneof("content") == "host_request_status_changed"
1304 assert res.messages[1].text.text == "Test request 1 message 3"
1305 assert res.messages[2].text.text == "Test request 1 message 2"
1306 assert res.messages[3].text.text == "Test request 1 message 1"
1307 assert res.messages[4].text.text == valid_request_text("Test request 1")
1308 assert res.messages[5].WhichOneof("content") == "chat_created"
1311def test_SendHostRequestMessage(db, moderator):
1312 user1, token1 = generate_user()
1313 user2, token2 = generate_user()
1314 user3, token3 = generate_user()
1315 today_plus_2 = today() + timedelta(days=2)
1316 today_plus_3 = today() + timedelta(days=3)
1317 with requests_session(token1) as api:
1318 host_request_id = api.CreateHostRequest(
1319 requests_pb2.CreateHostRequestReq(
1320 host_user_id=user2.id,
1321 from_date=today_plus_2.isoformat(),
1322 to_date=today_plus_3.isoformat(),
1323 text=valid_request_text("Test request 1"),
1324 )
1325 ).host_request_id
1327 moderator.approve_host_request(host_request_id)
1329 with requests_session(token1) as api:
1330 with pytest.raises(grpc.RpcError) as e:
1331 api.SendHostRequestMessage(
1332 requests_pb2.SendHostRequestMessageReq(host_request_id=999, text="Test message 1")
1333 )
1334 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1336 with pytest.raises(grpc.RpcError) as e:
1337 api.SendHostRequestMessage(requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text=""))
1338 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1339 assert e.value.details() == "Invalid message."
1341 api.SendHostRequestMessage(
1342 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1343 )
1344 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id))
1345 assert res.messages[0].text.text == "Test message 1"
1346 assert res.messages[0].author_user_id == user1.id
1348 with requests_session(token3) as api:
1349 # other user can't send
1350 with pytest.raises(grpc.RpcError) as e:
1351 api.SendHostRequestMessage(
1352 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1353 )
1354 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1355 assert e.value.details() == "Couldn't find that host request."
1357 with requests_session(token2) as api:
1358 api.SendHostRequestMessage(
1359 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1360 )
1361 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id))
1362 # including 2 for creation control message and message
1363 assert len(res.messages) == 4
1364 assert res.messages[0].text.text == "Test message 2"
1365 assert res.messages[0].author_user_id == user2.id
1367 # CAN send messages to a rejected, confirmed or cancelled request, and for accepted
1368 api.RespondHostRequest(
1369 requests_pb2.RespondHostRequestReq(
1370 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED
1371 )
1372 )
1373 api.SendHostRequestMessage(
1374 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3")
1375 )
1377 api.RespondHostRequest(
1378 requests_pb2.RespondHostRequestReq(
1379 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
1380 )
1381 )
1383 with requests_session(token1) as api:
1384 api.RespondHostRequest(
1385 requests_pb2.RespondHostRequestReq(
1386 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
1387 )
1388 )
1389 api.SendHostRequestMessage(
1390 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3")
1391 )
1393 api.RespondHostRequest(
1394 requests_pb2.RespondHostRequestReq(
1395 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1396 )
1397 )
1398 api.SendHostRequestMessage(
1399 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3")
1400 )
1403def test_get_updates(db, moderator):
1404 user1, token1 = generate_user()
1405 user2, token2 = generate_user()
1406 user3, token3 = generate_user()
1407 today_plus_2 = today() + timedelta(days=2)
1408 today_plus_3 = today() + timedelta(days=3)
1409 with requests_session(token1) as api:
1410 host_request_id = api.CreateHostRequest(
1411 requests_pb2.CreateHostRequestReq(
1412 host_user_id=user2.id,
1413 from_date=today_plus_2.isoformat(),
1414 to_date=today_plus_3.isoformat(),
1415 text=valid_request_text("Test message 0"),
1416 )
1417 ).host_request_id
1419 moderator.approve_host_request(host_request_id)
1421 with requests_session(token1) as api:
1422 api.SendHostRequestMessage(
1423 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1424 )
1425 api.SendHostRequestMessage(
1426 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1427 )
1428 api.RespondHostRequest(
1429 requests_pb2.RespondHostRequestReq(
1430 host_request_id=host_request_id,
1431 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED,
1432 text="Test message 3",
1433 )
1434 )
1436 backdate_conversations()
1437 api.CreateHostRequest(
1438 requests_pb2.CreateHostRequestReq(
1439 host_user_id=user2.id,
1440 from_date=today_plus_2.isoformat(),
1441 to_date=today_plus_3.isoformat(),
1442 text=valid_request_text("Test message 4"),
1443 )
1444 )
1446 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id))
1447 assert len(res.messages) == 6
1448 assert res.messages[0].text.text == "Test message 3"
1449 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1450 assert res.messages[2].text.text == "Test message 2"
1451 assert res.messages[3].text.text == "Test message 1"
1452 assert res.messages[4].text.text == valid_request_text("Test message 0")
1453 message_id_3 = res.messages[0].message_id
1454 message_id_cancel = res.messages[1].message_id
1455 message_id_2 = res.messages[2].message_id
1456 message_id_1 = res.messages[3].message_id
1457 message_id_0 = res.messages[4].message_id
1459 with pytest.raises(grpc.RpcError) as e:
1460 api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=0))
1461 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1463 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1))
1464 assert res.no_more
1465 assert len(res.updates) == 5
1466 assert res.updates[0].message.text.text == "Test message 2"
1467 assert res.updates[1].message.host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1468 assert res.updates[1].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1469 assert res.updates[2].message.text.text == "Test message 3"
1470 assert res.updates[3].message.WhichOneof("content") == "chat_created"
1471 assert res.updates[3].status == messages_pb2.HOST_REQUEST_STATUS_PENDING
1472 assert res.updates[4].message.text.text == valid_request_text("Test message 4")
1474 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1, number=1))
1475 assert not res.no_more
1476 assert len(res.updates) == 1
1477 assert res.updates[0].message.text.text == "Test message 2"
1478 assert res.updates[0].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1480 with requests_session(token3) as api:
1481 # other user can't access
1482 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1))
1483 assert len(res.updates) == 0
1486def test_archive_host_request(db, moderator):
1487 user1, token1 = generate_user()
1488 user2, token2 = generate_user()
1490 today_plus_2 = today() + timedelta(days=2)
1491 today_plus_3 = today() + timedelta(days=3)
1493 with requests_session(token1) as api:
1494 host_request_id = api.CreateHostRequest(
1495 requests_pb2.CreateHostRequestReq(
1496 host_user_id=user2.id,
1497 from_date=today_plus_2.isoformat(),
1498 to_date=today_plus_3.isoformat(),
1499 text=valid_request_text("Test message 0"),
1500 )
1501 ).host_request_id
1503 api.SendHostRequestMessage(
1504 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1505 )
1506 api.SendHostRequestMessage(
1507 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1508 )
1510 moderator.approve_host_request(host_request_id)
1512 # happy path archiving host request
1513 with requests_session(token1) as api:
1514 api.RespondHostRequest(
1515 requests_pb2.RespondHostRequestReq(
1516 host_request_id=host_request_id,
1517 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED,
1518 text="Test message 3",
1519 )
1520 )
1521 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True))
1522 assert len(res.host_requests) == 1
1523 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED
1525 # Verify is_archived is False before archiving
1526 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1527 assert not res.is_archived
1529 api.SetHostRequestArchiveStatus(
1530 requests_pb2.SetHostRequestArchiveStatusReq(host_request_id=host_request_id, is_archived=True)
1531 )
1532 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_archived=True))
1533 assert len(res.host_requests) == 1
1535 # Verify is_archived is True after archiving
1536 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1537 assert res.is_archived
1540def test_host_request_unseen_message_count(db, moderator):
1541 user1, token1 = generate_user()
1542 user2, token2 = generate_user()
1544 with requests_session(token1) as api:
1545 host_request_id = api.CreateHostRequest(
1546 requests_pb2.CreateHostRequestReq(
1547 host_user_id=user2.id,
1548 from_date=(today() + timedelta(days=2)).isoformat(),
1549 to_date=(today() + timedelta(days=3)).isoformat(),
1550 text=valid_request_text(),
1551 )
1552 ).host_request_id
1553 moderator.approve_host_request(host_request_id)
1555 with requests_session(token1) as api:
1556 api.SendHostRequestMessage(
1557 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="and one more thing")
1558 )
1559 # sending marks your own messages seen
1560 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
1561 assert res.unseen_message_count == 0
1563 with requests_session(token2) as api:
1564 listed = api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0]
1565 # the request's control message, its text, and the follow-up
1566 assert listed.unseen_message_count == 3
1567 assert (
1568 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)).unseen_message_count
1569 == 3
1570 )
1572 api.MarkLastSeenHostRequest(
1573 requests_pb2.MarkLastSeenHostRequestReq(
1574 host_request_id=host_request_id, last_seen_message_id=listed.latest_message.message_id
1575 )
1576 )
1577 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].unseen_message_count == 0
1578 assert (
1579 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)).unseen_message_count
1580 == 0
1581 )
1584def test_mark_last_seen(db, moderator):
1585 user1, token1 = generate_user()
1586 user2, token2 = generate_user()
1587 user3, token3 = generate_user()
1588 today_plus_2 = today() + timedelta(days=2)
1589 today_plus_3 = today() + timedelta(days=3)
1590 with requests_session(token1) as api:
1591 host_request_id = api.CreateHostRequest(
1592 requests_pb2.CreateHostRequestReq(
1593 host_user_id=user2.id,
1594 from_date=today_plus_2.isoformat(),
1595 to_date=today_plus_3.isoformat(),
1596 text=valid_request_text("Test message 0"),
1597 )
1598 ).host_request_id
1599 backdate_conversations()
1601 host_request_id_2 = api.CreateHostRequest(
1602 requests_pb2.CreateHostRequestReq(
1603 host_user_id=user2.id,
1604 from_date=today_plus_2.isoformat(),
1605 to_date=today_plus_3.isoformat(),
1606 text=valid_request_text("Test message 0a"),
1607 )
1608 ).host_request_id
1610 moderator.approve_host_request(host_request_id)
1611 moderator.approve_host_request(host_request_id_2)
1613 with requests_session(token1) as api:
1614 api.SendHostRequestMessage(
1615 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1")
1616 )
1617 api.SendHostRequestMessage(
1618 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2")
1619 )
1620 api.RespondHostRequest(
1621 requests_pb2.RespondHostRequestReq(
1622 host_request_id=host_request_id,
1623 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED,
1624 text="Test message 3",
1625 )
1626 )
1628 moderator.approve_host_request(host_request_id)
1629 moderator.approve_host_request(host_request_id_2)
1631 # test Ping unseen host request count, should be automarked after sending
1632 with api_session(token1) as api:
1633 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 0
1634 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1636 with api_session(token2) as api:
1637 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 2
1638 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1640 with requests_session(token2) as api:
1641 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].last_seen_message_id == 0
1643 api.MarkLastSeenHostRequest(
1644 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=3)
1645 )
1647 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].last_seen_message_id == 3
1649 with pytest.raises(grpc.RpcError) as e:
1650 api.MarkLastSeenHostRequest(
1651 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=1)
1652 )
1653 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1654 assert e.value.details() == "You can't unsee messages."
1656 # this will be used to test sent request notifications
1657 host_request_id_3 = api.CreateHostRequest(
1658 requests_pb2.CreateHostRequestReq(
1659 host_user_id=user1.id,
1660 from_date=today_plus_2.isoformat(),
1661 to_date=today_plus_3.isoformat(),
1662 text=valid_request_text("Another test request"),
1663 )
1664 ).host_request_id
1666 moderator.approve_host_request(host_request_id_3)
1668 with requests_session(token2) as api:
1669 # this should make id_2 all read
1670 api.SendHostRequestMessage(
1671 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id_2, text="Test")
1672 )
1674 with api_session(token2) as api:
1675 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 1
1676 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0
1678 # make sure sent and received count for unseen notifications
1679 with requests_session(token1) as api:
1680 api.SendHostRequestMessage(
1681 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id_3, text="Test message")
1682 )
1684 with api_session(token2) as api:
1685 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 1
1686 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 1
1689def _create_host_request_via_api(surfer_token: str, host_id: int, moderator, public_trip_id: int | None = None) -> int:
1690 with requests_session(surfer_token) as api:
1691 res = api.CreateHostRequest(
1692 requests_pb2.CreateHostRequestReq(
1693 host_user_id=host_id,
1694 from_date=(today() + timedelta(days=5)).isoformat(),
1695 to_date=(today() + timedelta(days=10)).isoformat(),
1696 text=valid_request_text(),
1697 public_trip_id=public_trip_id,
1698 )
1699 )
1700 moderator.approve_host_request(res.host_request_id)
1701 return int(res.host_request_id)
1704def test_ping_role_based_counts_match_direction_without_offers(db, moderator):
1705 user1, token1 = generate_user()
1706 user2, token2 = generate_user()
1708 # user2 surfs with user1 -> user1 is the host of the stay
1709 _create_host_request_via_api(token2, user1.id, moderator)
1711 with api_session(token1) as api:
1712 res = api.Ping(api_pb2.PingReq())
1713 # with no public-trip offers, role-based counts equal the direction-based ones
1714 assert res.unseen_received_host_request_count == 1
1715 assert res.unseen_hosting_host_request_count == 1
1716 assert res.unseen_sent_host_request_count == 0
1717 assert res.unseen_surfing_host_request_count == 0
1718 assert res.unseen_public_trip_offer_count == 0
1721def test_ping_counts_public_trip_offer_by_role(db, moderator):
1722 traveler, traveler_token = generate_user()
1723 host, host_token = generate_user()
1724 node_id = _make_node()
1725 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1727 request_id = _create_host_request_via_api(host_token, traveler.id, moderator, public_trip_id=trip_id)
1729 # the traveller (recipient) has the offer's create message unseen: counts as surfing + public-trip offer
1730 with api_session(traveler_token) as api:
1731 res = api.Ping(api_pb2.PingReq())
1732 assert res.unseen_surfing_host_request_count == 1
1733 assert res.unseen_public_trip_offer_count == 1
1734 assert res.unseen_hosting_host_request_count == 0
1736 # the traveller replies, so now the offering host has an unseen message under hosting
1737 with requests_session(traveler_token) as api:
1738 api.SendHostRequestMessage(
1739 requests_pb2.SendHostRequestMessageReq(host_request_id=request_id, text="thanks for the offer")
1740 )
1742 with api_session(host_token) as api:
1743 res = api.Ping(api_pb2.PingReq())
1744 assert res.unseen_hosting_host_request_count == 1
1745 assert res.unseen_surfing_host_request_count == 0
1746 assert res.unseen_public_trip_offer_count == 0
1749def test_ping_public_trip_offer_count_gated_by_flag(db, moderator, feature_flags):
1750 feature_flags.set("public_trips_enabled", False)
1752 traveler, traveler_token = generate_user()
1753 host, host_token = generate_user()
1754 node_id = _make_node()
1755 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
1757 _create_host_request_via_api(host_token, traveler.id, moderator, public_trip_id=trip_id)
1759 with api_session(traveler_token) as api:
1760 res = api.Ping(api_pb2.PingReq())
1761 # the dedicated offer count is gated off...
1762 assert res.unseen_public_trip_offer_count == 0
1763 # ...but the offer is a real conversation and still surfaces under surfing
1764 assert res.unseen_surfing_host_request_count == 1
1767def test_mark_last_seen_clears_notifications(db, moderator):
1768 user1, token1 = generate_user()
1769 user2, token2 = generate_user()
1770 today_plus_2 = today() + timedelta(days=2)
1771 today_plus_3 = today() + timedelta(days=3)
1773 with requests_session(token1) as api:
1774 host_request_id = api.CreateHostRequest(
1775 requests_pb2.CreateHostRequestReq(
1776 host_user_id=user2.id,
1777 from_date=today_plus_2.isoformat(),
1778 to_date=today_plus_3.isoformat(),
1779 text=valid_request_text("Test message"),
1780 )
1781 ).host_request_id
1783 moderator.approve_host_request(host_request_id)
1785 def unseen_notification_count(user_id):
1786 with session_scope() as session:
1787 return session.execute(
1788 select(func.count())
1789 .select_from(Notification)
1790 .where(Notification.user_id == user_id)
1791 .where(Notification.key == str(host_request_id))
1792 .where(Notification.is_seen == False)
1793 ).scalar_one()
1795 assert unseen_notification_count(user2.id) > 0
1797 with requests_session(token2) as api:
1798 api.MarkLastSeenHostRequest(
1799 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=1)
1800 )
1802 assert unseen_notification_count(user2.id) == 0
1805def test_response_rate(db, moderator):
1806 user1, token1 = generate_user()
1807 user2, token2 = generate_user()
1808 user3, token3 = generate_user(delete_user=True)
1810 today_plus_2 = today() + timedelta(days=2)
1811 today_plus_3 = today() + timedelta(days=3)
1813 with session_scope() as session:
1814 refresh_materialized_view(session, "user_response_rates")
1816 with requests_session(token1) as api:
1817 # deleted: not found
1818 with pytest.raises(grpc.RpcError) as e:
1819 api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user3.id))
1820 assert e.value.code() == grpc.StatusCode.NOT_FOUND
1821 assert e.value.details() == "Couldn't find that user."
1823 # no requests: insufficient
1824 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1825 assert res.HasField("insufficient_data")
1827 # send a request and back date it by 36 hours
1828 host_request_1 = api.CreateHostRequest(
1829 requests_pb2.CreateHostRequestReq(
1830 host_user_id=user2.id,
1831 from_date=today_plus_2.isoformat(),
1832 to_date=today_plus_3.isoformat(),
1833 text=valid_request_text("Test request"),
1834 )
1835 ).host_request_id
1836 moderator.approve_host_request(host_request_1)
1837 backdate_conversations()
1838 with session_scope() as session:
1839 session.execute(
1840 select(Message)
1841 .where(Message.conversation_id == host_request_1)
1842 .where(Message.message_type == MessageType.chat_created)
1843 ).scalar_one().time = now() - timedelta(hours=36)
1844 refresh_materialized_view(session, "user_response_rates")
1846 # still insufficient
1847 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1848 assert res.HasField("insufficient_data")
1850 # send a request and back date it by 35 hours
1851 host_request_2 = api.CreateHostRequest(
1852 requests_pb2.CreateHostRequestReq(
1853 host_user_id=user2.id,
1854 from_date=today_plus_2.isoformat(),
1855 to_date=today_plus_3.isoformat(),
1856 text=valid_request_text("Test request"),
1857 )
1858 ).host_request_id
1859 moderator.approve_host_request(host_request_2)
1860 backdate_conversations()
1861 with session_scope() as session:
1862 session.execute(
1863 select(Message)
1864 .where(Message.conversation_id == host_request_2)
1865 .where(Message.message_type == MessageType.chat_created)
1866 ).scalar_one().time = now() - timedelta(hours=35)
1867 refresh_materialized_view(session, "user_response_rates")
1869 # still insufficient
1870 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1871 assert res.HasField("insufficient_data")
1873 # send a request and back date it by 34 hours
1874 host_request_3 = api.CreateHostRequest(
1875 requests_pb2.CreateHostRequestReq(
1876 host_user_id=user2.id,
1877 from_date=today_plus_2.isoformat(),
1878 to_date=today_plus_3.isoformat(),
1879 text=valid_request_text("Test request"),
1880 )
1881 ).host_request_id
1882 moderator.approve_host_request(host_request_3)
1883 backdate_conversations()
1884 with session_scope() as session:
1885 session.execute(
1886 select(Message)
1887 .where(Message.conversation_id == host_request_3)
1888 .where(Message.message_type == MessageType.chat_created)
1889 ).scalar_one().time = now() - timedelta(hours=34)
1890 refresh_materialized_view(session, "user_response_rates")
1892 # now low
1893 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1894 assert res.HasField("low")
1896 with requests_session(token2) as api:
1897 # accept a host req
1898 api.RespondHostRequest(
1899 requests_pb2.RespondHostRequestReq(
1900 host_request_id=host_request_2,
1901 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1902 text="Accepting host request",
1903 )
1904 )
1906 with session_scope() as session:
1907 refresh_materialized_view(session, "user_response_rates")
1909 with requests_session(token1) as api:
1910 # now some w p33 = 35h
1911 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1912 assert res.HasField("some")
1913 assert res.some.response_time_p33.ToTimedelta() == timedelta(hours=35)
1915 with requests_session(token2) as api:
1916 # accept another host req
1917 api.RespondHostRequest(
1918 requests_pb2.RespondHostRequestReq(
1919 host_request_id=host_request_3,
1920 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1921 text="Accepting host request",
1922 )
1923 )
1925 with session_scope() as session:
1926 refresh_materialized_view(session, "user_response_rates")
1928 with requests_session(token1) as api:
1929 # now most w p33 = 34h, p66 = 35h
1930 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1931 assert res.HasField("most")
1932 assert res.most.response_time_p33.ToTimedelta() == timedelta(hours=34)
1933 assert res.most.response_time_p66.ToTimedelta() == timedelta(hours=35)
1935 with requests_session(token2) as api:
1936 # accept last host req
1937 api.RespondHostRequest(
1938 requests_pb2.RespondHostRequestReq(
1939 host_request_id=host_request_1,
1940 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
1941 text="Accepting host request",
1942 )
1943 )
1945 with session_scope() as session:
1946 refresh_materialized_view(session, "user_response_rates")
1948 with requests_session(token1) as api:
1949 # now all w p33 = 34h, p66 = 35h
1950 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1951 assert res.HasField("almost_all")
1952 assert res.almost_all.response_time_p33.ToTimedelta() == timedelta(hours=34)
1953 assert res.almost_all.response_time_p66.ToTimedelta() == timedelta(hours=35)
1955 # send a request and back date it by 2 hours
1956 host_request_4 = api.CreateHostRequest(
1957 requests_pb2.CreateHostRequestReq(
1958 host_user_id=user2.id,
1959 from_date=today_plus_2.isoformat(),
1960 to_date=today_plus_3.isoformat(),
1961 text=valid_request_text("Test request"),
1962 )
1963 ).host_request_id
1964 moderator.approve_host_request(host_request_4)
1965 backdate_conversations()
1966 with session_scope() as session:
1967 session.execute(
1968 select(Message)
1969 .where(Message.conversation_id == host_request_4)
1970 .where(Message.message_type == MessageType.chat_created)
1971 ).scalar_one().time = now() - timedelta(hours=2)
1972 refresh_materialized_view(session, "user_response_rates")
1974 # send a request and back date it by 4 hours
1975 host_request_5 = api.CreateHostRequest(
1976 requests_pb2.CreateHostRequestReq(
1977 host_user_id=user2.id,
1978 from_date=today_plus_2.isoformat(),
1979 to_date=today_plus_3.isoformat(),
1980 text=valid_request_text("Test request"),
1981 )
1982 ).host_request_id
1983 moderator.approve_host_request(host_request_5)
1984 backdate_conversations()
1985 with session_scope() as session:
1986 session.execute(
1987 select(Message)
1988 .where(Message.conversation_id == host_request_5)
1989 .where(Message.message_type == MessageType.chat_created)
1990 ).scalar_one().time = now() - timedelta(hours=4)
1991 refresh_materialized_view(session, "user_response_rates")
1993 # now some w p33 = 35h
1994 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
1995 assert res.HasField("some")
1996 assert res.some.response_time_p33.ToTimedelta() == timedelta(hours=35)
1998 with requests_session(token2) as api:
1999 # accept host req
2000 api.RespondHostRequest(
2001 requests_pb2.RespondHostRequestReq(
2002 host_request_id=host_request_5,
2003 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
2004 text="Accepting host request",
2005 )
2006 )
2008 with session_scope() as session:
2009 refresh_materialized_view(session, "user_response_rates")
2011 with requests_session(token1) as api:
2012 # now most w p33 = 34h, p66 = 36h
2013 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
2014 assert res.HasField("most")
2015 assert res.most.response_time_p33.ToTimedelta() == timedelta(hours=34)
2016 assert res.most.response_time_p66.ToTimedelta() == timedelta(hours=36)
2018 with requests_session(token2) as api:
2019 # accept host req
2020 api.RespondHostRequest(
2021 requests_pb2.RespondHostRequestReq(
2022 host_request_id=host_request_4,
2023 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
2024 text="Accepting host request",
2025 )
2026 )
2028 with session_scope() as session:
2029 refresh_materialized_view(session, "user_response_rates")
2031 with requests_session(token1) as api:
2032 # now most w p33 = 4h, p66 = 35h
2033 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id))
2034 assert res.HasField("almost_all")
2035 assert res.almost_all.response_time_p33.ToTimedelta() == timedelta(hours=4)
2036 assert res.almost_all.response_time_p66.ToTimedelta() == timedelta(hours=35)
2039def test_response_rate_ignores_public_trip_offers(db, moderator):
2040 traveler, _ = generate_user()
2041 _, surfer_token = generate_user()
2042 node_id = _make_node()
2043 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10))
2045 # the traveller leaves every offer on their trip unanswered
2046 for _ in range(3):
2047 _, host_token = generate_user()
2048 _create_host_request_via_api(host_token, traveler.id, moderator, public_trip_id=trip_id)
2050 # a regular request to the traveller as a host still counts
2051 _create_host_request_via_api(surfer_token, traveler.id, moderator)
2053 with session_scope() as session:
2054 refresh_materialized_view(session, "user_response_rates")
2055 response_rate = session.execute(
2056 select(UserResponseRate).where(UserResponseRate.user_id == traveler.id)
2057 ).scalar_one()
2058 assert response_rate.requests == 1
2059 assert response_rate.response_rate == 0
2062def test_request_notifications(db, email_collector: EmailCollector, push_collector: PushCollector, moderator):
2063 host, host_token = generate_user(complete_profile=True)
2064 surfer, surfer_token = generate_user(complete_profile=True)
2066 host_loc_context = LocalizationContext.from_user(host)
2067 surfer_loc_context = LocalizationContext.from_user(surfer)
2069 today_plus_2 = today() + timedelta(days=2)
2070 today_plus_3 = today() + timedelta(days=3)
2072 with requests_session(surfer_token) as api:
2073 hr_id = api.CreateHostRequest(
2074 requests_pb2.CreateHostRequestReq(
2075 host_user_id=host.id,
2076 from_date=today_plus_2.isoformat(),
2077 to_date=today_plus_3.isoformat(),
2078 text=valid_request_text("can i stay plz"),
2079 )
2080 ).host_request_id
2082 moderator.approve_host_request(hr_id)
2084 email = email_collector.pop_for_recipient(host.email, last=True)
2085 assert email.recipient == host.email
2086 assert "host request" in email.subject.lower()
2087 assert host.name in email.plain
2088 assert host.name in email.html
2089 assert "quick decline" in email.plain.lower(), email.plain
2090 assert "quick decline" in email.html.lower()
2091 assert surfer.name in email.plain
2092 assert surfer.name in email.html
2093 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
2094 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.html
2095 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
2096 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.html
2097 assert "http://localhost:5001/img/thumbnail/" not in email.plain
2098 assert "http://localhost:5001/img/thumbnail/" in email.html
2099 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
2100 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
2101 assert not email.attachments
2103 assert push_collector.pop_for_user(host.id, last=True).content.title == f"New host request from {surfer.name}"
2105 with requests_session(host_token) as api:
2106 api.RespondHostRequest(
2107 requests_pb2.RespondHostRequestReq(
2108 host_request_id=hr_id,
2109 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED,
2110 text="Accepting host request",
2111 )
2112 )
2114 email = email_collector.pop_for_recipient(surfer.email, last=True)
2115 assert email.recipient == surfer.email
2116 assert "host request" in email.subject.lower()
2117 assert host.name in email.plain
2118 assert host.name in email.html
2119 assert surfer.name in email.plain
2120 assert surfer.name in email.html
2121 assert surfer_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
2122 assert surfer_loc_context.localize_date(today_plus_2, with_year=False) in email.html
2123 assert surfer_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
2124 assert surfer_loc_context.localize_date(today_plus_3, with_year=False) in email.html
2125 assert "http://localhost:5001/img/thumbnail/" not in email.plain
2126 assert "http://localhost:5001/img/thumbnail/" in email.html
2127 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
2128 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
2129 assert len(email.attachments or []) == 1
2131 assert push_collector.pop_for_user(surfer.id, last=True).content.title == f"{host.name} accepted your host request"
2134def test_quick_decline(db, email_collector: EmailCollector, push_collector: PushCollector, moderator):
2135 host, host_token = generate_user(complete_profile=True)
2136 surfer, surfer_token = generate_user(complete_profile=True)
2138 host_loc_context = LocalizationContext.from_user(host)
2140 today_plus_2 = today() + timedelta(days=2)
2141 today_plus_3 = today() + timedelta(days=3)
2143 with requests_session(surfer_token) as api:
2144 hr_id = api.CreateHostRequest(
2145 requests_pb2.CreateHostRequestReq(
2146 host_user_id=host.id,
2147 from_date=today_plus_2.isoformat(),
2148 to_date=today_plus_3.isoformat(),
2149 text=valid_request_text("can i stay plz"),
2150 )
2151 ).host_request_id
2153 moderator.approve_host_request(hr_id)
2155 email = email_collector.pop_for_recipient(host.email, last=True)
2156 assert email.recipient == host.email
2157 assert "host request" in email.subject.lower()
2158 assert host.name in email.plain
2159 assert host.name in email.html
2160 assert "quick decline" in email.plain.lower(), email.plain
2161 assert "quick decline" in email.html.lower()
2162 assert surfer.name in email.plain
2163 assert surfer.name in email.html
2164 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.plain
2165 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.html
2166 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.plain
2167 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.html
2168 assert "http://localhost:5001/img/thumbnail/" not in email.plain
2169 assert "http://localhost:5001/img/thumbnail/" in email.html
2170 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain
2171 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html
2173 assert push_collector.pop_for_user(host.id, last=True).content.title == f"New host request from {surfer.name}"
2175 # very ugly
2176 # http://localhost:3000/quick-link?payload=CAEiGAoOZnJpZW5kX3JlcXVlc3QSBmFjY2VwdA==&sig=BQdk024NTATm8zlR0krSXTBhP5U9TlFv7VhJeIHZtUg=
2177 for link in re.findall(r'<a href="(.*?)"', email.html): 2177 ↛ 2196line 2177 didn't jump to line 2196 because the loop on line 2177 didn't complete
2178 if "payload" not in link:
2179 continue
2180 print(link)
2181 url_parts = urlparse(html.unescape(link))
2182 params = parse_qs(url_parts.query)
2183 print(params["payload"][0])
2184 payload = unsubscribe_pb2.UnsubscribePayload.FromString(b64decode(params["payload"][0]))
2185 if payload.HasField("host_request_quick_decline"): 2185 ↛ 2177line 2185 didn't jump to line 2177 because the condition on line 2185 was always true
2186 with auth_api_session() as (auth_api, metadata_interceptor):
2187 res = auth_api.Unsubscribe(
2188 auth_pb2.UnsubscribeReq(
2189 payload=b64decode(params["payload"][0]),
2190 sig=b64decode(params["sig"][0]),
2191 )
2192 )
2193 assert res.response == "Thank you for responding to the host request!"
2194 break
2195 else:
2196 raise Exception("Didn't find link")
2198 with requests_session(surfer_token) as api:
2199 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
2200 assert res.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED
2203def test_host_req_feedback(db, moderator):
2204 host, host_token = generate_user(complete_profile=True)
2205 host2, host2_token = generate_user(complete_profile=True)
2206 host3, host3_token = generate_user(complete_profile=True)
2207 surfer, surfer_token = generate_user(complete_profile=True)
2209 today_plus_2 = today() + timedelta(days=2)
2210 today_plus_3 = today() + timedelta(days=3)
2212 with requests_session(surfer_token) as api:
2213 hr_id = api.CreateHostRequest(
2214 requests_pb2.CreateHostRequestReq(
2215 host_user_id=host.id,
2216 from_date=today_plus_2.isoformat(),
2217 to_date=today_plus_3.isoformat(),
2218 text=valid_request_text("can i stay plz"),
2219 )
2220 ).host_request_id
2221 hr2_id = api.CreateHostRequest(
2222 requests_pb2.CreateHostRequestReq(
2223 host_user_id=host2.id,
2224 from_date=today_plus_2.isoformat(),
2225 to_date=today_plus_3.isoformat(),
2226 text=valid_request_text("can i stay plz"),
2227 )
2228 ).host_request_id
2229 hr3_id = api.CreateHostRequest(
2230 requests_pb2.CreateHostRequestReq(
2231 host_user_id=host3.id,
2232 from_date=today_plus_2.isoformat(),
2233 to_date=today_plus_3.isoformat(),
2234 text=valid_request_text("can i stay plz"),
2235 )
2236 ).host_request_id
2238 moderator.approve_host_request(hr_id)
2239 moderator.approve_host_request(hr2_id)
2240 moderator.approve_host_request(hr3_id)
2242 with requests_session(host_token) as api:
2243 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
2244 assert not res.need_host_request_feedback
2246 api.RespondHostRequest(
2247 requests_pb2.RespondHostRequestReq(
2248 host_request_id=hr_id,
2249 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED,
2250 )
2251 )
2253 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
2254 assert res.need_host_request_feedback
2256 # surfer can't leave feedback
2257 with requests_session(surfer_token) as api:
2258 with pytest.raises(grpc.RpcError) as e:
2259 api.SendHostRequestFeedback(
2260 requests_pb2.SendHostRequestFeedbackReq(
2261 host_request_id=hr_id,
2262 )
2263 )
2264 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2265 assert e.value.details() == "Couldn't find that host request."
2267 with requests_session(host_token) as api:
2268 api.SendHostRequestFeedback(
2269 requests_pb2.SendHostRequestFeedbackReq(
2270 host_request_id=hr_id,
2271 host_request_quality=requests_pb2.HOST_REQUEST_QUALITY_LOW,
2272 )
2273 )
2274 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
2275 assert not res.need_host_request_feedback
2277 # can't leave it twice
2278 with requests_session(host_token) as api:
2279 with pytest.raises(grpc.RpcError) as e:
2280 api.SendHostRequestFeedback(
2281 requests_pb2.SendHostRequestFeedbackReq(
2282 host_request_id=hr_id,
2283 )
2284 )
2285 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2286 assert e.value.details() == "You have already left feedback for this host request!"
2288 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id))
2289 assert not res.need_host_request_feedback
2291 with requests_session(host2_token) as api:
2292 api.RespondHostRequest(
2293 requests_pb2.RespondHostRequestReq(host_request_id=hr2_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED)
2294 )
2295 # can't leave feedback on the wrong one
2296 with pytest.raises(grpc.RpcError) as e:
2297 api.SendHostRequestFeedback(
2298 requests_pb2.SendHostRequestFeedbackReq(
2299 host_request_id=hr_id,
2300 )
2301 )
2302 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2303 assert e.value.details() == "Couldn't find that host request."
2305 # null feedback is still feedback
2306 api.SendHostRequestFeedback(requests_pb2.SendHostRequestFeedbackReq(host_request_id=hr2_id))
2308 with requests_session(host3_token) as api:
2309 api.RespondHostRequest(
2310 requests_pb2.RespondHostRequestReq(host_request_id=hr3_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED)
2311 )
2313 api.SendHostRequestFeedback(
2314 requests_pb2.SendHostRequestFeedbackReq(host_request_id=hr3_id, decline_reason="bad req")
2315 )
2318def _make_trip_node_admin(user_id: int, trip_id: int):
2319 with session_scope() as session:
2320 node_id = session.execute(select(PublicTrip.node_id).where(PublicTrip.id == trip_id)).scalar_one()
2321 cluster = session.execute(
2322 select(Cluster).where(Cluster.parent_node_id == node_id).where(Cluster.is_official_cluster)
2323 ).scalar_one_or_none()
2324 if cluster is None: 2324 ↛ 2333line 2324 didn't jump to line 2333 because the condition on line 2324 was always true
2325 cluster = Cluster(
2326 name="Test community",
2327 description="Test",
2328 parent_node_id=node_id,
2329 is_official_cluster=True,
2330 )
2331 session.add(cluster)
2332 session.flush()
2333 session.add(ClusterSubscription(cluster_id=cluster.id, user_id=user_id, role=ClusterRole.admin))
2336def _create_public_trip(
2337 user_id: int,
2338 from_date,
2339 to_date,
2340 *,
2341 status=None,
2342 same_gender_only: bool = False,
2343 visibility: ModerationVisibility = ModerationVisibility.visible,
2344):
2345 with session_scope() as session:
2346 node = session.execute(select(Node).limit(1)).scalar_one_or_none()
2347 if node is None:
2348 node = Node(
2349 geom=to_multi(create_polygon_lat_lng([[0, 0], [0, 2], [2, 2], [2, 0], [0, 0]])),
2350 node_type=NodeType.locality,
2351 )
2352 session.add(node)
2353 session.flush()
2354 moderation_state = ModerationState(
2355 object_type=ModerationObjectType.public_trip,
2356 object_id=0, # placeholder, set after PublicTrip flush
2357 visibility=visibility,
2358 )
2359 session.add(moderation_state)
2360 session.flush()
2361 trip = PublicTrip(
2362 user_id=user_id,
2363 node_id=node.id,
2364 from_date=from_date,
2365 to_date=to_date,
2366 description="Looking for a host!",
2367 status=status or PublicTripStatus.searching_for_host,
2368 same_gender_only=same_gender_only,
2369 moderation_state_id=moderation_state.id,
2370 )
2371 session.add(trip)
2372 session.flush()
2373 moderation_state.object_id = trip.id
2374 return trip.id
2377def test_create_request_with_public_trip(db, moderator):
2378 """Hosts can offer to host a public trip; offered dates must be within trip dates."""
2379 surfer, surfer_token = generate_user()
2380 host, host_token = generate_user()
2382 trip_from = today() + timedelta(days=10)
2383 trip_to = today() + timedelta(days=20)
2384 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2386 with requests_session(host_token) as api:
2387 # Happy path: dates within trip window
2388 res = api.CreateHostRequest(
2389 requests_pb2.CreateHostRequestReq(
2390 host_user_id=surfer.id,
2391 from_date=(trip_from + timedelta(days=1)).isoformat(),
2392 to_date=(trip_to - timedelta(days=1)).isoformat(),
2393 text=valid_request_text(),
2394 public_trip_id=trip_id,
2395 )
2396 )
2397 host_request_id = res.host_request_id
2399 moderator.approve_host_request(host_request_id)
2401 with requests_session(host_token) as api:
2402 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2403 assert hr.public_trip_id == trip_id
2406def test_public_trip_offer_min_length(db):
2407 """Offers on public trips have a shorter minimum length than regular host requests."""
2408 surfer, _ = generate_user()
2409 host, host_token = generate_user()
2411 trip_from = today() + timedelta(days=10)
2412 trip_to = today() + timedelta(days=20)
2413 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2415 def offer(text: str) -> None:
2416 with requests_session(host_token) as api:
2417 api.CreateHostRequest(
2418 requests_pb2.CreateHostRequestReq(
2419 host_user_id=surfer.id,
2420 from_date=trip_from.isoformat(),
2421 to_date=trip_to.isoformat(),
2422 text=text,
2423 public_trip_id=trip_id,
2424 )
2425 )
2427 with pytest.raises(grpc.RpcError) as e:
2428 offer("a" * (PUBLIC_TRIP_OFFER_MIN_LENGTH_UTF16 - 1))
2429 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2430 assert e.value.details() == "Your offer to host cannot be shorter than 100 characters."
2432 # Shorter than a regular host request, but long enough for an offer
2433 offer("a" * PUBLIC_TRIP_OFFER_MIN_LENGTH_UTF16)
2436def test_create_request_with_public_trip_hosting_snapshot(db, moderator):
2437 """An offer snapshots the offering host's place, not the traveller's."""
2438 surfer, _ = generate_user(city="Surfer city", geom=create_coordinate(1, 1), geom_radius=11)
2439 host, host_token = generate_user(city="Host city", geom=create_coordinate(2, 2), geom_radius=22)
2441 trip_from = today() + timedelta(days=10)
2442 trip_to = today() + timedelta(days=20)
2443 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2445 with requests_session(host_token) as api:
2446 host_request_id = api.CreateHostRequest(
2447 requests_pb2.CreateHostRequestReq(
2448 host_user_id=surfer.id,
2449 from_date=trip_from.isoformat(),
2450 to_date=trip_to.isoformat(),
2451 text=valid_request_text(),
2452 public_trip_id=trip_id,
2453 )
2454 ).host_request_id
2456 moderator.approve_host_request(host_request_id)
2458 with requests_session(host_token) as api:
2459 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2460 assert hr.hosting_city == "Host city"
2461 assert round(hr.hosting_lat, 4) == 2
2462 assert round(hr.hosting_lng, 4) == 2
2463 assert hr.hosting_radius == 22
2466def test_public_trip_offer_stay_roles(db, moderator):
2467 """An offer reverses initiator/recipient, but the surfer and host are still the traveller and the host."""
2468 surfer, surfer_token = generate_user()
2469 host, host_token = generate_user()
2471 trip_from = today() + timedelta(days=10)
2472 trip_to = today() + timedelta(days=20)
2473 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2475 with requests_session(host_token) as api:
2476 host_request_id = api.CreateHostRequest(
2477 requests_pb2.CreateHostRequestReq(
2478 host_user_id=surfer.id,
2479 from_date=trip_from.isoformat(),
2480 to_date=trip_to.isoformat(),
2481 text=valid_request_text(),
2482 public_trip_id=trip_id,
2483 )
2484 ).host_request_id
2486 moderator.approve_host_request(host_request_id)
2488 with session_scope() as session:
2489 host_request = session.execute(
2490 select(HostRequest).where(HostRequest.conversation_id == host_request_id)
2491 ).scalar_one()
2492 assert host_request.initiator_user_id == host.id
2493 assert host_request.recipient_user_id == surfer.id
2494 assert host_request.surfer_user_id == surfer.id
2495 assert host_request.host_user_id == host.id
2497 for token in (surfer_token, host_token):
2498 with requests_session(token) as api:
2499 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2500 assert hr.surfer_user_id == surfer.id
2501 assert hr.host_user_id == host.id
2503 listed = api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests
2504 assert [(r.surfer_user_id, r.host_user_id) for r in listed] == [(surfer.id, host.id)]
2507def test_create_request_with_public_trip_dates_out_of_range(db):
2508 """Offered dates outside the trip window are rejected."""
2509 surfer, _ = generate_user()
2510 host, host_token = generate_user()
2512 trip_from = today() + timedelta(days=10)
2513 trip_to = today() + timedelta(days=20)
2514 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2516 with requests_session(host_token) as api:
2517 # from_date before trip starts
2518 with pytest.raises(grpc.RpcError) as e:
2519 api.CreateHostRequest(
2520 requests_pb2.CreateHostRequestReq(
2521 host_user_id=surfer.id,
2522 from_date=(trip_from - timedelta(days=1)).isoformat(),
2523 to_date=(trip_from + timedelta(days=1)).isoformat(),
2524 text=valid_request_text(),
2525 public_trip_id=trip_id,
2526 )
2527 )
2528 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2530 # to_date after trip ends
2531 with pytest.raises(grpc.RpcError) as e:
2532 api.CreateHostRequest(
2533 requests_pb2.CreateHostRequestReq(
2534 host_user_id=surfer.id,
2535 from_date=(trip_to - timedelta(days=1)).isoformat(),
2536 to_date=(trip_to + timedelta(days=1)).isoformat(),
2537 text=valid_request_text(),
2538 public_trip_id=trip_id,
2539 )
2540 )
2541 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2544def test_create_request_with_public_trip_user_mismatch(db):
2545 """The host_user_id must match the public trip's traveler."""
2546 trip_owner, _ = generate_user()
2547 other_user, _ = generate_user()
2548 host, host_token = generate_user()
2550 trip_from = today() + timedelta(days=10)
2551 trip_to = today() + timedelta(days=20)
2552 trip_id = _create_public_trip(trip_owner.id, trip_from, trip_to)
2554 with requests_session(host_token) as api:
2555 with pytest.raises(grpc.RpcError) as e:
2556 api.CreateHostRequest(
2557 requests_pb2.CreateHostRequestReq(
2558 host_user_id=other_user.id, # not the trip owner
2559 from_date=(trip_from + timedelta(days=1)).isoformat(),
2560 to_date=(trip_to - timedelta(days=1)).isoformat(),
2561 text=valid_request_text(),
2562 public_trip_id=trip_id,
2563 )
2564 )
2565 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2568def test_create_request_with_closed_public_trip(db):
2569 """Cannot offer to host a trip that's been closed."""
2570 surfer, _ = generate_user()
2571 host, host_token = generate_user()
2573 trip_from = today() + timedelta(days=10)
2574 trip_to = today() + timedelta(days=20)
2575 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, status=PublicTripStatus.closed)
2577 with requests_session(host_token) as api:
2578 with pytest.raises(grpc.RpcError) as e:
2579 api.CreateHostRequest(
2580 requests_pb2.CreateHostRequestReq(
2581 host_user_id=surfer.id,
2582 from_date=(trip_from + timedelta(days=1)).isoformat(),
2583 to_date=(trip_to - timedelta(days=1)).isoformat(),
2584 text=valid_request_text(),
2585 public_trip_id=trip_id,
2586 )
2587 )
2588 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2591def test_create_request_with_nonexistent_public_trip(db):
2592 """Nonexistent public trip ID returns NOT_FOUND."""
2593 surfer, _ = generate_user()
2594 host, host_token = generate_user()
2596 with requests_session(host_token) as api:
2597 with pytest.raises(grpc.RpcError) as e:
2598 api.CreateHostRequest(
2599 requests_pb2.CreateHostRequestReq(
2600 host_user_id=surfer.id,
2601 from_date=(today() + timedelta(days=2)).isoformat(),
2602 to_date=(today() + timedelta(days=3)).isoformat(),
2603 text=valid_request_text(),
2604 public_trip_id=999999,
2605 )
2606 )
2607 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2610def test_create_request_with_shadowed_public_trip(db, moderator):
2611 """A trip awaiting moderation can't be offered on until it's approved."""
2612 surfer, _ = generate_user()
2613 host, host_token = generate_user()
2615 trip_from = today() + timedelta(days=10)
2616 trip_to = today() + timedelta(days=20)
2617 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, visibility=ModerationVisibility.shadowed)
2619 with requests_session(host_token) as api:
2620 with pytest.raises(grpc.RpcError) as e:
2621 api.CreateHostRequest(
2622 requests_pb2.CreateHostRequestReq(
2623 host_user_id=surfer.id,
2624 from_date=(trip_from + timedelta(days=1)).isoformat(),
2625 to_date=(trip_to - timedelta(days=1)).isoformat(),
2626 text=valid_request_text(),
2627 public_trip_id=trip_id,
2628 )
2629 )
2630 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2631 assert e.value.details() == "Couldn't find that public trip."
2633 moderator.approve_public_trip(trip_id)
2635 with requests_session(host_token) as api:
2636 res = api.CreateHostRequest(
2637 requests_pb2.CreateHostRequestReq(
2638 host_user_id=surfer.id,
2639 from_date=(trip_from + timedelta(days=1)).isoformat(),
2640 to_date=(trip_to - timedelta(days=1)).isoformat(),
2641 text=valid_request_text(),
2642 public_trip_id=trip_id,
2643 )
2644 )
2645 assert res.host_request_id
2648def test_create_request_with_hidden_public_trip(db):
2649 """A hidden trip can't be offered on."""
2650 surfer, _ = generate_user()
2651 host, host_token = generate_user()
2653 trip_from = today() + timedelta(days=10)
2654 trip_to = today() + timedelta(days=20)
2655 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, visibility=ModerationVisibility.hidden)
2657 with requests_session(host_token) as api:
2658 with pytest.raises(grpc.RpcError) as e:
2659 api.CreateHostRequest(
2660 requests_pb2.CreateHostRequestReq(
2661 host_user_id=surfer.id,
2662 from_date=(trip_from + timedelta(days=1)).isoformat(),
2663 to_date=(trip_to - timedelta(days=1)).isoformat(),
2664 text=valid_request_text(),
2665 public_trip_id=trip_id,
2666 )
2667 )
2668 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2669 assert e.value.details() == "Couldn't find that public trip."
2672def test_create_request_without_public_trip_id_unchanged(db, moderator):
2673 """Existing flow without public_trip_id still works (backwards compatibility)."""
2674 surfer, _ = generate_user()
2675 host, host_token = generate_user()
2677 with requests_session(host_token) as api:
2678 res = api.CreateHostRequest(
2679 requests_pb2.CreateHostRequestReq(
2680 host_user_id=surfer.id,
2681 from_date=(today() + timedelta(days=2)).isoformat(),
2682 to_date=(today() + timedelta(days=3)).isoformat(),
2683 text=valid_request_text(),
2684 )
2685 )
2686 host_request_id = res.host_request_id
2688 moderator.approve_host_request(host_request_id)
2690 with requests_session(host_token) as api:
2691 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id))
2692 assert not hr.HasField("public_trip_id")
2695def test_create_request_same_gender_only_wrong_gender_rejected(db):
2696 surfer, _ = generate_user(gender="Woman")
2697 _, host_token = generate_user(gender="Man")
2699 trip_from = today() + timedelta(days=10)
2700 trip_to = today() + timedelta(days=20)
2701 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2703 with requests_session(host_token) as api:
2704 with pytest.raises(grpc.RpcError) as e:
2705 api.CreateHostRequest(
2706 requests_pb2.CreateHostRequestReq(
2707 host_user_id=surfer.id,
2708 from_date=trip_from.isoformat(),
2709 to_date=trip_to.isoformat(),
2710 text=valid_request_text(),
2711 public_trip_id=trip_id,
2712 )
2713 )
2714 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2717def test_create_request_same_gender_only_same_gender_allowed(db, moderator):
2718 surfer, _ = generate_user(gender="Woman")
2719 _, host_token = generate_user(gender="Woman")
2721 trip_from = today() + timedelta(days=10)
2722 trip_to = today() + timedelta(days=20)
2723 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2725 with requests_session(host_token) as api:
2726 res = api.CreateHostRequest(
2727 requests_pb2.CreateHostRequestReq(
2728 host_user_id=surfer.id,
2729 from_date=trip_from.isoformat(),
2730 to_date=trip_to.isoformat(),
2731 text=valid_request_text(),
2732 public_trip_id=trip_id,
2733 )
2734 )
2735 assert res.host_request_id > 0
2738def test_create_request_same_gender_only_moderator_bypass(db, moderator):
2739 surfer, _ = generate_user(gender="Woman")
2740 host, host_token = generate_user(gender="Man")
2742 trip_from = today() + timedelta(days=10)
2743 trip_to = today() + timedelta(days=20)
2744 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True)
2745 _make_trip_node_admin(host.id, trip_id)
2747 with requests_session(host_token) as api:
2748 res = api.CreateHostRequest(
2749 requests_pb2.CreateHostRequestReq(
2750 host_user_id=surfer.id,
2751 from_date=trip_from.isoformat(),
2752 to_date=trip_to.isoformat(),
2753 text=valid_request_text(),
2754 public_trip_id=trip_id,
2755 )
2756 )
2757 assert res.host_request_id > 0
2760def test_list_host_requests_public_trip_offer_by_stay_role(db, moderator):
2761 """only_sent/only_received follow the stay roles, so an offer the host started is hosting for them."""
2762 traveller, traveller_token = generate_user()
2763 host, host_token = generate_user()
2765 trip_from = today() + timedelta(days=10)
2766 trip_to = today() + timedelta(days=20)
2767 trip_id = _create_public_trip(traveller.id, trip_from, trip_to)
2769 with requests_session(host_token) as api:
2770 offer_id = api.CreateHostRequest(
2771 requests_pb2.CreateHostRequestReq(
2772 host_user_id=traveller.id,
2773 from_date=trip_from.isoformat(),
2774 to_date=trip_to.isoformat(),
2775 text=valid_request_text(),
2776 public_trip_id=trip_id,
2777 )
2778 ).host_request_id
2780 moderator.approve_host_request(offer_id)
2782 def listed_ids(token: str, req: requests_pb2.ListHostRequestsReq) -> list[int]:
2783 with requests_session(token) as api:
2784 res = api.ListHostRequests(req)
2785 return [hr.host_request_id for hr in res.host_requests]
2787 hosting = requests_pb2.ListHostRequestsReq(only_received=True)
2788 surfing = requests_pb2.ListHostRequestsReq(only_sent=True)
2790 # the host offered, but they're the one hosting
2791 assert listed_ids(host_token, hosting) == [offer_id]
2792 assert listed_ids(host_token, surfing) == []
2793 # the traveller received the offer, but they're the one surfing
2794 assert listed_ids(traveller_token, surfing) == [offer_id]
2795 assert listed_ids(traveller_token, hosting) == []
2797 with requests_session(traveller_token) as api:
2798 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq())
2799 assert res.host_requests[0].public_trip_id == trip_id
2802def test_create_request_duplicate_offer_rejected(db):
2803 surfer, _ = generate_user()
2804 _, host_token = generate_user()
2806 trip_from = today() + timedelta(days=10)
2807 trip_to = today() + timedelta(days=20)
2808 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2810 with requests_session(host_token) as api:
2811 api.CreateHostRequest(
2812 requests_pb2.CreateHostRequestReq(
2813 host_user_id=surfer.id,
2814 from_date=trip_from.isoformat(),
2815 to_date=trip_to.isoformat(),
2816 text=valid_request_text(),
2817 public_trip_id=trip_id,
2818 )
2819 )
2820 with pytest.raises(grpc.RpcError) as e:
2821 api.CreateHostRequest(
2822 requests_pb2.CreateHostRequestReq(
2823 host_user_id=surfer.id,
2824 from_date=trip_from.isoformat(),
2825 to_date=trip_to.isoformat(),
2826 text=valid_request_text(),
2827 public_trip_id=trip_id,
2828 )
2829 )
2830 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2833def test_create_request_offer_allowed_after_withdrawal(db, moderator):
2834 """Withdrawing an offer frees the host to offer again, e.g. to renegotiate dates."""
2835 surfer, _ = generate_user()
2836 _, host_token = generate_user()
2838 trip_from = today() + timedelta(days=10)
2839 trip_to = today() + timedelta(days=20)
2840 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2842 with requests_session(host_token) as api:
2843 request_id = api.CreateHostRequest(
2844 requests_pb2.CreateHostRequestReq(
2845 host_user_id=surfer.id,
2846 from_date=trip_from.isoformat(),
2847 to_date=trip_to.isoformat(),
2848 text=valid_request_text(),
2849 public_trip_id=trip_id,
2850 )
2851 ).host_request_id
2853 moderator.approve_host_request(request_id)
2855 with requests_session(host_token) as api:
2856 api.RespondHostRequest(
2857 requests_pb2.RespondHostRequestReq(
2858 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
2859 )
2860 )
2862 # a second offer on shorter dates now goes through
2863 res = api.CreateHostRequest(
2864 requests_pb2.CreateHostRequestReq(
2865 host_user_id=surfer.id,
2866 from_date=trip_from.isoformat(),
2867 to_date=(trip_to - timedelta(days=2)).isoformat(),
2868 text=valid_request_text(),
2869 public_trip_id=trip_id,
2870 )
2871 )
2872 assert res.host_request_id != request_id
2874 # ...but that new offer is active, so a third is still blocked
2875 with pytest.raises(grpc.RpcError) as e:
2876 api.CreateHostRequest(
2877 requests_pb2.CreateHostRequestReq(
2878 host_user_id=surfer.id,
2879 from_date=trip_from.isoformat(),
2880 to_date=trip_to.isoformat(),
2881 text=valid_request_text(),
2882 public_trip_id=trip_id,
2883 )
2884 )
2885 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2888def test_no_feedback_for_declined_public_trip_offer(db, moderator):
2889 """Decline feedback is a host's view of a surfer's request, so a traveller declining an offer isn't asked."""
2890 surfer, surfer_token = generate_user()
2891 _, host_token = generate_user()
2893 trip_from = today() + timedelta(days=10)
2894 trip_to = today() + timedelta(days=20)
2895 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2897 with requests_session(host_token) as api:
2898 request_id = api.CreateHostRequest(
2899 requests_pb2.CreateHostRequestReq(
2900 host_user_id=surfer.id,
2901 from_date=trip_from.isoformat(),
2902 to_date=trip_to.isoformat(),
2903 text=valid_request_text(),
2904 public_trip_id=trip_id,
2905 )
2906 ).host_request_id
2908 moderator.approve_host_request(request_id)
2910 with requests_session(surfer_token) as api:
2911 api.RespondHostRequest(
2912 requests_pb2.RespondHostRequestReq(
2913 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED
2914 )
2915 )
2916 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=request_id))
2917 assert not res.need_host_request_feedback
2919 with pytest.raises(grpc.RpcError) as e:
2920 api.SendHostRequestFeedback(
2921 requests_pb2.SendHostRequestFeedbackReq(
2922 host_request_id=request_id,
2923 host_request_quality=requests_pb2.HOST_REQUEST_QUALITY_LOW,
2924 )
2925 )
2926 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2928 with requests_session(host_token) as api:
2929 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=request_id))
2930 assert not res.need_host_request_feedback
2933def test_db_allows_one_active_offer_per_trip(db, moderator):
2934 """The database backs up the duplicate-offer check, e.g. against two concurrent offers."""
2935 surfer, _ = generate_user()
2936 _, host_token = generate_user()
2938 trip_from = today() + timedelta(days=10)
2939 trip_to = today() + timedelta(days=20)
2940 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
2942 with requests_session(host_token) as api:
2943 withdrawn_id = api.CreateHostRequest(
2944 requests_pb2.CreateHostRequestReq(
2945 host_user_id=surfer.id,
2946 from_date=trip_from.isoformat(),
2947 to_date=trip_to.isoformat(),
2948 text=valid_request_text(),
2949 public_trip_id=trip_id,
2950 )
2951 ).host_request_id
2953 moderator.approve_host_request(withdrawn_id)
2955 with requests_session(host_token) as api:
2956 api.RespondHostRequest(
2957 requests_pb2.RespondHostRequestReq(
2958 host_request_id=withdrawn_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
2959 )
2960 )
2961 api.CreateHostRequest(
2962 requests_pb2.CreateHostRequestReq(
2963 host_user_id=surfer.id,
2964 from_date=trip_from.isoformat(),
2965 to_date=trip_to.isoformat(),
2966 text=valid_request_text(),
2967 public_trip_id=trip_id,
2968 )
2969 )
2971 # reviving the withdrawn offer would leave the host with two active offers on the trip
2972 with pytest.raises(IntegrityError):
2973 with session_scope() as session:
2974 session.execute(
2975 update(HostRequest)
2976 .where(HostRequest.conversation_id == withdrawn_id)
2977 .values(status=HostRequestStatus.pending)
2978 )
2981def test_public_trip_offer_responses_arent_host_response_metrics(db, moderator):
2982 """The traveller answering an offer on their trip isn't a host responding, so it's left out of host metrics."""
2983 traveller, traveller_token = generate_user()
2984 _, host_token = generate_user()
2985 other_host, other_host_token = generate_user()
2986 trip_id = _create_public_trip(traveller.id, today() + timedelta(days=5), today() + timedelta(days=10))
2987 offer_id = _create_host_request_via_api(host_token, traveller.id, moderator, public_trip_id=trip_id)
2988 request_id = _create_host_request_via_api(traveller_token, other_host.id, moderator)
2990 with patch.multiple(
2991 "couchers.servicers.requests",
2992 host_request_first_response_histogram=DEFAULT,
2993 host_request_responses_counter=DEFAULT,
2994 ) as metrics:
2995 with requests_session(traveller_token) as api:
2996 api.SendHostRequestMessage(
2997 requests_pb2.SendHostRequestMessageReq(host_request_id=offer_id, text="Thanks for the offer!")
2998 )
2999 api.RespondHostRequest(
3000 requests_pb2.RespondHostRequestReq(
3001 host_request_id=offer_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
3002 )
3003 )
3004 metrics["host_request_first_response_histogram"].labels.assert_not_called()
3005 metrics["host_request_responses_counter"].labels.assert_not_called()
3007 # a host answering a regular request is still counted
3008 with requests_session(other_host_token) as api:
3009 api.RespondHostRequest(
3010 requests_pb2.RespondHostRequestReq(
3011 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
3012 )
3013 )
3014 metrics["host_request_first_response_histogram"].labels.assert_called_once()
3015 metrics["host_request_responses_counter"].labels.assert_called_once()
3018def test_public_trip_offers_are_counted_separately_from_host_requests(db, moderator):
3019 traveller, traveller_token = generate_user(gender="Woman")
3020 _, host_token = generate_user(gender="Man")
3021 other_host, _ = generate_user(gender="Non-binary")
3022 trip_id = _create_public_trip(traveller.id, today() + timedelta(days=5), today() + timedelta(days=10))
3024 with patch.multiple(
3025 "couchers.servicers.requests",
3026 host_requests_sent_counter=DEFAULT,
3027 account_age_on_host_request_create_histogram=DEFAULT,
3028 public_trip_offers_sent_counter=DEFAULT,
3029 ) as metrics:
3030 _create_host_request_via_api(host_token, traveller.id, moderator, public_trip_id=trip_id)
3031 metrics["host_requests_sent_counter"].labels.assert_not_called()
3032 metrics["account_age_on_host_request_create_histogram"].labels.assert_not_called()
3033 metrics["public_trip_offers_sent_counter"].labels.assert_called_once_with("Man", "Woman")
3035 _create_host_request_via_api(traveller_token, other_host.id, moderator)
3036 metrics["host_requests_sent_counter"].labels.assert_called_once_with("Woman", "Non-binary")
3037 metrics["account_age_on_host_request_create_histogram"].labels.assert_called_once_with("Woman", "Non-binary")
3038 metrics["public_trip_offers_sent_counter"].labels.assert_called_once()
3041def test_excessive_public_trip_offers_report_the_traveller_as_recipient(
3042 db, low_rate_limits, email_collector: EmailCollector
3043):
3044 host, host_token = generate_user()
3045 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.host_request]
3046 trip_from = today() + timedelta(days=5)
3047 trip_to = today() + timedelta(days=10)
3049 travellers = []
3050 with requests_session(host_token) as api:
3051 for _ in range(rate_limit_definition.warning_limit + 1):
3052 traveller, _ = generate_user()
3053 travellers.append(traveller)
3054 api.CreateHostRequest(
3055 requests_pb2.CreateHostRequestReq(
3056 host_user_id=traveller.id,
3057 from_date=trip_from.isoformat(),
3058 to_date=trip_to.isoformat(),
3059 text=valid_request_text(),
3060 public_trip_id=_create_public_trip(traveller.id, trip_from, trip_to),
3061 )
3062 )
3064 email = email_collector.pop_for_reports(last=True)
3065 assert email.plain.startswith(f"User {host.username} has sent {rate_limit_definition.warning_limit} host requests")
3066 assert "recipient username" in email.plain
3067 assert "public trip ID" in email.plain
3068 assert "host username" not in email.plain
3069 assert travellers[0].username in email.plain
3072def test_create_request_offer_blocked_after_traveller_declines(db, moderator):
3073 """Only the host withdrawing frees them to re-offer; being declined does not."""
3074 surfer, surfer_token = generate_user()
3075 _, host_token = generate_user()
3077 trip_from = today() + timedelta(days=10)
3078 trip_to = today() + timedelta(days=20)
3079 trip_id = _create_public_trip(surfer.id, trip_from, trip_to)
3081 with requests_session(host_token) as api:
3082 request_id = api.CreateHostRequest(
3083 requests_pb2.CreateHostRequestReq(
3084 host_user_id=surfer.id,
3085 from_date=trip_from.isoformat(),
3086 to_date=trip_to.isoformat(),
3087 text=valid_request_text(),
3088 public_trip_id=trip_id,
3089 )
3090 ).host_request_id
3092 moderator.approve_host_request(request_id)
3094 # the traveller is the recipient on an offer, so they are the one who declines
3095 with requests_session(surfer_token) as api:
3096 api.RespondHostRequest(
3097 requests_pb2.RespondHostRequestReq(
3098 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED
3099 )
3100 )
3102 with requests_session(host_token) as api:
3103 with pytest.raises(grpc.RpcError) as e:
3104 api.CreateHostRequest(
3105 requests_pb2.CreateHostRequestReq(
3106 host_user_id=surfer.id,
3107 from_date=trip_from.isoformat(),
3108 to_date=trip_to.isoformat(),
3109 text=valid_request_text(),
3110 public_trip_id=trip_id,
3111 )
3112 )
3113 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
3116def _create_accepted_request(surfer_token: str, host_token: str, host_id: int, moderator, days_ahead: int = 2) -> int:
3117 with requests_session(surfer_token) as api:
3118 request_id = api.CreateHostRequest(
3119 requests_pb2.CreateHostRequestReq(
3120 host_user_id=host_id,
3121 from_date=(today() + timedelta(days=days_ahead)).isoformat(),
3122 to_date=(today() + timedelta(days=days_ahead + 1)).isoformat(),
3123 text=valid_request_text(),
3124 )
3125 ).host_request_id
3126 moderator.approve_host_request(request_id)
3127 with requests_session(host_token) as api:
3128 api.RespondHostRequest(
3129 requests_pb2.RespondHostRequestReq(
3130 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
3131 )
3132 )
3133 return int(request_id)
3136def _upcoming_stay_ids(token: str, role: requests_pb2.StayRole.ValueType) -> list[int]:
3137 with requests_session(token) as api:
3138 res = api.ListStays(requests_pb2.ListStaysReq(role=role))
3139 return [hr.host_request_id for hr in res.host_requests]
3142def test_ListStays_by_role(db, moderator):
3143 surfer, surfer_token = generate_user()
3144 host, host_token = generate_user()
3146 request_id = _create_accepted_request(surfer_token, host_token, host.id, moderator)
3148 assert _upcoming_stay_ids(host_token, requests_pb2.STAY_ROLE_HOSTING) == [request_id]
3149 assert _upcoming_stay_ids(host_token, requests_pb2.STAY_ROLE_SURFING) == []
3150 assert _upcoming_stay_ids(surfer_token, requests_pb2.STAY_ROLE_SURFING) == [request_id]
3151 assert _upcoming_stay_ids(surfer_token, requests_pb2.STAY_ROLE_HOSTING) == []
3153 with requests_session(host_token) as api:
3154 res = api.ListStays(requests_pb2.ListStaysReq(role=requests_pb2.STAY_ROLE_HOSTING))
3155 stay = res.host_requests[0]
3156 assert stay.surfer_user_id == surfer.id
3157 assert stay.host_user_id == host.id
3158 assert stay.status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
3159 assert stay.from_date == (today() + timedelta(days=2)).isoformat()
3160 # built with host_request_to_pb, so it's as complete as GetHostRequest's
3161 with requests_session(host_token) as api:
3162 assert stay == api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=stay.host_request_id))
3163 assert stay.timezone
3164 assert stay.latest_message.message_id
3167def test_ListStays_public_trip_offer_by_stay_role(db, moderator):
3168 """An offer is started by the host, but they're still the one hosting."""
3169 traveller, traveller_token = generate_user()
3170 _host, host_token = generate_user()
3172 trip_from = today() + timedelta(days=10)
3173 trip_to = today() + timedelta(days=20)
3174 trip_id = _create_public_trip(traveller.id, trip_from, trip_to)
3176 with requests_session(host_token) as api:
3177 offer_id = api.CreateHostRequest(
3178 requests_pb2.CreateHostRequestReq(
3179 host_user_id=traveller.id,
3180 from_date=trip_from.isoformat(),
3181 to_date=trip_to.isoformat(),
3182 text=valid_request_text(),
3183 public_trip_id=trip_id,
3184 )
3185 ).host_request_id
3186 moderator.approve_host_request(offer_id)
3188 # the traveller is the recipient of an offer, so they accept it
3189 with requests_session(traveller_token) as api:
3190 api.RespondHostRequest(
3191 requests_pb2.RespondHostRequestReq(
3192 host_request_id=offer_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED
3193 )
3194 )
3196 assert _upcoming_stay_ids(host_token, requests_pb2.STAY_ROLE_HOSTING) == [offer_id]
3197 assert _upcoming_stay_ids(host_token, requests_pb2.STAY_ROLE_SURFING) == []
3198 assert _upcoming_stay_ids(traveller_token, requests_pb2.STAY_ROLE_SURFING) == [offer_id]
3199 assert _upcoming_stay_ids(traveller_token, requests_pb2.STAY_ROLE_HOSTING) == []
3201 with requests_session(traveller_token) as api:
3202 res = api.ListStays(requests_pb2.ListStaysReq(role=requests_pb2.STAY_ROLE_SURFING))
3203 assert res.host_requests[0].public_trip_id == trip_id
3206def test_ListStays_only_accepted_or_confirmed_and_not_ended(db, moderator):
3207 _surfer, surfer_token = generate_user()
3208 host, host_token = generate_user()
3210 accepted_id = _create_accepted_request(surfer_token, host_token, host.id, moderator, days_ahead=2)
3211 confirmed_id = _create_accepted_request(surfer_token, host_token, host.id, moderator, days_ahead=4)
3212 rejected_id = _create_accepted_request(surfer_token, host_token, host.id, moderator, days_ahead=6)
3213 ended_id = _create_accepted_request(surfer_token, host_token, host.id, moderator, days_ahead=8)
3215 with requests_session(surfer_token) as api:
3216 api.RespondHostRequest(
3217 requests_pb2.RespondHostRequestReq(
3218 host_request_id=confirmed_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
3219 )
3220 )
3221 with requests_session(host_token) as api:
3222 api.RespondHostRequest(
3223 requests_pb2.RespondHostRequestReq(
3224 host_request_id=rejected_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED
3225 )
3226 )
3228 # a pending request isn't upcoming yet
3229 with requests_session(surfer_token) as api:
3230 pending_id = api.CreateHostRequest(
3231 requests_pb2.CreateHostRequestReq(
3232 host_user_id=host.id,
3233 from_date=(today() + timedelta(days=10)).isoformat(),
3234 to_date=(today() + timedelta(days=11)).isoformat(),
3235 text=valid_request_text(),
3236 )
3237 ).host_request_id
3238 moderator.approve_host_request(pending_id)
3240 with session_scope() as session:
3241 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == ended_id)).scalar_one()
3242 hr.from_date = today() - timedelta(days=3)
3243 hr.to_date = today() - timedelta(days=2)
3245 assert _upcoming_stay_ids(host_token, requests_pb2.STAY_ROLE_HOSTING) == [accepted_id, confirmed_id]
3246 assert _upcoming_stay_ids(surfer_token, requests_pb2.STAY_ROLE_SURFING) == [accepted_id, confirmed_id]
3249def test_ListStays_sorted_and_paginated(db, moderator):
3250 _surfer, surfer_token = generate_user()
3251 host, host_token = generate_user()
3253 # created out of order, listed soonest first
3254 later_id = _create_accepted_request(surfer_token, host_token, host.id, moderator, days_ahead=9)
3255 soonest_id = _create_accepted_request(surfer_token, host_token, host.id, moderator, days_ahead=2)
3256 middle_id = _create_accepted_request(surfer_token, host_token, host.id, moderator, days_ahead=5)
3258 with requests_session(host_token) as api:
3259 page1 = api.ListStays(requests_pb2.ListStaysReq(role=requests_pb2.STAY_ROLE_HOSTING, page_size=2))
3260 assert [hr.host_request_id for hr in page1.host_requests] == [soonest_id, middle_id]
3261 assert page1.next_page_token
3263 page2 = api.ListStays(
3264 requests_pb2.ListStaysReq(
3265 role=requests_pb2.STAY_ROLE_HOSTING, page_size=2, page_token=page1.next_page_token
3266 )
3267 )
3268 assert [hr.host_request_id for hr in page2.host_requests] == [later_id]
3269 assert not page2.next_page_token
3272def test_ListStays_hides_blocked_users(db, moderator):
3273 surfer, surfer_token = generate_user()
3274 host, host_token = generate_user()
3276 _create_accepted_request(surfer_token, host_token, host.id, moderator)
3277 make_user_block(host, surfer)
3279 assert _upcoming_stay_ids(host_token, requests_pb2.STAY_ROLE_HOSTING) == []
3280 assert _upcoming_stay_ids(surfer_token, requests_pb2.STAY_ROLE_SURFING) == []
3283def test_ListStays_requires_role(db):
3284 _, token = generate_user()
3286 with requests_session(token) as api:
3287 with pytest.raises(grpc.RpcError) as e:
3288 api.ListStays(requests_pb2.ListStaysReq())
3289 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT