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

857 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 09:44 +0000

1import html 

2import re 

3from datetime import timedelta 

4from urllib.parse import parse_qs, urlparse 

5 

6import grpc 

7import pytest 

8from sqlalchemy import select 

9from sqlalchemy_utils import refresh_materialized_view 

10 

11from couchers.constants import HOST_REQUEST_MIN_LENGTH_UTF16 

12from couchers.crypto import b64decode 

13from couchers.db import session_scope 

14from couchers.i18n import LocalizationContext 

15from couchers.models import ( 

16 Cluster, 

17 ClusterRole, 

18 ClusterSubscription, 

19 Message, 

20 MessageType, 

21 Node, 

22 NodeType, 

23 RateLimitAction, 

24) 

25from couchers.models.public_trips import PublicTrip, PublicTripStatus 

26from couchers.proto import ( 

27 api_pb2, 

28 auth_pb2, 

29 conversations_pb2, 

30 requests_pb2, 

31) 

32from couchers.proto.internal import unsubscribe_pb2 

33from couchers.rate_limits.definitions import RATE_LIMIT_DEFINITIONS, RATE_LIMIT_HOURS 

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

35from tests.fixtures.db import generate_user 

36from tests.fixtures.misc import PushCollector, email_fields, mock_notification_email 

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

38 

39 

40@pytest.fixture(autouse=True) 

41def _(testconfig): 

42 pass 

43 

44 

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

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

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

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

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

50 return text 

51 padding_length = HOST_REQUEST_MIN_LENGTH_UTF16 - utf16_length 

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

53 

54 

55def test_create_request(db, moderator): 

56 user1, token1 = generate_user() 

57 hosting_city = "Morningside Heights, New York City" 

58 hosting_lat = 40.8086 

59 hosting_lng = -73.9616 

60 hosting_radius = 500 

61 user2, token2 = generate_user( 

62 city=hosting_city, 

63 geom=create_coordinate(hosting_lat, hosting_lng), 

64 geom_radius=hosting_radius, 

65 ) 

66 

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

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

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

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

71 

72 with requests_session(token1) as api: 

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

74 api.CreateHostRequest( 

75 requests_pb2.CreateHostRequestReq( 

76 host_user_id=user1.id, 

77 from_date=today_plus_2.isoformat(), 

78 to_date=today_plus_3.isoformat(), 

79 text=valid_request_text(), 

80 ) 

81 ) 

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

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

84 

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

86 api.CreateHostRequest( 

87 requests_pb2.CreateHostRequestReq( 

88 host_user_id=999, 

89 from_date=today_plus_2.isoformat(), 

90 to_date=today_plus_3.isoformat(), 

91 text=valid_request_text(), 

92 ) 

93 ) 

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

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

96 

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

98 api.CreateHostRequest( 

99 requests_pb2.CreateHostRequestReq( 

100 host_user_id=user2.id, 

101 from_date=today_plus_3.isoformat(), 

102 to_date=today_plus_2.isoformat(), 

103 text=valid_request_text(), 

104 ) 

105 ) 

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

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

108 

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

110 api.CreateHostRequest( 

111 requests_pb2.CreateHostRequestReq( 

112 host_user_id=user2.id, 

113 from_date=today_minus_3.isoformat(), 

114 to_date=today_plus_2.isoformat(), 

115 text=valid_request_text(), 

116 ) 

117 ) 

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

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

120 

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

122 api.CreateHostRequest( 

123 requests_pb2.CreateHostRequestReq( 

124 host_user_id=user2.id, 

125 from_date=today_plus_2.isoformat(), 

126 to_date=today_minus_2.isoformat(), 

127 text=valid_request_text(), 

128 ) 

129 ) 

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

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

132 

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

134 api.CreateHostRequest( 

135 requests_pb2.CreateHostRequestReq( 

136 host_user_id=user2.id, 

137 from_date="2020-00-06", 

138 to_date=today_minus_2.isoformat(), 

139 text=valid_request_text(), 

140 ) 

141 ) 

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

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

144 

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

146 api.CreateHostRequest( 

147 requests_pb2.CreateHostRequestReq( 

148 host_user_id=user2.id, 

149 from_date=today_plus_2.isoformat(), 

150 to_date=today_plus_3.isoformat(), 

151 text="Too short.", 

152 ) 

153 ) 

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

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

156 

157 res = api.CreateHostRequest( 

158 requests_pb2.CreateHostRequestReq( 

159 host_user_id=user2.id, 

160 from_date=today_plus_2.isoformat(), 

161 to_date=today_plus_3.isoformat(), 

162 text=valid_request_text(), 

163 ) 

164 ) 

165 host_request_id = res.host_request_id 

166 

167 moderator.approve_host_request(host_request_id) 

168 

169 with requests_session(token1) as api: 

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

171 

172 assert len(host_requests) == 1 

173 hr = host_requests[0] 

174 

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

176 

177 assert hr.hosting_city == hosting_city 

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

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

180 assert hr.hosting_radius == hosting_radius 

181 

182 today_ = today() 

183 today_plus_one_year = today_ + timedelta(days=365) 

184 today_plus_one_year_plus_2 = today_plus_one_year + timedelta(days=2) 

185 today_plus_one_year_plus_3 = today_plus_one_year + timedelta(days=3) 

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

187 api.CreateHostRequest( 

188 requests_pb2.CreateHostRequestReq( 

189 host_user_id=user2.id, 

190 from_date=today_plus_one_year_plus_2.isoformat(), 

191 to_date=today_plus_one_year_plus_3.isoformat(), 

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

193 ) 

194 ) 

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

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

197 

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

199 api.CreateHostRequest( 

200 requests_pb2.CreateHostRequestReq( 

201 host_user_id=user2.id, 

202 from_date=today_plus_2.isoformat(), 

203 to_date=today_plus_one_year_plus_3.isoformat(), 

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

205 ) 

206 ) 

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

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

209 

210 

211def test_create_request_incomplete_profile(db): 

212 user1, token1 = generate_user(complete_profile=False) 

213 user2, _ = generate_user() 

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

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

216 with requests_session(token1) as api: 

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

218 api.CreateHostRequest( 

219 requests_pb2.CreateHostRequestReq( 

220 host_user_id=user2.id, 

221 from_date=today_plus_2.isoformat(), 

222 to_date=today_plus_3.isoformat(), 

223 text=valid_request_text(), 

224 ) 

225 ) 

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

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

228 

229 

230def test_excessive_requests_are_reported(db): 

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

232 user, token = generate_user() 

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

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

235 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.host_request] 

236 with requests_session(token) as api: 

237 # Test warning email 

238 with mock_notification_email() as mock_email: 

239 for _ in range(rate_limit_definition.warning_limit): 

240 host_user, _ = generate_user() 

241 _ = api.CreateHostRequest( 

242 requests_pb2.CreateHostRequestReq( 

243 host_user_id=host_user.id, 

244 from_date=today_plus_2.isoformat(), 

245 to_date=today_plus_3.isoformat(), 

246 text=valid_request_text(), 

247 ) 

248 ) 

249 

250 assert mock_email.call_count == 0 

251 host_user, _ = generate_user() 

252 _ = api.CreateHostRequest( 

253 requests_pb2.CreateHostRequestReq( 

254 host_user_id=host_user.id, 

255 from_date=today_plus_2.isoformat(), 

256 to_date=today_plus_3.isoformat(), 

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

258 ) 

259 ) 

260 assert mock_email.call_count == 1 

261 email = email_fields(mock_email).plain 

262 assert email.startswith( 

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

264 ) 

265 

266 # Test ban after exceeding HOST_REQUEST_HARD_LIMIT 

267 with mock_notification_email() as mock_email: 

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

269 host_user, _ = generate_user() 

270 _ = api.CreateHostRequest( 

271 requests_pb2.CreateHostRequestReq( 

272 host_user_id=host_user.id, 

273 from_date=today_plus_2.isoformat(), 

274 to_date=today_plus_3.isoformat(), 

275 text=valid_request_text(), 

276 ) 

277 ) 

278 

279 assert mock_email.call_count == 0 

280 host_user, _ = generate_user() 

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

282 _ = api.CreateHostRequest( 

283 requests_pb2.CreateHostRequestReq( 

284 host_user_id=host_user.id, 

285 from_date=today_plus_2.isoformat(), 

286 to_date=today_plus_3.isoformat(), 

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

288 ) 

289 ) 

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

291 assert ( 

292 exc_info.value.details() 

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

294 ) 

295 

296 assert mock_email.call_count == 1 

297 email = email_fields(mock_email).plain 

298 assert email.startswith( 

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

300 ) 

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

302 

303 

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

305 with session_scope() as session: 

306 message = Message( 

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

308 ) 

309 

310 session.add(message) 

311 

312 

313def test_GetHostRequest(db): 

314 user1, token1 = generate_user() 

315 user2, token2 = generate_user() 

316 user3, token3 = generate_user() 

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

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

319 with requests_session(token1) as api: 

320 host_request_id = api.CreateHostRequest( 

321 requests_pb2.CreateHostRequestReq( 

322 host_user_id=user2.id, 

323 from_date=today_plus_2.isoformat(), 

324 to_date=today_plus_3.isoformat(), 

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

326 ) 

327 ).host_request_id 

328 

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

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

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

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

333 

334 api.SendHostRequestMessage( 

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

336 ) 

337 

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

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

340 

341 

342def test_ListHostRequests(db, moderator): 

343 user1, token1 = generate_user() 

344 user2, token2 = generate_user() 

345 user3, token3 = generate_user() 

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

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

348 with requests_session(token1) as api: 

349 host_request_1 = api.CreateHostRequest( 

350 requests_pb2.CreateHostRequestReq( 

351 host_user_id=user2.id, 

352 from_date=today_plus_2.isoformat(), 

353 to_date=today_plus_3.isoformat(), 

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

355 ) 

356 ).host_request_id 

357 

358 host_request_2 = api.CreateHostRequest( 

359 requests_pb2.CreateHostRequestReq( 

360 host_user_id=user3.id, 

361 from_date=today_plus_2.isoformat(), 

362 to_date=today_plus_3.isoformat(), 

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

364 ) 

365 ).host_request_id 

366 

367 moderator.approve_host_request(host_request_1) 

368 moderator.approve_host_request(host_request_2) 

369 

370 with requests_session(token1) as api: 

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

372 assert res.no_more 

373 assert len(res.host_requests) == 2 

374 

375 with requests_session(token2) as api: 

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

377 assert res.no_more 

378 assert len(res.host_requests) == 1 

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

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

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

382 assert res.host_requests[0].status == conversations_pb2.HOST_REQUEST_STATUS_PENDING 

383 

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

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

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

387 

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

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

390 

391 host_request_3 = api.CreateHostRequest( 

392 requests_pb2.CreateHostRequestReq( 

393 host_user_id=user1.id, 

394 from_date=today_plus_2.isoformat(), 

395 to_date=today_plus_3.isoformat(), 

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

397 ) 

398 ).host_request_id 

399 

400 moderator.approve_host_request(host_request_3) 

401 

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

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

404 

405 with requests_session(token3) as api: 

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

407 assert res.no_more 

408 assert len(res.host_requests) == 1 

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

410 

411 with requests_session(token1) as api: 

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

413 assert len(res.host_requests) == 1 

414 

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

416 assert len(res.host_requests) == 3 

417 

418 

419def test_ListHostRequests_pagination_regression(db, moderator): 

420 """ 

421 ListHostRequests was skipping a request when getting multiple pages 

422 """ 

423 user1, token1 = generate_user() 

424 user2, token2 = generate_user() 

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

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

427 with requests_session(token1) as api: 

428 host_request_1 = api.CreateHostRequest( 

429 requests_pb2.CreateHostRequestReq( 

430 host_user_id=user2.id, 

431 from_date=today_plus_2.isoformat(), 

432 to_date=today_plus_3.isoformat(), 

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

434 ) 

435 ).host_request_id 

436 

437 host_request_2 = api.CreateHostRequest( 

438 requests_pb2.CreateHostRequestReq( 

439 host_user_id=user2.id, 

440 from_date=today_plus_2.isoformat(), 

441 to_date=today_plus_3.isoformat(), 

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

443 ) 

444 ).host_request_id 

445 

446 host_request_3 = api.CreateHostRequest( 

447 requests_pb2.CreateHostRequestReq( 

448 host_user_id=user2.id, 

449 from_date=today_plus_2.isoformat(), 

450 to_date=today_plus_3.isoformat(), 

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

452 ) 

453 ).host_request_id 

454 

455 moderator.approve_host_request(host_request_1) 

456 moderator.approve_host_request(host_request_2) 

457 moderator.approve_host_request(host_request_3) 

458 

459 with requests_session(token2) as api: 

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

461 assert res.no_more 

462 assert len(res.host_requests) == 3 

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

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

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

466 

467 with requests_session(token2) as api: 

468 api.RespondHostRequest( 

469 requests_pb2.RespondHostRequestReq( 

470 host_request_id=host_request_2, 

471 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

472 text="Accepting host request 2", 

473 ) 

474 ) 

475 api.RespondHostRequest( 

476 requests_pb2.RespondHostRequestReq( 

477 host_request_id=host_request_1, 

478 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

479 text="Accepting host request 1", 

480 ) 

481 ) 

482 api.RespondHostRequest( 

483 requests_pb2.RespondHostRequestReq( 

484 host_request_id=host_request_3, 

485 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

486 text="Accepting host request 3", 

487 ) 

488 ) 

489 

490 with requests_session(token2) as api: 

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

492 assert res.no_more 

493 assert len(res.host_requests) == 3 

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

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

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

497 

498 with requests_session(token2) as api: 

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

500 assert not res.no_more 

501 assert len(res.host_requests) == 1 

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

503 res = api.ListHostRequests( 

504 requests_pb2.ListHostRequestsReq(only_received=True, number=1, last_request_id=res.last_request_id) 

505 ) 

506 assert not res.no_more 

507 assert len(res.host_requests) == 1 

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

509 res = api.ListHostRequests( 

510 requests_pb2.ListHostRequestsReq(only_received=True, number=1, last_request_id=res.last_request_id) 

511 ) 

512 assert res.no_more 

513 assert len(res.host_requests) == 1 

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

515 

516 

517def test_ListHostRequests_active_filter(db, moderator): 

518 user1, token1 = generate_user() 

519 user2, token2 = generate_user() 

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

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

522 

523 with requests_session(token1) as api: 

524 request_id = api.CreateHostRequest( 

525 requests_pb2.CreateHostRequestReq( 

526 host_user_id=user2.id, 

527 from_date=today_plus_2.isoformat(), 

528 to_date=today_plus_3.isoformat(), 

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

530 ) 

531 ).host_request_id 

532 

533 moderator.approve_host_request(request_id) 

534 

535 with requests_session(token1) as api: 

536 api.RespondHostRequest( 

537 requests_pb2.RespondHostRequestReq( 

538 host_request_id=request_id, status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

539 ) 

540 ) 

541 

542 with requests_session(token2) as api: 

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

544 assert len(res.host_requests) == 1 

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

546 assert len(res.host_requests) == 0 

547 

548 

549def test_RespondHostRequests(db, moderator): 

550 user1, token1 = generate_user() 

551 user2, token2 = generate_user() 

552 user3, token3 = generate_user() 

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

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

555 

556 with requests_session(token1) as api: 

557 request_id = api.CreateHostRequest( 

558 requests_pb2.CreateHostRequestReq( 

559 host_user_id=user2.id, 

560 from_date=today_plus_2.isoformat(), 

561 to_date=today_plus_3.isoformat(), 

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

563 ) 

564 ).host_request_id 

565 

566 moderator.approve_host_request(request_id) 

567 

568 # another user can't access 

569 with requests_session(token3) as api: 

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

571 api.RespondHostRequest( 

572 requests_pb2.RespondHostRequestReq( 

573 host_request_id=request_id, status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

574 ) 

575 ) 

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

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

578 

579 with requests_session(token1) as api: 

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

581 api.RespondHostRequest( 

582 requests_pb2.RespondHostRequestReq( 

583 host_request_id=request_id, status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED 

584 ) 

585 ) 

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

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

588 

589 with requests_session(token2) as api: 

590 # non existing id 

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

592 api.RespondHostRequest( 

593 requests_pb2.RespondHostRequestReq( 

594 host_request_id=9999, status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

595 ) 

596 ) 

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

598 

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

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

601 api.RespondHostRequest( 

602 requests_pb2.RespondHostRequestReq( 

603 host_request_id=request_id, status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED 

604 ) 

605 ) 

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

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

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

609 api.RespondHostRequest( 

610 requests_pb2.RespondHostRequestReq( 

611 host_request_id=request_id, status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

612 ) 

613 ) 

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

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

616 

617 api.RespondHostRequest( 

618 requests_pb2.RespondHostRequestReq( 

619 host_request_id=request_id, 

620 status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED, 

621 text="Test rejection message", 

622 ) 

623 ) 

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

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

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

627 assert res.messages[1].host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_REJECTED 

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

629 api.RespondHostRequest( 

630 requests_pb2.RespondHostRequestReq( 

631 host_request_id=request_id, status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED 

632 ) 

633 ) 

634 

635 with requests_session(token1) as api: 

636 # can't make pending 

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

638 api.RespondHostRequest( 

639 requests_pb2.RespondHostRequestReq( 

640 host_request_id=request_id, status=conversations_pb2.HOST_REQUEST_STATUS_PENDING 

641 ) 

642 ) 

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

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

645 

646 # can confirm then cancel 

647 api.RespondHostRequest( 

648 requests_pb2.RespondHostRequestReq( 

649 host_request_id=request_id, status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED 

650 ) 

651 ) 

652 

653 api.RespondHostRequest( 

654 requests_pb2.RespondHostRequestReq( 

655 host_request_id=request_id, status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

656 ) 

657 ) 

658 

659 # can't confirm after having cancelled 

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

661 api.RespondHostRequest( 

662 requests_pb2.RespondHostRequestReq( 

663 host_request_id=request_id, status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED 

664 ) 

665 ) 

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

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

668 

669 # at this point there should be 7 messages 

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

671 with requests_session(token1) as api: 

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

673 assert len(res.messages) == 7 

674 assert res.messages[0].host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

675 assert res.messages[1].host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED 

676 assert res.messages[2].host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED 

677 assert res.messages[4].host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_REJECTED 

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

679 

680 

681def test_get_host_request_messages(db, moderator): 

682 user1, token1 = generate_user() 

683 user2, token2 = generate_user() 

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

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

686 with requests_session(token1) as api: 

687 res = api.CreateHostRequest( 

688 requests_pb2.CreateHostRequestReq( 

689 host_user_id=user2.id, 

690 from_date=today_plus_2.isoformat(), 

691 to_date=today_plus_3.isoformat(), 

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

693 ) 

694 ) 

695 conversation_id = res.host_request_id 

696 

697 moderator.approve_host_request(conversation_id) 

698 

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

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

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

702 

703 with requests_session(token2) as api: 

704 api.RespondHostRequest( 

705 requests_pb2.RespondHostRequestReq( 

706 host_request_id=conversation_id, status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED 

707 ) 

708 ) 

709 

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

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

712 

713 api.RespondHostRequest( 

714 requests_pb2.RespondHostRequestReq( 

715 host_request_id=conversation_id, status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED 

716 ) 

717 ) 

718 

719 with requests_session(token1) as api: 

720 # 9 including initial message 

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

722 assert len(res.messages) == 9 

723 assert res.no_more 

724 

725 res = api.GetHostRequestMessages( 

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

727 ) 

728 assert not res.no_more 

729 assert len(res.messages) == 3 

730 assert res.messages[0].host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_REJECTED 

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

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

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

734 

735 res = api.GetHostRequestMessages( 

736 requests_pb2.GetHostRequestMessagesReq( 

737 host_request_id=conversation_id, 

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

739 number=6, 

740 ) 

741 ) 

742 assert res.no_more 

743 assert len(res.messages) == 6 

744 assert res.messages[0].host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED 

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

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

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

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

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

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

751 

752 

753def test_SendHostRequestMessage(db, moderator): 

754 user1, token1 = generate_user() 

755 user2, token2 = generate_user() 

756 user3, token3 = generate_user() 

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

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

759 with requests_session(token1) as api: 

760 host_request_id = api.CreateHostRequest( 

761 requests_pb2.CreateHostRequestReq( 

762 host_user_id=user2.id, 

763 from_date=today_plus_2.isoformat(), 

764 to_date=today_plus_3.isoformat(), 

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

766 ) 

767 ).host_request_id 

768 

769 moderator.approve_host_request(host_request_id) 

770 

771 with requests_session(token1) as api: 

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

773 api.SendHostRequestMessage( 

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

775 ) 

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

777 

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

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

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

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

782 

783 api.SendHostRequestMessage( 

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

785 ) 

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

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

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

789 

790 with requests_session(token3) as api: 

791 # other user can't send 

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

793 api.SendHostRequestMessage( 

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

795 ) 

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

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

798 

799 with requests_session(token2) as api: 

800 api.SendHostRequestMessage( 

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

802 ) 

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

804 # including 2 for creation control message and message 

805 assert len(res.messages) == 4 

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

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

808 

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

810 api.RespondHostRequest( 

811 requests_pb2.RespondHostRequestReq( 

812 host_request_id=host_request_id, status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED 

813 ) 

814 ) 

815 api.SendHostRequestMessage( 

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

817 ) 

818 

819 api.RespondHostRequest( 

820 requests_pb2.RespondHostRequestReq( 

821 host_request_id=host_request_id, status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED 

822 ) 

823 ) 

824 

825 with requests_session(token1) as api: 

826 api.RespondHostRequest( 

827 requests_pb2.RespondHostRequestReq( 

828 host_request_id=host_request_id, status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED 

829 ) 

830 ) 

831 api.SendHostRequestMessage( 

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

833 ) 

834 

835 api.RespondHostRequest( 

836 requests_pb2.RespondHostRequestReq( 

837 host_request_id=host_request_id, status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

838 ) 

839 ) 

840 api.SendHostRequestMessage( 

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

842 ) 

843 

844 

845def test_get_updates(db, moderator): 

846 user1, token1 = generate_user() 

847 user2, token2 = generate_user() 

848 user3, token3 = generate_user() 

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

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

851 with requests_session(token1) as api: 

852 host_request_id = api.CreateHostRequest( 

853 requests_pb2.CreateHostRequestReq( 

854 host_user_id=user2.id, 

855 from_date=today_plus_2.isoformat(), 

856 to_date=today_plus_3.isoformat(), 

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

858 ) 

859 ).host_request_id 

860 

861 moderator.approve_host_request(host_request_id) 

862 

863 with requests_session(token1) as api: 

864 api.SendHostRequestMessage( 

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

866 ) 

867 api.SendHostRequestMessage( 

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

869 ) 

870 api.RespondHostRequest( 

871 requests_pb2.RespondHostRequestReq( 

872 host_request_id=host_request_id, 

873 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED, 

874 text="Test message 3", 

875 ) 

876 ) 

877 

878 api.CreateHostRequest( 

879 requests_pb2.CreateHostRequestReq( 

880 host_user_id=user2.id, 

881 from_date=today_plus_2.isoformat(), 

882 to_date=today_plus_3.isoformat(), 

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

884 ) 

885 ) 

886 

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

888 assert len(res.messages) == 6 

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

890 assert res.messages[1].host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

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

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

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

894 message_id_3 = res.messages[0].message_id 

895 message_id_cancel = res.messages[1].message_id 

896 message_id_2 = res.messages[2].message_id 

897 message_id_1 = res.messages[3].message_id 

898 message_id_0 = res.messages[4].message_id 

899 

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

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

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

903 

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

905 assert res.no_more 

906 assert len(res.updates) == 5 

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

908 assert ( 

909 res.updates[1].message.host_request_status_changed.status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

910 ) 

911 assert res.updates[1].status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

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

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

914 assert res.updates[3].status == conversations_pb2.HOST_REQUEST_STATUS_PENDING 

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

916 

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

918 assert not res.no_more 

919 assert len(res.updates) == 1 

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

921 assert res.updates[0].status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

922 

923 with requests_session(token3) as api: 

924 # other user can't access 

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

926 assert len(res.updates) == 0 

927 

928 

929def test_archive_host_request(db, moderator): 

930 user1, token1 = generate_user() 

931 user2, token2 = generate_user() 

932 

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

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

935 

936 with requests_session(token1) as api: 

937 host_request_id = api.CreateHostRequest( 

938 requests_pb2.CreateHostRequestReq( 

939 host_user_id=user2.id, 

940 from_date=today_plus_2.isoformat(), 

941 to_date=today_plus_3.isoformat(), 

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

943 ) 

944 ).host_request_id 

945 

946 api.SendHostRequestMessage( 

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

948 ) 

949 api.SendHostRequestMessage( 

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

951 ) 

952 

953 moderator.approve_host_request(host_request_id) 

954 

955 # happy path archiving host request 

956 with requests_session(token1) as api: 

957 api.RespondHostRequest( 

958 requests_pb2.RespondHostRequestReq( 

959 host_request_id=host_request_id, 

960 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED, 

961 text="Test message 3", 

962 ) 

963 ) 

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

965 assert len(res.host_requests) == 1 

966 assert res.host_requests[0].status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED 

967 

968 # Verify is_archived is False before archiving 

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

970 assert not res.is_archived 

971 

972 api.SetHostRequestArchiveStatus( 

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

974 ) 

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

976 assert len(res.host_requests) == 1 

977 

978 # Verify is_archived is True after archiving 

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

980 assert res.is_archived 

981 

982 

983def test_mark_last_seen(db, moderator): 

984 user1, token1 = generate_user() 

985 user2, token2 = generate_user() 

986 user3, token3 = generate_user() 

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

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

989 with requests_session(token1) as api: 

990 host_request_id = api.CreateHostRequest( 

991 requests_pb2.CreateHostRequestReq( 

992 host_user_id=user2.id, 

993 from_date=today_plus_2.isoformat(), 

994 to_date=today_plus_3.isoformat(), 

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

996 ) 

997 ).host_request_id 

998 

999 host_request_id_2 = api.CreateHostRequest( 

1000 requests_pb2.CreateHostRequestReq( 

1001 host_user_id=user2.id, 

1002 from_date=today_plus_2.isoformat(), 

1003 to_date=today_plus_3.isoformat(), 

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

1005 ) 

1006 ).host_request_id 

1007 

1008 moderator.approve_host_request(host_request_id) 

1009 moderator.approve_host_request(host_request_id_2) 

1010 

1011 with requests_session(token1) as api: 

1012 api.SendHostRequestMessage( 

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

1014 ) 

1015 api.SendHostRequestMessage( 

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

1017 ) 

1018 api.RespondHostRequest( 

1019 requests_pb2.RespondHostRequestReq( 

1020 host_request_id=host_request_id, 

1021 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED, 

1022 text="Test message 3", 

1023 ) 

1024 ) 

1025 

1026 moderator.approve_host_request(host_request_id) 

1027 moderator.approve_host_request(host_request_id_2) 

1028 

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

1030 with api_session(token1) as api: 

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

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

1033 

1034 with api_session(token2) as api: 

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

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

1037 

1038 with requests_session(token2) as api: 

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

1040 

1041 api.MarkLastSeenHostRequest( 

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

1043 ) 

1044 

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

1046 

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

1048 api.MarkLastSeenHostRequest( 

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

1050 ) 

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

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

1053 

1054 # this will be used to test sent request notifications 

1055 host_request_id_3 = api.CreateHostRequest( 

1056 requests_pb2.CreateHostRequestReq( 

1057 host_user_id=user1.id, 

1058 from_date=today_plus_2.isoformat(), 

1059 to_date=today_plus_3.isoformat(), 

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

1061 ) 

1062 ).host_request_id 

1063 

1064 moderator.approve_host_request(host_request_id_3) 

1065 

1066 with requests_session(token2) as api: 

1067 # this should make id_2 all read 

1068 api.SendHostRequestMessage( 

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

1070 ) 

1071 

1072 with api_session(token2) as api: 

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

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

1075 

1076 # make sure sent and received count for unseen notifications 

1077 with requests_session(token1) as api: 

1078 api.SendHostRequestMessage( 

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

1080 ) 

1081 

1082 with api_session(token2) as api: 

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

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

1085 

1086 

1087def test_response_rate(db, moderator): 

1088 user1, token1 = generate_user() 

1089 user2, token2 = generate_user() 

1090 user3, token3 = generate_user(delete_user=True) 

1091 

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

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

1094 

1095 with session_scope() as session: 

1096 refresh_materialized_view(session, "user_response_rates") 

1097 

1098 with requests_session(token1) as api: 

1099 # deleted: not found 

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

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

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

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

1104 

1105 # no requests: insufficient 

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

1107 assert res.HasField("insufficient_data") 

1108 

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

1110 host_request_1 = api.CreateHostRequest( 

1111 requests_pb2.CreateHostRequestReq( 

1112 host_user_id=user2.id, 

1113 from_date=today_plus_2.isoformat(), 

1114 to_date=today_plus_3.isoformat(), 

1115 text=valid_request_text("Test request"), 

1116 ) 

1117 ).host_request_id 

1118 moderator.approve_host_request(host_request_1) 

1119 with session_scope() as session: 

1120 session.execute( 

1121 select(Message) 

1122 .where(Message.conversation_id == host_request_1) 

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

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

1125 refresh_materialized_view(session, "user_response_rates") 

1126 

1127 # still insufficient 

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

1129 assert res.HasField("insufficient_data") 

1130 

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

1132 host_request_2 = api.CreateHostRequest( 

1133 requests_pb2.CreateHostRequestReq( 

1134 host_user_id=user2.id, 

1135 from_date=today_plus_2.isoformat(), 

1136 to_date=today_plus_3.isoformat(), 

1137 text=valid_request_text("Test request"), 

1138 ) 

1139 ).host_request_id 

1140 moderator.approve_host_request(host_request_2) 

1141 with session_scope() as session: 

1142 session.execute( 

1143 select(Message) 

1144 .where(Message.conversation_id == host_request_2) 

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

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

1147 refresh_materialized_view(session, "user_response_rates") 

1148 

1149 # still insufficient 

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

1151 assert res.HasField("insufficient_data") 

1152 

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

1154 host_request_3 = api.CreateHostRequest( 

1155 requests_pb2.CreateHostRequestReq( 

1156 host_user_id=user2.id, 

1157 from_date=today_plus_2.isoformat(), 

1158 to_date=today_plus_3.isoformat(), 

1159 text=valid_request_text("Test request"), 

1160 ) 

1161 ).host_request_id 

1162 moderator.approve_host_request(host_request_3) 

1163 with session_scope() as session: 

1164 session.execute( 

1165 select(Message) 

1166 .where(Message.conversation_id == host_request_3) 

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

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

1169 refresh_materialized_view(session, "user_response_rates") 

1170 

1171 # now low 

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

1173 assert res.HasField("low") 

1174 

1175 with requests_session(token2) as api: 

1176 # accept a host req 

1177 api.RespondHostRequest( 

1178 requests_pb2.RespondHostRequestReq( 

1179 host_request_id=host_request_2, 

1180 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1181 text="Accepting host request", 

1182 ) 

1183 ) 

1184 

1185 with session_scope() as session: 

1186 refresh_materialized_view(session, "user_response_rates") 

1187 

1188 with requests_session(token1) as api: 

1189 # now some w p33 = 35h 

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

1191 assert res.HasField("some") 

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

1193 

1194 with requests_session(token2) as api: 

1195 # accept another host req 

1196 api.RespondHostRequest( 

1197 requests_pb2.RespondHostRequestReq( 

1198 host_request_id=host_request_3, 

1199 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1200 text="Accepting host request", 

1201 ) 

1202 ) 

1203 

1204 with session_scope() as session: 

1205 refresh_materialized_view(session, "user_response_rates") 

1206 

1207 with requests_session(token1) as api: 

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

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

1210 assert res.HasField("most") 

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

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

1213 

1214 with requests_session(token2) as api: 

1215 # accept last host req 

1216 api.RespondHostRequest( 

1217 requests_pb2.RespondHostRequestReq( 

1218 host_request_id=host_request_1, 

1219 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1220 text="Accepting host request", 

1221 ) 

1222 ) 

1223 

1224 with session_scope() as session: 

1225 refresh_materialized_view(session, "user_response_rates") 

1226 

1227 with requests_session(token1) as api: 

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

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

1230 assert res.HasField("almost_all") 

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

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

1233 

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

1235 host_request_4 = api.CreateHostRequest( 

1236 requests_pb2.CreateHostRequestReq( 

1237 host_user_id=user2.id, 

1238 from_date=today_plus_2.isoformat(), 

1239 to_date=today_plus_3.isoformat(), 

1240 text=valid_request_text("Test request"), 

1241 ) 

1242 ).host_request_id 

1243 moderator.approve_host_request(host_request_4) 

1244 with session_scope() as session: 

1245 session.execute( 

1246 select(Message) 

1247 .where(Message.conversation_id == host_request_4) 

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

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

1250 refresh_materialized_view(session, "user_response_rates") 

1251 

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

1253 host_request_5 = api.CreateHostRequest( 

1254 requests_pb2.CreateHostRequestReq( 

1255 host_user_id=user2.id, 

1256 from_date=today_plus_2.isoformat(), 

1257 to_date=today_plus_3.isoformat(), 

1258 text=valid_request_text("Test request"), 

1259 ) 

1260 ).host_request_id 

1261 moderator.approve_host_request(host_request_5) 

1262 with session_scope() as session: 

1263 session.execute( 

1264 select(Message) 

1265 .where(Message.conversation_id == host_request_5) 

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

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

1268 refresh_materialized_view(session, "user_response_rates") 

1269 

1270 # now some w p33 = 35h 

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

1272 assert res.HasField("some") 

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

1274 

1275 with requests_session(token2) as api: 

1276 # accept host req 

1277 api.RespondHostRequest( 

1278 requests_pb2.RespondHostRequestReq( 

1279 host_request_id=host_request_5, 

1280 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1281 text="Accepting host request", 

1282 ) 

1283 ) 

1284 

1285 with session_scope() as session: 

1286 refresh_materialized_view(session, "user_response_rates") 

1287 

1288 with requests_session(token1) as api: 

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

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

1291 assert res.HasField("most") 

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

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

1294 

1295 with requests_session(token2) as api: 

1296 # accept host req 

1297 api.RespondHostRequest( 

1298 requests_pb2.RespondHostRequestReq( 

1299 host_request_id=host_request_4, 

1300 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1301 text="Accepting host request", 

1302 ) 

1303 ) 

1304 

1305 with session_scope() as session: 

1306 refresh_materialized_view(session, "user_response_rates") 

1307 

1308 with requests_session(token1) as api: 

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

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

1311 assert res.HasField("almost_all") 

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

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

1314 

1315 

1316def test_request_notifications(db, push_collector: PushCollector, moderator): 

1317 host, host_token = generate_user(complete_profile=True) 

1318 surfer, surfer_token = generate_user(complete_profile=True) 

1319 

1320 host_loc_context = LocalizationContext.from_user(host) 

1321 surfer_loc_context = LocalizationContext.from_user(surfer) 

1322 

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

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

1325 

1326 with requests_session(surfer_token) as api: 

1327 hr_id = api.CreateHostRequest( 

1328 requests_pb2.CreateHostRequestReq( 

1329 host_user_id=host.id, 

1330 from_date=today_plus_2.isoformat(), 

1331 to_date=today_plus_3.isoformat(), 

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

1333 ) 

1334 ).host_request_id 

1335 

1336 with mock_notification_email() as mock: 

1337 moderator.approve_host_request(hr_id) 

1338 

1339 mock.assert_called_once() 

1340 e = email_fields(mock) 

1341 assert e.recipient == host.email 

1342 assert "host request" in e.subject.lower() 

1343 assert host.name in e.plain 

1344 assert host.name in e.html 

1345 assert "quick decline" in e.plain.lower(), e.plain 

1346 assert "quick decline" in e.html.lower() 

1347 assert surfer.name in e.plain 

1348 assert surfer.name in e.html 

1349 assert host_loc_context.localize_date(today_plus_2) in e.plain 

1350 assert host_loc_context.localize_date(today_plus_2) in e.html 

1351 assert host_loc_context.localize_date(today_plus_3) in e.plain 

1352 assert host_loc_context.localize_date(today_plus_3) in e.html 

1353 assert "http://localhost:5001/img/thumbnail/" not in e.plain 

1354 assert "http://localhost:5001/img/thumbnail/" in e.html 

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

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

1357 

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

1359 

1360 with requests_session(host_token) as api: 

1361 with mock_notification_email() as mock: 

1362 api.RespondHostRequest( 

1363 requests_pb2.RespondHostRequestReq( 

1364 host_request_id=hr_id, 

1365 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

1366 text="Accepting host request", 

1367 ) 

1368 ) 

1369 

1370 e = email_fields(mock) 

1371 assert e.recipient == surfer.email 

1372 assert "host request" in e.subject.lower() 

1373 assert host.name in e.plain 

1374 assert host.name in e.html 

1375 assert surfer.name in e.plain 

1376 assert surfer.name in e.html 

1377 assert surfer_loc_context.localize_date(today_plus_2) in e.plain 

1378 assert surfer_loc_context.localize_date(today_plus_2) in e.html 

1379 assert surfer_loc_context.localize_date(today_plus_3) in e.plain 

1380 assert surfer_loc_context.localize_date(today_plus_3) in e.html 

1381 assert "http://localhost:5001/img/thumbnail/" not in e.plain 

1382 assert "http://localhost:5001/img/thumbnail/" in e.html 

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

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

1385 

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

1387 

1388 

1389def test_quick_decline(db, push_collector: PushCollector, moderator): 

1390 host, host_token = generate_user(complete_profile=True) 

1391 surfer, surfer_token = generate_user(complete_profile=True) 

1392 

1393 host_loc_context = LocalizationContext.from_user(host) 

1394 

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

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

1397 

1398 with requests_session(surfer_token) as api: 

1399 hr_id = api.CreateHostRequest( 

1400 requests_pb2.CreateHostRequestReq( 

1401 host_user_id=host.id, 

1402 from_date=today_plus_2.isoformat(), 

1403 to_date=today_plus_3.isoformat(), 

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

1405 ) 

1406 ).host_request_id 

1407 

1408 with mock_notification_email() as mock: 

1409 moderator.approve_host_request(hr_id) 

1410 

1411 mock.assert_called_once() 

1412 e = email_fields(mock) 

1413 assert e.recipient == host.email 

1414 assert "host request" in e.subject.lower() 

1415 assert host.name in e.plain 

1416 assert host.name in e.html 

1417 assert "quick decline" in e.plain.lower(), e.plain 

1418 assert "quick decline" in e.html.lower() 

1419 assert surfer.name in e.plain 

1420 assert surfer.name in e.html 

1421 assert host_loc_context.localize_date(today_plus_2) in e.plain 

1422 assert host_loc_context.localize_date(today_plus_2) in e.html 

1423 assert host_loc_context.localize_date(today_plus_3) in e.plain 

1424 assert host_loc_context.localize_date(today_plus_3) in e.html 

1425 assert "http://localhost:5001/img/thumbnail/" not in e.plain 

1426 assert "http://localhost:5001/img/thumbnail/" in e.html 

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

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

1429 

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

1431 

1432 # very ugly 

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

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

1435 if "payload" not in link: 

1436 continue 

1437 print(link) 

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

1439 params = parse_qs(url_parts.query) 

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

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

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

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

1444 res = auth_api.Unsubscribe( 

1445 auth_pb2.UnsubscribeReq( 

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

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

1448 ) 

1449 ) 

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

1451 break 

1452 else: 

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

1454 

1455 with requests_session(surfer_token) as api: 

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

1457 assert res.status == conversations_pb2.HOST_REQUEST_STATUS_REJECTED 

1458 

1459 

1460def test_host_req_feedback(db, moderator): 

1461 host, host_token = generate_user(complete_profile=True) 

1462 host2, host2_token = generate_user(complete_profile=True) 

1463 host3, host3_token = generate_user(complete_profile=True) 

1464 surfer, surfer_token = generate_user(complete_profile=True) 

1465 

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

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

1468 

1469 with requests_session(surfer_token) as api: 

1470 hr_id = api.CreateHostRequest( 

1471 requests_pb2.CreateHostRequestReq( 

1472 host_user_id=host.id, 

1473 from_date=today_plus_2.isoformat(), 

1474 to_date=today_plus_3.isoformat(), 

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

1476 ) 

1477 ).host_request_id 

1478 hr2_id = api.CreateHostRequest( 

1479 requests_pb2.CreateHostRequestReq( 

1480 host_user_id=host2.id, 

1481 from_date=today_plus_2.isoformat(), 

1482 to_date=today_plus_3.isoformat(), 

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

1484 ) 

1485 ).host_request_id 

1486 hr3_id = api.CreateHostRequest( 

1487 requests_pb2.CreateHostRequestReq( 

1488 host_user_id=host3.id, 

1489 from_date=today_plus_2.isoformat(), 

1490 to_date=today_plus_3.isoformat(), 

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

1492 ) 

1493 ).host_request_id 

1494 

1495 moderator.approve_host_request(hr_id) 

1496 moderator.approve_host_request(hr2_id) 

1497 moderator.approve_host_request(hr3_id) 

1498 

1499 with requests_session(host_token) as api: 

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

1501 assert not res.need_host_request_feedback 

1502 

1503 api.RespondHostRequest( 

1504 requests_pb2.RespondHostRequestReq( 

1505 host_request_id=hr_id, 

1506 status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED, 

1507 ) 

1508 ) 

1509 

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

1511 assert res.need_host_request_feedback 

1512 

1513 # surfer can't leave feedback 

1514 with requests_session(surfer_token) as api: 

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

1516 api.SendHostRequestFeedback( 

1517 requests_pb2.SendHostRequestFeedbackReq( 

1518 host_request_id=hr_id, 

1519 ) 

1520 ) 

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

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

1523 

1524 with requests_session(host_token) as api: 

1525 api.SendHostRequestFeedback( 

1526 requests_pb2.SendHostRequestFeedbackReq( 

1527 host_request_id=hr_id, 

1528 host_request_quality=requests_pb2.HOST_REQUEST_QUALITY_LOW, 

1529 ) 

1530 ) 

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

1532 assert not res.need_host_request_feedback 

1533 

1534 # can't leave it twice 

1535 with requests_session(host_token) as api: 

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

1537 api.SendHostRequestFeedback( 

1538 requests_pb2.SendHostRequestFeedbackReq( 

1539 host_request_id=hr_id, 

1540 ) 

1541 ) 

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

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

1544 

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

1546 assert not res.need_host_request_feedback 

1547 

1548 with requests_session(host2_token) as api: 

1549 api.RespondHostRequest( 

1550 requests_pb2.RespondHostRequestReq( 

1551 host_request_id=hr2_id, status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED 

1552 ) 

1553 ) 

1554 # can't leave feedback on the wrong one 

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

1556 api.SendHostRequestFeedback( 

1557 requests_pb2.SendHostRequestFeedbackReq( 

1558 host_request_id=hr_id, 

1559 ) 

1560 ) 

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

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

1563 

1564 # null feedback is still feedback 

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

1566 

1567 with requests_session(host3_token) as api: 

1568 api.RespondHostRequest( 

1569 requests_pb2.RespondHostRequestReq( 

1570 host_request_id=hr3_id, status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED 

1571 ) 

1572 ) 

1573 

1574 api.SendHostRequestFeedback( 

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

1576 ) 

1577 

1578 

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

1580 with session_scope() as session: 

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

1582 cluster = session.execute( 

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

1584 ).scalar_one_or_none() 

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

1586 cluster = Cluster( 

1587 name="Test community", 

1588 description="Test", 

1589 parent_node_id=node_id, 

1590 is_official_cluster=True, 

1591 ) 

1592 session.add(cluster) 

1593 session.flush() 

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

1595 

1596 

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

1598 with session_scope() as session: 

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

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

1601 node = Node( 

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

1603 node_type=NodeType.locality, 

1604 ) 

1605 session.add(node) 

1606 session.flush() 

1607 trip = PublicTrip( 

1608 user_id=user_id, 

1609 node_id=node.id, 

1610 from_date=from_date, 

1611 to_date=to_date, 

1612 description="Looking for a host!", 

1613 status=status or PublicTripStatus.searching_for_host, 

1614 same_gender_only=same_gender_only, 

1615 ) 

1616 session.add(trip) 

1617 session.flush() 

1618 return trip.id 

1619 

1620 

1621def test_create_request_with_public_trip(db, moderator): 

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

1623 surfer, surfer_token = generate_user() 

1624 host, host_token = generate_user() 

1625 

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

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

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

1629 

1630 with requests_session(host_token) as api: 

1631 # Happy path: dates within trip window 

1632 res = api.CreateHostRequest( 

1633 requests_pb2.CreateHostRequestReq( 

1634 host_user_id=surfer.id, 

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

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

1637 text=valid_request_text(), 

1638 public_trip_id=trip_id, 

1639 ) 

1640 ) 

1641 host_request_id = res.host_request_id 

1642 

1643 moderator.approve_host_request(host_request_id) 

1644 

1645 with requests_session(host_token) as api: 

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

1647 assert hr.public_trip_id == trip_id 

1648 

1649 

1650def test_create_request_with_public_trip_dates_out_of_range(db): 

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

1652 surfer, _ = generate_user() 

1653 host, host_token = generate_user() 

1654 

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

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

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

1658 

1659 with requests_session(host_token) as api: 

1660 # from_date before trip starts 

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

1662 api.CreateHostRequest( 

1663 requests_pb2.CreateHostRequestReq( 

1664 host_user_id=surfer.id, 

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

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

1667 text=valid_request_text(), 

1668 public_trip_id=trip_id, 

1669 ) 

1670 ) 

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

1672 

1673 # to_date after trip ends 

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

1675 api.CreateHostRequest( 

1676 requests_pb2.CreateHostRequestReq( 

1677 host_user_id=surfer.id, 

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

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

1680 text=valid_request_text(), 

1681 public_trip_id=trip_id, 

1682 ) 

1683 ) 

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

1685 

1686 

1687def test_create_request_with_public_trip_user_mismatch(db): 

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

1689 trip_owner, _ = generate_user() 

1690 other_user, _ = generate_user() 

1691 host, host_token = generate_user() 

1692 

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

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

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

1696 

1697 with requests_session(host_token) as api: 

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

1699 api.CreateHostRequest( 

1700 requests_pb2.CreateHostRequestReq( 

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

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

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

1704 text=valid_request_text(), 

1705 public_trip_id=trip_id, 

1706 ) 

1707 ) 

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

1709 

1710 

1711def test_create_request_with_closed_public_trip(db): 

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

1713 surfer, _ = generate_user() 

1714 host, host_token = generate_user() 

1715 

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

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

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

1719 

1720 with requests_session(host_token) as api: 

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

1722 api.CreateHostRequest( 

1723 requests_pb2.CreateHostRequestReq( 

1724 host_user_id=surfer.id, 

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

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

1727 text=valid_request_text(), 

1728 public_trip_id=trip_id, 

1729 ) 

1730 ) 

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

1732 

1733 

1734def test_create_request_with_nonexistent_public_trip(db): 

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

1736 surfer, _ = generate_user() 

1737 host, host_token = generate_user() 

1738 

1739 with requests_session(host_token) as api: 

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

1741 api.CreateHostRequest( 

1742 requests_pb2.CreateHostRequestReq( 

1743 host_user_id=surfer.id, 

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

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

1746 text=valid_request_text(), 

1747 public_trip_id=999999, 

1748 ) 

1749 ) 

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

1751 

1752 

1753def test_create_request_without_public_trip_id_unchanged(db, moderator): 

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

1755 surfer, _ = generate_user() 

1756 host, host_token = generate_user() 

1757 

1758 with requests_session(host_token) as api: 

1759 res = api.CreateHostRequest( 

1760 requests_pb2.CreateHostRequestReq( 

1761 host_user_id=surfer.id, 

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

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

1764 text=valid_request_text(), 

1765 ) 

1766 ) 

1767 host_request_id = res.host_request_id 

1768 

1769 moderator.approve_host_request(host_request_id) 

1770 

1771 with requests_session(host_token) as api: 

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

1773 assert not hr.HasField("public_trip_id") 

1774 

1775 

1776def test_create_request_same_gender_only_wrong_gender_rejected(db): 

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

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

1779 

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

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

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

1783 

1784 with requests_session(host_token) as api: 

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

1786 api.CreateHostRequest( 

1787 requests_pb2.CreateHostRequestReq( 

1788 host_user_id=surfer.id, 

1789 from_date=trip_from.isoformat(), 

1790 to_date=trip_to.isoformat(), 

1791 text=valid_request_text(), 

1792 public_trip_id=trip_id, 

1793 ) 

1794 ) 

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

1796 

1797 

1798def test_create_request_same_gender_only_same_gender_allowed(db, moderator): 

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

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

1801 

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

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

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

1805 

1806 with requests_session(host_token) as api: 

1807 res = api.CreateHostRequest( 

1808 requests_pb2.CreateHostRequestReq( 

1809 host_user_id=surfer.id, 

1810 from_date=trip_from.isoformat(), 

1811 to_date=trip_to.isoformat(), 

1812 text=valid_request_text(), 

1813 public_trip_id=trip_id, 

1814 ) 

1815 ) 

1816 assert res.host_request_id > 0 

1817 

1818 

1819def test_create_request_same_gender_only_moderator_bypass(db, moderator): 

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

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

1822 

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

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

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

1826 _make_trip_node_admin(host.id, trip_id) 

1827 

1828 with requests_session(host_token) as api: 

1829 res = api.CreateHostRequest( 

1830 requests_pb2.CreateHostRequestReq( 

1831 host_user_id=surfer.id, 

1832 from_date=trip_from.isoformat(), 

1833 to_date=trip_to.isoformat(), 

1834 text=valid_request_text(), 

1835 public_trip_id=trip_id, 

1836 ) 

1837 ) 

1838 assert res.host_request_id > 0 

1839 

1840 

1841def test_create_request_duplicate_offer_rejected(db): 

1842 surfer, _ = generate_user() 

1843 _, host_token = generate_user() 

1844 

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

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

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

1848 

1849 with requests_session(host_token) as api: 

1850 api.CreateHostRequest( 

1851 requests_pb2.CreateHostRequestReq( 

1852 host_user_id=surfer.id, 

1853 from_date=trip_from.isoformat(), 

1854 to_date=trip_to.isoformat(), 

1855 text=valid_request_text(), 

1856 public_trip_id=trip_id, 

1857 ) 

1858 ) 

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

1860 api.CreateHostRequest( 

1861 requests_pb2.CreateHostRequestReq( 

1862 host_user_id=surfer.id, 

1863 from_date=trip_from.isoformat(), 

1864 to_date=trip_to.isoformat(), 

1865 text=valid_request_text(), 

1866 public_trip_id=trip_id, 

1867 ) 

1868 ) 

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