Coverage for app/backend/src/tests/test_requests.py: 99%

1171 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-19 15:47 +0000

1import html 

2import re 

3from datetime import date, timedelta 

4from unittest.mock import patch 

5from urllib.parse import parse_qs, urlparse 

6 

7import grpc 

8import pytest 

9from sqlalchemy import func, select 

10from sqlalchemy_utils import refresh_materialized_view 

11 

12from couchers.constants import HOST_REQUEST_MIN_LENGTH_UTF16 

13from couchers.crypto import b64decode 

14from couchers.db import session_scope 

15from couchers.i18n import LocalizationContext 

16from couchers.models import ( 

17 Cluster, 

18 ClusterRole, 

19 ClusterSubscription, 

20 HostRequest, 

21 Message, 

22 MessageType, 

23 ModerationObjectType, 

24 ModerationState, 

25 ModerationVisibility, 

26 Node, 

27 NodeType, 

28 Notification, 

29 RateLimitAction, 

30) 

31from couchers.models.public_trips import PublicTrip, PublicTripStatus 

32from couchers.proto import ( 

33 api_pb2, 

34 auth_pb2, 

35 messages_pb2, 

36 requests_pb2, 

37) 

38from couchers.proto.internal import unsubscribe_pb2 

39from couchers.rate_limits.definitions import RATE_LIMIT_DEFINITIONS, RATE_LIMIT_HOURS 

40from couchers.utils import create_coordinate, create_polygon_lat_lng, now, to_multi, today 

41from tests.fixtures.db import backdate_conversations, generate_user 

42from tests.fixtures.misc import EmailCollector, PushCollector 

43from tests.fixtures.sessions import api_session, auth_api_session, requests_session 

44from tests.test_public_trips import _create_trip_directly, _make_node 

45 

46 

47def valid_request_text(text: str = "Test request") -> str: 

48 """Pads a request text to a valid length.""" 

49 # Request lengths are measured in utf-16 code units to match the frontend. 

50 utf16_length = len(text.encode("utf-16-le")) // 2 

51 if utf16_length >= HOST_REQUEST_MIN_LENGTH_UTF16: 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true

52 return text 

53 padding_length = HOST_REQUEST_MIN_LENGTH_UTF16 - utf16_length 

54 return text + ("_" * padding_length) # Each "_" adds one utf16 code unit. 

55 

56 

57def test_create_request(db, moderator): 

58 user1, token1 = generate_user() 

59 hosting_city = "Morningside Heights, New York City" 

60 hosting_lat = 40.8086 

61 hosting_lng = -73.9616 

62 hosting_radius = 500 

63 user2, token2 = generate_user( 

64 city=hosting_city, 

65 geom=create_coordinate(hosting_lat, hosting_lng), 

66 geom_radius=hosting_radius, 

67 ) 

68 

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

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

71 today_minus_2 = today() - timedelta(days=2) 

72 today_minus_3 = today() - timedelta(days=3) 

73 

74 with requests_session(token1) as api: 

75 with pytest.raises(grpc.RpcError) as e: 

76 api.CreateHostRequest( 

77 requests_pb2.CreateHostRequestReq( 

78 host_user_id=user1.id, 

79 from_date=today_plus_2.isoformat(), 

80 to_date=today_plus_3.isoformat(), 

81 text=valid_request_text(), 

82 ) 

83 ) 

84 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

85 assert e.value.details() == "Can't request hosting from yourself." 

86 

87 with pytest.raises(grpc.RpcError) as e: 

88 api.CreateHostRequest( 

89 requests_pb2.CreateHostRequestReq( 

90 host_user_id=999, 

91 from_date=today_plus_2.isoformat(), 

92 to_date=today_plus_3.isoformat(), 

93 text=valid_request_text(), 

94 ) 

95 ) 

96 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

97 assert e.value.details() == "Couldn't find that user." 

98 

99 with pytest.raises(grpc.RpcError) as e: 

100 api.CreateHostRequest( 

101 requests_pb2.CreateHostRequestReq( 

102 host_user_id=user2.id, 

103 from_date=today_plus_3.isoformat(), 

104 to_date=today_plus_2.isoformat(), 

105 text=valid_request_text(), 

106 ) 

107 ) 

108 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

109 assert e.value.details() == "From date can't be after to date." 

110 

111 with pytest.raises(grpc.RpcError) as e: 

112 api.CreateHostRequest( 

113 requests_pb2.CreateHostRequestReq( 

114 host_user_id=user2.id, 

115 from_date=today_minus_3.isoformat(), 

116 to_date=today_plus_2.isoformat(), 

117 text=valid_request_text(), 

118 ) 

119 ) 

120 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

121 assert e.value.details() == "From date must be today or later." 

122 

123 with pytest.raises(grpc.RpcError) as e: 

124 api.CreateHostRequest( 

125 requests_pb2.CreateHostRequestReq( 

126 host_user_id=user2.id, 

127 from_date=today_plus_2.isoformat(), 

128 to_date=today_minus_2.isoformat(), 

129 text=valid_request_text(), 

130 ) 

131 ) 

132 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

133 assert e.value.details() == "From date can't be after to date." 

134 

135 with pytest.raises(grpc.RpcError) as e: 

136 api.CreateHostRequest( 

137 requests_pb2.CreateHostRequestReq( 

138 host_user_id=user2.id, 

139 from_date="2020-00-06", 

140 to_date=today_minus_2.isoformat(), 

141 text=valid_request_text(), 

142 ) 

143 ) 

144 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

145 assert e.value.details() == "Invalid date." 

146 

147 with pytest.raises(grpc.RpcError) as e: 

148 api.CreateHostRequest( 

149 requests_pb2.CreateHostRequestReq( 

150 host_user_id=user2.id, 

151 from_date=today_plus_2.isoformat(), 

152 to_date=today_plus_3.isoformat(), 

153 text="Too short.", 

154 ) 

155 ) 

156 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

157 assert e.value.details() == "Host request cannot be shorter than 250 characters." 

158 

159 res = api.CreateHostRequest( 

160 requests_pb2.CreateHostRequestReq( 

161 host_user_id=user2.id, 

162 from_date=today_plus_2.isoformat(), 

163 to_date=today_plus_3.isoformat(), 

164 text=valid_request_text(), 

165 ) 

166 ) 

167 host_request_id = res.host_request_id 

168 

169 moderator.approve_host_request(host_request_id) 

170 

171 with requests_session(token1) as api: 

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

173 

174 assert len(host_requests) == 1 

175 hr = host_requests[0] 

176 

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

178 

179 assert hr.hosting_city == hosting_city 

180 assert round(hr.hosting_lat, 4) == hosting_lat 

181 assert round(hr.hosting_lng, 4) == hosting_lng 

182 assert hr.hosting_radius == hosting_radius 

183 

184 today_ = today() 

185 today_plus_one_year = today_ + timedelta(days=365) 

186 today_plus_one_year_plus_2 = today_plus_one_year + timedelta(days=2) 

187 today_plus_one_year_plus_3 = today_plus_one_year + timedelta(days=3) 

188 with pytest.raises(grpc.RpcError) as e: 

189 api.CreateHostRequest( 

190 requests_pb2.CreateHostRequestReq( 

191 host_user_id=user2.id, 

192 from_date=today_plus_one_year_plus_2.isoformat(), 

193 to_date=today_plus_one_year_plus_3.isoformat(), 

194 text=valid_request_text("Test from date after one year"), 

195 ) 

196 ) 

197 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

198 assert e.value.details() == "The start date must be within one year from today." 

199 

200 with pytest.raises(grpc.RpcError) as e: 

201 api.CreateHostRequest( 

202 requests_pb2.CreateHostRequestReq( 

203 host_user_id=user2.id, 

204 from_date=today_plus_2.isoformat(), 

205 to_date=today_plus_one_year_plus_3.isoformat(), 

206 text=valid_request_text("Test to date one year after from date"), 

207 ) 

208 ) 

209 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

210 assert e.value.details() == "You cannot request to stay with someone for longer than one year." 

211 

212 

213def test_create_host_request_rejects_date_past_in_host_timezone(db): 

214 # When the host's timezone has already rolled over to the next day, a 

215 # from_date of "today in UTC" is in the past from the host's perspective and 

216 # must be rejected. The frontend blocks this date before submission; the 

217 # backend enforces the same rule for consistency. 

218 user1, token1 = generate_user() 

219 # geom inside the fake Europe/Helsinki timezone polygon used in tests 

220 user2, _ = generate_user(geom=create_coordinate(61, 25)) 

221 

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

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

224 

225 with patch( 

226 "couchers.servicers.requests.today_in_timezone", 

227 side_effect=lambda tz: fake_today_by_tz.get(tz, date(2026, 1, 15)), 

228 ): 

229 with requests_session(token1) as api: 

230 with pytest.raises(grpc.RpcError) as e: 

231 api.CreateHostRequest( 

232 requests_pb2.CreateHostRequestReq( 

233 host_user_id=user2.id, 

234 from_date="2026-01-15", 

235 to_date="2026-01-18", 

236 text=valid_request_text(), 

237 ) 

238 ) 

239 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

240 

241 

242def test_create_host_request_date_valid_when_host_behind_requester(db): 

243 # Simulate the opposite timezone direction: the host (America/New_York) is 

244 # still on 2026-01-15 while the requester has already rolled into 2026-01-16. 

245 # A from_date of 2026-01-16 is "today" for the requester and "tomorrow" for 

246 # the host — must be accepted without issue. 

247 user1, token1 = generate_user() 

248 user2, _ = generate_user() # default geom resolves to America/New_York 

249 

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

251 

252 with patch( 

253 "couchers.servicers.requests.today_in_timezone", 

254 side_effect=lambda tz: fake_today_by_tz.get(tz, date(2026, 1, 15)), 

255 ): 

256 with requests_session(token1) as api: 

257 res = api.CreateHostRequest( 

258 requests_pb2.CreateHostRequestReq( 

259 host_user_id=user2.id, 

260 from_date="2026-01-16", 

261 to_date="2026-01-20", 

262 text=valid_request_text(), 

263 ) 

264 ) 

265 assert res.host_request_id 

266 

267 

268def test_create_request_duplicate_within_window(db): 

269 """A second request to the same host for overlapping dates inside the window is rejected, so a 

270 client that resends (or a user who re-taps Send) doesn't create a pile of duplicates.""" 

271 user1, token1 = generate_user() 

272 user2, _ = generate_user() 

273 user3, _ = generate_user() 

274 from_date = today() + timedelta(days=10) 

275 to_date = today() + timedelta(days=15) 

276 

277 with requests_session(token1) as api: 

278 assert api.CreateHostRequest( 

279 requests_pb2.CreateHostRequestReq( 

280 host_user_id=user2.id, 

281 from_date=from_date.isoformat(), 

282 to_date=to_date.isoformat(), 

283 text=valid_request_text(), 

284 ) 

285 ).host_request_id 

286 

287 with pytest.raises(grpc.RpcError) as e: 

288 api.CreateHostRequest( 

289 requests_pb2.CreateHostRequestReq( 

290 host_user_id=user2.id, 

291 from_date=from_date.isoformat(), 

292 to_date=to_date.isoformat(), 

293 text=valid_request_text(), 

294 ) 

295 ) 

296 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

297 assert "past 3 hours" in (e.value.details() or "") 

298 

299 # partly overlapping dates are blocked too 

300 with pytest.raises(grpc.RpcError) as e: 

301 api.CreateHostRequest( 

302 requests_pb2.CreateHostRequestReq( 

303 host_user_id=user2.id, 

304 from_date=(to_date - timedelta(days=1)).isoformat(), 

305 to_date=(to_date + timedelta(days=5)).isoformat(), 

306 text=valid_request_text(), 

307 ) 

308 ) 

309 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

310 

311 # a different host is unaffected 

312 assert api.CreateHostRequest( 

313 requests_pb2.CreateHostRequestReq( 

314 host_user_id=user3.id, 

315 from_date=from_date.isoformat(), 

316 to_date=to_date.isoformat(), 

317 text=valid_request_text(), 

318 ) 

319 ).host_request_id 

320 

321 

322def test_create_request_duplicate_window_allows_other_dates(db): 

323 """Only overlapping stays count as duplicates, including a stay starting the day another one 

324 ends.""" 

325 user1, token1 = generate_user() 

326 user2, _ = generate_user() 

327 from_date = today() + timedelta(days=10) 

328 to_date = today() + timedelta(days=15) 

329 

330 with requests_session(token1) as api: 

331 assert api.CreateHostRequest( 

332 requests_pb2.CreateHostRequestReq( 

333 host_user_id=user2.id, 

334 from_date=from_date.isoformat(), 

335 to_date=to_date.isoformat(), 

336 text=valid_request_text(), 

337 ) 

338 ).host_request_id 

339 

340 assert api.CreateHostRequest( 

341 requests_pb2.CreateHostRequestReq( 

342 host_user_id=user2.id, 

343 from_date=to_date.isoformat(), 

344 to_date=(to_date + timedelta(days=5)).isoformat(), 

345 text=valid_request_text(), 

346 ) 

347 ).host_request_id 

348 

349 assert api.CreateHostRequest( 

350 requests_pb2.CreateHostRequestReq( 

351 host_user_id=user2.id, 

352 from_date=(from_date + timedelta(days=90)).isoformat(), 

353 to_date=(to_date + timedelta(days=90)).isoformat(), 

354 text=valid_request_text(), 

355 ) 

356 ).host_request_id 

357 

358 

359def test_create_request_duplicate_allowed_after_window(db): 

360 user1, token1 = generate_user() 

361 user2, _ = generate_user() 

362 from_date = today() + timedelta(days=10) 

363 to_date = today() + timedelta(days=15) 

364 

365 with requests_session(token1) as api: 

366 api.CreateHostRequest( 

367 requests_pb2.CreateHostRequestReq( 

368 host_user_id=user2.id, 

369 from_date=from_date.isoformat(), 

370 to_date=to_date.isoformat(), 

371 text=valid_request_text(), 

372 ) 

373 ) 

374 backdate_conversations() 

375 

376 assert api.CreateHostRequest( 

377 requests_pb2.CreateHostRequestReq( 

378 host_user_id=user2.id, 

379 from_date=from_date.isoformat(), 

380 to_date=to_date.isoformat(), 

381 text=valid_request_text(), 

382 ) 

383 ).host_request_id 

384 

385 

386def test_create_request_duplicate_window_ignores_public_trip_offers(db): 

387 """Offers on public trips are deduplicated per trip, so two offers to the same traveler for 

388 two different trips must both go through.""" 

389 surfer, _ = generate_user() 

390 host, host_token = generate_user() 

391 

392 trip_1_from = today() + timedelta(days=10) 

393 trip_1_to = today() + timedelta(days=20) 

394 trip_2_from = today() + timedelta(days=40) 

395 trip_2_to = today() + timedelta(days=50) 

396 trip_1_id = _create_public_trip(surfer.id, trip_1_from, trip_1_to) 

397 trip_2_id = _create_public_trip(surfer.id, trip_2_from, trip_2_to) 

398 

399 with requests_session(host_token) as api: 

400 assert api.CreateHostRequest( 

401 requests_pb2.CreateHostRequestReq( 

402 host_user_id=surfer.id, 

403 from_date=trip_1_from.isoformat(), 

404 to_date=trip_1_to.isoformat(), 

405 text=valid_request_text(), 

406 public_trip_id=trip_1_id, 

407 ) 

408 ).host_request_id 

409 

410 assert api.CreateHostRequest( 

411 requests_pb2.CreateHostRequestReq( 

412 host_user_id=surfer.id, 

413 from_date=trip_2_from.isoformat(), 

414 to_date=trip_2_to.isoformat(), 

415 text=valid_request_text(), 

416 public_trip_id=trip_2_id, 

417 ) 

418 ).host_request_id 

419 

420 # the window only counts non-trip requests, so trip offers don't block a plain request 

421 # even for dates they cover 

422 assert api.CreateHostRequest( 

423 requests_pb2.CreateHostRequestReq( 

424 host_user_id=surfer.id, 

425 from_date=(trip_1_from + timedelta(days=2)).isoformat(), 

426 to_date=(trip_1_to - timedelta(days=2)).isoformat(), 

427 text=valid_request_text(), 

428 ) 

429 ).host_request_id 

430 

431 

432def test_create_request_incomplete_profile(db): 

433 user1, token1 = generate_user(complete_profile=False) 

434 user2, _ = generate_user() 

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

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

437 with requests_session(token1) as api: 

438 with pytest.raises(grpc.RpcError) as e: 

439 api.CreateHostRequest( 

440 requests_pb2.CreateHostRequestReq( 

441 host_user_id=user2.id, 

442 from_date=today_plus_2.isoformat(), 

443 to_date=today_plus_3.isoformat(), 

444 text=valid_request_text(), 

445 ) 

446 ) 

447 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

448 assert e.value.details() == "You have to complete your profile before you can send a request." 

449 

450 

451def test_excessive_requests_are_reported(db, low_rate_limits, email_collector: EmailCollector): 

452 """Test that excessive host requests are first reported in a warning email and finally lead blocking of further requests.""" 

453 user, token = generate_user() 

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

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

456 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.host_request] 

457 with requests_session(token) as api: 

458 # Test warning email 

459 for _ in range(rate_limit_definition.warning_limit): 

460 host_user, _ = generate_user() 

461 _ = api.CreateHostRequest( 

462 requests_pb2.CreateHostRequestReq( 

463 host_user_id=host_user.id, 

464 from_date=today_plus_2.isoformat(), 

465 to_date=today_plus_3.isoformat(), 

466 text=valid_request_text(), 

467 ) 

468 ) 

469 

470 assert email_collector.count_for_reports() == 0 

471 host_user, _ = generate_user() 

472 _ = api.CreateHostRequest( 

473 requests_pb2.CreateHostRequestReq( 

474 host_user_id=host_user.id, 

475 from_date=today_plus_2.isoformat(), 

476 to_date=today_plus_3.isoformat(), 

477 text=valid_request_text("Excessive test request"), 

478 ) 

479 ) 

480 

481 email = email_collector.pop_for_reports(last=True) 

482 assert email.plain.startswith( 

483 f"User {user.username} has sent {rate_limit_definition.warning_limit} host requests in the past {RATE_LIMIT_HOURS} hours." 

484 ) 

485 

486 # Test ban after exceeding HOST_REQUEST_HARD_LIMIT 

487 for _ in range(rate_limit_definition.hard_limit - rate_limit_definition.warning_limit - 1): 

488 host_user, _ = generate_user() 

489 _ = api.CreateHostRequest( 

490 requests_pb2.CreateHostRequestReq( 

491 host_user_id=host_user.id, 

492 from_date=today_plus_2.isoformat(), 

493 to_date=today_plus_3.isoformat(), 

494 text=valid_request_text(), 

495 ) 

496 ) 

497 

498 assert email_collector.count_for_reports() == 0 

499 

500 host_user, _ = generate_user() 

501 with pytest.raises(grpc.RpcError) as exc_info: 

502 _ = api.CreateHostRequest( 

503 requests_pb2.CreateHostRequestReq( 

504 host_user_id=host_user.id, 

505 from_date=today_plus_2.isoformat(), 

506 to_date=today_plus_3.isoformat(), 

507 text=valid_request_text("Excessive test request"), 

508 ) 

509 ) 

510 assert exc_info.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED 

511 assert ( 

512 exc_info.value.details() 

513 == "You have sent a lot of host requests in the past 24 hours. To avoid spam, you can't send any more for now." 

514 ) 

515 

516 email = email_collector.pop_for_reports(last=True) 

517 assert email.plain.startswith( 

518 f"User {user.username} has sent {rate_limit_definition.hard_limit} host requests in the past {RATE_LIMIT_HOURS} hours." 

519 ) 

520 assert "The user has been blocked from sending further host requests for now." in email.plain 

521 

522 

523def add_message(db, text, author_id, conversation_id): 

524 with session_scope() as session: 

525 message = Message( 

526 conversation_id=conversation_id, author_id=author_id, text=text, message_type=MessageType.text 

527 ) 

528 

529 session.add(message) 

530 

531 

532def test_GetHostRequest(db): 

533 user1, token1 = generate_user() 

534 user2, token2 = generate_user() 

535 user3, token3 = generate_user() 

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

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

538 with requests_session(token1) as api: 

539 host_request_id = api.CreateHostRequest( 

540 requests_pb2.CreateHostRequestReq( 

541 host_user_id=user2.id, 

542 from_date=today_plus_2.isoformat(), 

543 to_date=today_plus_3.isoformat(), 

544 text=valid_request_text("Test request 1"), 

545 ) 

546 ).host_request_id 

547 

548 with pytest.raises(grpc.RpcError) as e: 

549 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=999)) 

550 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

551 assert e.value.details() == "Couldn't find that host request." 

552 

553 api.SendHostRequestMessage( 

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

555 ) 

556 

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

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

559 

560 

561def test_ListHostRequests(db, moderator): 

562 user1, token1 = generate_user() 

563 user2, token2 = generate_user() 

564 user3, token3 = generate_user() 

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

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

567 with requests_session(token1) as api: 

568 host_request_1 = api.CreateHostRequest( 

569 requests_pb2.CreateHostRequestReq( 

570 host_user_id=user2.id, 

571 from_date=today_plus_2.isoformat(), 

572 to_date=today_plus_3.isoformat(), 

573 text=valid_request_text("Test request 1"), 

574 ) 

575 ).host_request_id 

576 

577 host_request_2 = api.CreateHostRequest( 

578 requests_pb2.CreateHostRequestReq( 

579 host_user_id=user3.id, 

580 from_date=today_plus_2.isoformat(), 

581 to_date=today_plus_3.isoformat(), 

582 text=valid_request_text("Test request 2"), 

583 ) 

584 ).host_request_id 

585 

586 moderator.approve_host_request(host_request_1) 

587 moderator.approve_host_request(host_request_2) 

588 

589 with requests_session(token1) as api: 

590 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)) 

591 assert res.no_more 

592 assert len(res.host_requests) == 2 

593 

594 with requests_session(token2) as api: 

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

596 assert res.no_more 

597 assert len(res.host_requests) == 1 

598 assert res.host_requests[0].latest_message.text.text == valid_request_text("Test request 1") 

599 assert res.host_requests[0].surfer_user_id == user1.id 

600 assert res.host_requests[0].host_user_id == user2.id 

601 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_PENDING 

602 

603 add_message(db, "Test request 1 message 1", user2.id, host_request_1) 

604 add_message(db, "Test request 1 message 2", user2.id, host_request_1) 

605 add_message(db, "Test request 1 message 3", user2.id, host_request_1) 

606 

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

608 assert res.host_requests[0].latest_message.text.text == "Test request 1 message 3" 

609 

610 host_request_3 = api.CreateHostRequest( 

611 requests_pb2.CreateHostRequestReq( 

612 host_user_id=user1.id, 

613 from_date=today_plus_2.isoformat(), 

614 to_date=today_plus_3.isoformat(), 

615 text=valid_request_text("Test request 3"), 

616 ) 

617 ).host_request_id 

618 

619 moderator.approve_host_request(host_request_3) 

620 

621 add_message(db, "Test request 2 message 1", user1.id, host_request_2) 

622 add_message(db, "Test request 2 message 2", user3.id, host_request_2) 

623 

624 with requests_session(token3) as api: 

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

626 assert res.no_more 

627 assert len(res.host_requests) == 1 

628 assert res.host_requests[0].latest_message.text.text == "Test request 2 message 2" 

629 

630 with requests_session(token1) as api: 

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

632 assert len(res.host_requests) == 1 

633 

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

635 assert len(res.host_requests) == 3 

636 

637 

638def test_ListHostRequests_pagination_regression(db, moderator): 

639 """ 

640 ListHostRequests was skipping a request when getting multiple pages 

641 """ 

642 user1, token1 = generate_user() 

643 user2, token2 = generate_user() 

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

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

646 with requests_session(token1) as api: 

647 host_request_1 = api.CreateHostRequest( 

648 requests_pb2.CreateHostRequestReq( 

649 host_user_id=user2.id, 

650 from_date=today_plus_2.isoformat(), 

651 to_date=today_plus_3.isoformat(), 

652 text=valid_request_text("Test request 1"), 

653 ) 

654 ).host_request_id 

655 backdate_conversations() 

656 

657 host_request_2 = api.CreateHostRequest( 

658 requests_pb2.CreateHostRequestReq( 

659 host_user_id=user2.id, 

660 from_date=today_plus_2.isoformat(), 

661 to_date=today_plus_3.isoformat(), 

662 text=valid_request_text("Test request 2"), 

663 ) 

664 ).host_request_id 

665 backdate_conversations() 

666 

667 host_request_3 = api.CreateHostRequest( 

668 requests_pb2.CreateHostRequestReq( 

669 host_user_id=user2.id, 

670 from_date=today_plus_2.isoformat(), 

671 to_date=today_plus_3.isoformat(), 

672 text=valid_request_text("Test request 3"), 

673 ) 

674 ).host_request_id 

675 

676 moderator.approve_host_request(host_request_1) 

677 moderator.approve_host_request(host_request_2) 

678 moderator.approve_host_request(host_request_3) 

679 

680 with requests_session(token2) as api: 

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

682 assert res.no_more 

683 assert len(res.host_requests) == 3 

684 assert res.host_requests[0].latest_message.text.text == valid_request_text("Test request 3") 

685 assert res.host_requests[1].latest_message.text.text == valid_request_text("Test request 2") 

686 assert res.host_requests[2].latest_message.text.text == valid_request_text("Test request 1") 

687 

688 with requests_session(token2) as api: 

689 api.RespondHostRequest( 

690 requests_pb2.RespondHostRequestReq( 

691 host_request_id=host_request_2, 

692 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

693 text="Accepting host request 2", 

694 ) 

695 ) 

696 api.RespondHostRequest( 

697 requests_pb2.RespondHostRequestReq( 

698 host_request_id=host_request_1, 

699 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

700 text="Accepting host request 1", 

701 ) 

702 ) 

703 api.RespondHostRequest( 

704 requests_pb2.RespondHostRequestReq( 

705 host_request_id=host_request_3, 

706 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

707 text="Accepting host request 3", 

708 ) 

709 ) 

710 

711 with requests_session(token2) as api: 

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

713 assert res.no_more 

714 assert len(res.host_requests) == 3 

715 assert res.host_requests[0].latest_message.text.text == "Accepting host request 3" 

716 assert res.host_requests[1].latest_message.text.text == "Accepting host request 1" 

717 assert res.host_requests[2].latest_message.text.text == "Accepting host request 2" 

718 

719 with requests_session(token2) as api: 

720 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_received=True, number=1)) 

721 assert not res.no_more 

722 assert len(res.host_requests) == 1 

723 assert res.host_requests[0].latest_message.text.text == "Accepting host request 3" 

724 res = api.ListHostRequests( 

725 requests_pb2.ListHostRequestsReq(only_received=True, number=1, page_token=res.next_page_token) 

726 ) 

727 assert not res.no_more 

728 assert len(res.host_requests) == 1 

729 assert res.host_requests[0].latest_message.text.text == "Accepting host request 1" 

730 res = api.ListHostRequests( 

731 requests_pb2.ListHostRequestsReq(only_received=True, number=1, page_token=res.next_page_token) 

732 ) 

733 assert res.no_more 

734 assert len(res.host_requests) == 1 

735 assert res.host_requests[0].latest_message.text.text == "Accepting host request 2" 

736 

737 

738def test_ListHostRequests_sort_by_from_date(db, moderator): 

739 user1, token1 = generate_user() 

740 user2, token2 = generate_user() 

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

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

743 today_plus_5 = today() + timedelta(days=5) 

744 today_plus_7 = today() + timedelta(days=7) 

745 today_plus_10 = today() + timedelta(days=10) 

746 

747 with requests_session(token1) as api: 

748 hr_late = api.CreateHostRequest( 

749 requests_pb2.CreateHostRequestReq( 

750 host_user_id=user2.id, 

751 from_date=today_plus_7.isoformat(), 

752 to_date=today_plus_10.isoformat(), 

753 text=valid_request_text("Late request"), 

754 ) 

755 ).host_request_id 

756 

757 hr_early = api.CreateHostRequest( 

758 requests_pb2.CreateHostRequestReq( 

759 host_user_id=user2.id, 

760 from_date=today_plus_2.isoformat(), 

761 to_date=today_plus_3.isoformat(), 

762 text=valid_request_text("Early request"), 

763 ) 

764 ).host_request_id 

765 

766 hr_mid = api.CreateHostRequest( 

767 requests_pb2.CreateHostRequestReq( 

768 host_user_id=user2.id, 

769 from_date=today_plus_5.isoformat(), 

770 to_date=today_plus_7.isoformat(), 

771 text=valid_request_text("Mid request"), 

772 ) 

773 ).host_request_id 

774 

775 moderator.approve_host_request(hr_late) 

776 moderator.approve_host_request(hr_early) 

777 moderator.approve_host_request(hr_mid) 

778 

779 with requests_session(token2) as api: 

780 # default sort: latest message first (creation order reversed) 

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

782 assert res.no_more 

783 assert [r.host_request_id for r in res.host_requests] == [hr_mid, hr_early, hr_late] 

784 

785 # from_date sort: ascending by travel date 

786 res = api.ListHostRequests( 

787 requests_pb2.ListHostRequestsReq( 

788 only_received=True, 

789 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

790 ) 

791 ) 

792 assert res.no_more 

793 assert [r.host_request_id for r in res.host_requests] == [hr_early, hr_mid, hr_late] 

794 

795 

796def test_ListHostRequests_sort_by_from_date_pagination(db, moderator): 

797 """Pagination cursor correctly handles both different and identical from_dates.""" 

798 user1, token1 = generate_user() 

799 user2, token2 = generate_user() 

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

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

802 today_plus_5 = today() + timedelta(days=5) 

803 

804 with requests_session(token1) as api: 

805 hr_a = api.CreateHostRequest( 

806 requests_pb2.CreateHostRequestReq( 

807 host_user_id=user2.id, 

808 from_date=today_plus_2.isoformat(), 

809 to_date=today_plus_3.isoformat(), 

810 text=valid_request_text("Request A"), 

811 ) 

812 ).host_request_id 

813 backdate_conversations() 

814 

815 # Same from_date as A — tiebreaker by conversation_id 

816 hr_b = api.CreateHostRequest( 

817 requests_pb2.CreateHostRequestReq( 

818 host_user_id=user2.id, 

819 from_date=today_plus_2.isoformat(), 

820 to_date=today_plus_3.isoformat(), 

821 text=valid_request_text("Request B"), 

822 ) 

823 ).host_request_id 

824 backdate_conversations() 

825 

826 hr_c = api.CreateHostRequest( 

827 requests_pb2.CreateHostRequestReq( 

828 host_user_id=user2.id, 

829 from_date=today_plus_5.isoformat(), 

830 to_date=(today_plus_5 + timedelta(days=2)).isoformat(), 

831 text=valid_request_text("Request C"), 

832 ) 

833 ).host_request_id 

834 

835 moderator.approve_host_request(hr_a) 

836 moderator.approve_host_request(hr_b) 

837 moderator.approve_host_request(hr_c) 

838 

839 with requests_session(token2) as api: 

840 res = api.ListHostRequests( 

841 requests_pb2.ListHostRequestsReq( 

842 only_received=True, 

843 number=1, 

844 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

845 ) 

846 ) 

847 assert not res.no_more 

848 assert len(res.host_requests) == 1 

849 assert res.host_requests[0].host_request_id == hr_a 

850 

851 res = api.ListHostRequests( 

852 requests_pb2.ListHostRequestsReq( 

853 only_received=True, 

854 number=1, 

855 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

856 page_token=res.next_page_token, 

857 ) 

858 ) 

859 assert not res.no_more 

860 assert len(res.host_requests) == 1 

861 assert res.host_requests[0].host_request_id == hr_b 

862 

863 res = api.ListHostRequests( 

864 requests_pb2.ListHostRequestsReq( 

865 only_received=True, 

866 number=1, 

867 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

868 page_token=res.next_page_token, 

869 ) 

870 ) 

871 assert res.no_more 

872 assert len(res.host_requests) == 1 

873 assert res.host_requests[0].host_request_id == hr_c 

874 

875 

876def test_ListHostRequests_active_filter(db, moderator): 

877 user1, token1 = generate_user() 

878 user2, token2 = generate_user() 

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

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

881 

882 with requests_session(token1) as api: 

883 request_id = api.CreateHostRequest( 

884 requests_pb2.CreateHostRequestReq( 

885 host_user_id=user2.id, 

886 from_date=today_plus_2.isoformat(), 

887 to_date=today_plus_3.isoformat(), 

888 text=valid_request_text("Test request 1"), 

889 ) 

890 ).host_request_id 

891 

892 moderator.approve_host_request(request_id) 

893 

894 with requests_session(token1) as api: 

895 api.RespondHostRequest( 

896 requests_pb2.RespondHostRequestReq( 

897 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

898 ) 

899 ) 

900 

901 with requests_session(token2) as api: 

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

903 assert len(res.host_requests) == 1 

904 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True)) 

905 assert len(res.host_requests) == 0 

906 

907 

908def test_ListHostRequests_active_filter_excludes_past(db, moderator): 

909 """only_active must exclude requests whose end date has passed (regression test for <= bug).""" 

910 user1, token1 = generate_user() 

911 user2, token2 = generate_user() 

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

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

914 

915 with requests_session(token1) as api: 

916 request_id = api.CreateHostRequest( 

917 requests_pb2.CreateHostRequestReq( 

918 host_user_id=user2.id, 

919 from_date=today_plus_2.isoformat(), 

920 to_date=today_plus_3.isoformat(), 

921 text=valid_request_text("Past stay regression"), 

922 ) 

923 ).host_request_id 

924 

925 moderator.approve_host_request(request_id) 

926 

927 with requests_session(token2) as api: 

928 api.RespondHostRequest( 

929 requests_pb2.RespondHostRequestReq( 

930 host_request_id=request_id, 

931 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

932 ) 

933 ) 

934 

935 # Future request is visible with only_active 

936 with requests_session(token2) as api: 

937 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True)) 

938 assert len(res.host_requests) == 1 

939 

940 # Move dates into the past 

941 with session_scope() as session: 

942 hr = session.execute(select(HostRequest).where(HostRequest.conversation_id == request_id)).scalar_one() 

943 hr.from_date = today() - timedelta(days=3) 

944 hr.to_date = today() - timedelta(days=2) 

945 

946 # Past request must be excluded by only_active 

947 with requests_session(token2) as api: 

948 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_active=True)) 

949 assert len(res.host_requests) == 0 

950 

951 # Still visible without the filter 

952 with requests_session(token2) as api: 

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

954 assert len(res.host_requests) == 1 

955 

956 

957def test_ListHostRequests_status_in_filter(db, moderator): 

958 """status_in must return only requests with the specified statuses.""" 

959 user1, token1 = generate_user() 

960 user2, token2 = generate_user() 

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

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

963 today_plus_4 = today() + timedelta(days=4) 

964 today_plus_5 = today() + timedelta(days=5) 

965 

966 # Create a pending request 

967 with requests_session(token1) as api: 

968 pending_id = api.CreateHostRequest( 

969 requests_pb2.CreateHostRequestReq( 

970 host_user_id=user2.id, 

971 from_date=today_plus_2.isoformat(), 

972 to_date=today_plus_3.isoformat(), 

973 text=valid_request_text("Pending"), 

974 ) 

975 ).host_request_id 

976 

977 moderator.approve_host_request(pending_id) 

978 

979 # Create an accepted request 

980 with requests_session(token1) as api: 

981 accepted_id = api.CreateHostRequest( 

982 requests_pb2.CreateHostRequestReq( 

983 host_user_id=user2.id, 

984 from_date=today_plus_4.isoformat(), 

985 to_date=today_plus_5.isoformat(), 

986 text=valid_request_text("Accepted"), 

987 ) 

988 ).host_request_id 

989 

990 moderator.approve_host_request(accepted_id) 

991 

992 with requests_session(token2) as api: 

993 api.RespondHostRequest( 

994 requests_pb2.RespondHostRequestReq( 

995 host_request_id=accepted_id, 

996 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

997 ) 

998 ) 

999 

1000 # Filter to accepted only 

1001 with requests_session(token2) as api: 

1002 res = api.ListHostRequests( 

1003 requests_pb2.ListHostRequestsReq( 

1004 status_in=[messages_pb2.HOST_REQUEST_STATUS_ACCEPTED], 

1005 ) 

1006 ) 

1007 assert len(res.host_requests) == 1 

1008 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1009 

1010 # Filter to pending only 

1011 with requests_session(token2) as api: 

1012 res = api.ListHostRequests( 

1013 requests_pb2.ListHostRequestsReq( 

1014 status_in=[messages_pb2.HOST_REQUEST_STATUS_PENDING], 

1015 ) 

1016 ) 

1017 assert len(res.host_requests) == 1 

1018 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_PENDING 

1019 

1020 # Filter to accepted + pending — both appear 

1021 with requests_session(token2) as api: 

1022 res = api.ListHostRequests( 

1023 requests_pb2.ListHostRequestsReq( 

1024 status_in=[ 

1025 messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1026 messages_pb2.HOST_REQUEST_STATUS_PENDING, 

1027 ], 

1028 ) 

1029 ) 

1030 assert len(res.host_requests) == 2 

1031 

1032 # Filter to confirmed — none appear 

1033 with requests_session(token2) as api: 

1034 res = api.ListHostRequests( 

1035 requests_pb2.ListHostRequestsReq( 

1036 status_in=[messages_pb2.HOST_REQUEST_STATUS_CONFIRMED], 

1037 ) 

1038 ) 

1039 assert len(res.host_requests) == 0 

1040 

1041 # Empty status_in — all requests returned (no filter applied) 

1042 with requests_session(token2) as api: 

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

1044 assert len(res.host_requests) == 2 

1045 

1046 

1047def test_RespondHostRequests(db, moderator): 

1048 user1, token1 = generate_user() 

1049 user2, token2 = generate_user() 

1050 user3, token3 = generate_user() 

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

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

1053 

1054 with requests_session(token1) as api: 

1055 request_id = api.CreateHostRequest( 

1056 requests_pb2.CreateHostRequestReq( 

1057 host_user_id=user2.id, 

1058 from_date=today_plus_2.isoformat(), 

1059 to_date=today_plus_3.isoformat(), 

1060 text=valid_request_text("Test request 1"), 

1061 ) 

1062 ).host_request_id 

1063 

1064 moderator.approve_host_request(request_id) 

1065 

1066 # another user can't access 

1067 with requests_session(token3) as api: 

1068 with pytest.raises(grpc.RpcError) as e: 

1069 api.RespondHostRequest( 

1070 requests_pb2.RespondHostRequestReq( 

1071 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1072 ) 

1073 ) 

1074 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

1075 assert e.value.details() == "Couldn't find that host request." 

1076 

1077 with requests_session(token1) as api: 

1078 with pytest.raises(grpc.RpcError) as e: 

1079 api.RespondHostRequest( 

1080 requests_pb2.RespondHostRequestReq( 

1081 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1082 ) 

1083 ) 

1084 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1085 assert e.value.details() == "You are not the host of this request." 

1086 

1087 with requests_session(token2) as api: 

1088 # non existing id 

1089 with pytest.raises(grpc.RpcError) as e: 

1090 api.RespondHostRequest( 

1091 requests_pb2.RespondHostRequestReq( 

1092 host_request_id=9999, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1093 ) 

1094 ) 

1095 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

1096 

1097 # host can't confirm or cancel (host should accept/reject) 

1098 with pytest.raises(grpc.RpcError) as e: 

1099 api.RespondHostRequest( 

1100 requests_pb2.RespondHostRequestReq( 

1101 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1102 ) 

1103 ) 

1104 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1105 assert e.value.details() == "You can't set the host request status to that." 

1106 with pytest.raises(grpc.RpcError) as e: 

1107 api.RespondHostRequest( 

1108 requests_pb2.RespondHostRequestReq( 

1109 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1110 ) 

1111 ) 

1112 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1113 assert e.value.details() == "You can't set the host request status to that." 

1114 

1115 api.RespondHostRequest( 

1116 requests_pb2.RespondHostRequestReq( 

1117 host_request_id=request_id, 

1118 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED, 

1119 text="Test rejection message", 

1120 ) 

1121 ) 

1122 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=request_id)) 

1123 assert res.messages[0].text.text == "Test rejection message" 

1124 assert res.messages[1].WhichOneof("content") == "host_request_status_changed" 

1125 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1126 # should be able to move from rejected -> accepted 

1127 api.RespondHostRequest( 

1128 requests_pb2.RespondHostRequestReq( 

1129 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1130 ) 

1131 ) 

1132 

1133 with requests_session(token1) as api: 

1134 # can't make pending 

1135 with pytest.raises(grpc.RpcError) as e: 

1136 api.RespondHostRequest( 

1137 requests_pb2.RespondHostRequestReq( 

1138 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_PENDING 

1139 ) 

1140 ) 

1141 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1142 assert e.value.details() == "You can't set the host request status to that." 

1143 

1144 # can confirm then cancel 

1145 api.RespondHostRequest( 

1146 requests_pb2.RespondHostRequestReq( 

1147 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1148 ) 

1149 ) 

1150 

1151 api.RespondHostRequest( 

1152 requests_pb2.RespondHostRequestReq( 

1153 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1154 ) 

1155 ) 

1156 

1157 # can't confirm after having cancelled 

1158 with pytest.raises(grpc.RpcError) as e: 

1159 api.RespondHostRequest( 

1160 requests_pb2.RespondHostRequestReq( 

1161 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1162 ) 

1163 ) 

1164 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1165 assert e.value.details() == "You can't set the host request status to that." 

1166 

1167 # at this point there should be 7 messages 

1168 # 2 for creation, 2 for the status change with message, 3 for the other status changed 

1169 with requests_session(token1) as api: 

1170 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=request_id)) 

1171 assert len(res.messages) == 7 

1172 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1173 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1174 assert res.messages[2].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1175 assert res.messages[4].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1176 assert res.messages[6].WhichOneof("content") == "chat_created" 

1177 

1178 

1179def test_get_host_request_messages(db, moderator): 

1180 user1, token1 = generate_user() 

1181 user2, token2 = generate_user() 

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

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

1184 with requests_session(token1) as api: 

1185 res = api.CreateHostRequest( 

1186 requests_pb2.CreateHostRequestReq( 

1187 host_user_id=user2.id, 

1188 from_date=today_plus_2.isoformat(), 

1189 to_date=today_plus_3.isoformat(), 

1190 text=valid_request_text("Test request 1"), 

1191 ) 

1192 ) 

1193 conversation_id = res.host_request_id 

1194 

1195 moderator.approve_host_request(conversation_id) 

1196 

1197 add_message(db, "Test request 1 message 1", user1.id, conversation_id) 

1198 add_message(db, "Test request 1 message 2", user1.id, conversation_id) 

1199 add_message(db, "Test request 1 message 3", user1.id, conversation_id) 

1200 

1201 with requests_session(token2) as api: 

1202 api.RespondHostRequest( 

1203 requests_pb2.RespondHostRequestReq( 

1204 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1205 ) 

1206 ) 

1207 

1208 add_message(db, "Test request 1 message 4", user2.id, conversation_id) 

1209 add_message(db, "Test request 1 message 5", user2.id, conversation_id) 

1210 

1211 api.RespondHostRequest( 

1212 requests_pb2.RespondHostRequestReq( 

1213 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1214 ) 

1215 ) 

1216 

1217 with requests_session(token1) as api: 

1218 # 9 including initial message 

1219 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=conversation_id)) 

1220 assert len(res.messages) == 9 

1221 assert res.no_more 

1222 

1223 res = api.GetHostRequestMessages( 

1224 requests_pb2.GetHostRequestMessagesReq(host_request_id=conversation_id, number=3) 

1225 ) 

1226 assert not res.no_more 

1227 assert len(res.messages) == 3 

1228 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1229 assert res.messages[0].WhichOneof("content") == "host_request_status_changed" 

1230 assert res.messages[1].text.text == "Test request 1 message 5" 

1231 assert res.messages[2].text.text == "Test request 1 message 4" 

1232 

1233 res = api.GetHostRequestMessages( 

1234 requests_pb2.GetHostRequestMessagesReq( 

1235 host_request_id=conversation_id, 

1236 last_message_id=res.messages[2].message_id, 

1237 number=6, 

1238 ) 

1239 ) 

1240 assert res.no_more 

1241 assert len(res.messages) == 6 

1242 assert res.messages[0].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1243 assert res.messages[0].WhichOneof("content") == "host_request_status_changed" 

1244 assert res.messages[1].text.text == "Test request 1 message 3" 

1245 assert res.messages[2].text.text == "Test request 1 message 2" 

1246 assert res.messages[3].text.text == "Test request 1 message 1" 

1247 assert res.messages[4].text.text == valid_request_text("Test request 1") 

1248 assert res.messages[5].WhichOneof("content") == "chat_created" 

1249 

1250 

1251def test_SendHostRequestMessage(db, moderator): 

1252 user1, token1 = generate_user() 

1253 user2, token2 = generate_user() 

1254 user3, token3 = generate_user() 

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

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

1257 with requests_session(token1) as api: 

1258 host_request_id = api.CreateHostRequest( 

1259 requests_pb2.CreateHostRequestReq( 

1260 host_user_id=user2.id, 

1261 from_date=today_plus_2.isoformat(), 

1262 to_date=today_plus_3.isoformat(), 

1263 text=valid_request_text("Test request 1"), 

1264 ) 

1265 ).host_request_id 

1266 

1267 moderator.approve_host_request(host_request_id) 

1268 

1269 with requests_session(token1) as api: 

1270 with pytest.raises(grpc.RpcError) as e: 

1271 api.SendHostRequestMessage( 

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

1273 ) 

1274 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

1275 

1276 with pytest.raises(grpc.RpcError) as e: 

1277 api.SendHostRequestMessage(requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="")) 

1278 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

1279 assert e.value.details() == "Invalid message." 

1280 

1281 api.SendHostRequestMessage( 

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

1283 ) 

1284 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id)) 

1285 assert res.messages[0].text.text == "Test message 1" 

1286 assert res.messages[0].author_user_id == user1.id 

1287 

1288 with requests_session(token3) as api: 

1289 # other user can't send 

1290 with pytest.raises(grpc.RpcError) as e: 

1291 api.SendHostRequestMessage( 

1292 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2") 

1293 ) 

1294 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

1295 assert e.value.details() == "Couldn't find that host request." 

1296 

1297 with requests_session(token2) as api: 

1298 api.SendHostRequestMessage( 

1299 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2") 

1300 ) 

1301 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id)) 

1302 # including 2 for creation control message and message 

1303 assert len(res.messages) == 4 

1304 assert res.messages[0].text.text == "Test message 2" 

1305 assert res.messages[0].author_user_id == user2.id 

1306 

1307 # CAN send messages to a rejected, confirmed or cancelled request, and for accepted 

1308 api.RespondHostRequest( 

1309 requests_pb2.RespondHostRequestReq( 

1310 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1311 ) 

1312 ) 

1313 api.SendHostRequestMessage( 

1314 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3") 

1315 ) 

1316 

1317 api.RespondHostRequest( 

1318 requests_pb2.RespondHostRequestReq( 

1319 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1320 ) 

1321 ) 

1322 

1323 with requests_session(token1) as api: 

1324 api.RespondHostRequest( 

1325 requests_pb2.RespondHostRequestReq( 

1326 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1327 ) 

1328 ) 

1329 api.SendHostRequestMessage( 

1330 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3") 

1331 ) 

1332 

1333 api.RespondHostRequest( 

1334 requests_pb2.RespondHostRequestReq( 

1335 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1336 ) 

1337 ) 

1338 api.SendHostRequestMessage( 

1339 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 3") 

1340 ) 

1341 

1342 

1343def test_get_updates(db, moderator): 

1344 user1, token1 = generate_user() 

1345 user2, token2 = generate_user() 

1346 user3, token3 = generate_user() 

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

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

1349 with requests_session(token1) as api: 

1350 host_request_id = api.CreateHostRequest( 

1351 requests_pb2.CreateHostRequestReq( 

1352 host_user_id=user2.id, 

1353 from_date=today_plus_2.isoformat(), 

1354 to_date=today_plus_3.isoformat(), 

1355 text=valid_request_text("Test message 0"), 

1356 ) 

1357 ).host_request_id 

1358 

1359 moderator.approve_host_request(host_request_id) 

1360 

1361 with requests_session(token1) as api: 

1362 api.SendHostRequestMessage( 

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

1364 ) 

1365 api.SendHostRequestMessage( 

1366 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2") 

1367 ) 

1368 api.RespondHostRequest( 

1369 requests_pb2.RespondHostRequestReq( 

1370 host_request_id=host_request_id, 

1371 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

1372 text="Test message 3", 

1373 ) 

1374 ) 

1375 

1376 backdate_conversations() 

1377 api.CreateHostRequest( 

1378 requests_pb2.CreateHostRequestReq( 

1379 host_user_id=user2.id, 

1380 from_date=today_plus_2.isoformat(), 

1381 to_date=today_plus_3.isoformat(), 

1382 text=valid_request_text("Test message 4"), 

1383 ) 

1384 ) 

1385 

1386 res = api.GetHostRequestMessages(requests_pb2.GetHostRequestMessagesReq(host_request_id=host_request_id)) 

1387 assert len(res.messages) == 6 

1388 assert res.messages[0].text.text == "Test message 3" 

1389 assert res.messages[1].host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1390 assert res.messages[2].text.text == "Test message 2" 

1391 assert res.messages[3].text.text == "Test message 1" 

1392 assert res.messages[4].text.text == valid_request_text("Test message 0") 

1393 message_id_3 = res.messages[0].message_id 

1394 message_id_cancel = res.messages[1].message_id 

1395 message_id_2 = res.messages[2].message_id 

1396 message_id_1 = res.messages[3].message_id 

1397 message_id_0 = res.messages[4].message_id 

1398 

1399 with pytest.raises(grpc.RpcError) as e: 

1400 api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=0)) 

1401 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

1402 

1403 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1)) 

1404 assert res.no_more 

1405 assert len(res.updates) == 5 

1406 assert res.updates[0].message.text.text == "Test message 2" 

1407 assert res.updates[1].message.host_request_status_changed.status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1408 assert res.updates[1].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1409 assert res.updates[2].message.text.text == "Test message 3" 

1410 assert res.updates[3].message.WhichOneof("content") == "chat_created" 

1411 assert res.updates[3].status == messages_pb2.HOST_REQUEST_STATUS_PENDING 

1412 assert res.updates[4].message.text.text == valid_request_text("Test message 4") 

1413 

1414 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1, number=1)) 

1415 assert not res.no_more 

1416 assert len(res.updates) == 1 

1417 assert res.updates[0].message.text.text == "Test message 2" 

1418 assert res.updates[0].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1419 

1420 with requests_session(token3) as api: 

1421 # other user can't access 

1422 res = api.GetHostRequestUpdates(requests_pb2.GetHostRequestUpdatesReq(newest_message_id=message_id_1)) 

1423 assert len(res.updates) == 0 

1424 

1425 

1426def test_archive_host_request(db, moderator): 

1427 user1, token1 = generate_user() 

1428 user2, token2 = generate_user() 

1429 

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

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

1432 

1433 with requests_session(token1) as api: 

1434 host_request_id = api.CreateHostRequest( 

1435 requests_pb2.CreateHostRequestReq( 

1436 host_user_id=user2.id, 

1437 from_date=today_plus_2.isoformat(), 

1438 to_date=today_plus_3.isoformat(), 

1439 text=valid_request_text("Test message 0"), 

1440 ) 

1441 ).host_request_id 

1442 

1443 api.SendHostRequestMessage( 

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

1445 ) 

1446 api.SendHostRequestMessage( 

1447 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2") 

1448 ) 

1449 

1450 moderator.approve_host_request(host_request_id) 

1451 

1452 # happy path archiving host request 

1453 with requests_session(token1) as api: 

1454 api.RespondHostRequest( 

1455 requests_pb2.RespondHostRequestReq( 

1456 host_request_id=host_request_id, 

1457 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

1458 text="Test message 3", 

1459 ) 

1460 ) 

1461 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)) 

1462 assert len(res.host_requests) == 1 

1463 assert res.host_requests[0].status == messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1464 

1465 # Verify is_archived is False before archiving 

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

1467 assert not res.is_archived 

1468 

1469 api.SetHostRequestArchiveStatus( 

1470 requests_pb2.SetHostRequestArchiveStatusReq(host_request_id=host_request_id, is_archived=True) 

1471 ) 

1472 res = api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_archived=True)) 

1473 assert len(res.host_requests) == 1 

1474 

1475 # Verify is_archived is True after archiving 

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

1477 assert res.is_archived 

1478 

1479 

1480def test_host_request_unseen_message_count(db, moderator): 

1481 user1, token1 = generate_user() 

1482 user2, token2 = generate_user() 

1483 

1484 with requests_session(token1) as api: 

1485 host_request_id = api.CreateHostRequest( 

1486 requests_pb2.CreateHostRequestReq( 

1487 host_user_id=user2.id, 

1488 from_date=(today() + timedelta(days=2)).isoformat(), 

1489 to_date=(today() + timedelta(days=3)).isoformat(), 

1490 text=valid_request_text(), 

1491 ) 

1492 ).host_request_id 

1493 moderator.approve_host_request(host_request_id) 

1494 

1495 with requests_session(token1) as api: 

1496 api.SendHostRequestMessage( 

1497 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="and one more thing") 

1498 ) 

1499 # sending marks your own messages seen 

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

1501 assert res.unseen_message_count == 0 

1502 

1503 with requests_session(token2) as api: 

1504 listed = api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0] 

1505 # the request's control message, its text, and the follow-up 

1506 assert listed.unseen_message_count == 3 

1507 assert ( 

1508 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)).unseen_message_count 

1509 == 3 

1510 ) 

1511 

1512 api.MarkLastSeenHostRequest( 

1513 requests_pb2.MarkLastSeenHostRequestReq( 

1514 host_request_id=host_request_id, last_seen_message_id=listed.latest_message.message_id 

1515 ) 

1516 ) 

1517 assert api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests[0].unseen_message_count == 0 

1518 assert ( 

1519 api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)).unseen_message_count 

1520 == 0 

1521 ) 

1522 

1523 

1524def test_mark_last_seen(db, moderator): 

1525 user1, token1 = generate_user() 

1526 user2, token2 = generate_user() 

1527 user3, token3 = generate_user() 

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

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

1530 with requests_session(token1) as api: 

1531 host_request_id = api.CreateHostRequest( 

1532 requests_pb2.CreateHostRequestReq( 

1533 host_user_id=user2.id, 

1534 from_date=today_plus_2.isoformat(), 

1535 to_date=today_plus_3.isoformat(), 

1536 text=valid_request_text("Test message 0"), 

1537 ) 

1538 ).host_request_id 

1539 backdate_conversations() 

1540 

1541 host_request_id_2 = api.CreateHostRequest( 

1542 requests_pb2.CreateHostRequestReq( 

1543 host_user_id=user2.id, 

1544 from_date=today_plus_2.isoformat(), 

1545 to_date=today_plus_3.isoformat(), 

1546 text=valid_request_text("Test message 0a"), 

1547 ) 

1548 ).host_request_id 

1549 

1550 moderator.approve_host_request(host_request_id) 

1551 moderator.approve_host_request(host_request_id_2) 

1552 

1553 with requests_session(token1) as api: 

1554 api.SendHostRequestMessage( 

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

1556 ) 

1557 api.SendHostRequestMessage( 

1558 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id, text="Test message 2") 

1559 ) 

1560 api.RespondHostRequest( 

1561 requests_pb2.RespondHostRequestReq( 

1562 host_request_id=host_request_id, 

1563 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

1564 text="Test message 3", 

1565 ) 

1566 ) 

1567 

1568 moderator.approve_host_request(host_request_id) 

1569 moderator.approve_host_request(host_request_id_2) 

1570 

1571 # test Ping unseen host request count, should be automarked after sending 

1572 with api_session(token1) as api: 

1573 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 0 

1574 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0 

1575 

1576 with api_session(token2) as api: 

1577 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 2 

1578 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0 

1579 

1580 with requests_session(token2) as api: 

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

1582 

1583 api.MarkLastSeenHostRequest( 

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

1585 ) 

1586 

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

1588 

1589 with pytest.raises(grpc.RpcError) as e: 

1590 api.MarkLastSeenHostRequest( 

1591 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=1) 

1592 ) 

1593 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1594 assert e.value.details() == "You can't unsee messages." 

1595 

1596 # this will be used to test sent request notifications 

1597 host_request_id_3 = api.CreateHostRequest( 

1598 requests_pb2.CreateHostRequestReq( 

1599 host_user_id=user1.id, 

1600 from_date=today_plus_2.isoformat(), 

1601 to_date=today_plus_3.isoformat(), 

1602 text=valid_request_text("Another test request"), 

1603 ) 

1604 ).host_request_id 

1605 

1606 moderator.approve_host_request(host_request_id_3) 

1607 

1608 with requests_session(token2) as api: 

1609 # this should make id_2 all read 

1610 api.SendHostRequestMessage( 

1611 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id_2, text="Test") 

1612 ) 

1613 

1614 with api_session(token2) as api: 

1615 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 1 

1616 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 0 

1617 

1618 # make sure sent and received count for unseen notifications 

1619 with requests_session(token1) as api: 

1620 api.SendHostRequestMessage( 

1621 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request_id_3, text="Test message") 

1622 ) 

1623 

1624 with api_session(token2) as api: 

1625 assert api.Ping(api_pb2.PingReq()).unseen_received_host_request_count == 1 

1626 assert api.Ping(api_pb2.PingReq()).unseen_sent_host_request_count == 1 

1627 

1628 

1629def _create_host_request_via_api(surfer_token: str, host_id: int, moderator, public_trip_id: int | None = None) -> int: 

1630 with requests_session(surfer_token) as api: 

1631 res = api.CreateHostRequest( 

1632 requests_pb2.CreateHostRequestReq( 

1633 host_user_id=host_id, 

1634 from_date=(today() + timedelta(days=5)).isoformat(), 

1635 to_date=(today() + timedelta(days=10)).isoformat(), 

1636 text=valid_request_text(), 

1637 public_trip_id=public_trip_id, 

1638 ) 

1639 ) 

1640 moderator.approve_host_request(res.host_request_id) 

1641 return int(res.host_request_id) 

1642 

1643 

1644def test_ping_role_based_counts_match_direction_without_offers(db, moderator): 

1645 user1, token1 = generate_user() 

1646 user2, token2 = generate_user() 

1647 

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

1649 _create_host_request_via_api(token2, user1.id, moderator) 

1650 

1651 with api_session(token1) as api: 

1652 res = api.Ping(api_pb2.PingReq()) 

1653 # with no public-trip offers, role-based counts equal the direction-based ones 

1654 assert res.unseen_received_host_request_count == 1 

1655 assert res.unseen_hosting_host_request_count == 1 

1656 assert res.unseen_sent_host_request_count == 0 

1657 assert res.unseen_surfing_host_request_count == 0 

1658 assert res.unseen_public_trip_offer_count == 0 

1659 

1660 

1661def test_ping_counts_public_trip_offer_by_role(db, moderator): 

1662 traveler, traveler_token = generate_user() 

1663 host, host_token = generate_user() 

1664 node_id = _make_node() 

1665 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10)) 

1666 

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

1668 

1669 # the traveller (recipient) has the offer's create message unseen: counts as surfing + public-trip offer 

1670 with api_session(traveler_token) as api: 

1671 res = api.Ping(api_pb2.PingReq()) 

1672 assert res.unseen_surfing_host_request_count == 1 

1673 assert res.unseen_public_trip_offer_count == 1 

1674 assert res.unseen_hosting_host_request_count == 0 

1675 

1676 # the traveller replies, so now the offering host has an unseen message under hosting 

1677 with requests_session(traveler_token) as api: 

1678 api.SendHostRequestMessage( 

1679 requests_pb2.SendHostRequestMessageReq(host_request_id=request_id, text="thanks for the offer") 

1680 ) 

1681 

1682 with api_session(host_token) as api: 

1683 res = api.Ping(api_pb2.PingReq()) 

1684 assert res.unseen_hosting_host_request_count == 1 

1685 assert res.unseen_surfing_host_request_count == 0 

1686 assert res.unseen_public_trip_offer_count == 0 

1687 

1688 

1689def test_ping_public_trip_offer_count_gated_by_flag(db, moderator, feature_flags): 

1690 feature_flags.set("public_trips_enabled", False) 

1691 

1692 traveler, traveler_token = generate_user() 

1693 host, host_token = generate_user() 

1694 node_id = _make_node() 

1695 trip_id = _create_trip_directly(traveler.id, node_id, today() + timedelta(days=5), today() + timedelta(days=10)) 

1696 

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

1698 

1699 with api_session(traveler_token) as api: 

1700 res = api.Ping(api_pb2.PingReq()) 

1701 # the dedicated offer count is gated off... 

1702 assert res.unseen_public_trip_offer_count == 0 

1703 # ...but the offer is a real conversation and still surfaces under surfing 

1704 assert res.unseen_surfing_host_request_count == 1 

1705 

1706 

1707def test_mark_last_seen_clears_notifications(db, moderator): 

1708 user1, token1 = generate_user() 

1709 user2, token2 = generate_user() 

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

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

1712 

1713 with requests_session(token1) as api: 

1714 host_request_id = api.CreateHostRequest( 

1715 requests_pb2.CreateHostRequestReq( 

1716 host_user_id=user2.id, 

1717 from_date=today_plus_2.isoformat(), 

1718 to_date=today_plus_3.isoformat(), 

1719 text=valid_request_text("Test message"), 

1720 ) 

1721 ).host_request_id 

1722 

1723 moderator.approve_host_request(host_request_id) 

1724 

1725 def unseen_notification_count(user_id): 

1726 with session_scope() as session: 

1727 return session.execute( 

1728 select(func.count()) 

1729 .select_from(Notification) 

1730 .where(Notification.user_id == user_id) 

1731 .where(Notification.key == str(host_request_id)) 

1732 .where(Notification.is_seen == False) 

1733 ).scalar_one() 

1734 

1735 assert unseen_notification_count(user2.id) > 0 

1736 

1737 with requests_session(token2) as api: 

1738 api.MarkLastSeenHostRequest( 

1739 requests_pb2.MarkLastSeenHostRequestReq(host_request_id=host_request_id, last_seen_message_id=1) 

1740 ) 

1741 

1742 assert unseen_notification_count(user2.id) == 0 

1743 

1744 

1745def test_response_rate(db, moderator): 

1746 user1, token1 = generate_user() 

1747 user2, token2 = generate_user() 

1748 user3, token3 = generate_user(delete_user=True) 

1749 

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

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

1752 

1753 with session_scope() as session: 

1754 refresh_materialized_view(session, "user_response_rates") 

1755 

1756 with requests_session(token1) as api: 

1757 # deleted: not found 

1758 with pytest.raises(grpc.RpcError) as e: 

1759 api.GetResponseRate(requests_pb2.GetResponseRateReq(user_id=user3.id)) 

1760 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

1761 assert e.value.details() == "Couldn't find that user." 

1762 

1763 # no requests: insufficient 

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

1765 assert res.HasField("insufficient_data") 

1766 

1767 # send a request and back date it by 36 hours 

1768 host_request_1 = api.CreateHostRequest( 

1769 requests_pb2.CreateHostRequestReq( 

1770 host_user_id=user2.id, 

1771 from_date=today_plus_2.isoformat(), 

1772 to_date=today_plus_3.isoformat(), 

1773 text=valid_request_text("Test request"), 

1774 ) 

1775 ).host_request_id 

1776 moderator.approve_host_request(host_request_1) 

1777 backdate_conversations() 

1778 with session_scope() as session: 

1779 session.execute( 

1780 select(Message) 

1781 .where(Message.conversation_id == host_request_1) 

1782 .where(Message.message_type == MessageType.chat_created) 

1783 ).scalar_one().time = now() - timedelta(hours=36) 

1784 refresh_materialized_view(session, "user_response_rates") 

1785 

1786 # still insufficient 

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

1788 assert res.HasField("insufficient_data") 

1789 

1790 # send a request and back date it by 35 hours 

1791 host_request_2 = api.CreateHostRequest( 

1792 requests_pb2.CreateHostRequestReq( 

1793 host_user_id=user2.id, 

1794 from_date=today_plus_2.isoformat(), 

1795 to_date=today_plus_3.isoformat(), 

1796 text=valid_request_text("Test request"), 

1797 ) 

1798 ).host_request_id 

1799 moderator.approve_host_request(host_request_2) 

1800 backdate_conversations() 

1801 with session_scope() as session: 

1802 session.execute( 

1803 select(Message) 

1804 .where(Message.conversation_id == host_request_2) 

1805 .where(Message.message_type == MessageType.chat_created) 

1806 ).scalar_one().time = now() - timedelta(hours=35) 

1807 refresh_materialized_view(session, "user_response_rates") 

1808 

1809 # still insufficient 

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

1811 assert res.HasField("insufficient_data") 

1812 

1813 # send a request and back date it by 34 hours 

1814 host_request_3 = api.CreateHostRequest( 

1815 requests_pb2.CreateHostRequestReq( 

1816 host_user_id=user2.id, 

1817 from_date=today_plus_2.isoformat(), 

1818 to_date=today_plus_3.isoformat(), 

1819 text=valid_request_text("Test request"), 

1820 ) 

1821 ).host_request_id 

1822 moderator.approve_host_request(host_request_3) 

1823 backdate_conversations() 

1824 with session_scope() as session: 

1825 session.execute( 

1826 select(Message) 

1827 .where(Message.conversation_id == host_request_3) 

1828 .where(Message.message_type == MessageType.chat_created) 

1829 ).scalar_one().time = now() - timedelta(hours=34) 

1830 refresh_materialized_view(session, "user_response_rates") 

1831 

1832 # now low 

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

1834 assert res.HasField("low") 

1835 

1836 with requests_session(token2) as api: 

1837 # accept a host req 

1838 api.RespondHostRequest( 

1839 requests_pb2.RespondHostRequestReq( 

1840 host_request_id=host_request_2, 

1841 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1842 text="Accepting host request", 

1843 ) 

1844 ) 

1845 

1846 with session_scope() as session: 

1847 refresh_materialized_view(session, "user_response_rates") 

1848 

1849 with requests_session(token1) as api: 

1850 # now some w p33 = 35h 

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

1852 assert res.HasField("some") 

1853 assert res.some.response_time_p33.ToTimedelta() == timedelta(hours=35) 

1854 

1855 with requests_session(token2) as api: 

1856 # accept another host req 

1857 api.RespondHostRequest( 

1858 requests_pb2.RespondHostRequestReq( 

1859 host_request_id=host_request_3, 

1860 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1861 text="Accepting host request", 

1862 ) 

1863 ) 

1864 

1865 with session_scope() as session: 

1866 refresh_materialized_view(session, "user_response_rates") 

1867 

1868 with requests_session(token1) as api: 

1869 # now most w p33 = 34h, p66 = 35h 

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

1871 assert res.HasField("most") 

1872 assert res.most.response_time_p33.ToTimedelta() == timedelta(hours=34) 

1873 assert res.most.response_time_p66.ToTimedelta() == timedelta(hours=35) 

1874 

1875 with requests_session(token2) as api: 

1876 # accept last host req 

1877 api.RespondHostRequest( 

1878 requests_pb2.RespondHostRequestReq( 

1879 host_request_id=host_request_1, 

1880 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1881 text="Accepting host request", 

1882 ) 

1883 ) 

1884 

1885 with session_scope() as session: 

1886 refresh_materialized_view(session, "user_response_rates") 

1887 

1888 with requests_session(token1) as api: 

1889 # now all w p33 = 34h, p66 = 35h 

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

1891 assert res.HasField("almost_all") 

1892 assert res.almost_all.response_time_p33.ToTimedelta() == timedelta(hours=34) 

1893 assert res.almost_all.response_time_p66.ToTimedelta() == timedelta(hours=35) 

1894 

1895 # send a request and back date it by 2 hours 

1896 host_request_4 = api.CreateHostRequest( 

1897 requests_pb2.CreateHostRequestReq( 

1898 host_user_id=user2.id, 

1899 from_date=today_plus_2.isoformat(), 

1900 to_date=today_plus_3.isoformat(), 

1901 text=valid_request_text("Test request"), 

1902 ) 

1903 ).host_request_id 

1904 moderator.approve_host_request(host_request_4) 

1905 backdate_conversations() 

1906 with session_scope() as session: 

1907 session.execute( 

1908 select(Message) 

1909 .where(Message.conversation_id == host_request_4) 

1910 .where(Message.message_type == MessageType.chat_created) 

1911 ).scalar_one().time = now() - timedelta(hours=2) 

1912 refresh_materialized_view(session, "user_response_rates") 

1913 

1914 # send a request and back date it by 4 hours 

1915 host_request_5 = api.CreateHostRequest( 

1916 requests_pb2.CreateHostRequestReq( 

1917 host_user_id=user2.id, 

1918 from_date=today_plus_2.isoformat(), 

1919 to_date=today_plus_3.isoformat(), 

1920 text=valid_request_text("Test request"), 

1921 ) 

1922 ).host_request_id 

1923 moderator.approve_host_request(host_request_5) 

1924 backdate_conversations() 

1925 with session_scope() as session: 

1926 session.execute( 

1927 select(Message) 

1928 .where(Message.conversation_id == host_request_5) 

1929 .where(Message.message_type == MessageType.chat_created) 

1930 ).scalar_one().time = now() - timedelta(hours=4) 

1931 refresh_materialized_view(session, "user_response_rates") 

1932 

1933 # now some w p33 = 35h 

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

1935 assert res.HasField("some") 

1936 assert res.some.response_time_p33.ToTimedelta() == timedelta(hours=35) 

1937 

1938 with requests_session(token2) as api: 

1939 # accept host req 

1940 api.RespondHostRequest( 

1941 requests_pb2.RespondHostRequestReq( 

1942 host_request_id=host_request_5, 

1943 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1944 text="Accepting host request", 

1945 ) 

1946 ) 

1947 

1948 with session_scope() as session: 

1949 refresh_materialized_view(session, "user_response_rates") 

1950 

1951 with requests_session(token1) as api: 

1952 # now most w p33 = 34h, p66 = 36h 

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

1954 assert res.HasField("most") 

1955 assert res.most.response_time_p33.ToTimedelta() == timedelta(hours=34) 

1956 assert res.most.response_time_p66.ToTimedelta() == timedelta(hours=36) 

1957 

1958 with requests_session(token2) as api: 

1959 # accept host req 

1960 api.RespondHostRequest( 

1961 requests_pb2.RespondHostRequestReq( 

1962 host_request_id=host_request_4, 

1963 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1964 text="Accepting host request", 

1965 ) 

1966 ) 

1967 

1968 with session_scope() as session: 

1969 refresh_materialized_view(session, "user_response_rates") 

1970 

1971 with requests_session(token1) as api: 

1972 # now most w p33 = 4h, p66 = 35h 

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

1974 assert res.HasField("almost_all") 

1975 assert res.almost_all.response_time_p33.ToTimedelta() == timedelta(hours=4) 

1976 assert res.almost_all.response_time_p66.ToTimedelta() == timedelta(hours=35) 

1977 

1978 

1979def test_request_notifications(db, email_collector: EmailCollector, push_collector: PushCollector, moderator): 

1980 host, host_token = generate_user(complete_profile=True) 

1981 surfer, surfer_token = generate_user(complete_profile=True) 

1982 

1983 host_loc_context = LocalizationContext.from_user(host) 

1984 surfer_loc_context = LocalizationContext.from_user(surfer) 

1985 

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

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

1988 

1989 with requests_session(surfer_token) as api: 

1990 hr_id = api.CreateHostRequest( 

1991 requests_pb2.CreateHostRequestReq( 

1992 host_user_id=host.id, 

1993 from_date=today_plus_2.isoformat(), 

1994 to_date=today_plus_3.isoformat(), 

1995 text=valid_request_text("can i stay plz"), 

1996 ) 

1997 ).host_request_id 

1998 

1999 moderator.approve_host_request(hr_id) 

2000 

2001 email = email_collector.pop_for_recipient(host.email, last=True) 

2002 assert email.recipient == host.email 

2003 assert "host request" in email.subject.lower() 

2004 assert host.name in email.plain 

2005 assert host.name in email.html 

2006 assert "quick decline" in email.plain.lower(), email.plain 

2007 assert "quick decline" in email.html.lower() 

2008 assert surfer.name in email.plain 

2009 assert surfer.name in email.html 

2010 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.plain 

2011 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.html 

2012 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.plain 

2013 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.html 

2014 assert "http://localhost:5001/img/thumbnail/" not in email.plain 

2015 assert "http://localhost:5001/img/thumbnail/" in email.html 

2016 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain 

2017 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html 

2018 assert not email.attachments 

2019 

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

2021 

2022 with requests_session(host_token) as api: 

2023 api.RespondHostRequest( 

2024 requests_pb2.RespondHostRequestReq( 

2025 host_request_id=hr_id, 

2026 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

2027 text="Accepting host request", 

2028 ) 

2029 ) 

2030 

2031 email = email_collector.pop_for_recipient(surfer.email, last=True) 

2032 assert email.recipient == surfer.email 

2033 assert "host request" in email.subject.lower() 

2034 assert host.name in email.plain 

2035 assert host.name in email.html 

2036 assert surfer.name in email.plain 

2037 assert surfer.name in email.html 

2038 assert surfer_loc_context.localize_date(today_plus_2, with_year=False) in email.plain 

2039 assert surfer_loc_context.localize_date(today_plus_2, with_year=False) in email.html 

2040 assert surfer_loc_context.localize_date(today_plus_3, with_year=False) in email.plain 

2041 assert surfer_loc_context.localize_date(today_plus_3, with_year=False) in email.html 

2042 assert "http://localhost:5001/img/thumbnail/" not in email.plain 

2043 assert "http://localhost:5001/img/thumbnail/" in email.html 

2044 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain 

2045 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html 

2046 assert len(email.attachments or []) == 1 

2047 

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

2049 

2050 

2051def test_quick_decline(db, email_collector: EmailCollector, push_collector: PushCollector, moderator): 

2052 host, host_token = generate_user(complete_profile=True) 

2053 surfer, surfer_token = generate_user(complete_profile=True) 

2054 

2055 host_loc_context = LocalizationContext.from_user(host) 

2056 

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

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

2059 

2060 with requests_session(surfer_token) as api: 

2061 hr_id = api.CreateHostRequest( 

2062 requests_pb2.CreateHostRequestReq( 

2063 host_user_id=host.id, 

2064 from_date=today_plus_2.isoformat(), 

2065 to_date=today_plus_3.isoformat(), 

2066 text=valid_request_text("can i stay plz"), 

2067 ) 

2068 ).host_request_id 

2069 

2070 moderator.approve_host_request(hr_id) 

2071 

2072 email = email_collector.pop_for_recipient(host.email, last=True) 

2073 assert email.recipient == host.email 

2074 assert "host request" in email.subject.lower() 

2075 assert host.name in email.plain 

2076 assert host.name in email.html 

2077 assert "quick decline" in email.plain.lower(), email.plain 

2078 assert "quick decline" in email.html.lower() 

2079 assert surfer.name in email.plain 

2080 assert surfer.name in email.html 

2081 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.plain 

2082 assert host_loc_context.localize_date(today_plus_2, with_year=False) in email.html 

2083 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.plain 

2084 assert host_loc_context.localize_date(today_plus_3, with_year=False) in email.html 

2085 assert "http://localhost:5001/img/thumbnail/" not in email.plain 

2086 assert "http://localhost:5001/img/thumbnail/" in email.html 

2087 assert f"http://localhost:3000/messages/request/{hr_id}" in email.plain 

2088 assert f"http://localhost:3000/messages/request/{hr_id}" in email.html 

2089 

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

2091 

2092 # very ugly 

2093 # http://localhost:3000/quick-link?payload=CAEiGAoOZnJpZW5kX3JlcXVlc3QSBmFjY2VwdA==&sig=BQdk024NTATm8zlR0krSXTBhP5U9TlFv7VhJeIHZtUg= 

2094 for link in re.findall(r'<a href="(.*?)"', email.html): 2094 ↛ 2113line 2094 didn't jump to line 2113 because the loop on line 2094 didn't complete

2095 if "payload" not in link: 

2096 continue 

2097 print(link) 

2098 url_parts = urlparse(html.unescape(link)) 

2099 params = parse_qs(url_parts.query) 

2100 print(params["payload"][0]) 

2101 payload = unsubscribe_pb2.UnsubscribePayload.FromString(b64decode(params["payload"][0])) 

2102 if payload.HasField("host_request_quick_decline"): 2102 ↛ 2094line 2102 didn't jump to line 2094 because the condition on line 2102 was always true

2103 with auth_api_session() as (auth_api, metadata_interceptor): 

2104 res = auth_api.Unsubscribe( 

2105 auth_pb2.UnsubscribeReq( 

2106 payload=b64decode(params["payload"][0]), 

2107 sig=b64decode(params["sig"][0]), 

2108 ) 

2109 ) 

2110 assert res.response == "Thank you for responding to the host request!" 

2111 break 

2112 else: 

2113 raise Exception("Didn't find link") 

2114 

2115 with requests_session(surfer_token) as api: 

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

2117 assert res.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED 

2118 

2119 

2120def test_host_req_feedback(db, moderator): 

2121 host, host_token = generate_user(complete_profile=True) 

2122 host2, host2_token = generate_user(complete_profile=True) 

2123 host3, host3_token = generate_user(complete_profile=True) 

2124 surfer, surfer_token = generate_user(complete_profile=True) 

2125 

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

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

2128 

2129 with requests_session(surfer_token) as api: 

2130 hr_id = api.CreateHostRequest( 

2131 requests_pb2.CreateHostRequestReq( 

2132 host_user_id=host.id, 

2133 from_date=today_plus_2.isoformat(), 

2134 to_date=today_plus_3.isoformat(), 

2135 text=valid_request_text("can i stay plz"), 

2136 ) 

2137 ).host_request_id 

2138 hr2_id = api.CreateHostRequest( 

2139 requests_pb2.CreateHostRequestReq( 

2140 host_user_id=host2.id, 

2141 from_date=today_plus_2.isoformat(), 

2142 to_date=today_plus_3.isoformat(), 

2143 text=valid_request_text("can i stay plz"), 

2144 ) 

2145 ).host_request_id 

2146 hr3_id = api.CreateHostRequest( 

2147 requests_pb2.CreateHostRequestReq( 

2148 host_user_id=host3.id, 

2149 from_date=today_plus_2.isoformat(), 

2150 to_date=today_plus_3.isoformat(), 

2151 text=valid_request_text("can i stay plz"), 

2152 ) 

2153 ).host_request_id 

2154 

2155 moderator.approve_host_request(hr_id) 

2156 moderator.approve_host_request(hr2_id) 

2157 moderator.approve_host_request(hr3_id) 

2158 

2159 with requests_session(host_token) as api: 

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

2161 assert not res.need_host_request_feedback 

2162 

2163 api.RespondHostRequest( 

2164 requests_pb2.RespondHostRequestReq( 

2165 host_request_id=hr_id, 

2166 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED, 

2167 ) 

2168 ) 

2169 

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

2171 assert res.need_host_request_feedback 

2172 

2173 # surfer can't leave feedback 

2174 with requests_session(surfer_token) as api: 

2175 with pytest.raises(grpc.RpcError) as e: 

2176 api.SendHostRequestFeedback( 

2177 requests_pb2.SendHostRequestFeedbackReq( 

2178 host_request_id=hr_id, 

2179 ) 

2180 ) 

2181 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2182 assert e.value.details() == "Couldn't find that host request." 

2183 

2184 with requests_session(host_token) as api: 

2185 api.SendHostRequestFeedback( 

2186 requests_pb2.SendHostRequestFeedbackReq( 

2187 host_request_id=hr_id, 

2188 host_request_quality=requests_pb2.HOST_REQUEST_QUALITY_LOW, 

2189 ) 

2190 ) 

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

2192 assert not res.need_host_request_feedback 

2193 

2194 # can't leave it twice 

2195 with requests_session(host_token) as api: 

2196 with pytest.raises(grpc.RpcError) as e: 

2197 api.SendHostRequestFeedback( 

2198 requests_pb2.SendHostRequestFeedbackReq( 

2199 host_request_id=hr_id, 

2200 ) 

2201 ) 

2202 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

2203 assert e.value.details() == "You have already left feedback for this host request!" 

2204 

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

2206 assert not res.need_host_request_feedback 

2207 

2208 with requests_session(host2_token) as api: 

2209 api.RespondHostRequest( 

2210 requests_pb2.RespondHostRequestReq(host_request_id=hr2_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED) 

2211 ) 

2212 # can't leave feedback on the wrong one 

2213 with pytest.raises(grpc.RpcError) as e: 

2214 api.SendHostRequestFeedback( 

2215 requests_pb2.SendHostRequestFeedbackReq( 

2216 host_request_id=hr_id, 

2217 ) 

2218 ) 

2219 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2220 assert e.value.details() == "Couldn't find that host request." 

2221 

2222 # null feedback is still feedback 

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

2224 

2225 with requests_session(host3_token) as api: 

2226 api.RespondHostRequest( 

2227 requests_pb2.RespondHostRequestReq(host_request_id=hr3_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED) 

2228 ) 

2229 

2230 api.SendHostRequestFeedback( 

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

2232 ) 

2233 

2234 

2235def _make_trip_node_admin(user_id: int, trip_id: int): 

2236 with session_scope() as session: 

2237 node_id = session.execute(select(PublicTrip.node_id).where(PublicTrip.id == trip_id)).scalar_one() 

2238 cluster = session.execute( 

2239 select(Cluster).where(Cluster.parent_node_id == node_id).where(Cluster.is_official_cluster) 

2240 ).scalar_one_or_none() 

2241 if cluster is None: 2241 ↛ 2250line 2241 didn't jump to line 2250 because the condition on line 2241 was always true

2242 cluster = Cluster( 

2243 name="Test community", 

2244 description="Test", 

2245 parent_node_id=node_id, 

2246 is_official_cluster=True, 

2247 ) 

2248 session.add(cluster) 

2249 session.flush() 

2250 session.add(ClusterSubscription(cluster_id=cluster.id, user_id=user_id, role=ClusterRole.admin)) 

2251 

2252 

2253def _create_public_trip( 

2254 user_id: int, 

2255 from_date, 

2256 to_date, 

2257 *, 

2258 status=None, 

2259 same_gender_only: bool = False, 

2260 visibility: ModerationVisibility = ModerationVisibility.visible, 

2261): 

2262 with session_scope() as session: 

2263 node = session.execute(select(Node).limit(1)).scalar_one_or_none() 

2264 if node is None: 

2265 node = Node( 

2266 geom=to_multi(create_polygon_lat_lng([[0, 0], [0, 2], [2, 2], [2, 0], [0, 0]])), 

2267 node_type=NodeType.locality, 

2268 ) 

2269 session.add(node) 

2270 session.flush() 

2271 moderation_state = ModerationState( 

2272 object_type=ModerationObjectType.public_trip, 

2273 object_id=0, # placeholder, set after PublicTrip flush 

2274 visibility=visibility, 

2275 ) 

2276 session.add(moderation_state) 

2277 session.flush() 

2278 trip = PublicTrip( 

2279 user_id=user_id, 

2280 node_id=node.id, 

2281 from_date=from_date, 

2282 to_date=to_date, 

2283 description="Looking for a host!", 

2284 status=status or PublicTripStatus.searching_for_host, 

2285 same_gender_only=same_gender_only, 

2286 moderation_state_id=moderation_state.id, 

2287 ) 

2288 session.add(trip) 

2289 session.flush() 

2290 moderation_state.object_id = trip.id 

2291 return trip.id 

2292 

2293 

2294def test_create_request_with_public_trip(db, moderator): 

2295 """Hosts can offer to host a public trip; offered dates must be within trip dates.""" 

2296 surfer, surfer_token = generate_user() 

2297 host, host_token = generate_user() 

2298 

2299 trip_from = today() + timedelta(days=10) 

2300 trip_to = today() + timedelta(days=20) 

2301 trip_id = _create_public_trip(surfer.id, trip_from, trip_to) 

2302 

2303 with requests_session(host_token) as api: 

2304 # Happy path: dates within trip window 

2305 res = api.CreateHostRequest( 

2306 requests_pb2.CreateHostRequestReq( 

2307 host_user_id=surfer.id, 

2308 from_date=(trip_from + timedelta(days=1)).isoformat(), 

2309 to_date=(trip_to - timedelta(days=1)).isoformat(), 

2310 text=valid_request_text(), 

2311 public_trip_id=trip_id, 

2312 ) 

2313 ) 

2314 host_request_id = res.host_request_id 

2315 

2316 moderator.approve_host_request(host_request_id) 

2317 

2318 with requests_session(host_token) as api: 

2319 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2320 assert hr.public_trip_id == trip_id 

2321 

2322 

2323def test_create_request_with_public_trip_hosting_snapshot(db, moderator): 

2324 """An offer snapshots the offering host's place, not the traveller's.""" 

2325 surfer, _ = generate_user(city="Surfer city", geom=create_coordinate(1, 1), geom_radius=11) 

2326 host, host_token = generate_user(city="Host city", geom=create_coordinate(2, 2), geom_radius=22) 

2327 

2328 trip_from = today() + timedelta(days=10) 

2329 trip_to = today() + timedelta(days=20) 

2330 trip_id = _create_public_trip(surfer.id, trip_from, trip_to) 

2331 

2332 with requests_session(host_token) as api: 

2333 host_request_id = api.CreateHostRequest( 

2334 requests_pb2.CreateHostRequestReq( 

2335 host_user_id=surfer.id, 

2336 from_date=trip_from.isoformat(), 

2337 to_date=trip_to.isoformat(), 

2338 text=valid_request_text(), 

2339 public_trip_id=trip_id, 

2340 ) 

2341 ).host_request_id 

2342 

2343 moderator.approve_host_request(host_request_id) 

2344 

2345 with requests_session(host_token) as api: 

2346 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2347 assert hr.hosting_city == "Host city" 

2348 assert round(hr.hosting_lat, 4) == 2 

2349 assert round(hr.hosting_lng, 4) == 2 

2350 assert hr.hosting_radius == 22 

2351 

2352 

2353def test_public_trip_offer_stay_roles(db, moderator): 

2354 """An offer reverses initiator/recipient, but the surfer and host are still the traveller and the host.""" 

2355 surfer, surfer_token = generate_user() 

2356 host, host_token = generate_user() 

2357 

2358 trip_from = today() + timedelta(days=10) 

2359 trip_to = today() + timedelta(days=20) 

2360 trip_id = _create_public_trip(surfer.id, trip_from, trip_to) 

2361 

2362 with requests_session(host_token) as api: 

2363 host_request_id = api.CreateHostRequest( 

2364 requests_pb2.CreateHostRequestReq( 

2365 host_user_id=surfer.id, 

2366 from_date=trip_from.isoformat(), 

2367 to_date=trip_to.isoformat(), 

2368 text=valid_request_text(), 

2369 public_trip_id=trip_id, 

2370 ) 

2371 ).host_request_id 

2372 

2373 moderator.approve_host_request(host_request_id) 

2374 

2375 with session_scope() as session: 

2376 host_request = session.execute( 

2377 select(HostRequest).where(HostRequest.conversation_id == host_request_id) 

2378 ).scalar_one() 

2379 assert host_request.initiator_user_id == host.id 

2380 assert host_request.recipient_user_id == surfer.id 

2381 assert host_request.surfer_user_id == surfer.id 

2382 assert host_request.host_user_id == host.id 

2383 

2384 for token in (surfer_token, host_token): 

2385 with requests_session(token) as api: 

2386 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2387 assert hr.surfer_user_id == surfer.id 

2388 assert hr.host_user_id == host.id 

2389 

2390 listed = api.ListHostRequests(requests_pb2.ListHostRequestsReq()).host_requests 

2391 assert [(r.surfer_user_id, r.host_user_id) for r in listed] == [(surfer.id, host.id)] 

2392 

2393 

2394def test_create_request_with_public_trip_dates_out_of_range(db): 

2395 """Offered dates outside the trip window are rejected.""" 

2396 surfer, _ = generate_user() 

2397 host, host_token = generate_user() 

2398 

2399 trip_from = today() + timedelta(days=10) 

2400 trip_to = today() + timedelta(days=20) 

2401 trip_id = _create_public_trip(surfer.id, trip_from, trip_to) 

2402 

2403 with requests_session(host_token) as api: 

2404 # from_date before trip starts 

2405 with pytest.raises(grpc.RpcError) as e: 

2406 api.CreateHostRequest( 

2407 requests_pb2.CreateHostRequestReq( 

2408 host_user_id=surfer.id, 

2409 from_date=(trip_from - timedelta(days=1)).isoformat(), 

2410 to_date=(trip_from + timedelta(days=1)).isoformat(), 

2411 text=valid_request_text(), 

2412 public_trip_id=trip_id, 

2413 ) 

2414 ) 

2415 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

2416 

2417 # to_date after trip ends 

2418 with pytest.raises(grpc.RpcError) as e: 

2419 api.CreateHostRequest( 

2420 requests_pb2.CreateHostRequestReq( 

2421 host_user_id=surfer.id, 

2422 from_date=(trip_to - timedelta(days=1)).isoformat(), 

2423 to_date=(trip_to + timedelta(days=1)).isoformat(), 

2424 text=valid_request_text(), 

2425 public_trip_id=trip_id, 

2426 ) 

2427 ) 

2428 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

2429 

2430 

2431def test_create_request_with_public_trip_user_mismatch(db): 

2432 """The host_user_id must match the public trip's traveler.""" 

2433 trip_owner, _ = generate_user() 

2434 other_user, _ = generate_user() 

2435 host, host_token = generate_user() 

2436 

2437 trip_from = today() + timedelta(days=10) 

2438 trip_to = today() + timedelta(days=20) 

2439 trip_id = _create_public_trip(trip_owner.id, trip_from, trip_to) 

2440 

2441 with requests_session(host_token) as api: 

2442 with pytest.raises(grpc.RpcError) as e: 

2443 api.CreateHostRequest( 

2444 requests_pb2.CreateHostRequestReq( 

2445 host_user_id=other_user.id, # not the trip owner 

2446 from_date=(trip_from + timedelta(days=1)).isoformat(), 

2447 to_date=(trip_to - timedelta(days=1)).isoformat(), 

2448 text=valid_request_text(), 

2449 public_trip_id=trip_id, 

2450 ) 

2451 ) 

2452 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

2453 

2454 

2455def test_create_request_with_closed_public_trip(db): 

2456 """Cannot offer to host a trip that's been closed.""" 

2457 surfer, _ = generate_user() 

2458 host, host_token = generate_user() 

2459 

2460 trip_from = today() + timedelta(days=10) 

2461 trip_to = today() + timedelta(days=20) 

2462 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, status=PublicTripStatus.closed) 

2463 

2464 with requests_session(host_token) as api: 

2465 with pytest.raises(grpc.RpcError) as e: 

2466 api.CreateHostRequest( 

2467 requests_pb2.CreateHostRequestReq( 

2468 host_user_id=surfer.id, 

2469 from_date=(trip_from + timedelta(days=1)).isoformat(), 

2470 to_date=(trip_to - timedelta(days=1)).isoformat(), 

2471 text=valid_request_text(), 

2472 public_trip_id=trip_id, 

2473 ) 

2474 ) 

2475 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

2476 

2477 

2478def test_create_request_with_nonexistent_public_trip(db): 

2479 """Nonexistent public trip ID returns NOT_FOUND.""" 

2480 surfer, _ = generate_user() 

2481 host, host_token = generate_user() 

2482 

2483 with requests_session(host_token) as api: 

2484 with pytest.raises(grpc.RpcError) as e: 

2485 api.CreateHostRequest( 

2486 requests_pb2.CreateHostRequestReq( 

2487 host_user_id=surfer.id, 

2488 from_date=(today() + timedelta(days=2)).isoformat(), 

2489 to_date=(today() + timedelta(days=3)).isoformat(), 

2490 text=valid_request_text(), 

2491 public_trip_id=999999, 

2492 ) 

2493 ) 

2494 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2495 

2496 

2497def test_create_request_with_shadowed_public_trip(db, moderator): 

2498 """A trip awaiting moderation can't be offered on until it's approved.""" 

2499 surfer, _ = generate_user() 

2500 host, host_token = generate_user() 

2501 

2502 trip_from = today() + timedelta(days=10) 

2503 trip_to = today() + timedelta(days=20) 

2504 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, visibility=ModerationVisibility.shadowed) 

2505 

2506 with requests_session(host_token) as api: 

2507 with pytest.raises(grpc.RpcError) as e: 

2508 api.CreateHostRequest( 

2509 requests_pb2.CreateHostRequestReq( 

2510 host_user_id=surfer.id, 

2511 from_date=(trip_from + timedelta(days=1)).isoformat(), 

2512 to_date=(trip_to - timedelta(days=1)).isoformat(), 

2513 text=valid_request_text(), 

2514 public_trip_id=trip_id, 

2515 ) 

2516 ) 

2517 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2518 assert e.value.details() == "Couldn't find that public trip." 

2519 

2520 moderator.approve_public_trip(trip_id) 

2521 

2522 with requests_session(host_token) as api: 

2523 res = api.CreateHostRequest( 

2524 requests_pb2.CreateHostRequestReq( 

2525 host_user_id=surfer.id, 

2526 from_date=(trip_from + timedelta(days=1)).isoformat(), 

2527 to_date=(trip_to - timedelta(days=1)).isoformat(), 

2528 text=valid_request_text(), 

2529 public_trip_id=trip_id, 

2530 ) 

2531 ) 

2532 assert res.host_request_id 

2533 

2534 

2535def test_create_request_with_hidden_public_trip(db): 

2536 """A hidden trip can't be offered on.""" 

2537 surfer, _ = generate_user() 

2538 host, host_token = generate_user() 

2539 

2540 trip_from = today() + timedelta(days=10) 

2541 trip_to = today() + timedelta(days=20) 

2542 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, visibility=ModerationVisibility.hidden) 

2543 

2544 with requests_session(host_token) as api: 

2545 with pytest.raises(grpc.RpcError) as e: 

2546 api.CreateHostRequest( 

2547 requests_pb2.CreateHostRequestReq( 

2548 host_user_id=surfer.id, 

2549 from_date=(trip_from + timedelta(days=1)).isoformat(), 

2550 to_date=(trip_to - timedelta(days=1)).isoformat(), 

2551 text=valid_request_text(), 

2552 public_trip_id=trip_id, 

2553 ) 

2554 ) 

2555 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2556 assert e.value.details() == "Couldn't find that public trip." 

2557 

2558 

2559def test_create_request_without_public_trip_id_unchanged(db, moderator): 

2560 """Existing flow without public_trip_id still works (backwards compatibility).""" 

2561 surfer, _ = generate_user() 

2562 host, host_token = generate_user() 

2563 

2564 with requests_session(host_token) as api: 

2565 res = api.CreateHostRequest( 

2566 requests_pb2.CreateHostRequestReq( 

2567 host_user_id=surfer.id, 

2568 from_date=(today() + timedelta(days=2)).isoformat(), 

2569 to_date=(today() + timedelta(days=3)).isoformat(), 

2570 text=valid_request_text(), 

2571 ) 

2572 ) 

2573 host_request_id = res.host_request_id 

2574 

2575 moderator.approve_host_request(host_request_id) 

2576 

2577 with requests_session(host_token) as api: 

2578 hr = api.GetHostRequest(requests_pb2.GetHostRequestReq(host_request_id=host_request_id)) 

2579 assert not hr.HasField("public_trip_id") 

2580 

2581 

2582def test_create_request_same_gender_only_wrong_gender_rejected(db): 

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

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

2585 

2586 trip_from = today() + timedelta(days=10) 

2587 trip_to = today() + timedelta(days=20) 

2588 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True) 

2589 

2590 with requests_session(host_token) as api: 

2591 with pytest.raises(grpc.RpcError) as e: 

2592 api.CreateHostRequest( 

2593 requests_pb2.CreateHostRequestReq( 

2594 host_user_id=surfer.id, 

2595 from_date=trip_from.isoformat(), 

2596 to_date=trip_to.isoformat(), 

2597 text=valid_request_text(), 

2598 public_trip_id=trip_id, 

2599 ) 

2600 ) 

2601 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

2602 

2603 

2604def test_create_request_same_gender_only_same_gender_allowed(db, moderator): 

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

2606 _, host_token = generate_user(gender="Woman") 

2607 

2608 trip_from = today() + timedelta(days=10) 

2609 trip_to = today() + timedelta(days=20) 

2610 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True) 

2611 

2612 with requests_session(host_token) as api: 

2613 res = api.CreateHostRequest( 

2614 requests_pb2.CreateHostRequestReq( 

2615 host_user_id=surfer.id, 

2616 from_date=trip_from.isoformat(), 

2617 to_date=trip_to.isoformat(), 

2618 text=valid_request_text(), 

2619 public_trip_id=trip_id, 

2620 ) 

2621 ) 

2622 assert res.host_request_id > 0 

2623 

2624 

2625def test_create_request_same_gender_only_moderator_bypass(db, moderator): 

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

2627 host, host_token = generate_user(gender="Man") 

2628 

2629 trip_from = today() + timedelta(days=10) 

2630 trip_to = today() + timedelta(days=20) 

2631 trip_id = _create_public_trip(surfer.id, trip_from, trip_to, same_gender_only=True) 

2632 _make_trip_node_admin(host.id, trip_id) 

2633 

2634 with requests_session(host_token) as api: 

2635 res = api.CreateHostRequest( 

2636 requests_pb2.CreateHostRequestReq( 

2637 host_user_id=surfer.id, 

2638 from_date=trip_from.isoformat(), 

2639 to_date=trip_to.isoformat(), 

2640 text=valid_request_text(), 

2641 public_trip_id=trip_id, 

2642 ) 

2643 ) 

2644 assert res.host_request_id > 0 

2645 

2646 

2647def test_create_request_duplicate_offer_rejected(db): 

2648 surfer, _ = generate_user() 

2649 _, host_token = generate_user() 

2650 

2651 trip_from = today() + timedelta(days=10) 

2652 trip_to = today() + timedelta(days=20) 

2653 trip_id = _create_public_trip(surfer.id, trip_from, trip_to) 

2654 

2655 with requests_session(host_token) as api: 

2656 api.CreateHostRequest( 

2657 requests_pb2.CreateHostRequestReq( 

2658 host_user_id=surfer.id, 

2659 from_date=trip_from.isoformat(), 

2660 to_date=trip_to.isoformat(), 

2661 text=valid_request_text(), 

2662 public_trip_id=trip_id, 

2663 ) 

2664 ) 

2665 with pytest.raises(grpc.RpcError) as e: 

2666 api.CreateHostRequest( 

2667 requests_pb2.CreateHostRequestReq( 

2668 host_user_id=surfer.id, 

2669 from_date=trip_from.isoformat(), 

2670 to_date=trip_to.isoformat(), 

2671 text=valid_request_text(), 

2672 public_trip_id=trip_id, 

2673 ) 

2674 ) 

2675 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION