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

1074 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-04 22:32 +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 

44 

45 

46@pytest.fixture(autouse=True) 

47def _(testconfig): 

48 pass 

49 

50 

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

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

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

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

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

56 return text 

57 padding_length = HOST_REQUEST_MIN_LENGTH_UTF16 - utf16_length 

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

59 

60 

61def test_create_request(db, moderator): 

62 user1, token1 = generate_user() 

63 hosting_city = "Morningside Heights, New York City" 

64 hosting_lat = 40.8086 

65 hosting_lng = -73.9616 

66 hosting_radius = 500 

67 user2, token2 = generate_user( 

68 city=hosting_city, 

69 geom=create_coordinate(hosting_lat, hosting_lng), 

70 geom_radius=hosting_radius, 

71 ) 

72 

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

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

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

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

77 

78 with requests_session(token1) as api: 

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

80 api.CreateHostRequest( 

81 requests_pb2.CreateHostRequestReq( 

82 host_user_id=user1.id, 

83 from_date=today_plus_2.isoformat(), 

84 to_date=today_plus_3.isoformat(), 

85 text=valid_request_text(), 

86 ) 

87 ) 

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

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

90 

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

92 api.CreateHostRequest( 

93 requests_pb2.CreateHostRequestReq( 

94 host_user_id=999, 

95 from_date=today_plus_2.isoformat(), 

96 to_date=today_plus_3.isoformat(), 

97 text=valid_request_text(), 

98 ) 

99 ) 

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

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

102 

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

104 api.CreateHostRequest( 

105 requests_pb2.CreateHostRequestReq( 

106 host_user_id=user2.id, 

107 from_date=today_plus_3.isoformat(), 

108 to_date=today_plus_2.isoformat(), 

109 text=valid_request_text(), 

110 ) 

111 ) 

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

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

114 

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

116 api.CreateHostRequest( 

117 requests_pb2.CreateHostRequestReq( 

118 host_user_id=user2.id, 

119 from_date=today_minus_3.isoformat(), 

120 to_date=today_plus_2.isoformat(), 

121 text=valid_request_text(), 

122 ) 

123 ) 

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

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

126 

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

128 api.CreateHostRequest( 

129 requests_pb2.CreateHostRequestReq( 

130 host_user_id=user2.id, 

131 from_date=today_plus_2.isoformat(), 

132 to_date=today_minus_2.isoformat(), 

133 text=valid_request_text(), 

134 ) 

135 ) 

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

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

138 

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

140 api.CreateHostRequest( 

141 requests_pb2.CreateHostRequestReq( 

142 host_user_id=user2.id, 

143 from_date="2020-00-06", 

144 to_date=today_minus_2.isoformat(), 

145 text=valid_request_text(), 

146 ) 

147 ) 

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

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

150 

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

152 api.CreateHostRequest( 

153 requests_pb2.CreateHostRequestReq( 

154 host_user_id=user2.id, 

155 from_date=today_plus_2.isoformat(), 

156 to_date=today_plus_3.isoformat(), 

157 text="Too short.", 

158 ) 

159 ) 

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

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

162 

163 res = api.CreateHostRequest( 

164 requests_pb2.CreateHostRequestReq( 

165 host_user_id=user2.id, 

166 from_date=today_plus_2.isoformat(), 

167 to_date=today_plus_3.isoformat(), 

168 text=valid_request_text(), 

169 ) 

170 ) 

171 host_request_id = res.host_request_id 

172 

173 moderator.approve_host_request(host_request_id) 

174 

175 with requests_session(token1) as api: 

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

177 

178 assert len(host_requests) == 1 

179 hr = host_requests[0] 

180 

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

182 

183 assert hr.hosting_city == hosting_city 

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

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

186 assert hr.hosting_radius == hosting_radius 

187 

188 today_ = today() 

189 today_plus_one_year = today_ + timedelta(days=365) 

190 today_plus_one_year_plus_2 = today_plus_one_year + timedelta(days=2) 

191 today_plus_one_year_plus_3 = today_plus_one_year + timedelta(days=3) 

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

193 api.CreateHostRequest( 

194 requests_pb2.CreateHostRequestReq( 

195 host_user_id=user2.id, 

196 from_date=today_plus_one_year_plus_2.isoformat(), 

197 to_date=today_plus_one_year_plus_3.isoformat(), 

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

199 ) 

200 ) 

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

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

203 

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

205 api.CreateHostRequest( 

206 requests_pb2.CreateHostRequestReq( 

207 host_user_id=user2.id, 

208 from_date=today_plus_2.isoformat(), 

209 to_date=today_plus_one_year_plus_3.isoformat(), 

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

211 ) 

212 ) 

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

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

215 

216 

217def test_create_host_request_rejects_date_past_in_host_timezone(db): 

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

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

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

221 # backend enforces the same rule for consistency. 

222 user1, token1 = generate_user() 

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

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

225 

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

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

228 

229 with patch( 

230 "couchers.servicers.requests.today_in_timezone", 

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

232 ): 

233 with requests_session(token1) as api: 

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

235 api.CreateHostRequest( 

236 requests_pb2.CreateHostRequestReq( 

237 host_user_id=user2.id, 

238 from_date="2026-01-15", 

239 to_date="2026-01-18", 

240 text=valid_request_text(), 

241 ) 

242 ) 

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

244 

245 

246def test_create_host_request_date_valid_when_host_behind_requester(db): 

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

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

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

250 # the host — must be accepted without issue. 

251 user1, token1 = generate_user() 

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

253 

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

255 

256 with patch( 

257 "couchers.servicers.requests.today_in_timezone", 

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

259 ): 

260 with requests_session(token1) as api: 

261 res = api.CreateHostRequest( 

262 requests_pb2.CreateHostRequestReq( 

263 host_user_id=user2.id, 

264 from_date="2026-01-16", 

265 to_date="2026-01-20", 

266 text=valid_request_text(), 

267 ) 

268 ) 

269 assert res.host_request_id 

270 

271 

272def test_create_request_duplicate_within_window(db): 

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

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

275 user1, token1 = generate_user() 

276 user2, _ = generate_user() 

277 user3, _ = generate_user() 

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

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

280 

281 with requests_session(token1) as api: 

282 assert api.CreateHostRequest( 

283 requests_pb2.CreateHostRequestReq( 

284 host_user_id=user2.id, 

285 from_date=from_date.isoformat(), 

286 to_date=to_date.isoformat(), 

287 text=valid_request_text(), 

288 ) 

289 ).host_request_id 

290 

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

292 api.CreateHostRequest( 

293 requests_pb2.CreateHostRequestReq( 

294 host_user_id=user2.id, 

295 from_date=from_date.isoformat(), 

296 to_date=to_date.isoformat(), 

297 text=valid_request_text(), 

298 ) 

299 ) 

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

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

302 

303 # partly overlapping dates are blocked too 

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

305 api.CreateHostRequest( 

306 requests_pb2.CreateHostRequestReq( 

307 host_user_id=user2.id, 

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

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

310 text=valid_request_text(), 

311 ) 

312 ) 

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

314 

315 # a different host is unaffected 

316 assert api.CreateHostRequest( 

317 requests_pb2.CreateHostRequestReq( 

318 host_user_id=user3.id, 

319 from_date=from_date.isoformat(), 

320 to_date=to_date.isoformat(), 

321 text=valid_request_text(), 

322 ) 

323 ).host_request_id 

324 

325 

326def test_create_request_duplicate_window_allows_other_dates(db): 

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

328 ends.""" 

329 user1, token1 = generate_user() 

330 user2, _ = generate_user() 

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

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

333 

334 with requests_session(token1) as api: 

335 assert api.CreateHostRequest( 

336 requests_pb2.CreateHostRequestReq( 

337 host_user_id=user2.id, 

338 from_date=from_date.isoformat(), 

339 to_date=to_date.isoformat(), 

340 text=valid_request_text(), 

341 ) 

342 ).host_request_id 

343 

344 assert api.CreateHostRequest( 

345 requests_pb2.CreateHostRequestReq( 

346 host_user_id=user2.id, 

347 from_date=to_date.isoformat(), 

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

349 text=valid_request_text(), 

350 ) 

351 ).host_request_id 

352 

353 assert api.CreateHostRequest( 

354 requests_pb2.CreateHostRequestReq( 

355 host_user_id=user2.id, 

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

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

358 text=valid_request_text(), 

359 ) 

360 ).host_request_id 

361 

362 

363def test_create_request_duplicate_allowed_after_window(db): 

364 user1, token1 = generate_user() 

365 user2, _ = generate_user() 

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

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

368 

369 with requests_session(token1) as api: 

370 api.CreateHostRequest( 

371 requests_pb2.CreateHostRequestReq( 

372 host_user_id=user2.id, 

373 from_date=from_date.isoformat(), 

374 to_date=to_date.isoformat(), 

375 text=valid_request_text(), 

376 ) 

377 ) 

378 backdate_conversations() 

379 

380 assert api.CreateHostRequest( 

381 requests_pb2.CreateHostRequestReq( 

382 host_user_id=user2.id, 

383 from_date=from_date.isoformat(), 

384 to_date=to_date.isoformat(), 

385 text=valid_request_text(), 

386 ) 

387 ).host_request_id 

388 

389 

390def test_create_request_duplicate_window_ignores_public_trip_offers(db): 

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

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

393 surfer, _ = generate_user() 

394 host, host_token = generate_user() 

395 

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

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

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

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

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

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

402 

403 with requests_session(host_token) as api: 

404 assert api.CreateHostRequest( 

405 requests_pb2.CreateHostRequestReq( 

406 host_user_id=surfer.id, 

407 from_date=trip_1_from.isoformat(), 

408 to_date=trip_1_to.isoformat(), 

409 text=valid_request_text(), 

410 public_trip_id=trip_1_id, 

411 ) 

412 ).host_request_id 

413 

414 assert api.CreateHostRequest( 

415 requests_pb2.CreateHostRequestReq( 

416 host_user_id=surfer.id, 

417 from_date=trip_2_from.isoformat(), 

418 to_date=trip_2_to.isoformat(), 

419 text=valid_request_text(), 

420 public_trip_id=trip_2_id, 

421 ) 

422 ).host_request_id 

423 

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

425 # even for dates they cover 

426 assert api.CreateHostRequest( 

427 requests_pb2.CreateHostRequestReq( 

428 host_user_id=surfer.id, 

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

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

431 text=valid_request_text(), 

432 ) 

433 ).host_request_id 

434 

435 

436def test_create_request_incomplete_profile(db): 

437 user1, token1 = generate_user(complete_profile=False) 

438 user2, _ = generate_user() 

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

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

441 with requests_session(token1) as api: 

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

443 api.CreateHostRequest( 

444 requests_pb2.CreateHostRequestReq( 

445 host_user_id=user2.id, 

446 from_date=today_plus_2.isoformat(), 

447 to_date=today_plus_3.isoformat(), 

448 text=valid_request_text(), 

449 ) 

450 ) 

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

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

453 

454 

455def test_excessive_requests_are_reported(db, email_collector: EmailCollector): 

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

457 user, token = generate_user() 

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

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

460 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.host_request] 

461 with requests_session(token) as api: 

462 # Test warning email 

463 for _ in range(rate_limit_definition.warning_limit): 

464 host_user, _ = generate_user() 

465 _ = api.CreateHostRequest( 

466 requests_pb2.CreateHostRequestReq( 

467 host_user_id=host_user.id, 

468 from_date=today_plus_2.isoformat(), 

469 to_date=today_plus_3.isoformat(), 

470 text=valid_request_text(), 

471 ) 

472 ) 

473 

474 assert email_collector.count_for_reports() == 0 

475 host_user, _ = generate_user() 

476 _ = api.CreateHostRequest( 

477 requests_pb2.CreateHostRequestReq( 

478 host_user_id=host_user.id, 

479 from_date=today_plus_2.isoformat(), 

480 to_date=today_plus_3.isoformat(), 

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

482 ) 

483 ) 

484 

485 email = email_collector.pop_for_reports(last=True) 

486 assert email.plain.startswith( 

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

488 ) 

489 

490 # Test ban after exceeding HOST_REQUEST_HARD_LIMIT 

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

492 host_user, _ = generate_user() 

493 _ = api.CreateHostRequest( 

494 requests_pb2.CreateHostRequestReq( 

495 host_user_id=host_user.id, 

496 from_date=today_plus_2.isoformat(), 

497 to_date=today_plus_3.isoformat(), 

498 text=valid_request_text(), 

499 ) 

500 ) 

501 

502 assert email_collector.count_for_reports() == 0 

503 

504 host_user, _ = generate_user() 

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

506 _ = api.CreateHostRequest( 

507 requests_pb2.CreateHostRequestReq( 

508 host_user_id=host_user.id, 

509 from_date=today_plus_2.isoformat(), 

510 to_date=today_plus_3.isoformat(), 

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

512 ) 

513 ) 

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

515 assert ( 

516 exc_info.value.details() 

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

518 ) 

519 

520 email = email_collector.pop_for_reports(last=True) 

521 assert email.plain.startswith( 

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

523 ) 

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

525 

526 

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

528 with session_scope() as session: 

529 message = Message( 

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

531 ) 

532 

533 session.add(message) 

534 

535 

536def test_GetHostRequest(db): 

537 user1, token1 = generate_user() 

538 user2, token2 = generate_user() 

539 user3, token3 = generate_user() 

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

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

542 with requests_session(token1) as api: 

543 host_request_id = api.CreateHostRequest( 

544 requests_pb2.CreateHostRequestReq( 

545 host_user_id=user2.id, 

546 from_date=today_plus_2.isoformat(), 

547 to_date=today_plus_3.isoformat(), 

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

549 ) 

550 ).host_request_id 

551 

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

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

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

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

556 

557 api.SendHostRequestMessage( 

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

559 ) 

560 

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

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

563 

564 

565def test_ListHostRequests(db, moderator): 

566 user1, token1 = generate_user() 

567 user2, token2 = generate_user() 

568 user3, token3 = generate_user() 

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

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

571 with requests_session(token1) as api: 

572 host_request_1 = api.CreateHostRequest( 

573 requests_pb2.CreateHostRequestReq( 

574 host_user_id=user2.id, 

575 from_date=today_plus_2.isoformat(), 

576 to_date=today_plus_3.isoformat(), 

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

578 ) 

579 ).host_request_id 

580 

581 host_request_2 = api.CreateHostRequest( 

582 requests_pb2.CreateHostRequestReq( 

583 host_user_id=user3.id, 

584 from_date=today_plus_2.isoformat(), 

585 to_date=today_plus_3.isoformat(), 

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

587 ) 

588 ).host_request_id 

589 

590 moderator.approve_host_request(host_request_1) 

591 moderator.approve_host_request(host_request_2) 

592 

593 with requests_session(token1) as api: 

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

595 assert res.no_more 

596 assert len(res.host_requests) == 2 

597 

598 with requests_session(token2) as api: 

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

600 assert res.no_more 

601 assert len(res.host_requests) == 1 

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

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

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

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

606 

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

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

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

610 

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

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

613 

614 host_request_3 = api.CreateHostRequest( 

615 requests_pb2.CreateHostRequestReq( 

616 host_user_id=user1.id, 

617 from_date=today_plus_2.isoformat(), 

618 to_date=today_plus_3.isoformat(), 

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

620 ) 

621 ).host_request_id 

622 

623 moderator.approve_host_request(host_request_3) 

624 

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

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

627 

628 with requests_session(token3) as api: 

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

630 assert res.no_more 

631 assert len(res.host_requests) == 1 

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

633 

634 with requests_session(token1) as api: 

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

636 assert len(res.host_requests) == 1 

637 

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

639 assert len(res.host_requests) == 3 

640 

641 

642def test_ListHostRequests_pagination_regression(db, moderator): 

643 """ 

644 ListHostRequests was skipping a request when getting multiple pages 

645 """ 

646 user1, token1 = generate_user() 

647 user2, token2 = generate_user() 

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

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

650 with requests_session(token1) as api: 

651 host_request_1 = api.CreateHostRequest( 

652 requests_pb2.CreateHostRequestReq( 

653 host_user_id=user2.id, 

654 from_date=today_plus_2.isoformat(), 

655 to_date=today_plus_3.isoformat(), 

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

657 ) 

658 ).host_request_id 

659 backdate_conversations() 

660 

661 host_request_2 = api.CreateHostRequest( 

662 requests_pb2.CreateHostRequestReq( 

663 host_user_id=user2.id, 

664 from_date=today_plus_2.isoformat(), 

665 to_date=today_plus_3.isoformat(), 

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

667 ) 

668 ).host_request_id 

669 backdate_conversations() 

670 

671 host_request_3 = api.CreateHostRequest( 

672 requests_pb2.CreateHostRequestReq( 

673 host_user_id=user2.id, 

674 from_date=today_plus_2.isoformat(), 

675 to_date=today_plus_3.isoformat(), 

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

677 ) 

678 ).host_request_id 

679 

680 moderator.approve_host_request(host_request_1) 

681 moderator.approve_host_request(host_request_2) 

682 moderator.approve_host_request(host_request_3) 

683 

684 with requests_session(token2) as api: 

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

686 assert res.no_more 

687 assert len(res.host_requests) == 3 

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

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

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

691 

692 with requests_session(token2) as api: 

693 api.RespondHostRequest( 

694 requests_pb2.RespondHostRequestReq( 

695 host_request_id=host_request_2, 

696 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

697 text="Accepting host request 2", 

698 ) 

699 ) 

700 api.RespondHostRequest( 

701 requests_pb2.RespondHostRequestReq( 

702 host_request_id=host_request_1, 

703 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

704 text="Accepting host request 1", 

705 ) 

706 ) 

707 api.RespondHostRequest( 

708 requests_pb2.RespondHostRequestReq( 

709 host_request_id=host_request_3, 

710 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

711 text="Accepting host request 3", 

712 ) 

713 ) 

714 

715 with requests_session(token2) as api: 

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

717 assert res.no_more 

718 assert len(res.host_requests) == 3 

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

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

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

722 

723 with requests_session(token2) as api: 

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

725 assert not res.no_more 

726 assert len(res.host_requests) == 1 

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

728 res = api.ListHostRequests( 

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

730 ) 

731 assert not res.no_more 

732 assert len(res.host_requests) == 1 

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

734 res = api.ListHostRequests( 

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

736 ) 

737 assert res.no_more 

738 assert len(res.host_requests) == 1 

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

740 

741 

742def test_ListHostRequests_sort_by_from_date(db, moderator): 

743 user1, token1 = generate_user() 

744 user2, token2 = generate_user() 

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

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

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

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

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

750 

751 with requests_session(token1) as api: 

752 hr_late = api.CreateHostRequest( 

753 requests_pb2.CreateHostRequestReq( 

754 host_user_id=user2.id, 

755 from_date=today_plus_7.isoformat(), 

756 to_date=today_plus_10.isoformat(), 

757 text=valid_request_text("Late request"), 

758 ) 

759 ).host_request_id 

760 

761 hr_early = api.CreateHostRequest( 

762 requests_pb2.CreateHostRequestReq( 

763 host_user_id=user2.id, 

764 from_date=today_plus_2.isoformat(), 

765 to_date=today_plus_3.isoformat(), 

766 text=valid_request_text("Early request"), 

767 ) 

768 ).host_request_id 

769 

770 hr_mid = api.CreateHostRequest( 

771 requests_pb2.CreateHostRequestReq( 

772 host_user_id=user2.id, 

773 from_date=today_plus_5.isoformat(), 

774 to_date=today_plus_7.isoformat(), 

775 text=valid_request_text("Mid request"), 

776 ) 

777 ).host_request_id 

778 

779 moderator.approve_host_request(hr_late) 

780 moderator.approve_host_request(hr_early) 

781 moderator.approve_host_request(hr_mid) 

782 

783 with requests_session(token2) as api: 

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

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

786 assert res.no_more 

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

788 

789 # from_date sort: ascending by travel date 

790 res = api.ListHostRequests( 

791 requests_pb2.ListHostRequestsReq( 

792 only_received=True, 

793 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

794 ) 

795 ) 

796 assert res.no_more 

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

798 

799 

800def test_ListHostRequests_sort_by_from_date_pagination(db, moderator): 

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

802 user1, token1 = generate_user() 

803 user2, token2 = generate_user() 

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

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

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

807 

808 with requests_session(token1) as api: 

809 hr_a = api.CreateHostRequest( 

810 requests_pb2.CreateHostRequestReq( 

811 host_user_id=user2.id, 

812 from_date=today_plus_2.isoformat(), 

813 to_date=today_plus_3.isoformat(), 

814 text=valid_request_text("Request A"), 

815 ) 

816 ).host_request_id 

817 backdate_conversations() 

818 

819 # Same from_date as A — tiebreaker by conversation_id 

820 hr_b = api.CreateHostRequest( 

821 requests_pb2.CreateHostRequestReq( 

822 host_user_id=user2.id, 

823 from_date=today_plus_2.isoformat(), 

824 to_date=today_plus_3.isoformat(), 

825 text=valid_request_text("Request B"), 

826 ) 

827 ).host_request_id 

828 backdate_conversations() 

829 

830 hr_c = api.CreateHostRequest( 

831 requests_pb2.CreateHostRequestReq( 

832 host_user_id=user2.id, 

833 from_date=today_plus_5.isoformat(), 

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

835 text=valid_request_text("Request C"), 

836 ) 

837 ).host_request_id 

838 

839 moderator.approve_host_request(hr_a) 

840 moderator.approve_host_request(hr_b) 

841 moderator.approve_host_request(hr_c) 

842 

843 with requests_session(token2) as api: 

844 res = api.ListHostRequests( 

845 requests_pb2.ListHostRequestsReq( 

846 only_received=True, 

847 number=1, 

848 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

849 ) 

850 ) 

851 assert not res.no_more 

852 assert len(res.host_requests) == 1 

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

854 

855 res = api.ListHostRequests( 

856 requests_pb2.ListHostRequestsReq( 

857 only_received=True, 

858 number=1, 

859 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

860 page_token=res.next_page_token, 

861 ) 

862 ) 

863 assert not res.no_more 

864 assert len(res.host_requests) == 1 

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

866 

867 res = api.ListHostRequests( 

868 requests_pb2.ListHostRequestsReq( 

869 only_received=True, 

870 number=1, 

871 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

872 page_token=res.next_page_token, 

873 ) 

874 ) 

875 assert res.no_more 

876 assert len(res.host_requests) == 1 

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

878 

879 

880def test_ListHostRequests_active_filter(db, moderator): 

881 user1, token1 = generate_user() 

882 user2, token2 = generate_user() 

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

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

885 

886 with requests_session(token1) as api: 

887 request_id = api.CreateHostRequest( 

888 requests_pb2.CreateHostRequestReq( 

889 host_user_id=user2.id, 

890 from_date=today_plus_2.isoformat(), 

891 to_date=today_plus_3.isoformat(), 

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

893 ) 

894 ).host_request_id 

895 

896 moderator.approve_host_request(request_id) 

897 

898 with requests_session(token1) as api: 

899 api.RespondHostRequest( 

900 requests_pb2.RespondHostRequestReq( 

901 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

902 ) 

903 ) 

904 

905 with requests_session(token2) as api: 

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

907 assert len(res.host_requests) == 1 

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

909 assert len(res.host_requests) == 0 

910 

911 

912def test_ListHostRequests_active_filter_excludes_past(db, moderator): 

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

914 user1, token1 = generate_user() 

915 user2, token2 = generate_user() 

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

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

918 

919 with requests_session(token1) as api: 

920 request_id = api.CreateHostRequest( 

921 requests_pb2.CreateHostRequestReq( 

922 host_user_id=user2.id, 

923 from_date=today_plus_2.isoformat(), 

924 to_date=today_plus_3.isoformat(), 

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

926 ) 

927 ).host_request_id 

928 

929 moderator.approve_host_request(request_id) 

930 

931 with requests_session(token2) as api: 

932 api.RespondHostRequest( 

933 requests_pb2.RespondHostRequestReq( 

934 host_request_id=request_id, 

935 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

936 ) 

937 ) 

938 

939 # Future request is visible with only_active 

940 with requests_session(token2) as api: 

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

942 assert len(res.host_requests) == 1 

943 

944 # Move dates into the past 

945 with session_scope() as session: 

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

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

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

949 

950 # Past request must be excluded by only_active 

951 with requests_session(token2) as api: 

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

953 assert len(res.host_requests) == 0 

954 

955 # Still visible without the filter 

956 with requests_session(token2) as api: 

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

958 assert len(res.host_requests) == 1 

959 

960 

961def test_ListHostRequests_status_in_filter(db, moderator): 

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

963 user1, token1 = generate_user() 

964 user2, token2 = generate_user() 

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

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

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

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

969 

970 # Create a pending request 

971 with requests_session(token1) as api: 

972 pending_id = api.CreateHostRequest( 

973 requests_pb2.CreateHostRequestReq( 

974 host_user_id=user2.id, 

975 from_date=today_plus_2.isoformat(), 

976 to_date=today_plus_3.isoformat(), 

977 text=valid_request_text("Pending"), 

978 ) 

979 ).host_request_id 

980 

981 moderator.approve_host_request(pending_id) 

982 

983 # Create an accepted request 

984 with requests_session(token1) as api: 

985 accepted_id = api.CreateHostRequest( 

986 requests_pb2.CreateHostRequestReq( 

987 host_user_id=user2.id, 

988 from_date=today_plus_4.isoformat(), 

989 to_date=today_plus_5.isoformat(), 

990 text=valid_request_text("Accepted"), 

991 ) 

992 ).host_request_id 

993 

994 moderator.approve_host_request(accepted_id) 

995 

996 with requests_session(token2) as api: 

997 api.RespondHostRequest( 

998 requests_pb2.RespondHostRequestReq( 

999 host_request_id=accepted_id, 

1000 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1001 ) 

1002 ) 

1003 

1004 # Filter to accepted only 

1005 with requests_session(token2) as api: 

1006 res = api.ListHostRequests( 

1007 requests_pb2.ListHostRequestsReq( 

1008 status_in=[messages_pb2.HOST_REQUEST_STATUS_ACCEPTED], 

1009 ) 

1010 ) 

1011 assert len(res.host_requests) == 1 

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

1013 

1014 # Filter to pending only 

1015 with requests_session(token2) as api: 

1016 res = api.ListHostRequests( 

1017 requests_pb2.ListHostRequestsReq( 

1018 status_in=[messages_pb2.HOST_REQUEST_STATUS_PENDING], 

1019 ) 

1020 ) 

1021 assert len(res.host_requests) == 1 

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

1023 

1024 # Filter to accepted + pending — both appear 

1025 with requests_session(token2) as api: 

1026 res = api.ListHostRequests( 

1027 requests_pb2.ListHostRequestsReq( 

1028 status_in=[ 

1029 messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1030 messages_pb2.HOST_REQUEST_STATUS_PENDING, 

1031 ], 

1032 ) 

1033 ) 

1034 assert len(res.host_requests) == 2 

1035 

1036 # Filter to confirmed — none appear 

1037 with requests_session(token2) as api: 

1038 res = api.ListHostRequests( 

1039 requests_pb2.ListHostRequestsReq( 

1040 status_in=[messages_pb2.HOST_REQUEST_STATUS_CONFIRMED], 

1041 ) 

1042 ) 

1043 assert len(res.host_requests) == 0 

1044 

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

1046 with requests_session(token2) as api: 

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

1048 assert len(res.host_requests) == 2 

1049 

1050 

1051def test_RespondHostRequests(db, moderator): 

1052 user1, token1 = generate_user() 

1053 user2, token2 = generate_user() 

1054 user3, token3 = generate_user() 

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

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

1057 

1058 with requests_session(token1) as api: 

1059 request_id = api.CreateHostRequest( 

1060 requests_pb2.CreateHostRequestReq( 

1061 host_user_id=user2.id, 

1062 from_date=today_plus_2.isoformat(), 

1063 to_date=today_plus_3.isoformat(), 

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

1065 ) 

1066 ).host_request_id 

1067 

1068 moderator.approve_host_request(request_id) 

1069 

1070 # another user can't access 

1071 with requests_session(token3) as api: 

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

1073 api.RespondHostRequest( 

1074 requests_pb2.RespondHostRequestReq( 

1075 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1076 ) 

1077 ) 

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

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

1080 

1081 with requests_session(token1) as api: 

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

1083 api.RespondHostRequest( 

1084 requests_pb2.RespondHostRequestReq( 

1085 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1086 ) 

1087 ) 

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

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

1090 

1091 with requests_session(token2) as api: 

1092 # non existing id 

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

1094 api.RespondHostRequest( 

1095 requests_pb2.RespondHostRequestReq( 

1096 host_request_id=9999, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1097 ) 

1098 ) 

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

1100 

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

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

1103 api.RespondHostRequest( 

1104 requests_pb2.RespondHostRequestReq( 

1105 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1106 ) 

1107 ) 

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

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

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

1111 api.RespondHostRequest( 

1112 requests_pb2.RespondHostRequestReq( 

1113 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1114 ) 

1115 ) 

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

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

1118 

1119 api.RespondHostRequest( 

1120 requests_pb2.RespondHostRequestReq( 

1121 host_request_id=request_id, 

1122 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED, 

1123 text="Test rejection message", 

1124 ) 

1125 ) 

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

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

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

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

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

1131 api.RespondHostRequest( 

1132 requests_pb2.RespondHostRequestReq( 

1133 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1134 ) 

1135 ) 

1136 

1137 with requests_session(token1) as api: 

1138 # can't make pending 

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

1140 api.RespondHostRequest( 

1141 requests_pb2.RespondHostRequestReq( 

1142 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_PENDING 

1143 ) 

1144 ) 

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

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

1147 

1148 # can confirm then cancel 

1149 api.RespondHostRequest( 

1150 requests_pb2.RespondHostRequestReq( 

1151 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1152 ) 

1153 ) 

1154 

1155 api.RespondHostRequest( 

1156 requests_pb2.RespondHostRequestReq( 

1157 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1158 ) 

1159 ) 

1160 

1161 # can't confirm after having cancelled 

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

1163 api.RespondHostRequest( 

1164 requests_pb2.RespondHostRequestReq( 

1165 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1166 ) 

1167 ) 

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

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

1170 

1171 # at this point there should be 7 messages 

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

1173 with requests_session(token1) as api: 

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

1175 assert len(res.messages) == 7 

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

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

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

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

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

1181 

1182 

1183def test_get_host_request_messages(db, moderator): 

1184 user1, token1 = generate_user() 

1185 user2, token2 = generate_user() 

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

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

1188 with requests_session(token1) as api: 

1189 res = api.CreateHostRequest( 

1190 requests_pb2.CreateHostRequestReq( 

1191 host_user_id=user2.id, 

1192 from_date=today_plus_2.isoformat(), 

1193 to_date=today_plus_3.isoformat(), 

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

1195 ) 

1196 ) 

1197 conversation_id = res.host_request_id 

1198 

1199 moderator.approve_host_request(conversation_id) 

1200 

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

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

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

1204 

1205 with requests_session(token2) as api: 

1206 api.RespondHostRequest( 

1207 requests_pb2.RespondHostRequestReq( 

1208 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1209 ) 

1210 ) 

1211 

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

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

1214 

1215 api.RespondHostRequest( 

1216 requests_pb2.RespondHostRequestReq( 

1217 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1218 ) 

1219 ) 

1220 

1221 with requests_session(token1) as api: 

1222 # 9 including initial message 

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

1224 assert len(res.messages) == 9 

1225 assert res.no_more 

1226 

1227 res = api.GetHostRequestMessages( 

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

1229 ) 

1230 assert not res.no_more 

1231 assert len(res.messages) == 3 

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

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

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

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

1236 

1237 res = api.GetHostRequestMessages( 

1238 requests_pb2.GetHostRequestMessagesReq( 

1239 host_request_id=conversation_id, 

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

1241 number=6, 

1242 ) 

1243 ) 

1244 assert res.no_more 

1245 assert len(res.messages) == 6 

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

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

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

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

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

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

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

1253 

1254 

1255def test_SendHostRequestMessage(db, moderator): 

1256 user1, token1 = generate_user() 

1257 user2, token2 = generate_user() 

1258 user3, token3 = generate_user() 

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

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

1261 with requests_session(token1) as api: 

1262 host_request_id = api.CreateHostRequest( 

1263 requests_pb2.CreateHostRequestReq( 

1264 host_user_id=user2.id, 

1265 from_date=today_plus_2.isoformat(), 

1266 to_date=today_plus_3.isoformat(), 

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

1268 ) 

1269 ).host_request_id 

1270 

1271 moderator.approve_host_request(host_request_id) 

1272 

1273 with requests_session(token1) as api: 

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

1275 api.SendHostRequestMessage( 

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

1277 ) 

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

1279 

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

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

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

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

1284 

1285 api.SendHostRequestMessage( 

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

1287 ) 

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

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

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

1291 

1292 with requests_session(token3) as api: 

1293 # other user can't send 

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

1295 api.SendHostRequestMessage( 

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

1297 ) 

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

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

1300 

1301 with requests_session(token2) as api: 

1302 api.SendHostRequestMessage( 

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

1304 ) 

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

1306 # including 2 for creation control message and message 

1307 assert len(res.messages) == 4 

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

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

1310 

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

1312 api.RespondHostRequest( 

1313 requests_pb2.RespondHostRequestReq( 

1314 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1315 ) 

1316 ) 

1317 api.SendHostRequestMessage( 

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

1319 ) 

1320 

1321 api.RespondHostRequest( 

1322 requests_pb2.RespondHostRequestReq( 

1323 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1324 ) 

1325 ) 

1326 

1327 with requests_session(token1) as api: 

1328 api.RespondHostRequest( 

1329 requests_pb2.RespondHostRequestReq( 

1330 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1331 ) 

1332 ) 

1333 api.SendHostRequestMessage( 

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

1335 ) 

1336 

1337 api.RespondHostRequest( 

1338 requests_pb2.RespondHostRequestReq( 

1339 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1340 ) 

1341 ) 

1342 api.SendHostRequestMessage( 

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

1344 ) 

1345 

1346 

1347def test_get_updates(db, moderator): 

1348 user1, token1 = generate_user() 

1349 user2, token2 = generate_user() 

1350 user3, token3 = generate_user() 

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

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

1353 with requests_session(token1) as api: 

1354 host_request_id = api.CreateHostRequest( 

1355 requests_pb2.CreateHostRequestReq( 

1356 host_user_id=user2.id, 

1357 from_date=today_plus_2.isoformat(), 

1358 to_date=today_plus_3.isoformat(), 

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

1360 ) 

1361 ).host_request_id 

1362 

1363 moderator.approve_host_request(host_request_id) 

1364 

1365 with requests_session(token1) as api: 

1366 api.SendHostRequestMessage( 

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

1368 ) 

1369 api.SendHostRequestMessage( 

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

1371 ) 

1372 api.RespondHostRequest( 

1373 requests_pb2.RespondHostRequestReq( 

1374 host_request_id=host_request_id, 

1375 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

1376 text="Test message 3", 

1377 ) 

1378 ) 

1379 

1380 backdate_conversations() 

1381 api.CreateHostRequest( 

1382 requests_pb2.CreateHostRequestReq( 

1383 host_user_id=user2.id, 

1384 from_date=today_plus_2.isoformat(), 

1385 to_date=today_plus_3.isoformat(), 

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

1387 ) 

1388 ) 

1389 

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

1391 assert len(res.messages) == 6 

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

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

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

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

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

1397 message_id_3 = res.messages[0].message_id 

1398 message_id_cancel = res.messages[1].message_id 

1399 message_id_2 = res.messages[2].message_id 

1400 message_id_1 = res.messages[3].message_id 

1401 message_id_0 = res.messages[4].message_id 

1402 

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

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

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

1406 

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

1408 assert res.no_more 

1409 assert len(res.updates) == 5 

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

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

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

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

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

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

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

1417 

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

1419 assert not res.no_more 

1420 assert len(res.updates) == 1 

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

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

1423 

1424 with requests_session(token3) as api: 

1425 # other user can't access 

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

1427 assert len(res.updates) == 0 

1428 

1429 

1430def test_archive_host_request(db, moderator): 

1431 user1, token1 = generate_user() 

1432 user2, token2 = generate_user() 

1433 

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

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

1436 

1437 with requests_session(token1) as api: 

1438 host_request_id = api.CreateHostRequest( 

1439 requests_pb2.CreateHostRequestReq( 

1440 host_user_id=user2.id, 

1441 from_date=today_plus_2.isoformat(), 

1442 to_date=today_plus_3.isoformat(), 

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

1444 ) 

1445 ).host_request_id 

1446 

1447 api.SendHostRequestMessage( 

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

1449 ) 

1450 api.SendHostRequestMessage( 

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

1452 ) 

1453 

1454 moderator.approve_host_request(host_request_id) 

1455 

1456 # happy path archiving host request 

1457 with requests_session(token1) as api: 

1458 api.RespondHostRequest( 

1459 requests_pb2.RespondHostRequestReq( 

1460 host_request_id=host_request_id, 

1461 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

1462 text="Test message 3", 

1463 ) 

1464 ) 

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

1466 assert len(res.host_requests) == 1 

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

1468 

1469 # Verify is_archived is False before archiving 

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

1471 assert not res.is_archived 

1472 

1473 api.SetHostRequestArchiveStatus( 

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

1475 ) 

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

1477 assert len(res.host_requests) == 1 

1478 

1479 # Verify is_archived is True after archiving 

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

1481 assert res.is_archived 

1482 

1483 

1484def test_mark_last_seen(db, moderator): 

1485 user1, token1 = generate_user() 

1486 user2, token2 = generate_user() 

1487 user3, token3 = generate_user() 

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

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

1490 with requests_session(token1) as api: 

1491 host_request_id = api.CreateHostRequest( 

1492 requests_pb2.CreateHostRequestReq( 

1493 host_user_id=user2.id, 

1494 from_date=today_plus_2.isoformat(), 

1495 to_date=today_plus_3.isoformat(), 

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

1497 ) 

1498 ).host_request_id 

1499 backdate_conversations() 

1500 

1501 host_request_id_2 = api.CreateHostRequest( 

1502 requests_pb2.CreateHostRequestReq( 

1503 host_user_id=user2.id, 

1504 from_date=today_plus_2.isoformat(), 

1505 to_date=today_plus_3.isoformat(), 

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

1507 ) 

1508 ).host_request_id 

1509 

1510 moderator.approve_host_request(host_request_id) 

1511 moderator.approve_host_request(host_request_id_2) 

1512 

1513 with requests_session(token1) as api: 

1514 api.SendHostRequestMessage( 

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

1516 ) 

1517 api.SendHostRequestMessage( 

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

1519 ) 

1520 api.RespondHostRequest( 

1521 requests_pb2.RespondHostRequestReq( 

1522 host_request_id=host_request_id, 

1523 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

1524 text="Test message 3", 

1525 ) 

1526 ) 

1527 

1528 moderator.approve_host_request(host_request_id) 

1529 moderator.approve_host_request(host_request_id_2) 

1530 

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

1532 with api_session(token1) as api: 

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

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

1535 

1536 with api_session(token2) as api: 

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

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

1539 

1540 with requests_session(token2) as api: 

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

1542 

1543 api.MarkLastSeenHostRequest( 

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

1545 ) 

1546 

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

1548 

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

1550 api.MarkLastSeenHostRequest( 

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

1552 ) 

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

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

1555 

1556 # this will be used to test sent request notifications 

1557 host_request_id_3 = api.CreateHostRequest( 

1558 requests_pb2.CreateHostRequestReq( 

1559 host_user_id=user1.id, 

1560 from_date=today_plus_2.isoformat(), 

1561 to_date=today_plus_3.isoformat(), 

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

1563 ) 

1564 ).host_request_id 

1565 

1566 moderator.approve_host_request(host_request_id_3) 

1567 

1568 with requests_session(token2) as api: 

1569 # this should make id_2 all read 

1570 api.SendHostRequestMessage( 

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

1572 ) 

1573 

1574 with api_session(token2) as api: 

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

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

1577 

1578 # make sure sent and received count for unseen notifications 

1579 with requests_session(token1) as api: 

1580 api.SendHostRequestMessage( 

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

1582 ) 

1583 

1584 with api_session(token2) as api: 

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

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

1587 

1588 

1589def test_mark_last_seen_clears_notifications(db, moderator): 

1590 user1, token1 = generate_user() 

1591 user2, token2 = generate_user() 

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

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

1594 

1595 with requests_session(token1) as api: 

1596 host_request_id = api.CreateHostRequest( 

1597 requests_pb2.CreateHostRequestReq( 

1598 host_user_id=user2.id, 

1599 from_date=today_plus_2.isoformat(), 

1600 to_date=today_plus_3.isoformat(), 

1601 text=valid_request_text("Test message"), 

1602 ) 

1603 ).host_request_id 

1604 

1605 moderator.approve_host_request(host_request_id) 

1606 

1607 def unseen_notification_count(user_id): 

1608 with session_scope() as session: 

1609 return session.execute( 

1610 select(func.count()) 

1611 .select_from(Notification) 

1612 .where(Notification.user_id == user_id) 

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

1614 .where(Notification.is_seen == False) 

1615 ).scalar_one() 

1616 

1617 assert unseen_notification_count(user2.id) > 0 

1618 

1619 with requests_session(token2) as api: 

1620 api.MarkLastSeenHostRequest( 

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

1622 ) 

1623 

1624 assert unseen_notification_count(user2.id) == 0 

1625 

1626 

1627def test_response_rate(db, moderator): 

1628 user1, token1 = generate_user() 

1629 user2, token2 = generate_user() 

1630 user3, token3 = generate_user(delete_user=True) 

1631 

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

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

1634 

1635 with session_scope() as session: 

1636 refresh_materialized_view(session, "user_response_rates") 

1637 

1638 with requests_session(token1) as api: 

1639 # deleted: not found 

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

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

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

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

1644 

1645 # no requests: insufficient 

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

1647 assert res.HasField("insufficient_data") 

1648 

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

1650 host_request_1 = api.CreateHostRequest( 

1651 requests_pb2.CreateHostRequestReq( 

1652 host_user_id=user2.id, 

1653 from_date=today_plus_2.isoformat(), 

1654 to_date=today_plus_3.isoformat(), 

1655 text=valid_request_text("Test request"), 

1656 ) 

1657 ).host_request_id 

1658 moderator.approve_host_request(host_request_1) 

1659 backdate_conversations() 

1660 with session_scope() as session: 

1661 session.execute( 

1662 select(Message) 

1663 .where(Message.conversation_id == host_request_1) 

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

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

1666 refresh_materialized_view(session, "user_response_rates") 

1667 

1668 # still insufficient 

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

1670 assert res.HasField("insufficient_data") 

1671 

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

1673 host_request_2 = api.CreateHostRequest( 

1674 requests_pb2.CreateHostRequestReq( 

1675 host_user_id=user2.id, 

1676 from_date=today_plus_2.isoformat(), 

1677 to_date=today_plus_3.isoformat(), 

1678 text=valid_request_text("Test request"), 

1679 ) 

1680 ).host_request_id 

1681 moderator.approve_host_request(host_request_2) 

1682 backdate_conversations() 

1683 with session_scope() as session: 

1684 session.execute( 

1685 select(Message) 

1686 .where(Message.conversation_id == host_request_2) 

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

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

1689 refresh_materialized_view(session, "user_response_rates") 

1690 

1691 # still insufficient 

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

1693 assert res.HasField("insufficient_data") 

1694 

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

1696 host_request_3 = api.CreateHostRequest( 

1697 requests_pb2.CreateHostRequestReq( 

1698 host_user_id=user2.id, 

1699 from_date=today_plus_2.isoformat(), 

1700 to_date=today_plus_3.isoformat(), 

1701 text=valid_request_text("Test request"), 

1702 ) 

1703 ).host_request_id 

1704 moderator.approve_host_request(host_request_3) 

1705 backdate_conversations() 

1706 with session_scope() as session: 

1707 session.execute( 

1708 select(Message) 

1709 .where(Message.conversation_id == host_request_3) 

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

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

1712 refresh_materialized_view(session, "user_response_rates") 

1713 

1714 # now low 

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

1716 assert res.HasField("low") 

1717 

1718 with requests_session(token2) as api: 

1719 # accept a host req 

1720 api.RespondHostRequest( 

1721 requests_pb2.RespondHostRequestReq( 

1722 host_request_id=host_request_2, 

1723 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1724 text="Accepting host request", 

1725 ) 

1726 ) 

1727 

1728 with session_scope() as session: 

1729 refresh_materialized_view(session, "user_response_rates") 

1730 

1731 with requests_session(token1) as api: 

1732 # now some w p33 = 35h 

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

1734 assert res.HasField("some") 

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

1736 

1737 with requests_session(token2) as api: 

1738 # accept another host req 

1739 api.RespondHostRequest( 

1740 requests_pb2.RespondHostRequestReq( 

1741 host_request_id=host_request_3, 

1742 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1743 text="Accepting host request", 

1744 ) 

1745 ) 

1746 

1747 with session_scope() as session: 

1748 refresh_materialized_view(session, "user_response_rates") 

1749 

1750 with requests_session(token1) as api: 

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

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

1753 assert res.HasField("most") 

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

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

1756 

1757 with requests_session(token2) as api: 

1758 # accept last host req 

1759 api.RespondHostRequest( 

1760 requests_pb2.RespondHostRequestReq( 

1761 host_request_id=host_request_1, 

1762 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1763 text="Accepting host request", 

1764 ) 

1765 ) 

1766 

1767 with session_scope() as session: 

1768 refresh_materialized_view(session, "user_response_rates") 

1769 

1770 with requests_session(token1) as api: 

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

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

1773 assert res.HasField("almost_all") 

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

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

1776 

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

1778 host_request_4 = api.CreateHostRequest( 

1779 requests_pb2.CreateHostRequestReq( 

1780 host_user_id=user2.id, 

1781 from_date=today_plus_2.isoformat(), 

1782 to_date=today_plus_3.isoformat(), 

1783 text=valid_request_text("Test request"), 

1784 ) 

1785 ).host_request_id 

1786 moderator.approve_host_request(host_request_4) 

1787 backdate_conversations() 

1788 with session_scope() as session: 

1789 session.execute( 

1790 select(Message) 

1791 .where(Message.conversation_id == host_request_4) 

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

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

1794 refresh_materialized_view(session, "user_response_rates") 

1795 

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

1797 host_request_5 = api.CreateHostRequest( 

1798 requests_pb2.CreateHostRequestReq( 

1799 host_user_id=user2.id, 

1800 from_date=today_plus_2.isoformat(), 

1801 to_date=today_plus_3.isoformat(), 

1802 text=valid_request_text("Test request"), 

1803 ) 

1804 ).host_request_id 

1805 moderator.approve_host_request(host_request_5) 

1806 backdate_conversations() 

1807 with session_scope() as session: 

1808 session.execute( 

1809 select(Message) 

1810 .where(Message.conversation_id == host_request_5) 

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

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

1813 refresh_materialized_view(session, "user_response_rates") 

1814 

1815 # now some w p33 = 35h 

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

1817 assert res.HasField("some") 

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

1819 

1820 with requests_session(token2) as api: 

1821 # accept host req 

1822 api.RespondHostRequest( 

1823 requests_pb2.RespondHostRequestReq( 

1824 host_request_id=host_request_5, 

1825 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1826 text="Accepting host request", 

1827 ) 

1828 ) 

1829 

1830 with session_scope() as session: 

1831 refresh_materialized_view(session, "user_response_rates") 

1832 

1833 with requests_session(token1) as api: 

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

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

1836 assert res.HasField("most") 

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

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

1839 

1840 with requests_session(token2) as api: 

1841 # accept host req 

1842 api.RespondHostRequest( 

1843 requests_pb2.RespondHostRequestReq( 

1844 host_request_id=host_request_4, 

1845 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1846 text="Accepting host request", 

1847 ) 

1848 ) 

1849 

1850 with session_scope() as session: 

1851 refresh_materialized_view(session, "user_response_rates") 

1852 

1853 with requests_session(token1) as api: 

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

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

1856 assert res.HasField("almost_all") 

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

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

1859 

1860 

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

1862 host, host_token = generate_user(complete_profile=True) 

1863 surfer, surfer_token = generate_user(complete_profile=True) 

1864 

1865 host_loc_context = LocalizationContext.from_user(host) 

1866 surfer_loc_context = LocalizationContext.from_user(surfer) 

1867 

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

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

1870 

1871 with requests_session(surfer_token) as api: 

1872 hr_id = api.CreateHostRequest( 

1873 requests_pb2.CreateHostRequestReq( 

1874 host_user_id=host.id, 

1875 from_date=today_plus_2.isoformat(), 

1876 to_date=today_plus_3.isoformat(), 

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

1878 ) 

1879 ).host_request_id 

1880 

1881 moderator.approve_host_request(hr_id) 

1882 

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

1884 assert email.recipient == host.email 

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

1886 assert host.name in email.plain 

1887 assert host.name in email.html 

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

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

1890 assert surfer.name in email.plain 

1891 assert surfer.name in email.html 

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

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

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

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

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

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

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

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

1900 assert not email.attachments 

1901 

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

1903 

1904 with requests_session(host_token) as api: 

1905 api.RespondHostRequest( 

1906 requests_pb2.RespondHostRequestReq( 

1907 host_request_id=hr_id, 

1908 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1909 text="Accepting host request", 

1910 ) 

1911 ) 

1912 

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

1914 assert email.recipient == surfer.email 

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

1916 assert host.name in email.plain 

1917 assert host.name in email.html 

1918 assert surfer.name in email.plain 

1919 assert surfer.name in email.html 

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

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

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

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

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

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

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

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

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

1929 

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

1931 

1932 

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

1934 host, host_token = generate_user(complete_profile=True) 

1935 surfer, surfer_token = generate_user(complete_profile=True) 

1936 

1937 host_loc_context = LocalizationContext.from_user(host) 

1938 

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

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

1941 

1942 with requests_session(surfer_token) as api: 

1943 hr_id = api.CreateHostRequest( 

1944 requests_pb2.CreateHostRequestReq( 

1945 host_user_id=host.id, 

1946 from_date=today_plus_2.isoformat(), 

1947 to_date=today_plus_3.isoformat(), 

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

1949 ) 

1950 ).host_request_id 

1951 

1952 moderator.approve_host_request(hr_id) 

1953 

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

1955 assert email.recipient == host.email 

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

1957 assert host.name in email.plain 

1958 assert host.name in email.html 

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

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

1961 assert surfer.name in email.plain 

1962 assert surfer.name in email.html 

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

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

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

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

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

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

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

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

1971 

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

1973 

1974 # very ugly 

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

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

1977 if "payload" not in link: 

1978 continue 

1979 print(link) 

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

1981 params = parse_qs(url_parts.query) 

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

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

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

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

1986 res = auth_api.Unsubscribe( 

1987 auth_pb2.UnsubscribeReq( 

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

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

1990 ) 

1991 ) 

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

1993 break 

1994 else: 

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

1996 

1997 with requests_session(surfer_token) as api: 

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

1999 assert res.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED 

2000 

2001 

2002def test_host_req_feedback(db, moderator): 

2003 host, host_token = generate_user(complete_profile=True) 

2004 host2, host2_token = generate_user(complete_profile=True) 

2005 host3, host3_token = generate_user(complete_profile=True) 

2006 surfer, surfer_token = generate_user(complete_profile=True) 

2007 

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

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

2010 

2011 with requests_session(surfer_token) as api: 

2012 hr_id = api.CreateHostRequest( 

2013 requests_pb2.CreateHostRequestReq( 

2014 host_user_id=host.id, 

2015 from_date=today_plus_2.isoformat(), 

2016 to_date=today_plus_3.isoformat(), 

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

2018 ) 

2019 ).host_request_id 

2020 hr2_id = api.CreateHostRequest( 

2021 requests_pb2.CreateHostRequestReq( 

2022 host_user_id=host2.id, 

2023 from_date=today_plus_2.isoformat(), 

2024 to_date=today_plus_3.isoformat(), 

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

2026 ) 

2027 ).host_request_id 

2028 hr3_id = api.CreateHostRequest( 

2029 requests_pb2.CreateHostRequestReq( 

2030 host_user_id=host3.id, 

2031 from_date=today_plus_2.isoformat(), 

2032 to_date=today_plus_3.isoformat(), 

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

2034 ) 

2035 ).host_request_id 

2036 

2037 moderator.approve_host_request(hr_id) 

2038 moderator.approve_host_request(hr2_id) 

2039 moderator.approve_host_request(hr3_id) 

2040 

2041 with requests_session(host_token) as api: 

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

2043 assert not res.need_host_request_feedback 

2044 

2045 api.RespondHostRequest( 

2046 requests_pb2.RespondHostRequestReq( 

2047 host_request_id=hr_id, 

2048 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED, 

2049 ) 

2050 ) 

2051 

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

2053 assert res.need_host_request_feedback 

2054 

2055 # surfer can't leave feedback 

2056 with requests_session(surfer_token) as api: 

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

2058 api.SendHostRequestFeedback( 

2059 requests_pb2.SendHostRequestFeedbackReq( 

2060 host_request_id=hr_id, 

2061 ) 

2062 ) 

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

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

2065 

2066 with requests_session(host_token) as api: 

2067 api.SendHostRequestFeedback( 

2068 requests_pb2.SendHostRequestFeedbackReq( 

2069 host_request_id=hr_id, 

2070 host_request_quality=requests_pb2.HOST_REQUEST_QUALITY_LOW, 

2071 ) 

2072 ) 

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

2074 assert not res.need_host_request_feedback 

2075 

2076 # can't leave it twice 

2077 with requests_session(host_token) as api: 

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

2079 api.SendHostRequestFeedback( 

2080 requests_pb2.SendHostRequestFeedbackReq( 

2081 host_request_id=hr_id, 

2082 ) 

2083 ) 

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

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

2086 

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

2088 assert not res.need_host_request_feedback 

2089 

2090 with requests_session(host2_token) as api: 

2091 api.RespondHostRequest( 

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

2093 ) 

2094 # can't leave feedback on the wrong one 

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

2096 api.SendHostRequestFeedback( 

2097 requests_pb2.SendHostRequestFeedbackReq( 

2098 host_request_id=hr_id, 

2099 ) 

2100 ) 

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

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

2103 

2104 # null feedback is still feedback 

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

2106 

2107 with requests_session(host3_token) as api: 

2108 api.RespondHostRequest( 

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

2110 ) 

2111 

2112 api.SendHostRequestFeedback( 

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

2114 ) 

2115 

2116 

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

2118 with session_scope() as session: 

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

2120 cluster = session.execute( 

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

2122 ).scalar_one_or_none() 

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

2124 cluster = Cluster( 

2125 name="Test community", 

2126 description="Test", 

2127 parent_node_id=node_id, 

2128 is_official_cluster=True, 

2129 ) 

2130 session.add(cluster) 

2131 session.flush() 

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

2133 

2134 

2135def _create_public_trip( 

2136 user_id: int, 

2137 from_date, 

2138 to_date, 

2139 *, 

2140 status=None, 

2141 same_gender_only: bool = False, 

2142 visibility: ModerationVisibility = ModerationVisibility.visible, 

2143): 

2144 with session_scope() as session: 

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

2146 if node is None: 

2147 node = Node( 

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

2149 node_type=NodeType.locality, 

2150 ) 

2151 session.add(node) 

2152 session.flush() 

2153 moderation_state = ModerationState( 

2154 object_type=ModerationObjectType.public_trip, 

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

2156 visibility=visibility, 

2157 ) 

2158 session.add(moderation_state) 

2159 session.flush() 

2160 trip = PublicTrip( 

2161 user_id=user_id, 

2162 node_id=node.id, 

2163 from_date=from_date, 

2164 to_date=to_date, 

2165 description="Looking for a host!", 

2166 status=status or PublicTripStatus.searching_for_host, 

2167 same_gender_only=same_gender_only, 

2168 moderation_state_id=moderation_state.id, 

2169 ) 

2170 session.add(trip) 

2171 session.flush() 

2172 moderation_state.object_id = trip.id 

2173 return trip.id 

2174 

2175 

2176def test_create_request_with_public_trip(db, moderator): 

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

2178 surfer, surfer_token = generate_user() 

2179 host, host_token = generate_user() 

2180 

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

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

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

2184 

2185 with requests_session(host_token) as api: 

2186 # Happy path: dates within trip window 

2187 res = api.CreateHostRequest( 

2188 requests_pb2.CreateHostRequestReq( 

2189 host_user_id=surfer.id, 

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

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

2192 text=valid_request_text(), 

2193 public_trip_id=trip_id, 

2194 ) 

2195 ) 

2196 host_request_id = res.host_request_id 

2197 

2198 moderator.approve_host_request(host_request_id) 

2199 

2200 with requests_session(host_token) as api: 

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

2202 assert hr.public_trip_id == trip_id 

2203 

2204 

2205def test_create_request_with_public_trip_dates_out_of_range(db): 

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

2207 surfer, _ = generate_user() 

2208 host, host_token = generate_user() 

2209 

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

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

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

2213 

2214 with requests_session(host_token) as api: 

2215 # from_date before trip starts 

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

2217 api.CreateHostRequest( 

2218 requests_pb2.CreateHostRequestReq( 

2219 host_user_id=surfer.id, 

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

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

2222 text=valid_request_text(), 

2223 public_trip_id=trip_id, 

2224 ) 

2225 ) 

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

2227 

2228 # to_date after trip ends 

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

2230 api.CreateHostRequest( 

2231 requests_pb2.CreateHostRequestReq( 

2232 host_user_id=surfer.id, 

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

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

2235 text=valid_request_text(), 

2236 public_trip_id=trip_id, 

2237 ) 

2238 ) 

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

2240 

2241 

2242def test_create_request_with_public_trip_user_mismatch(db): 

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

2244 trip_owner, _ = generate_user() 

2245 other_user, _ = generate_user() 

2246 host, host_token = generate_user() 

2247 

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

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

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

2251 

2252 with requests_session(host_token) as api: 

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

2254 api.CreateHostRequest( 

2255 requests_pb2.CreateHostRequestReq( 

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

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

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

2259 text=valid_request_text(), 

2260 public_trip_id=trip_id, 

2261 ) 

2262 ) 

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

2264 

2265 

2266def test_create_request_with_closed_public_trip(db): 

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

2268 surfer, _ = generate_user() 

2269 host, host_token = generate_user() 

2270 

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

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

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

2274 

2275 with requests_session(host_token) as api: 

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

2277 api.CreateHostRequest( 

2278 requests_pb2.CreateHostRequestReq( 

2279 host_user_id=surfer.id, 

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

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

2282 text=valid_request_text(), 

2283 public_trip_id=trip_id, 

2284 ) 

2285 ) 

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

2287 

2288 

2289def test_create_request_with_nonexistent_public_trip(db): 

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

2291 surfer, _ = generate_user() 

2292 host, host_token = generate_user() 

2293 

2294 with requests_session(host_token) as api: 

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

2296 api.CreateHostRequest( 

2297 requests_pb2.CreateHostRequestReq( 

2298 host_user_id=surfer.id, 

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

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

2301 text=valid_request_text(), 

2302 public_trip_id=999999, 

2303 ) 

2304 ) 

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

2306 

2307 

2308def test_create_request_with_shadowed_public_trip(db, moderator): 

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

2310 surfer, _ = generate_user() 

2311 host, host_token = generate_user() 

2312 

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

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

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

2316 

2317 with requests_session(host_token) as api: 

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

2319 api.CreateHostRequest( 

2320 requests_pb2.CreateHostRequestReq( 

2321 host_user_id=surfer.id, 

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

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

2324 text=valid_request_text(), 

2325 public_trip_id=trip_id, 

2326 ) 

2327 ) 

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

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

2330 

2331 moderator.approve_public_trip(trip_id) 

2332 

2333 with requests_session(host_token) as api: 

2334 res = api.CreateHostRequest( 

2335 requests_pb2.CreateHostRequestReq( 

2336 host_user_id=surfer.id, 

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

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

2339 text=valid_request_text(), 

2340 public_trip_id=trip_id, 

2341 ) 

2342 ) 

2343 assert res.host_request_id 

2344 

2345 

2346def test_create_request_with_hidden_public_trip(db): 

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

2348 surfer, _ = generate_user() 

2349 host, host_token = generate_user() 

2350 

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

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

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

2354 

2355 with requests_session(host_token) as api: 

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

2357 api.CreateHostRequest( 

2358 requests_pb2.CreateHostRequestReq( 

2359 host_user_id=surfer.id, 

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

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

2362 text=valid_request_text(), 

2363 public_trip_id=trip_id, 

2364 ) 

2365 ) 

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

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

2368 

2369 

2370def test_create_request_without_public_trip_id_unchanged(db, moderator): 

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

2372 surfer, _ = generate_user() 

2373 host, host_token = generate_user() 

2374 

2375 with requests_session(host_token) as api: 

2376 res = api.CreateHostRequest( 

2377 requests_pb2.CreateHostRequestReq( 

2378 host_user_id=surfer.id, 

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

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

2381 text=valid_request_text(), 

2382 ) 

2383 ) 

2384 host_request_id = res.host_request_id 

2385 

2386 moderator.approve_host_request(host_request_id) 

2387 

2388 with requests_session(host_token) as api: 

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

2390 assert not hr.HasField("public_trip_id") 

2391 

2392 

2393def test_create_request_same_gender_only_wrong_gender_rejected(db): 

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

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

2396 

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

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

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

2400 

2401 with requests_session(host_token) as api: 

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

2403 api.CreateHostRequest( 

2404 requests_pb2.CreateHostRequestReq( 

2405 host_user_id=surfer.id, 

2406 from_date=trip_from.isoformat(), 

2407 to_date=trip_to.isoformat(), 

2408 text=valid_request_text(), 

2409 public_trip_id=trip_id, 

2410 ) 

2411 ) 

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

2413 

2414 

2415def test_create_request_same_gender_only_same_gender_allowed(db, moderator): 

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

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

2418 

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

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

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

2422 

2423 with requests_session(host_token) as api: 

2424 res = api.CreateHostRequest( 

2425 requests_pb2.CreateHostRequestReq( 

2426 host_user_id=surfer.id, 

2427 from_date=trip_from.isoformat(), 

2428 to_date=trip_to.isoformat(), 

2429 text=valid_request_text(), 

2430 public_trip_id=trip_id, 

2431 ) 

2432 ) 

2433 assert res.host_request_id > 0 

2434 

2435 

2436def test_create_request_same_gender_only_moderator_bypass(db, moderator): 

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

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

2439 

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

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

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

2443 _make_trip_node_admin(host.id, trip_id) 

2444 

2445 with requests_session(host_token) as api: 

2446 res = api.CreateHostRequest( 

2447 requests_pb2.CreateHostRequestReq( 

2448 host_user_id=surfer.id, 

2449 from_date=trip_from.isoformat(), 

2450 to_date=trip_to.isoformat(), 

2451 text=valid_request_text(), 

2452 public_trip_id=trip_id, 

2453 ) 

2454 ) 

2455 assert res.host_request_id > 0 

2456 

2457 

2458def test_create_request_duplicate_offer_rejected(db): 

2459 surfer, _ = generate_user() 

2460 _, host_token = generate_user() 

2461 

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

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

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

2465 

2466 with requests_session(host_token) as api: 

2467 api.CreateHostRequest( 

2468 requests_pb2.CreateHostRequestReq( 

2469 host_user_id=surfer.id, 

2470 from_date=trip_from.isoformat(), 

2471 to_date=trip_to.isoformat(), 

2472 text=valid_request_text(), 

2473 public_trip_id=trip_id, 

2474 ) 

2475 ) 

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

2477 api.CreateHostRequest( 

2478 requests_pb2.CreateHostRequestReq( 

2479 host_user_id=surfer.id, 

2480 from_date=trip_from.isoformat(), 

2481 to_date=trip_to.isoformat(), 

2482 text=valid_request_text(), 

2483 public_trip_id=trip_id, 

2484 ) 

2485 ) 

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