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

986 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-26 00:31 +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 Node, 

24 NodeType, 

25 Notification, 

26 RateLimitAction, 

27) 

28from couchers.models.public_trips import PublicTrip, PublicTripStatus 

29from couchers.proto import ( 

30 api_pb2, 

31 auth_pb2, 

32 messages_pb2, 

33 requests_pb2, 

34) 

35from couchers.proto.internal import unsubscribe_pb2 

36from couchers.rate_limits.definitions import RATE_LIMIT_DEFINITIONS, RATE_LIMIT_HOURS 

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

38from tests.fixtures.db import generate_user 

39from tests.fixtures.misc import EmailCollector, PushCollector 

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

41 

42 

43@pytest.fixture(autouse=True) 

44def _(testconfig): 

45 pass 

46 

47 

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

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

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

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

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

53 return text 

54 padding_length = HOST_REQUEST_MIN_LENGTH_UTF16 - utf16_length 

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

56 

57 

58def test_create_request(db, moderator): 

59 user1, token1 = generate_user() 

60 hosting_city = "Morningside Heights, New York City" 

61 hosting_lat = 40.8086 

62 hosting_lng = -73.9616 

63 hosting_radius = 500 

64 user2, token2 = generate_user( 

65 city=hosting_city, 

66 geom=create_coordinate(hosting_lat, hosting_lng), 

67 geom_radius=hosting_radius, 

68 ) 

69 

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

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

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

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

74 

75 with requests_session(token1) as api: 

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

77 api.CreateHostRequest( 

78 requests_pb2.CreateHostRequestReq( 

79 host_user_id=user1.id, 

80 from_date=today_plus_2.isoformat(), 

81 to_date=today_plus_3.isoformat(), 

82 text=valid_request_text(), 

83 ) 

84 ) 

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

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

87 

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

89 api.CreateHostRequest( 

90 requests_pb2.CreateHostRequestReq( 

91 host_user_id=999, 

92 from_date=today_plus_2.isoformat(), 

93 to_date=today_plus_3.isoformat(), 

94 text=valid_request_text(), 

95 ) 

96 ) 

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

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

99 

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

101 api.CreateHostRequest( 

102 requests_pb2.CreateHostRequestReq( 

103 host_user_id=user2.id, 

104 from_date=today_plus_3.isoformat(), 

105 to_date=today_plus_2.isoformat(), 

106 text=valid_request_text(), 

107 ) 

108 ) 

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

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

111 

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

113 api.CreateHostRequest( 

114 requests_pb2.CreateHostRequestReq( 

115 host_user_id=user2.id, 

116 from_date=today_minus_3.isoformat(), 

117 to_date=today_plus_2.isoformat(), 

118 text=valid_request_text(), 

119 ) 

120 ) 

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

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

123 

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

125 api.CreateHostRequest( 

126 requests_pb2.CreateHostRequestReq( 

127 host_user_id=user2.id, 

128 from_date=today_plus_2.isoformat(), 

129 to_date=today_minus_2.isoformat(), 

130 text=valid_request_text(), 

131 ) 

132 ) 

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

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

135 

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

137 api.CreateHostRequest( 

138 requests_pb2.CreateHostRequestReq( 

139 host_user_id=user2.id, 

140 from_date="2020-00-06", 

141 to_date=today_minus_2.isoformat(), 

142 text=valid_request_text(), 

143 ) 

144 ) 

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

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

147 

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

149 api.CreateHostRequest( 

150 requests_pb2.CreateHostRequestReq( 

151 host_user_id=user2.id, 

152 from_date=today_plus_2.isoformat(), 

153 to_date=today_plus_3.isoformat(), 

154 text="Too short.", 

155 ) 

156 ) 

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

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

159 

160 res = api.CreateHostRequest( 

161 requests_pb2.CreateHostRequestReq( 

162 host_user_id=user2.id, 

163 from_date=today_plus_2.isoformat(), 

164 to_date=today_plus_3.isoformat(), 

165 text=valid_request_text(), 

166 ) 

167 ) 

168 host_request_id = res.host_request_id 

169 

170 moderator.approve_host_request(host_request_id) 

171 

172 with requests_session(token1) as api: 

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

174 

175 assert len(host_requests) == 1 

176 hr = host_requests[0] 

177 

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

179 

180 assert hr.hosting_city == hosting_city 

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

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

183 assert hr.hosting_radius == hosting_radius 

184 

185 today_ = today() 

186 today_plus_one_year = today_ + timedelta(days=365) 

187 today_plus_one_year_plus_2 = today_plus_one_year + timedelta(days=2) 

188 today_plus_one_year_plus_3 = today_plus_one_year + timedelta(days=3) 

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

190 api.CreateHostRequest( 

191 requests_pb2.CreateHostRequestReq( 

192 host_user_id=user2.id, 

193 from_date=today_plus_one_year_plus_2.isoformat(), 

194 to_date=today_plus_one_year_plus_3.isoformat(), 

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

196 ) 

197 ) 

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

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

200 

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

202 api.CreateHostRequest( 

203 requests_pb2.CreateHostRequestReq( 

204 host_user_id=user2.id, 

205 from_date=today_plus_2.isoformat(), 

206 to_date=today_plus_one_year_plus_3.isoformat(), 

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

208 ) 

209 ) 

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

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

212 

213 

214def test_create_host_request_rejects_date_past_in_host_timezone(db): 

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

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

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

218 # backend enforces the same rule for consistency. 

219 user1, token1 = generate_user() 

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

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

222 

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

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

225 

226 with patch( 

227 "couchers.servicers.requests.today_in_timezone", 

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

229 ): 

230 with requests_session(token1) as api: 

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

232 api.CreateHostRequest( 

233 requests_pb2.CreateHostRequestReq( 

234 host_user_id=user2.id, 

235 from_date="2026-01-15", 

236 to_date="2026-01-18", 

237 text=valid_request_text(), 

238 ) 

239 ) 

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

241 

242 

243def test_create_host_request_date_valid_when_host_behind_requester(db): 

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

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

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

247 # the host — must be accepted without issue. 

248 user1, token1 = generate_user() 

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

250 

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

252 

253 with patch( 

254 "couchers.servicers.requests.today_in_timezone", 

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

256 ): 

257 with requests_session(token1) as api: 

258 res = api.CreateHostRequest( 

259 requests_pb2.CreateHostRequestReq( 

260 host_user_id=user2.id, 

261 from_date="2026-01-16", 

262 to_date="2026-01-20", 

263 text=valid_request_text(), 

264 ) 

265 ) 

266 assert res.host_request_id 

267 

268 

269def test_create_request_incomplete_profile(db): 

270 user1, token1 = generate_user(complete_profile=False) 

271 user2, _ = generate_user() 

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

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

274 with requests_session(token1) as api: 

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

276 api.CreateHostRequest( 

277 requests_pb2.CreateHostRequestReq( 

278 host_user_id=user2.id, 

279 from_date=today_plus_2.isoformat(), 

280 to_date=today_plus_3.isoformat(), 

281 text=valid_request_text(), 

282 ) 

283 ) 

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

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

286 

287 

288def test_excessive_requests_are_reported(db, email_collector: EmailCollector): 

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

290 user, token = generate_user() 

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

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

293 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.host_request] 

294 with requests_session(token) as api: 

295 # Test warning email 

296 for _ in range(rate_limit_definition.warning_limit): 

297 host_user, _ = generate_user() 

298 _ = api.CreateHostRequest( 

299 requests_pb2.CreateHostRequestReq( 

300 host_user_id=host_user.id, 

301 from_date=today_plus_2.isoformat(), 

302 to_date=today_plus_3.isoformat(), 

303 text=valid_request_text(), 

304 ) 

305 ) 

306 

307 assert email_collector.count_for_reports() == 0 

308 host_user, _ = generate_user() 

309 _ = api.CreateHostRequest( 

310 requests_pb2.CreateHostRequestReq( 

311 host_user_id=host_user.id, 

312 from_date=today_plus_2.isoformat(), 

313 to_date=today_plus_3.isoformat(), 

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

315 ) 

316 ) 

317 

318 email = email_collector.pop_for_reports(last=True) 

319 assert email.plain.startswith( 

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

321 ) 

322 

323 # Test ban after exceeding HOST_REQUEST_HARD_LIMIT 

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

325 host_user, _ = generate_user() 

326 _ = api.CreateHostRequest( 

327 requests_pb2.CreateHostRequestReq( 

328 host_user_id=host_user.id, 

329 from_date=today_plus_2.isoformat(), 

330 to_date=today_plus_3.isoformat(), 

331 text=valid_request_text(), 

332 ) 

333 ) 

334 

335 assert email_collector.count_for_reports() == 0 

336 

337 host_user, _ = generate_user() 

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

339 _ = api.CreateHostRequest( 

340 requests_pb2.CreateHostRequestReq( 

341 host_user_id=host_user.id, 

342 from_date=today_plus_2.isoformat(), 

343 to_date=today_plus_3.isoformat(), 

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

345 ) 

346 ) 

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

348 assert ( 

349 exc_info.value.details() 

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

351 ) 

352 

353 email = email_collector.pop_for_reports(last=True) 

354 assert email.plain.startswith( 

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

356 ) 

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

358 

359 

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

361 with session_scope() as session: 

362 message = Message( 

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

364 ) 

365 

366 session.add(message) 

367 

368 

369def test_GetHostRequest(db): 

370 user1, token1 = generate_user() 

371 user2, token2 = generate_user() 

372 user3, token3 = generate_user() 

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

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

375 with requests_session(token1) as api: 

376 host_request_id = api.CreateHostRequest( 

377 requests_pb2.CreateHostRequestReq( 

378 host_user_id=user2.id, 

379 from_date=today_plus_2.isoformat(), 

380 to_date=today_plus_3.isoformat(), 

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

382 ) 

383 ).host_request_id 

384 

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

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

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

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

389 

390 api.SendHostRequestMessage( 

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

392 ) 

393 

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

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

396 

397 

398def test_ListHostRequests(db, moderator): 

399 user1, token1 = generate_user() 

400 user2, token2 = generate_user() 

401 user3, token3 = generate_user() 

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

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

404 with requests_session(token1) as api: 

405 host_request_1 = api.CreateHostRequest( 

406 requests_pb2.CreateHostRequestReq( 

407 host_user_id=user2.id, 

408 from_date=today_plus_2.isoformat(), 

409 to_date=today_plus_3.isoformat(), 

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

411 ) 

412 ).host_request_id 

413 

414 host_request_2 = api.CreateHostRequest( 

415 requests_pb2.CreateHostRequestReq( 

416 host_user_id=user3.id, 

417 from_date=today_plus_2.isoformat(), 

418 to_date=today_plus_3.isoformat(), 

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

420 ) 

421 ).host_request_id 

422 

423 moderator.approve_host_request(host_request_1) 

424 moderator.approve_host_request(host_request_2) 

425 

426 with requests_session(token1) as api: 

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

428 assert res.no_more 

429 assert len(res.host_requests) == 2 

430 

431 with requests_session(token2) as api: 

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

433 assert res.no_more 

434 assert len(res.host_requests) == 1 

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

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

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

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

439 

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

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

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

443 

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

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

446 

447 host_request_3 = api.CreateHostRequest( 

448 requests_pb2.CreateHostRequestReq( 

449 host_user_id=user1.id, 

450 from_date=today_plus_2.isoformat(), 

451 to_date=today_plus_3.isoformat(), 

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

453 ) 

454 ).host_request_id 

455 

456 moderator.approve_host_request(host_request_3) 

457 

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

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

460 

461 with requests_session(token3) as api: 

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

463 assert res.no_more 

464 assert len(res.host_requests) == 1 

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

466 

467 with requests_session(token1) as api: 

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

469 assert len(res.host_requests) == 1 

470 

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

472 assert len(res.host_requests) == 3 

473 

474 

475def test_ListHostRequests_pagination_regression(db, moderator): 

476 """ 

477 ListHostRequests was skipping a request when getting multiple pages 

478 """ 

479 user1, token1 = generate_user() 

480 user2, token2 = generate_user() 

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

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

483 with requests_session(token1) as api: 

484 host_request_1 = api.CreateHostRequest( 

485 requests_pb2.CreateHostRequestReq( 

486 host_user_id=user2.id, 

487 from_date=today_plus_2.isoformat(), 

488 to_date=today_plus_3.isoformat(), 

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

490 ) 

491 ).host_request_id 

492 

493 host_request_2 = api.CreateHostRequest( 

494 requests_pb2.CreateHostRequestReq( 

495 host_user_id=user2.id, 

496 from_date=today_plus_2.isoformat(), 

497 to_date=today_plus_3.isoformat(), 

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

499 ) 

500 ).host_request_id 

501 

502 host_request_3 = api.CreateHostRequest( 

503 requests_pb2.CreateHostRequestReq( 

504 host_user_id=user2.id, 

505 from_date=today_plus_2.isoformat(), 

506 to_date=today_plus_3.isoformat(), 

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

508 ) 

509 ).host_request_id 

510 

511 moderator.approve_host_request(host_request_1) 

512 moderator.approve_host_request(host_request_2) 

513 moderator.approve_host_request(host_request_3) 

514 

515 with requests_session(token2) as api: 

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

517 assert res.no_more 

518 assert len(res.host_requests) == 3 

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

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

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

522 

523 with requests_session(token2) as api: 

524 api.RespondHostRequest( 

525 requests_pb2.RespondHostRequestReq( 

526 host_request_id=host_request_2, 

527 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

528 text="Accepting host request 2", 

529 ) 

530 ) 

531 api.RespondHostRequest( 

532 requests_pb2.RespondHostRequestReq( 

533 host_request_id=host_request_1, 

534 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

535 text="Accepting host request 1", 

536 ) 

537 ) 

538 api.RespondHostRequest( 

539 requests_pb2.RespondHostRequestReq( 

540 host_request_id=host_request_3, 

541 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

542 text="Accepting host request 3", 

543 ) 

544 ) 

545 

546 with requests_session(token2) as api: 

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

548 assert res.no_more 

549 assert len(res.host_requests) == 3 

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

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

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

553 

554 with requests_session(token2) as api: 

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

556 assert not res.no_more 

557 assert len(res.host_requests) == 1 

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

559 res = api.ListHostRequests( 

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

561 ) 

562 assert not res.no_more 

563 assert len(res.host_requests) == 1 

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

565 res = api.ListHostRequests( 

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

567 ) 

568 assert res.no_more 

569 assert len(res.host_requests) == 1 

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

571 

572 

573def test_ListHostRequests_sort_by_from_date(db, moderator): 

574 user1, token1 = generate_user() 

575 user2, token2 = generate_user() 

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

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

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

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

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

581 

582 with requests_session(token1) as api: 

583 hr_late = api.CreateHostRequest( 

584 requests_pb2.CreateHostRequestReq( 

585 host_user_id=user2.id, 

586 from_date=today_plus_7.isoformat(), 

587 to_date=today_plus_10.isoformat(), 

588 text=valid_request_text("Late request"), 

589 ) 

590 ).host_request_id 

591 

592 hr_early = api.CreateHostRequest( 

593 requests_pb2.CreateHostRequestReq( 

594 host_user_id=user2.id, 

595 from_date=today_plus_2.isoformat(), 

596 to_date=today_plus_3.isoformat(), 

597 text=valid_request_text("Early request"), 

598 ) 

599 ).host_request_id 

600 

601 hr_mid = api.CreateHostRequest( 

602 requests_pb2.CreateHostRequestReq( 

603 host_user_id=user2.id, 

604 from_date=today_plus_5.isoformat(), 

605 to_date=today_plus_7.isoformat(), 

606 text=valid_request_text("Mid request"), 

607 ) 

608 ).host_request_id 

609 

610 moderator.approve_host_request(hr_late) 

611 moderator.approve_host_request(hr_early) 

612 moderator.approve_host_request(hr_mid) 

613 

614 with requests_session(token2) as api: 

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

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

617 assert res.no_more 

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

619 

620 # from_date sort: ascending by travel date 

621 res = api.ListHostRequests( 

622 requests_pb2.ListHostRequestsReq( 

623 only_received=True, 

624 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

625 ) 

626 ) 

627 assert res.no_more 

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

629 

630 

631def test_ListHostRequests_sort_by_from_date_pagination(db, moderator): 

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

633 user1, token1 = generate_user() 

634 user2, token2 = generate_user() 

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

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

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

638 

639 with requests_session(token1) as api: 

640 hr_a = api.CreateHostRequest( 

641 requests_pb2.CreateHostRequestReq( 

642 host_user_id=user2.id, 

643 from_date=today_plus_2.isoformat(), 

644 to_date=today_plus_3.isoformat(), 

645 text=valid_request_text("Request A"), 

646 ) 

647 ).host_request_id 

648 

649 # Same from_date as A — tiebreaker by conversation_id 

650 hr_b = api.CreateHostRequest( 

651 requests_pb2.CreateHostRequestReq( 

652 host_user_id=user2.id, 

653 from_date=today_plus_2.isoformat(), 

654 to_date=today_plus_3.isoformat(), 

655 text=valid_request_text("Request B"), 

656 ) 

657 ).host_request_id 

658 

659 hr_c = api.CreateHostRequest( 

660 requests_pb2.CreateHostRequestReq( 

661 host_user_id=user2.id, 

662 from_date=today_plus_5.isoformat(), 

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

664 text=valid_request_text("Request C"), 

665 ) 

666 ).host_request_id 

667 

668 moderator.approve_host_request(hr_a) 

669 moderator.approve_host_request(hr_b) 

670 moderator.approve_host_request(hr_c) 

671 

672 with requests_session(token2) as api: 

673 res = api.ListHostRequests( 

674 requests_pb2.ListHostRequestsReq( 

675 only_received=True, 

676 number=1, 

677 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

678 ) 

679 ) 

680 assert not res.no_more 

681 assert len(res.host_requests) == 1 

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

683 

684 res = api.ListHostRequests( 

685 requests_pb2.ListHostRequestsReq( 

686 only_received=True, 

687 number=1, 

688 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

689 page_token=res.next_page_token, 

690 ) 

691 ) 

692 assert not res.no_more 

693 assert len(res.host_requests) == 1 

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

695 

696 res = api.ListHostRequests( 

697 requests_pb2.ListHostRequestsReq( 

698 only_received=True, 

699 number=1, 

700 sort_by=requests_pb2.HOST_REQUEST_SORT_BY_FROM_DATE, 

701 page_token=res.next_page_token, 

702 ) 

703 ) 

704 assert res.no_more 

705 assert len(res.host_requests) == 1 

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

707 

708 

709def test_ListHostRequests_active_filter(db, moderator): 

710 user1, token1 = generate_user() 

711 user2, token2 = generate_user() 

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

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

714 

715 with requests_session(token1) as api: 

716 request_id = api.CreateHostRequest( 

717 requests_pb2.CreateHostRequestReq( 

718 host_user_id=user2.id, 

719 from_date=today_plus_2.isoformat(), 

720 to_date=today_plus_3.isoformat(), 

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

722 ) 

723 ).host_request_id 

724 

725 moderator.approve_host_request(request_id) 

726 

727 with requests_session(token1) as api: 

728 api.RespondHostRequest( 

729 requests_pb2.RespondHostRequestReq( 

730 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

731 ) 

732 ) 

733 

734 with requests_session(token2) as api: 

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

736 assert len(res.host_requests) == 1 

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

738 assert len(res.host_requests) == 0 

739 

740 

741def test_ListHostRequests_active_filter_excludes_past(db, moderator): 

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

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 

748 with requests_session(token1) as api: 

749 request_id = api.CreateHostRequest( 

750 requests_pb2.CreateHostRequestReq( 

751 host_user_id=user2.id, 

752 from_date=today_plus_2.isoformat(), 

753 to_date=today_plus_3.isoformat(), 

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

755 ) 

756 ).host_request_id 

757 

758 moderator.approve_host_request(request_id) 

759 

760 with requests_session(token2) as api: 

761 api.RespondHostRequest( 

762 requests_pb2.RespondHostRequestReq( 

763 host_request_id=request_id, 

764 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

765 ) 

766 ) 

767 

768 # Future request is visible with only_active 

769 with requests_session(token2) as api: 

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

771 assert len(res.host_requests) == 1 

772 

773 # Move dates into the past 

774 with session_scope() as session: 

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

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

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

778 

779 # Past request must be excluded by only_active 

780 with requests_session(token2) as api: 

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

782 assert len(res.host_requests) == 0 

783 

784 # Still visible without the filter 

785 with requests_session(token2) as api: 

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

787 assert len(res.host_requests) == 1 

788 

789 

790def test_ListHostRequests_status_in_filter(db, moderator): 

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

792 user1, token1 = generate_user() 

793 user2, token2 = generate_user() 

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

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

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

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

798 

799 # Create a pending request 

800 with requests_session(token1) as api: 

801 pending_id = api.CreateHostRequest( 

802 requests_pb2.CreateHostRequestReq( 

803 host_user_id=user2.id, 

804 from_date=today_plus_2.isoformat(), 

805 to_date=today_plus_3.isoformat(), 

806 text=valid_request_text("Pending"), 

807 ) 

808 ).host_request_id 

809 

810 moderator.approve_host_request(pending_id) 

811 

812 # Create an accepted request 

813 with requests_session(token1) as api: 

814 accepted_id = api.CreateHostRequest( 

815 requests_pb2.CreateHostRequestReq( 

816 host_user_id=user2.id, 

817 from_date=today_plus_4.isoformat(), 

818 to_date=today_plus_5.isoformat(), 

819 text=valid_request_text("Accepted"), 

820 ) 

821 ).host_request_id 

822 

823 moderator.approve_host_request(accepted_id) 

824 

825 with requests_session(token2) as api: 

826 api.RespondHostRequest( 

827 requests_pb2.RespondHostRequestReq( 

828 host_request_id=accepted_id, 

829 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

830 ) 

831 ) 

832 

833 # Filter to accepted only 

834 with requests_session(token2) as api: 

835 res = api.ListHostRequests( 

836 requests_pb2.ListHostRequestsReq( 

837 status_in=[messages_pb2.HOST_REQUEST_STATUS_ACCEPTED], 

838 ) 

839 ) 

840 assert len(res.host_requests) == 1 

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

842 

843 # Filter to pending only 

844 with requests_session(token2) as api: 

845 res = api.ListHostRequests( 

846 requests_pb2.ListHostRequestsReq( 

847 status_in=[messages_pb2.HOST_REQUEST_STATUS_PENDING], 

848 ) 

849 ) 

850 assert len(res.host_requests) == 1 

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

852 

853 # Filter to accepted + pending — both appear 

854 with requests_session(token2) as api: 

855 res = api.ListHostRequests( 

856 requests_pb2.ListHostRequestsReq( 

857 status_in=[ 

858 messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

859 messages_pb2.HOST_REQUEST_STATUS_PENDING, 

860 ], 

861 ) 

862 ) 

863 assert len(res.host_requests) == 2 

864 

865 # Filter to confirmed — none appear 

866 with requests_session(token2) as api: 

867 res = api.ListHostRequests( 

868 requests_pb2.ListHostRequestsReq( 

869 status_in=[messages_pb2.HOST_REQUEST_STATUS_CONFIRMED], 

870 ) 

871 ) 

872 assert len(res.host_requests) == 0 

873 

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

875 with requests_session(token2) as api: 

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

877 assert len(res.host_requests) == 2 

878 

879 

880def test_RespondHostRequests(db, moderator): 

881 user1, token1 = generate_user() 

882 user2, token2 = generate_user() 

883 user3, token3 = generate_user() 

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

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

886 

887 with requests_session(token1) as api: 

888 request_id = api.CreateHostRequest( 

889 requests_pb2.CreateHostRequestReq( 

890 host_user_id=user2.id, 

891 from_date=today_plus_2.isoformat(), 

892 to_date=today_plus_3.isoformat(), 

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

894 ) 

895 ).host_request_id 

896 

897 moderator.approve_host_request(request_id) 

898 

899 # another user can't access 

900 with requests_session(token3) as api: 

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

902 api.RespondHostRequest( 

903 requests_pb2.RespondHostRequestReq( 

904 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

905 ) 

906 ) 

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

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

909 

910 with requests_session(token1) as api: 

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

912 api.RespondHostRequest( 

913 requests_pb2.RespondHostRequestReq( 

914 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

915 ) 

916 ) 

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

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

919 

920 with requests_session(token2) as api: 

921 # non existing id 

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

923 api.RespondHostRequest( 

924 requests_pb2.RespondHostRequestReq( 

925 host_request_id=9999, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

926 ) 

927 ) 

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

929 

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

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

932 api.RespondHostRequest( 

933 requests_pb2.RespondHostRequestReq( 

934 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

935 ) 

936 ) 

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

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

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

940 api.RespondHostRequest( 

941 requests_pb2.RespondHostRequestReq( 

942 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

943 ) 

944 ) 

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

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

947 

948 api.RespondHostRequest( 

949 requests_pb2.RespondHostRequestReq( 

950 host_request_id=request_id, 

951 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED, 

952 text="Test rejection message", 

953 ) 

954 ) 

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

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

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

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

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

960 api.RespondHostRequest( 

961 requests_pb2.RespondHostRequestReq( 

962 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

963 ) 

964 ) 

965 

966 with requests_session(token1) as api: 

967 # can't make pending 

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

969 api.RespondHostRequest( 

970 requests_pb2.RespondHostRequestReq( 

971 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_PENDING 

972 ) 

973 ) 

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

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

976 

977 # can confirm then cancel 

978 api.RespondHostRequest( 

979 requests_pb2.RespondHostRequestReq( 

980 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

981 ) 

982 ) 

983 

984 api.RespondHostRequest( 

985 requests_pb2.RespondHostRequestReq( 

986 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

987 ) 

988 ) 

989 

990 # can't confirm after having cancelled 

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

992 api.RespondHostRequest( 

993 requests_pb2.RespondHostRequestReq( 

994 host_request_id=request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

995 ) 

996 ) 

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

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

999 

1000 # at this point there should be 7 messages 

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

1002 with requests_session(token1) as api: 

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

1004 assert len(res.messages) == 7 

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

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

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

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

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

1010 

1011 

1012def test_get_host_request_messages(db, moderator): 

1013 user1, token1 = generate_user() 

1014 user2, token2 = generate_user() 

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

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

1017 with requests_session(token1) as api: 

1018 res = api.CreateHostRequest( 

1019 requests_pb2.CreateHostRequestReq( 

1020 host_user_id=user2.id, 

1021 from_date=today_plus_2.isoformat(), 

1022 to_date=today_plus_3.isoformat(), 

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

1024 ) 

1025 ) 

1026 conversation_id = res.host_request_id 

1027 

1028 moderator.approve_host_request(conversation_id) 

1029 

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

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

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

1033 

1034 with requests_session(token2) as api: 

1035 api.RespondHostRequest( 

1036 requests_pb2.RespondHostRequestReq( 

1037 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1038 ) 

1039 ) 

1040 

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

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

1043 

1044 api.RespondHostRequest( 

1045 requests_pb2.RespondHostRequestReq( 

1046 host_request_id=conversation_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1047 ) 

1048 ) 

1049 

1050 with requests_session(token1) as api: 

1051 # 9 including initial message 

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

1053 assert len(res.messages) == 9 

1054 assert res.no_more 

1055 

1056 res = api.GetHostRequestMessages( 

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

1058 ) 

1059 assert not res.no_more 

1060 assert len(res.messages) == 3 

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

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

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

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

1065 

1066 res = api.GetHostRequestMessages( 

1067 requests_pb2.GetHostRequestMessagesReq( 

1068 host_request_id=conversation_id, 

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

1070 number=6, 

1071 ) 

1072 ) 

1073 assert res.no_more 

1074 assert len(res.messages) == 6 

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

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

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

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

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

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

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

1082 

1083 

1084def test_SendHostRequestMessage(db, moderator): 

1085 user1, token1 = generate_user() 

1086 user2, token2 = generate_user() 

1087 user3, token3 = generate_user() 

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

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

1090 with requests_session(token1) as api: 

1091 host_request_id = api.CreateHostRequest( 

1092 requests_pb2.CreateHostRequestReq( 

1093 host_user_id=user2.id, 

1094 from_date=today_plus_2.isoformat(), 

1095 to_date=today_plus_3.isoformat(), 

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

1097 ) 

1098 ).host_request_id 

1099 

1100 moderator.approve_host_request(host_request_id) 

1101 

1102 with requests_session(token1) as api: 

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

1104 api.SendHostRequestMessage( 

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

1106 ) 

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

1108 

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

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

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

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

1113 

1114 api.SendHostRequestMessage( 

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

1116 ) 

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

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

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

1120 

1121 with requests_session(token3) as api: 

1122 # other user can't send 

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

1124 api.SendHostRequestMessage( 

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

1126 ) 

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

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

1129 

1130 with requests_session(token2) as api: 

1131 api.SendHostRequestMessage( 

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

1133 ) 

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

1135 # including 2 for creation control message and message 

1136 assert len(res.messages) == 4 

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

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

1139 

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

1141 api.RespondHostRequest( 

1142 requests_pb2.RespondHostRequestReq( 

1143 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1144 ) 

1145 ) 

1146 api.SendHostRequestMessage( 

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

1148 ) 

1149 

1150 api.RespondHostRequest( 

1151 requests_pb2.RespondHostRequestReq( 

1152 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1153 ) 

1154 ) 

1155 

1156 with requests_session(token1) as api: 

1157 api.RespondHostRequest( 

1158 requests_pb2.RespondHostRequestReq( 

1159 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1160 ) 

1161 ) 

1162 api.SendHostRequestMessage( 

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

1164 ) 

1165 

1166 api.RespondHostRequest( 

1167 requests_pb2.RespondHostRequestReq( 

1168 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

1169 ) 

1170 ) 

1171 api.SendHostRequestMessage( 

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

1173 ) 

1174 

1175 

1176def test_get_updates(db, moderator): 

1177 user1, token1 = generate_user() 

1178 user2, token2 = generate_user() 

1179 user3, token3 = generate_user() 

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

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

1182 with requests_session(token1) as api: 

1183 host_request_id = api.CreateHostRequest( 

1184 requests_pb2.CreateHostRequestReq( 

1185 host_user_id=user2.id, 

1186 from_date=today_plus_2.isoformat(), 

1187 to_date=today_plus_3.isoformat(), 

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

1189 ) 

1190 ).host_request_id 

1191 

1192 moderator.approve_host_request(host_request_id) 

1193 

1194 with requests_session(token1) as api: 

1195 api.SendHostRequestMessage( 

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

1197 ) 

1198 api.SendHostRequestMessage( 

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

1200 ) 

1201 api.RespondHostRequest( 

1202 requests_pb2.RespondHostRequestReq( 

1203 host_request_id=host_request_id, 

1204 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

1205 text="Test message 3", 

1206 ) 

1207 ) 

1208 

1209 api.CreateHostRequest( 

1210 requests_pb2.CreateHostRequestReq( 

1211 host_user_id=user2.id, 

1212 from_date=today_plus_2.isoformat(), 

1213 to_date=today_plus_3.isoformat(), 

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

1215 ) 

1216 ) 

1217 

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

1219 assert len(res.messages) == 6 

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

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

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

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

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

1225 message_id_3 = res.messages[0].message_id 

1226 message_id_cancel = res.messages[1].message_id 

1227 message_id_2 = res.messages[2].message_id 

1228 message_id_1 = res.messages[3].message_id 

1229 message_id_0 = res.messages[4].message_id 

1230 

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

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

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

1234 

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

1236 assert res.no_more 

1237 assert len(res.updates) == 5 

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

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

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

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

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

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

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

1245 

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

1247 assert not res.no_more 

1248 assert len(res.updates) == 1 

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

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

1251 

1252 with requests_session(token3) as api: 

1253 # other user can't access 

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

1255 assert len(res.updates) == 0 

1256 

1257 

1258def test_archive_host_request(db, moderator): 

1259 user1, token1 = generate_user() 

1260 user2, token2 = generate_user() 

1261 

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

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

1264 

1265 with requests_session(token1) as api: 

1266 host_request_id = api.CreateHostRequest( 

1267 requests_pb2.CreateHostRequestReq( 

1268 host_user_id=user2.id, 

1269 from_date=today_plus_2.isoformat(), 

1270 to_date=today_plus_3.isoformat(), 

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

1272 ) 

1273 ).host_request_id 

1274 

1275 api.SendHostRequestMessage( 

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

1277 ) 

1278 api.SendHostRequestMessage( 

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

1280 ) 

1281 

1282 moderator.approve_host_request(host_request_id) 

1283 

1284 # happy path archiving host request 

1285 with requests_session(token1) as api: 

1286 api.RespondHostRequest( 

1287 requests_pb2.RespondHostRequestReq( 

1288 host_request_id=host_request_id, 

1289 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

1290 text="Test message 3", 

1291 ) 

1292 ) 

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

1294 assert len(res.host_requests) == 1 

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

1296 

1297 # Verify is_archived is False before archiving 

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

1299 assert not res.is_archived 

1300 

1301 api.SetHostRequestArchiveStatus( 

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

1303 ) 

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

1305 assert len(res.host_requests) == 1 

1306 

1307 # Verify is_archived is True after archiving 

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

1309 assert res.is_archived 

1310 

1311 

1312def test_mark_last_seen(db, moderator): 

1313 user1, token1 = generate_user() 

1314 user2, token2 = generate_user() 

1315 user3, token3 = generate_user() 

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

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

1318 with requests_session(token1) as api: 

1319 host_request_id = api.CreateHostRequest( 

1320 requests_pb2.CreateHostRequestReq( 

1321 host_user_id=user2.id, 

1322 from_date=today_plus_2.isoformat(), 

1323 to_date=today_plus_3.isoformat(), 

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

1325 ) 

1326 ).host_request_id 

1327 

1328 host_request_id_2 = api.CreateHostRequest( 

1329 requests_pb2.CreateHostRequestReq( 

1330 host_user_id=user2.id, 

1331 from_date=today_plus_2.isoformat(), 

1332 to_date=today_plus_3.isoformat(), 

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

1334 ) 

1335 ).host_request_id 

1336 

1337 moderator.approve_host_request(host_request_id) 

1338 moderator.approve_host_request(host_request_id_2) 

1339 

1340 with requests_session(token1) as api: 

1341 api.SendHostRequestMessage( 

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

1343 ) 

1344 api.SendHostRequestMessage( 

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

1346 ) 

1347 api.RespondHostRequest( 

1348 requests_pb2.RespondHostRequestReq( 

1349 host_request_id=host_request_id, 

1350 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

1351 text="Test message 3", 

1352 ) 

1353 ) 

1354 

1355 moderator.approve_host_request(host_request_id) 

1356 moderator.approve_host_request(host_request_id_2) 

1357 

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

1359 with api_session(token1) as api: 

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

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

1362 

1363 with api_session(token2) as api: 

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

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

1366 

1367 with requests_session(token2) as api: 

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

1369 

1370 api.MarkLastSeenHostRequest( 

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

1372 ) 

1373 

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

1375 

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

1377 api.MarkLastSeenHostRequest( 

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

1379 ) 

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

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

1382 

1383 # this will be used to test sent request notifications 

1384 host_request_id_3 = api.CreateHostRequest( 

1385 requests_pb2.CreateHostRequestReq( 

1386 host_user_id=user1.id, 

1387 from_date=today_plus_2.isoformat(), 

1388 to_date=today_plus_3.isoformat(), 

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

1390 ) 

1391 ).host_request_id 

1392 

1393 moderator.approve_host_request(host_request_id_3) 

1394 

1395 with requests_session(token2) as api: 

1396 # this should make id_2 all read 

1397 api.SendHostRequestMessage( 

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

1399 ) 

1400 

1401 with api_session(token2) as api: 

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

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

1404 

1405 # make sure sent and received count for unseen notifications 

1406 with requests_session(token1) as api: 

1407 api.SendHostRequestMessage( 

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

1409 ) 

1410 

1411 with api_session(token2) as api: 

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

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

1414 

1415 

1416def test_mark_last_seen_clears_notifications(db, moderator): 

1417 user1, token1 = generate_user() 

1418 user2, token2 = generate_user() 

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

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

1421 

1422 with requests_session(token1) as api: 

1423 host_request_id = api.CreateHostRequest( 

1424 requests_pb2.CreateHostRequestReq( 

1425 host_user_id=user2.id, 

1426 from_date=today_plus_2.isoformat(), 

1427 to_date=today_plus_3.isoformat(), 

1428 text=valid_request_text("Test message"), 

1429 ) 

1430 ).host_request_id 

1431 

1432 moderator.approve_host_request(host_request_id) 

1433 

1434 def unseen_notification_count(user_id): 

1435 with session_scope() as session: 

1436 return session.execute( 

1437 select(func.count()) 

1438 .select_from(Notification) 

1439 .where(Notification.user_id == user_id) 

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

1441 .where(Notification.is_seen == False) 

1442 ).scalar_one() 

1443 

1444 assert unseen_notification_count(user2.id) > 0 

1445 

1446 with requests_session(token2) as api: 

1447 api.MarkLastSeenHostRequest( 

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

1449 ) 

1450 

1451 assert unseen_notification_count(user2.id) == 0 

1452 

1453 

1454def test_response_rate(db, moderator): 

1455 user1, token1 = generate_user() 

1456 user2, token2 = generate_user() 

1457 user3, token3 = generate_user(delete_user=True) 

1458 

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

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

1461 

1462 with session_scope() as session: 

1463 refresh_materialized_view(session, "user_response_rates") 

1464 

1465 with requests_session(token1) as api: 

1466 # deleted: not found 

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

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

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

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

1471 

1472 # no requests: insufficient 

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

1474 assert res.HasField("insufficient_data") 

1475 

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

1477 host_request_1 = api.CreateHostRequest( 

1478 requests_pb2.CreateHostRequestReq( 

1479 host_user_id=user2.id, 

1480 from_date=today_plus_2.isoformat(), 

1481 to_date=today_plus_3.isoformat(), 

1482 text=valid_request_text("Test request"), 

1483 ) 

1484 ).host_request_id 

1485 moderator.approve_host_request(host_request_1) 

1486 with session_scope() as session: 

1487 session.execute( 

1488 select(Message) 

1489 .where(Message.conversation_id == host_request_1) 

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

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

1492 refresh_materialized_view(session, "user_response_rates") 

1493 

1494 # still insufficient 

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

1496 assert res.HasField("insufficient_data") 

1497 

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

1499 host_request_2 = api.CreateHostRequest( 

1500 requests_pb2.CreateHostRequestReq( 

1501 host_user_id=user2.id, 

1502 from_date=today_plus_2.isoformat(), 

1503 to_date=today_plus_3.isoformat(), 

1504 text=valid_request_text("Test request"), 

1505 ) 

1506 ).host_request_id 

1507 moderator.approve_host_request(host_request_2) 

1508 with session_scope() as session: 

1509 session.execute( 

1510 select(Message) 

1511 .where(Message.conversation_id == host_request_2) 

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

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

1514 refresh_materialized_view(session, "user_response_rates") 

1515 

1516 # still insufficient 

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

1518 assert res.HasField("insufficient_data") 

1519 

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

1521 host_request_3 = api.CreateHostRequest( 

1522 requests_pb2.CreateHostRequestReq( 

1523 host_user_id=user2.id, 

1524 from_date=today_plus_2.isoformat(), 

1525 to_date=today_plus_3.isoformat(), 

1526 text=valid_request_text("Test request"), 

1527 ) 

1528 ).host_request_id 

1529 moderator.approve_host_request(host_request_3) 

1530 with session_scope() as session: 

1531 session.execute( 

1532 select(Message) 

1533 .where(Message.conversation_id == host_request_3) 

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

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

1536 refresh_materialized_view(session, "user_response_rates") 

1537 

1538 # now low 

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

1540 assert res.HasField("low") 

1541 

1542 with requests_session(token2) as api: 

1543 # accept a host req 

1544 api.RespondHostRequest( 

1545 requests_pb2.RespondHostRequestReq( 

1546 host_request_id=host_request_2, 

1547 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1548 text="Accepting host request", 

1549 ) 

1550 ) 

1551 

1552 with session_scope() as session: 

1553 refresh_materialized_view(session, "user_response_rates") 

1554 

1555 with requests_session(token1) as api: 

1556 # now some w p33 = 35h 

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

1558 assert res.HasField("some") 

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

1560 

1561 with requests_session(token2) as api: 

1562 # accept another host req 

1563 api.RespondHostRequest( 

1564 requests_pb2.RespondHostRequestReq( 

1565 host_request_id=host_request_3, 

1566 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1567 text="Accepting host request", 

1568 ) 

1569 ) 

1570 

1571 with session_scope() as session: 

1572 refresh_materialized_view(session, "user_response_rates") 

1573 

1574 with requests_session(token1) as api: 

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

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

1577 assert res.HasField("most") 

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

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

1580 

1581 with requests_session(token2) as api: 

1582 # accept last host req 

1583 api.RespondHostRequest( 

1584 requests_pb2.RespondHostRequestReq( 

1585 host_request_id=host_request_1, 

1586 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1587 text="Accepting host request", 

1588 ) 

1589 ) 

1590 

1591 with session_scope() as session: 

1592 refresh_materialized_view(session, "user_response_rates") 

1593 

1594 with requests_session(token1) as api: 

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

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

1597 assert res.HasField("almost_all") 

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

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

1600 

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

1602 host_request_4 = api.CreateHostRequest( 

1603 requests_pb2.CreateHostRequestReq( 

1604 host_user_id=user2.id, 

1605 from_date=today_plus_2.isoformat(), 

1606 to_date=today_plus_3.isoformat(), 

1607 text=valid_request_text("Test request"), 

1608 ) 

1609 ).host_request_id 

1610 moderator.approve_host_request(host_request_4) 

1611 with session_scope() as session: 

1612 session.execute( 

1613 select(Message) 

1614 .where(Message.conversation_id == host_request_4) 

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

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

1617 refresh_materialized_view(session, "user_response_rates") 

1618 

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

1620 host_request_5 = api.CreateHostRequest( 

1621 requests_pb2.CreateHostRequestReq( 

1622 host_user_id=user2.id, 

1623 from_date=today_plus_2.isoformat(), 

1624 to_date=today_plus_3.isoformat(), 

1625 text=valid_request_text("Test request"), 

1626 ) 

1627 ).host_request_id 

1628 moderator.approve_host_request(host_request_5) 

1629 with session_scope() as session: 

1630 session.execute( 

1631 select(Message) 

1632 .where(Message.conversation_id == host_request_5) 

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

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

1635 refresh_materialized_view(session, "user_response_rates") 

1636 

1637 # now some w p33 = 35h 

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

1639 assert res.HasField("some") 

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

1641 

1642 with requests_session(token2) as api: 

1643 # accept host req 

1644 api.RespondHostRequest( 

1645 requests_pb2.RespondHostRequestReq( 

1646 host_request_id=host_request_5, 

1647 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1648 text="Accepting host request", 

1649 ) 

1650 ) 

1651 

1652 with session_scope() as session: 

1653 refresh_materialized_view(session, "user_response_rates") 

1654 

1655 with requests_session(token1) as api: 

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

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

1658 assert res.HasField("most") 

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

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

1661 

1662 with requests_session(token2) as api: 

1663 # accept host req 

1664 api.RespondHostRequest( 

1665 requests_pb2.RespondHostRequestReq( 

1666 host_request_id=host_request_4, 

1667 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1668 text="Accepting host request", 

1669 ) 

1670 ) 

1671 

1672 with session_scope() as session: 

1673 refresh_materialized_view(session, "user_response_rates") 

1674 

1675 with requests_session(token1) as api: 

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

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

1678 assert res.HasField("almost_all") 

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

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

1681 

1682 

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

1684 host, host_token = generate_user(complete_profile=True) 

1685 surfer, surfer_token = generate_user(complete_profile=True) 

1686 

1687 host_loc_context = LocalizationContext.from_user(host) 

1688 surfer_loc_context = LocalizationContext.from_user(surfer) 

1689 

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

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

1692 

1693 with requests_session(surfer_token) as api: 

1694 hr_id = api.CreateHostRequest( 

1695 requests_pb2.CreateHostRequestReq( 

1696 host_user_id=host.id, 

1697 from_date=today_plus_2.isoformat(), 

1698 to_date=today_plus_3.isoformat(), 

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

1700 ) 

1701 ).host_request_id 

1702 

1703 moderator.approve_host_request(hr_id) 

1704 

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

1706 assert email.recipient == host.email 

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

1708 assert host.name in email.plain 

1709 assert host.name in email.html 

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

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

1712 assert surfer.name in email.plain 

1713 assert surfer.name in email.html 

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

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

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

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

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

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

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

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

1722 assert not email.attachments 

1723 

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

1725 

1726 with requests_session(host_token) as api: 

1727 api.RespondHostRequest( 

1728 requests_pb2.RespondHostRequestReq( 

1729 host_request_id=hr_id, 

1730 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1731 text="Accepting host request", 

1732 ) 

1733 ) 

1734 

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

1736 assert email.recipient == surfer.email 

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

1738 assert host.name in email.plain 

1739 assert host.name in email.html 

1740 assert surfer.name in email.plain 

1741 assert surfer.name in email.html 

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

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

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

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

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

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

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

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

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

1751 

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

1753 

1754 

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

1756 host, host_token = generate_user(complete_profile=True) 

1757 surfer, surfer_token = generate_user(complete_profile=True) 

1758 

1759 host_loc_context = LocalizationContext.from_user(host) 

1760 

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

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

1763 

1764 with requests_session(surfer_token) as api: 

1765 hr_id = api.CreateHostRequest( 

1766 requests_pb2.CreateHostRequestReq( 

1767 host_user_id=host.id, 

1768 from_date=today_plus_2.isoformat(), 

1769 to_date=today_plus_3.isoformat(), 

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

1771 ) 

1772 ).host_request_id 

1773 

1774 moderator.approve_host_request(hr_id) 

1775 

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

1777 assert email.recipient == host.email 

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

1779 assert host.name in email.plain 

1780 assert host.name in email.html 

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

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

1783 assert surfer.name in email.plain 

1784 assert surfer.name in email.html 

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

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

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

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

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

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

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

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

1793 

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

1795 

1796 # very ugly 

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

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

1799 if "payload" not in link: 

1800 continue 

1801 print(link) 

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

1803 params = parse_qs(url_parts.query) 

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

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

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

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

1808 res = auth_api.Unsubscribe( 

1809 auth_pb2.UnsubscribeReq( 

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

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

1812 ) 

1813 ) 

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

1815 break 

1816 else: 

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

1818 

1819 with requests_session(surfer_token) as api: 

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

1821 assert res.status == messages_pb2.HOST_REQUEST_STATUS_REJECTED 

1822 

1823 

1824def test_host_req_feedback(db, moderator): 

1825 host, host_token = generate_user(complete_profile=True) 

1826 host2, host2_token = generate_user(complete_profile=True) 

1827 host3, host3_token = generate_user(complete_profile=True) 

1828 surfer, surfer_token = generate_user(complete_profile=True) 

1829 

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

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

1832 

1833 with requests_session(surfer_token) as api: 

1834 hr_id = api.CreateHostRequest( 

1835 requests_pb2.CreateHostRequestReq( 

1836 host_user_id=host.id, 

1837 from_date=today_plus_2.isoformat(), 

1838 to_date=today_plus_3.isoformat(), 

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

1840 ) 

1841 ).host_request_id 

1842 hr2_id = api.CreateHostRequest( 

1843 requests_pb2.CreateHostRequestReq( 

1844 host_user_id=host2.id, 

1845 from_date=today_plus_2.isoformat(), 

1846 to_date=today_plus_3.isoformat(), 

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

1848 ) 

1849 ).host_request_id 

1850 hr3_id = api.CreateHostRequest( 

1851 requests_pb2.CreateHostRequestReq( 

1852 host_user_id=host3.id, 

1853 from_date=today_plus_2.isoformat(), 

1854 to_date=today_plus_3.isoformat(), 

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

1856 ) 

1857 ).host_request_id 

1858 

1859 moderator.approve_host_request(hr_id) 

1860 moderator.approve_host_request(hr2_id) 

1861 moderator.approve_host_request(hr3_id) 

1862 

1863 with requests_session(host_token) as api: 

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

1865 assert not res.need_host_request_feedback 

1866 

1867 api.RespondHostRequest( 

1868 requests_pb2.RespondHostRequestReq( 

1869 host_request_id=hr_id, 

1870 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED, 

1871 ) 

1872 ) 

1873 

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

1875 assert res.need_host_request_feedback 

1876 

1877 # surfer can't leave feedback 

1878 with requests_session(surfer_token) as api: 

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

1880 api.SendHostRequestFeedback( 

1881 requests_pb2.SendHostRequestFeedbackReq( 

1882 host_request_id=hr_id, 

1883 ) 

1884 ) 

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

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

1887 

1888 with requests_session(host_token) as api: 

1889 api.SendHostRequestFeedback( 

1890 requests_pb2.SendHostRequestFeedbackReq( 

1891 host_request_id=hr_id, 

1892 host_request_quality=requests_pb2.HOST_REQUEST_QUALITY_LOW, 

1893 ) 

1894 ) 

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

1896 assert not res.need_host_request_feedback 

1897 

1898 # can't leave it twice 

1899 with requests_session(host_token) as api: 

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

1901 api.SendHostRequestFeedback( 

1902 requests_pb2.SendHostRequestFeedbackReq( 

1903 host_request_id=hr_id, 

1904 ) 

1905 ) 

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

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

1908 

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

1910 assert not res.need_host_request_feedback 

1911 

1912 with requests_session(host2_token) as api: 

1913 api.RespondHostRequest( 

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

1915 ) 

1916 # can't leave feedback on the wrong one 

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

1918 api.SendHostRequestFeedback( 

1919 requests_pb2.SendHostRequestFeedbackReq( 

1920 host_request_id=hr_id, 

1921 ) 

1922 ) 

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

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

1925 

1926 # null feedback is still feedback 

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

1928 

1929 with requests_session(host3_token) as api: 

1930 api.RespondHostRequest( 

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

1932 ) 

1933 

1934 api.SendHostRequestFeedback( 

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

1936 ) 

1937 

1938 

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

1940 with session_scope() as session: 

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

1942 cluster = session.execute( 

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

1944 ).scalar_one_or_none() 

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

1946 cluster = Cluster( 

1947 name="Test community", 

1948 description="Test", 

1949 parent_node_id=node_id, 

1950 is_official_cluster=True, 

1951 ) 

1952 session.add(cluster) 

1953 session.flush() 

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

1955 

1956 

1957def _create_public_trip(user_id: int, from_date, to_date, *, status=None, same_gender_only: bool = False): 

1958 with session_scope() as session: 

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

1960 if node is None: 1960 ↛ 1967line 1960 didn't jump to line 1967 because the condition on line 1960 was always true

1961 node = Node( 

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

1963 node_type=NodeType.locality, 

1964 ) 

1965 session.add(node) 

1966 session.flush() 

1967 trip = PublicTrip( 

1968 user_id=user_id, 

1969 node_id=node.id, 

1970 from_date=from_date, 

1971 to_date=to_date, 

1972 description="Looking for a host!", 

1973 status=status or PublicTripStatus.searching_for_host, 

1974 same_gender_only=same_gender_only, 

1975 ) 

1976 session.add(trip) 

1977 session.flush() 

1978 return trip.id 

1979 

1980 

1981def test_create_request_with_public_trip(db, moderator): 

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

1983 surfer, surfer_token = generate_user() 

1984 host, host_token = generate_user() 

1985 

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

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

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

1989 

1990 with requests_session(host_token) as api: 

1991 # Happy path: dates within trip window 

1992 res = api.CreateHostRequest( 

1993 requests_pb2.CreateHostRequestReq( 

1994 host_user_id=surfer.id, 

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

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

1997 text=valid_request_text(), 

1998 public_trip_id=trip_id, 

1999 ) 

2000 ) 

2001 host_request_id = res.host_request_id 

2002 

2003 moderator.approve_host_request(host_request_id) 

2004 

2005 with requests_session(host_token) as api: 

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

2007 assert hr.public_trip_id == trip_id 

2008 

2009 

2010def test_create_request_with_public_trip_dates_out_of_range(db): 

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

2012 surfer, _ = generate_user() 

2013 host, host_token = generate_user() 

2014 

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

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

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

2018 

2019 with requests_session(host_token) as api: 

2020 # from_date before trip starts 

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

2022 api.CreateHostRequest( 

2023 requests_pb2.CreateHostRequestReq( 

2024 host_user_id=surfer.id, 

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

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

2027 text=valid_request_text(), 

2028 public_trip_id=trip_id, 

2029 ) 

2030 ) 

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

2032 

2033 # to_date after trip ends 

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

2035 api.CreateHostRequest( 

2036 requests_pb2.CreateHostRequestReq( 

2037 host_user_id=surfer.id, 

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

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

2040 text=valid_request_text(), 

2041 public_trip_id=trip_id, 

2042 ) 

2043 ) 

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

2045 

2046 

2047def test_create_request_with_public_trip_user_mismatch(db): 

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

2049 trip_owner, _ = generate_user() 

2050 other_user, _ = generate_user() 

2051 host, host_token = generate_user() 

2052 

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

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

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

2056 

2057 with requests_session(host_token) as api: 

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

2059 api.CreateHostRequest( 

2060 requests_pb2.CreateHostRequestReq( 

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

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

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

2064 text=valid_request_text(), 

2065 public_trip_id=trip_id, 

2066 ) 

2067 ) 

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

2069 

2070 

2071def test_create_request_with_closed_public_trip(db): 

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

2073 surfer, _ = generate_user() 

2074 host, host_token = generate_user() 

2075 

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

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

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

2079 

2080 with requests_session(host_token) as api: 

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

2082 api.CreateHostRequest( 

2083 requests_pb2.CreateHostRequestReq( 

2084 host_user_id=surfer.id, 

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

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

2087 text=valid_request_text(), 

2088 public_trip_id=trip_id, 

2089 ) 

2090 ) 

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

2092 

2093 

2094def test_create_request_with_nonexistent_public_trip(db): 

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

2096 surfer, _ = generate_user() 

2097 host, host_token = generate_user() 

2098 

2099 with requests_session(host_token) as api: 

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

2101 api.CreateHostRequest( 

2102 requests_pb2.CreateHostRequestReq( 

2103 host_user_id=surfer.id, 

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

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

2106 text=valid_request_text(), 

2107 public_trip_id=999999, 

2108 ) 

2109 ) 

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

2111 

2112 

2113def test_create_request_without_public_trip_id_unchanged(db, moderator): 

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

2115 surfer, _ = generate_user() 

2116 host, host_token = generate_user() 

2117 

2118 with requests_session(host_token) as api: 

2119 res = api.CreateHostRequest( 

2120 requests_pb2.CreateHostRequestReq( 

2121 host_user_id=surfer.id, 

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

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

2124 text=valid_request_text(), 

2125 ) 

2126 ) 

2127 host_request_id = res.host_request_id 

2128 

2129 moderator.approve_host_request(host_request_id) 

2130 

2131 with requests_session(host_token) as api: 

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

2133 assert not hr.HasField("public_trip_id") 

2134 

2135 

2136def test_create_request_same_gender_only_wrong_gender_rejected(db): 

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

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

2139 

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

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

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

2143 

2144 with requests_session(host_token) as api: 

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

2146 api.CreateHostRequest( 

2147 requests_pb2.CreateHostRequestReq( 

2148 host_user_id=surfer.id, 

2149 from_date=trip_from.isoformat(), 

2150 to_date=trip_to.isoformat(), 

2151 text=valid_request_text(), 

2152 public_trip_id=trip_id, 

2153 ) 

2154 ) 

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

2156 

2157 

2158def test_create_request_same_gender_only_same_gender_allowed(db, moderator): 

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

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

2161 

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

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

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

2165 

2166 with requests_session(host_token) as api: 

2167 res = api.CreateHostRequest( 

2168 requests_pb2.CreateHostRequestReq( 

2169 host_user_id=surfer.id, 

2170 from_date=trip_from.isoformat(), 

2171 to_date=trip_to.isoformat(), 

2172 text=valid_request_text(), 

2173 public_trip_id=trip_id, 

2174 ) 

2175 ) 

2176 assert res.host_request_id > 0 

2177 

2178 

2179def test_create_request_same_gender_only_moderator_bypass(db, moderator): 

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

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

2182 

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

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

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

2186 _make_trip_node_admin(host.id, trip_id) 

2187 

2188 with requests_session(host_token) as api: 

2189 res = api.CreateHostRequest( 

2190 requests_pb2.CreateHostRequestReq( 

2191 host_user_id=surfer.id, 

2192 from_date=trip_from.isoformat(), 

2193 to_date=trip_to.isoformat(), 

2194 text=valid_request_text(), 

2195 public_trip_id=trip_id, 

2196 ) 

2197 ) 

2198 assert res.host_request_id > 0 

2199 

2200 

2201def test_create_request_duplicate_offer_rejected(db): 

2202 surfer, _ = generate_user() 

2203 _, host_token = generate_user() 

2204 

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

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

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

2208 

2209 with requests_session(host_token) as api: 

2210 api.CreateHostRequest( 

2211 requests_pb2.CreateHostRequestReq( 

2212 host_user_id=surfer.id, 

2213 from_date=trip_from.isoformat(), 

2214 to_date=trip_to.isoformat(), 

2215 text=valid_request_text(), 

2216 public_trip_id=trip_id, 

2217 ) 

2218 ) 

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

2220 api.CreateHostRequest( 

2221 requests_pb2.CreateHostRequestReq( 

2222 host_user_id=surfer.id, 

2223 from_date=trip_from.isoformat(), 

2224 to_date=trip_to.isoformat(), 

2225 text=valid_request_text(), 

2226 public_trip_id=trip_id, 

2227 ) 

2228 ) 

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