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

1import html 

2import re 

3from datetime import date, timedelta 

4from unittest.mock import DEFAULT, patch 

5from urllib.parse import parse_qs, urlparse 

6 

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 

13 

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 

50 

51 

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. 

60 

61 

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 ) 

73 

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) 

78 

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." 

91 

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." 

103 

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." 

115 

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." 

127 

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." 

139 

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." 

151 

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." 

163 

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 

173 

174 moderator.approve_host_request(host_request_id) 

175 

176 with requests_session(token1) as api: 

177 host_requests = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)).host_requests 

178 

179 assert len(host_requests) == 1 

180 hr = host_requests[0] 

181 

182 assert hr.latest_message.text.text == valid_request_text() 

183 

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 

188 

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." 

204 

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." 

216 

217 

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)) 

226 

227 # Helsinki is already on 2026-01-16; requester submits 2026-01-15. 

228 fake_today_by_tz = {"Europe/Helsinki": date(2026, 1, 16)} 

229 

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 

245 

246 

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 

254 

255 fake_today_by_tz = {"America/New_York": date(2026, 1, 15)} 

256 

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 

271 

272 

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)) 

277 

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 

287 

288 moderator.approve_host_request(host_request_id) 

289 

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" 

293 

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" 

301 

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" 

305 

306 

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) 

315 

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 

325 

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 "") 

337 

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 

349 

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 

359 

360 

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) 

368 

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 

378 

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 

387 

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 

396 

397 

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) 

403 

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() 

414 

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 

423 

424 

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() 

430 

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) 

437 

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 

448 

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 

458 

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 

469 

470 

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." 

488 

489 

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 ) 

508 

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 ) 

519 

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 ) 

524 

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 ) 

536 

537 assert email_collector.count_for_reports() == 0 

538 

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 ) 

554 

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 

560 

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] 

567 

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 

581 

582 

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 ) 

588 

589 session.add(message) 

590 

591 

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 

607 

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." 

612 

613 api.SendHostRequestMessage( 

614 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 1") 

615 ) 

616 

617 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

618 assert res.latest_message.text.text == "Test message 1" 

619 

620 

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 

636 

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 

645 

646 moderator.approve_host_request(host_request_1) 

647 moderator.approve_host_request(host_request_2) 

648 

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 

653 

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 

662 

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) 

666 

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" 

669 

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 

678 

679 moderator.approve_host_request(host_request_3) 

680 

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) 

683 

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" 

689 

690 with requests_session(token1) as api: 

691 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True)) 

692 assert len(res.host_requests) == 1 

693 

694 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq()) 

695 assert len(res.host_requests) == 3 

696 

697 

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() 

716 

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() 

726 

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 

735 

736 moderator.approve_host_request(host_request_1) 

737 moderator.approve_host_request(host_request_2) 

738 moderator.approve_host_request(host_request_3) 

739 

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") 

747 

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 ) 

770 

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" 

778 

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" 

796 

797 

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) 

806 

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 

816 

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 

825 

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 

834 

835 moderator.approve_host_request(hr_late) 

836 moderator.approve_host_request(hr_early) 

837 moderator.approve_host_request(hr_mid) 

838 

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] 

844 

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] 

854 

855 

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) 

863 

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() 

874 

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() 

885 

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 

894 

895 moderator.approve_host_request(hr_a) 

896 moderator.approve_host_request(hr_b) 

897 moderator.approve_host_request(hr_c) 

898 

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 

910 

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 

922 

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 

934 

935 

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) 

941 

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 

951 

952 moderator.approve_host_request(request_id) 

953 

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 ) 

960 

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 

966 

967 

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) 

974 

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 

984 

985 moderator.approve_host_request(request_id) 

986 

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 ) 

994 

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 

999 

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) 

1005 

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 

1010 

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 

1015 

1016 

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) 

1025 

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 

1036 

1037 moderator.approve_host_request(pending_id) 

1038 

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 

1049 

1050 moderator.approve_host_request(accepted_id) 

1051 

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 ) 

1059 

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 

1069 

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 

1079 

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 

1091 

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 

1100 

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 

1105 

1106 

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) 

1113 

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 

1123 

1124 moderator.approve_host_request(request_id) 

1125 

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." 

1136 

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." 

1146 

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 

1156 

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." 

1174 

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 ) 

1192 

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." 

1203 

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 ) 

1210 

1211 api.RespondHostRequest( 

1212 requests_pb2.RespondHostRequestReq( 

1213 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1214 ) 

1215 ) 

1216 

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." 

1226 

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" 

1237 

1238 

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 

1254 

1255 moderator.approve_host_request(conversation_id) 

1256 

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) 

1260 

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 ) 

1267 

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) 

1270 

1271 api.RespondHostRequest( 

1272 requests_pb2.RespondHostRequestReq( 

1273 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1274 ) 

1275 ) 

1276 

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 

1282 

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" 

1292 

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" 

1309 

1310 

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 

1326 

1327 moderator.approve_host_request(host_request_id) 

1328 

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 

1335 

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." 

1340 

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 

1347 

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." 

1356 

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 

1366 

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 ) 

1376 

1377 api.RespondHostRequest( 

1378 requests_pb2.RespondHostRequestReq( 

1379 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1380 ) 

1381 ) 

1382 

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 ) 

1392 

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 ) 

1401 

1402 

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 

1418 

1419 moderator.approve_host_request(host_request_id) 

1420 

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 ) 

1435 

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 ) 

1445 

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 

1458 

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 

1462 

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") 

1473 

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 

1479 

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 

1484 

1485 

1486def test_archive_host_request(db, moderator): 

1487 user1, token1 = generate_user() 

1488 user2, token2 = generate_user() 

1489 

1490 today_plus_2 = today() + timedelta(days=2) 

1491 today_plus_3 = today() + timedelta(days=3) 

1492 

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 

1502 

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 ) 

1509 

1510 moderator.approve_host_request(host_request_id) 

1511 

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 

1524 

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 

1528 

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 

1534 

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 

1538 

1539 

1540def test_host_request_unseen_message_count(db, moderator): 

1541 user1, token1 = generate_user() 

1542 user2, token2 = generate_user() 

1543 

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) 

1554 

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 

1562 

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 ) 

1571 

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 ) 

1582 

1583 

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() 

1600 

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 

1609 

1610 moderator.approve_host_request(host_request_id) 

1611 moderator.approve_host_request(host_request_id_2) 

1612 

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 ) 

1627 

1628 moderator.approve_host_request(host_request_id) 

1629 moderator.approve_host_request(host_request_id_2) 

1630 

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 

1635 

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 

1639 

1640 with requests_session(token2) as api: 

1641 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].last_seen_message_id == 0 

1642 

1643 api.MarkLastSeenHostRequest( 

1644 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=3) 

1645 ) 

1646 

1647 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].last_seen_message_id == 3 

1648 

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." 

1655 

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 

1665 

1666 moderator.approve_host_request(host_request_id_3) 

1667 

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 ) 

1673 

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 

1677 

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 ) 

1683 

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 

1687 

1688 

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) 

1702 

1703 

1704def test_ping_role_based_counts_match_direction_without_offers(db, moderator): 

1705 user1, token1 = generate_user() 

1706 user2, token2 = generate_user() 

1707 

1708 # user2 surfs with user1 -> user1 is the host of the stay 

1709 _create_host_request_via_api(token2, user1.id, moderator) 

1710 

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 

1719 

1720 

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)) 

1726 

1727 request_id = _create_host_request_via_api(host_token, traveler.id, moderator, public_trip_id=trip_id) 

1728 

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 

1735 

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 ) 

1741 

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 

1747 

1748 

1749def test_ping_public_trip_offer_count_gated_by_flag(db, moderator, feature_flags): 

1750 feature_flags.set("public_trips_enabled", False) 

1751 

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)) 

1756 

1757 _create_host_request_via_api(host_token, traveler.id, moderator, public_trip_id=trip_id) 

1758 

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 

1765 

1766 

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) 

1772 

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 

1782 

1783 moderator.approve_host_request(host_request_id) 

1784 

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() 

1794 

1795 assert unseen_notification_count(user2.id) > 0 

1796 

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 ) 

1801 

1802 assert unseen_notification_count(user2.id) == 0 

1803 

1804 

1805def test_response_rate(db, moderator): 

1806 user1, token1 = generate_user() 

1807 user2, token2 = generate_user() 

1808 user3, token3 = generate_user(delete_user=True) 

1809 

1810 today_plus_2 = today() + timedelta(days=2) 

1811 today_plus_3 = today() + timedelta(days=3) 

1812 

1813 with session_scope() as session: 

1814 refresh_materialized_view(session, "user_response_rates") 

1815 

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." 

1822 

1823 # no requests: insufficient 

1824 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id)) 

1825 assert res.HasField("insufficient_data") 

1826 

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") 

1845 

1846 # still insufficient 

1847 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id)) 

1848 assert res.HasField("insufficient_data") 

1849 

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") 

1868 

1869 # still insufficient 

1870 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id)) 

1871 assert res.HasField("insufficient_data") 

1872 

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") 

1891 

1892 # now low 

1893 res = api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user2.id)) 

1894 assert res.HasField("low") 

1895 

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 ) 

1905 

1906 with session_scope() as session: 

1907 refresh_materialized_view(session, "user_response_rates") 

1908 

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) 

1914 

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 ) 

1924 

1925 with session_scope() as session: 

1926 refresh_materialized_view(session, "user_response_rates") 

1927 

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) 

1934 

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 ) 

1944 

1945 with session_scope() as session: 

1946 refresh_materialized_view(session, "user_response_rates") 

1947 

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) 

1954 

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") 

1973 

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") 

1992 

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) 

1997 

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 ) 

2007 

2008 with session_scope() as session: 

2009 refresh_materialized_view(session, "user_response_rates") 

2010 

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) 

2017 

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 ) 

2027 

2028 with session_scope() as session: 

2029 refresh_materialized_view(session, "user_response_rates") 

2030 

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) 

2037 

2038 

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)) 

2044 

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) 

2049 

2050 # a regular request to the traveller as a host still counts 

2051 _create_host_request_via_api(surfer_token, traveler.id, moderator) 

2052 

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 

2060 

2061 

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) 

2065 

2066 host_loc_context = LocalizationContext.from_user(host) 

2067 surfer_loc_context = LocalizationContext.from_user(surfer) 

2068 

2069 today_plus_2 = today() + timedelta(days=2) 

2070 today_plus_3 = today() + timedelta(days=3) 

2071 

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 

2081 

2082 moderator.approve_host_request(hr_id) 

2083 

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 

2102 

2103 assert push_collector.pop_for_user(host.id, last=True).content.title == f"New host request from {surfer.name}" 

2104 

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 ) 

2113 

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 

2130 

2131 assert push_collector.pop_for_user(surfer.id, last=True).content.title == f"{host.name} accepted your host request" 

2132 

2133 

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) 

2137 

2138 host_loc_context = LocalizationContext.from_user(host) 

2139 

2140 today_plus_2 = today() + timedelta(days=2) 

2141 today_plus_3 = today() + timedelta(days=3) 

2142 

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 

2152 

2153 moderator.approve_host_request(hr_id) 

2154 

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 

2172 

2173 assert push_collector.pop_for_user(host.id, last=True).content.title == f"New host request from {surfer.name}" 

2174 

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") 

2197 

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 

2201 

2202 

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) 

2208 

2209 today_plus_2 = today() + timedelta(days=2) 

2210 today_plus_3 = today() + timedelta(days=3) 

2211 

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 

2237 

2238 moderator.approve_host_request(hr_id) 

2239 moderator.approve_host_request(hr2_id) 

2240 moderator.approve_host_request(hr3_id) 

2241 

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 

2245 

2246 api.RespondHostRequest( 

2247 requests_pb2.RespondHostRequestReq( 

2248 host_request_id=hr_id, 

2249 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED, 

2250 ) 

2251 ) 

2252 

2253 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id)) 

2254 assert res.need_host_request_feedback 

2255 

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." 

2266 

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 

2276 

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!" 

2287 

2288 res = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=hr_id)) 

2289 assert not res.need_host_request_feedback 

2290 

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." 

2304 

2305 # null feedback is still feedback 

2306 api.SendHostRequestFeedback(requests_pb2.SendHostRequestFeedbackReq(host_request_id=hr2_id)) 

2307 

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 ) 

2312 

2313 api.SendHostRequestFeedback( 

2314 requests_pb2.SendHostRequestFeedbackReq(host_request_id=hr3_id, decline_reason="bad req") 

2315 ) 

2316 

2317 

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)) 

2334 

2335 

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 

2375 

2376 

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() 

2381 

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) 

2385 

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 

2398 

2399 moderator.approve_host_request(host_request_id) 

2400 

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 

2404 

2405 

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() 

2410 

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) 

2414 

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 ) 

2426 

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." 

2431 

2432 # Shorter than a regular host request, but long enough for an offer 

2433 offer("a" * PUBLIC_TRIP_OFFER_MIN_LENGTH_UTF16) 

2434 

2435 

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) 

2440 

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) 

2444 

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 

2455 

2456 moderator.approve_host_request(host_request_id) 

2457 

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 

2464 

2465 

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() 

2470 

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) 

2474 

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 

2485 

2486 moderator.approve_host_request(host_request_id) 

2487 

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 

2496 

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 

2502 

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)] 

2505 

2506 

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() 

2511 

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) 

2515 

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 

2529 

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 

2542 

2543 

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() 

2549 

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) 

2553 

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 

2566 

2567 

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() 

2572 

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) 

2576 

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 

2589 

2590 

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() 

2595 

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 

2608 

2609 

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() 

2614 

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) 

2618 

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." 

2632 

2633 moderator.approve_public_trip(trip_id) 

2634 

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 

2646 

2647 

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() 

2652 

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) 

2656 

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." 

2670 

2671 

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() 

2676 

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 

2687 

2688 moderator.approve_host_request(host_request_id) 

2689 

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") 

2693 

2694 

2695def test_create_request_same_gender_only_wrong_gender_rejected(db): 

2696 surfer, _ = generate_user(gender="Woman") 

2697 _, host_token = generate_user(gender="Man") 

2698 

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) 

2702 

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 

2715 

2716 

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") 

2720 

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) 

2724 

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 

2736 

2737 

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") 

2741 

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) 

2746 

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 

2758 

2759 

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() 

2764 

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) 

2768 

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 

2779 

2780 moderator.approve_host_request(offer_id) 

2781 

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] 

2786 

2787 hosting = requests_pb2.ListHostRequestsReq(only_received=True) 

2788 surfing = requests_pb2.ListHostRequestsReq(only_sent=True) 

2789 

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) == [] 

2796 

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 

2800 

2801 

2802def test_create_request_duplicate_offer_rejected(db): 

2803 surfer, _ = generate_user() 

2804 _, host_token = generate_user() 

2805 

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) 

2809 

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 

2831 

2832 

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() 

2837 

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) 

2841 

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 

2852 

2853 moderator.approve_host_request(request_id) 

2854 

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 ) 

2861 

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 

2873 

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 

2886 

2887 

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() 

2892 

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) 

2896 

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 

2907 

2908 moderator.approve_host_request(request_id) 

2909 

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 

2918 

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 

2927 

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 

2931 

2932 

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() 

2937 

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) 

2941 

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 

2952 

2953 moderator.approve_host_request(withdrawn_id) 

2954 

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 ) 

2970 

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 ) 

2979 

2980 

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) 

2989 

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() 

3006 

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() 

3016 

3017 

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)) 

3023 

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") 

3034 

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() 

3039 

3040 

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) 

3048 

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 ) 

3063 

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 

3070 

3071 

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() 

3076 

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) 

3080 

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 

3091 

3092 moderator.approve_host_request(request_id) 

3093 

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 ) 

3101 

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 

3114 

3115 

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) 

3134 

3135 

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] 

3140 

3141 

3142def test_ListStays_by_role(db, moderator): 

3143 surfer, surfer_token = generate_user() 

3144 host, host_token = generate_user() 

3145 

3146 request_id = _create_accepted_request(surfer_token, host_token, host.id, moderator) 

3147 

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) == [] 

3152 

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 

3165 

3166 

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() 

3171 

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) 

3175 

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) 

3187 

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 ) 

3195 

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) == [] 

3200 

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 

3204 

3205 

3206def test_ListStays_only_accepted_or_confirmed_and_not_ended(db, moderator): 

3207 _surfer, surfer_token = generate_user() 

3208 host, host_token = generate_user() 

3209 

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) 

3214 

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 ) 

3227 

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) 

3239 

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) 

3244 

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] 

3247 

3248 

3249def test_ListStays_sorted_and_paginated(db, moderator): 

3250 _surfer, surfer_token = generate_user() 

3251 host, host_token = generate_user() 

3252 

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) 

3257 

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 

3262 

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 

3270 

3271 

3272def test_ListStays_hides_blocked_users(db, moderator): 

3273 surfer, surfer_token = generate_user() 

3274 host, host_token = generate_user() 

3275 

3276 _create_accepted_request(surfer_token, host_token, host.id, moderator) 

3277 make_user_block(host, surfer) 

3278 

3279 assert _upcoming_stay_ids(host_token, requests_pb2.STAY_ROLE_HOSTING) == [] 

3280 assert _upcoming_stay_ids(surfer_token, requests_pb2.STAY_ROLE_SURFING) == [] 

3281 

3282 

3283def test_ListStays_requires_role(db): 

3284 _, token = generate_user() 

3285 

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