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

747 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-03 11:56 +0000

1from datetime import date, datetime, timedelta 

2 

3import grpc 

4import pytest 

5from google.protobuf import empty_pb2 

6from sqlalchemy import func, select, update 

7from sqlalchemy.orm import Session 

8 

9from couchers.db import session_scope 

10from couchers.materialized_views import refresh_materialized_views_rapid 

11from couchers.models import ( 

12 Conversation, 

13 FriendRelationship, 

14 FriendStatus, 

15 HostRequest, 

16 HostRequestStatus, 

17 Message, 

18 MessageType, 

19 ModerationObjectType, 

20 ModerationState, 

21 ModerationVisibility, 

22 Node, 

23 NodeType, 

24 Reference, 

25 ReferenceType, 

26 StayProposalKind, 

27 User, 

28) 

29from couchers.models.public_trips import PublicTrip, PublicTripStatus 

30from couchers.moderation.utils import create_moderation 

31from couchers.proto import api_pb2, messages_pb2, moderation_pb2, references_pb2, requests_pb2 

32from couchers.utils import create_coordinate, create_polygon_lat_lng, now, to_aware_datetime, to_multi, today 

33from tests.fixtures.db import generate_user, make_friends, make_user_block 

34from tests.fixtures.misc import EmailCollector, PushCollector 

35from tests.fixtures.sessions import ( 

36 account_session, 

37 api_session, 

38 real_moderation_session, 

39 references_session, 

40 requests_session, 

41) 

42from tests.test_requests import valid_request_text 

43 

44 

45def create_public_trip(session: Session, user_id: int, from_date: date, to_date: date) -> int: 

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

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

48 node = Node( 

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

50 node_type=NodeType.locality, 

51 ) 

52 session.add(node) 

53 session.flush() 

54 moderation_state = ModerationState( 

55 object_type=ModerationObjectType.public_trip, 

56 object_id=0, 

57 visibility=ModerationVisibility.visible, 

58 ) 

59 session.add(moderation_state) 

60 session.flush() 

61 trip = PublicTrip( 

62 user_id=user_id, 

63 node_id=node.id, 

64 from_date=from_date, 

65 to_date=to_date, 

66 description="Looking for a host!", 

67 status=PublicTripStatus.searching_for_host, 

68 moderation_state_id=moderation_state.id, 

69 ) 

70 session.add(trip) 

71 session.flush() 

72 moderation_state.object_id = trip.id 

73 return trip.id 

74 

75 

76def create_host_request( 

77 session: Session, 

78 surfer_user_id: int, 

79 host_user_id: int, 

80 host_request_age: timedelta = timedelta(days=15), 

81 status: HostRequestStatus = HostRequestStatus.confirmed, 

82 host_reason_didnt_meetup: str | None = None, 

83 surfer_reason_didnt_meetup: str | None = None, 

84 is_offer: bool = False, 

85) -> int: 

86 """ 

87 Create a host request that's `host_request_age` old 

88 

89 If `is_offer`, it's an offer on a public trip, so the host initiated it and the surfer received it. 

90 """ 

91 from_date = today() - host_request_age - timedelta(days=2) 

92 to_date = today() - host_request_age 

93 fake_created = now() - host_request_age - timedelta(days=3) 

94 

95 if is_offer: 

96 initiator_user_id, recipient_user_id = host_user_id, surfer_user_id 

97 initiator_reason, recipient_reason = host_reason_didnt_meetup, surfer_reason_didnt_meetup 

98 else: 

99 initiator_user_id, recipient_user_id = surfer_user_id, host_user_id 

100 initiator_reason, recipient_reason = surfer_reason_didnt_meetup, host_reason_didnt_meetup 

101 

102 public_trip_id = create_public_trip(session, surfer_user_id, from_date, to_date) if is_offer else None 

103 

104 conversation = Conversation() 

105 session.add(conversation) 

106 session.flush() 

107 

108 msg1 = Message( 

109 conversation_id=conversation.id, 

110 author_id=initiator_user_id, 

111 message_type=MessageType.chat_created, 

112 ) 

113 msg1.time = fake_created + timedelta(seconds=1) 

114 session.add(msg1) 

115 

116 msg2 = Message( 

117 conversation_id=conversation.id, 

118 author_id=initiator_user_id, 

119 text="Hi, I'm requesting to be hosted." if not is_offer else "Hi, I'd love to host you.", 

120 message_type=MessageType.text, 

121 ) 

122 msg2.time = fake_created + timedelta(seconds=2) 

123 session.add(msg2) 

124 session.flush() 

125 

126 moderation_state = create_moderation( 

127 session, 

128 ModerationObjectType.host_request, 

129 conversation.id, 

130 initiator_user_id, 

131 ) 

132 

133 host_request = HostRequest( 

134 conversation_id=conversation.id, 

135 initiator_user_id=initiator_user_id, 

136 recipient_user_id=recipient_user_id, 

137 proposal_kind=StayProposalKind.invitation if is_offer else StayProposalKind.request, 

138 from_date=from_date, 

139 to_date=to_date, 

140 status=status, 

141 initiator_last_seen_message_id=msg2.id, 

142 recipient_reason_didnt_meetup=recipient_reason, 

143 initiator_reason_didnt_meetup=initiator_reason, 

144 hosting_city="Test City", 

145 hosting_location=create_coordinate(0, 0), 

146 hosting_radius=10, 

147 moderation_state_id=moderation_state.id, 

148 public_trip_id=public_trip_id, 

149 ) 

150 session.add(host_request) 

151 session.commit() 

152 return host_request.conversation_id 

153 

154 

155def create_host_request_by_date( 

156 session: Session, 

157 surfer_user_id: int, 

158 host_user_id: int, 

159 from_date: date, 

160 to_date: date, 

161 status: HostRequestStatus, 

162 host_sent_request_reminders: int, 

163 last_sent_request_reminder_time: datetime, 

164) -> int: 

165 conversation = Conversation() 

166 session.add(conversation) 

167 session.flush() 

168 

169 msg1 = Message( 

170 conversation_id=conversation.id, 

171 author_id=surfer_user_id, 

172 message_type=MessageType.chat_created, 

173 ) 

174 msg1.time = from_date + timedelta(seconds=1) # type: ignore[assignment] 

175 session.add(msg1) 

176 

177 # Unused for now, but every host request must have a message. 

178 msg2 = Message( 

179 conversation_id=conversation.id, 

180 author_id=surfer_user_id, 

181 text="Hi, I'm requesting to be hosted.", 

182 message_type=MessageType.text, 

183 ) 

184 msg2.time = from_date + timedelta(seconds=2) # type: ignore[assignment] 

185 session.add(msg2) 

186 session.flush() 

187 

188 moderation_state = create_moderation( 

189 session, 

190 ModerationObjectType.host_request, 

191 conversation.id, 

192 surfer_user_id, 

193 ) 

194 

195 host_request = HostRequest( 

196 conversation_id=conversation.id, 

197 initiator_user_id=surfer_user_id, 

198 recipient_user_id=host_user_id, 

199 proposal_kind=StayProposalKind.request, 

200 from_date=from_date, 

201 to_date=to_date, 

202 status=status, 

203 hosting_city="Test City", 

204 hosting_location=create_coordinate(0, 0), 

205 hosting_radius=10, 

206 moderation_state_id=moderation_state.id, 

207 ) 

208 host_request.recipient_sent_request_reminders = host_sent_request_reminders 

209 host_request.last_sent_request_reminder_time = last_sent_request_reminder_time 

210 

211 session.add(host_request) 

212 session.commit() 

213 return host_request.conversation_id 

214 

215 

216def create_host_reference( 

217 session: Session, 

218 from_user_id: int, 

219 to_user_id: int, 

220 reference_age: timedelta, 

221 *, 

222 surfing: bool = True, 

223 host_request_id: int | None = None, 

224) -> tuple[int, int]: 

225 if host_request_id: 

226 actual_host_request_id = host_request_id 

227 else: 

228 if surfing: 

229 actual_host_request_id = host_request_id or create_host_request( 

230 session, from_user_id, to_user_id, reference_age + timedelta(days=1) 

231 ) 

232 else: 

233 actual_host_request_id = host_request_id or create_host_request( 

234 session, to_user_id, from_user_id, reference_age + timedelta(days=1) 

235 ) 

236 

237 host_request = session.execute( 

238 select(HostRequest).where(HostRequest.conversation_id == actual_host_request_id) 

239 ).scalar_one() 

240 

241 if host_request.initiator_user_id == from_user_id: 

242 reference_type = ReferenceType.surfed 

243 to_user_id = host_request.recipient_user_id 

244 assert from_user_id == host_request.initiator_user_id 

245 else: 

246 reference_type = ReferenceType.hosted 

247 to_user_id = host_request.initiator_user_id 

248 assert from_user_id == host_request.recipient_user_id 

249 

250 moderation_state = ModerationState( 

251 object_type=ModerationObjectType.reference, 

252 object_id=0, # placeholder, set after Reference flush 

253 visibility=ModerationVisibility.visible, 

254 ) 

255 session.add(moderation_state) 

256 session.flush() 

257 

258 reference = Reference( 

259 from_user_id=from_user_id, 

260 to_user_id=to_user_id, 

261 host_request_id=host_request.conversation_id, 

262 text="Dummy reference", 

263 rating=0.5, 

264 was_appropriate=True, 

265 reference_type=reference_type, 

266 moderation_state_id=moderation_state.id, 

267 ) 

268 reference.time = now() - reference_age 

269 

270 session.add(reference) 

271 session.flush() 

272 moderation_state.object_id = reference.id 

273 session.commit() 

274 return reference.id, actual_host_request_id 

275 

276 

277def create_friend_reference(session: Session, from_user_id: int, to_user_id: int, reference_age: timedelta) -> int: 

278 moderation_state = ModerationState( 

279 object_type=ModerationObjectType.reference, 

280 object_id=0, # placeholder, set after Reference flush 

281 visibility=ModerationVisibility.visible, 

282 ) 

283 session.add(moderation_state) 

284 session.flush() 

285 

286 reference = Reference( 

287 from_user_id=from_user_id, 

288 to_user_id=to_user_id, 

289 reference_type=ReferenceType.friend, 

290 text="Test friend request", 

291 rating=0.4, 

292 was_appropriate=True, 

293 moderation_state_id=moderation_state.id, 

294 ) 

295 reference.time = now() - reference_age 

296 session.add(reference) 

297 session.flush() 

298 moderation_state.object_id = reference.id 

299 session.commit() 

300 return reference.id 

301 

302 

303def test_ListPagination(db): 

304 user1, token1 = generate_user() 

305 user2, token2 = generate_user() 

306 user3, token3 = generate_user() 

307 user4, token4 = generate_user() 

308 user5, token5 = generate_user() 

309 user6, token6 = generate_user() 

310 user7, token7 = generate_user() 

311 user8, token8 = generate_user() 

312 user9, token9 = generate_user() 

313 

314 with session_scope() as session: 

315 # bidirectional references 

316 ref2, hr2 = create_host_reference(session, user2.id, user1.id, timedelta(days=16, seconds=110), surfing=True) 

317 ref2b, _ = create_host_reference( 

318 session, user1.id, user2.id, timedelta(days=16, seconds=100), host_request_id=hr2 

319 ) 

320 

321 ref3, _ = create_host_reference(session, user3.id, user1.id, timedelta(days=16, seconds=90), surfing=False) 

322 ref4, _ = create_host_reference(session, user4.id, user1.id, timedelta(days=16, seconds=80), surfing=True) 

323 ref4b = create_friend_reference(session, user1.id, user4.id, timedelta(days=16, seconds=70)) 

324 

325 ref5, hr5 = create_host_reference(session, user5.id, user1.id, timedelta(days=16, seconds=60), surfing=False) 

326 ref5b, _ = create_host_reference( 

327 session, user1.id, user5.id, timedelta(days=16, seconds=50), host_request_id=hr5 

328 ) 

329 

330 ref6, _ = create_host_reference(session, user6.id, user1.id, timedelta(days=16, seconds=40), surfing=True) 

331 

332 ref7 = create_friend_reference(session, user7.id, user1.id, timedelta(days=16, seconds=30)) 

333 

334 ref8, _ = create_host_reference(session, user8.id, user1.id, timedelta(days=16, seconds=20), surfing=False) 

335 ref9, _ = create_host_reference(session, user9.id, user1.id, timedelta(days=16, seconds=10), surfing=False) 

336 

337 # should be visible even under 2 weeks 

338 ref7b = create_friend_reference(session, user1.id, user7.id, timedelta(days=9)) 

339 

340 # hidden because it's less than 2 weeks 

341 ref6hidden, _ = create_host_reference(session, user6.id, user1.id, timedelta(days=5), surfing=False) 

342 

343 # visible because both were written 

344 ref8b, hr8 = create_host_reference(session, user8.id, user1.id, timedelta(days=3, seconds=20), surfing=False) 

345 ref8c, _ = create_host_reference( 

346 session, user1.id, user8.id, timedelta(days=3, seconds=10), host_request_id=hr8 

347 ) 

348 

349 # note that visibility tests don't really test real logic 

350 

351 # these check the right refs are in the right requests and appear in the right order (latest first) 

352 

353 with references_session(token2) as api: 

354 # written by user1 

355 res = api.ListReferences(references_pb2.ListReferencesReq(from_user_id=user1.id, page_size=2)) 

356 assert [ref.reference_id for ref in res.references] == [ref8c, ref7b] 

357 

358 res = api.ListReferences( 

359 references_pb2.ListReferencesReq(from_user_id=user1.id, page_token=res.next_page_token, page_size=2) 

360 ) 

361 assert [ref.reference_id for ref in res.references] == [ref5b, ref4b] 

362 

363 res = api.ListReferences( 

364 references_pb2.ListReferencesReq(from_user_id=user1.id, page_token=res.next_page_token, page_size=2) 

365 ) 

366 assert [ref.reference_id for ref in res.references] == [ref2b] 

367 assert not res.next_page_token 

368 

369 # received by user1 

370 res = api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user1.id, page_size=5)) 

371 assert [ref.reference_id for ref in res.references] == [ref8b, ref9, ref8, ref7, ref6] 

372 

373 res = api.ListReferences( 

374 references_pb2.ListReferencesReq(to_user_id=user1.id, page_token=res.next_page_token, page_size=5) 

375 ) 

376 assert [ref.reference_id for ref in res.references] == [ref5, ref4, ref3, ref2] 

377 assert not res.next_page_token 

378 

379 # same thing but with filters 

380 res = api.ListReferences( 

381 references_pb2.ListReferencesReq( 

382 to_user_id=user1.id, 

383 reference_type_filter=[ 

384 references_pb2.REFERENCE_TYPE_HOSTED, 

385 references_pb2.REFERENCE_TYPE_SURFED, 

386 references_pb2.REFERENCE_TYPE_FRIEND, 

387 ], 

388 page_size=5, 

389 ) 

390 ) 

391 assert [ref.reference_id for ref in res.references] == [ref8b, ref9, ref8, ref7, ref6] 

392 

393 res = api.ListReferences( 

394 references_pb2.ListReferencesReq( 

395 to_user_id=user1.id, 

396 reference_type_filter=[ 

397 references_pb2.REFERENCE_TYPE_HOSTED, 

398 references_pb2.REFERENCE_TYPE_SURFED, 

399 references_pb2.REFERENCE_TYPE_FRIEND, 

400 ], 

401 page_token=res.next_page_token, 

402 page_size=5, 

403 ) 

404 ) 

405 assert [ref.reference_id for ref in res.references] == [ref5, ref4, ref3, ref2] 

406 assert not res.next_page_token 

407 

408 # received hosting references 

409 res = api.ListReferences( 

410 references_pb2.ListReferencesReq( 

411 to_user_id=user1.id, reference_type_filter=[references_pb2.REFERENCE_TYPE_HOSTED], page_size=3 

412 ) 

413 ) 

414 assert [ref.reference_id for ref in res.references] == [ref8b, ref9, ref8] 

415 

416 res = api.ListReferences( 

417 references_pb2.ListReferencesReq( 

418 to_user_id=user1.id, 

419 reference_type_filter=[references_pb2.REFERENCE_TYPE_HOSTED], 

420 page_token=res.next_page_token, 

421 page_size=3, 

422 ) 

423 ) 

424 assert [ref.reference_id for ref in res.references] == [ref5, ref3] 

425 assert not res.next_page_token 

426 

427 # written friend references 

428 res = api.ListReferences( 

429 references_pb2.ListReferencesReq( 

430 from_user_id=user1.id, reference_type_filter=[references_pb2.REFERENCE_TYPE_FRIEND] 

431 ) 

432 ) 

433 assert [ref.reference_id for ref in res.references] == [ref7b, ref4b] 

434 assert not res.next_page_token 

435 

436 # written surfing references 

437 res = api.ListReferences( 

438 references_pb2.ListReferencesReq( 

439 from_user_id=user1.id, reference_type_filter=[references_pb2.REFERENCE_TYPE_SURFED] 

440 ) 

441 ) 

442 assert [ref.reference_id for ref in res.references] == [ref8c, ref5b] 

443 assert not res.next_page_token 

444 

445 with references_session(token7) as api: 

446 # need to set at least one of from or to user 

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

448 api.ListReferences( 

449 references_pb2.ListReferencesReq(reference_type_filter=[references_pb2.REFERENCE_TYPE_SURFED]) 

450 ) 

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

452 assert e.value.details() == "You need to specify at least one user." 

453 

454 with references_session(token5) as api: 

455 # from user1 to user2 

456 res = api.ListReferences(references_pb2.ListReferencesReq(from_user_id=user1.id, to_user_id=user2.id)) 

457 assert [ref.reference_id for ref in res.references] == [ref2b] 

458 assert not res.next_page_token 

459 

460 # from user5 to user1 

461 res = api.ListReferences(references_pb2.ListReferencesReq(from_user_id=user5.id, to_user_id=user1.id)) 

462 assert [ref.reference_id for ref in res.references] == [ref5] 

463 assert not res.next_page_token 

464 

465 with api_session(token2) as api: 

466 # the reference count matches the 9 references visible in the list; ref6hidden is excluded 

467 # because it's still hidden by the reciprocal-reference rule 

468 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 9 

469 # user8 received ref8c, visible because both references of the pair were written 

470 assert api.GetUser(api_pb2.GetUserReq(user=user8.username)).num_references == 1 

471 # user6 wrote references but received none 

472 assert api.GetUser(api_pb2.GetUserReq(user=user6.username)).num_references == 0 

473 

474 

475def test_num_references_matches_visible_list(db): 

476 # Regression test: the reference count (User.num_references) must not include references that 

477 # are still hidden by the reciprocal-reference rule, otherwise the count leaks the existence 

478 # of a hidden reference and disagrees with what ListReferences shows. 

479 user1, token1 = generate_user() 

480 user2, token2 = generate_user() 

481 

482 with session_scope() as session: 

483 # user2 wrote user1 a reference for a recent stay; the 2-week window is still open and 

484 # user1 has not written their reciprocal reference yet, so this reference is hidden. 

485 create_host_reference(session, user2.id, user1.id, timedelta(days=3), surfing=False) 

486 

487 with references_session(token2) as api: 

488 res = api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user1.id)) 

489 assert len(res.references) == 0 

490 

491 with api_session(token2) as api: 

492 res = api.GetUser(api_pb2.GetUserReq(user=user1.username)) 

493 # count must agree with the (empty) list, not leak the hidden reference 

494 assert res.num_references == 0 

495 

496 with session_scope() as session: 

497 host_request_id = session.execute(select(HostRequest.conversation_id)).scalar_one() 

498 # user1 writes the reciprocal reference: now both references become visible 

499 create_host_reference(session, user1.id, user2.id, timedelta(days=2), host_request_id=host_request_id) 

500 

501 with references_session(token2) as api: 

502 res = api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user1.id)) 

503 assert len(res.references) == 1 

504 

505 with api_session(token2) as api: 

506 res = api.GetUser(api_pb2.GetUserReq(user=user1.username)) 

507 assert res.num_references == 1 

508 

509 

510def test_ListReference_banned_deleted_users(db): 

511 user1, token1 = generate_user() 

512 user2, token2 = generate_user() 

513 user3, token3 = generate_user() 

514 

515 with session_scope() as session: 

516 create_friend_reference(session, user2.id, user1.id, timedelta(days=15)) 

517 create_friend_reference(session, user3.id, user1.id, timedelta(days=16)) 

518 create_friend_reference(session, user1.id, user2.id, timedelta(days=15)) 

519 create_friend_reference(session, user1.id, user3.id, timedelta(days=16)) 

520 

521 with references_session(token1) as api: 

522 refs_rec = api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user1.id)).references 

523 refs_sent = api.ListReferences(references_pb2.ListReferencesReq(from_user_id=user1.id)).references 

524 assert len(refs_rec) == 2 

525 assert len(refs_sent) == 2 

526 

527 with api_session(token1) as api: 

528 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 2 

529 

530 # ban user2 

531 with session_scope() as session: 

532 session.execute(update(User).where(User.username == user2.username).values(banned_at=func.now())) 

533 

534 # reference to and from banned user is hidden 

535 with references_session(token1) as api: 

536 refs_rec = api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user1.id)).references 

537 refs_sent = api.ListReferences(references_pb2.ListReferencesReq(from_user_id=user1.id)).references 

538 assert len(refs_rec) == 1 

539 assert len(refs_sent) == 1 

540 

541 with api_session(token1) as api: 

542 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 1 

543 

544 # delete user3 

545 with session_scope() as session: 

546 session.execute(update(User).where(User.username == user3.username).values(deleted_at=func.now())) 

547 

548 # doesn't change; references to and from deleted users remain 

549 with references_session(token1) as api: 

550 refs_rec = api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user1.id)).references 

551 refs_sent = api.ListReferences(references_pb2.ListReferencesReq(from_user_id=user1.id)).references 

552 assert len(refs_rec) == 1 

553 assert len(refs_sent) == 1 

554 

555 with api_session(token1) as api: 

556 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 1 

557 

558 

559def test_ListReference_blocked_shadowed_users(db): 

560 user1, token1 = generate_user() 

561 user2, token2 = generate_user() 

562 user3, token3 = generate_user() 

563 

564 with session_scope() as session: 

565 create_friend_reference(session, user2.id, user1.id, timedelta(days=15)) 

566 create_friend_reference(session, user3.id, user1.id, timedelta(days=16)) 

567 

568 # user1 blocks user2: reference from blocked user remains in both list and count 

569 make_user_block(user1, user2) 

570 

571 with references_session(token1) as api: 

572 refs_rec = api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user1.id)).references 

573 assert len(refs_rec) == 2 

574 

575 with api_session(token1) as api: 

576 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 2 

577 

578 # shadow user3: their reference is hidden from both list and count 

579 with session_scope() as session: 

580 session.execute(update(User).where(User.username == user3.username).values(shadowed_at=func.now())) 

581 

582 with references_session(token1) as api: 

583 refs_rec = api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user1.id)).references 

584 assert len(refs_rec) == 1 

585 

586 with api_session(token1) as api: 

587 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 1 

588 

589 # the shadowed author still sees their own reference in both, so the shadow ban isn't leaked 

590 with references_session(token3) as api: 

591 refs_rec = api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user1.id)).references 

592 assert len(refs_rec) == 2 

593 

594 with api_session(token3) as api: 

595 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 2 

596 

597 

598def test_WriteFriendReference(db, moderator): 

599 user1, token1 = generate_user() 

600 user2, token2 = generate_user() 

601 user3, token3 = generate_user() 

602 

603 # Make user1 and user2 friends 

604 make_friends(user1, user2) 

605 

606 with references_session(token1) as api: 

607 # can write normal friend reference 

608 res = api.WriteFriendReference( 

609 references_pb2.WriteFriendReferenceReq( 

610 to_user_id=user2.id, 

611 text="A test reference", 

612 was_appropriate=True, 

613 rating=0.5, 

614 ) 

615 ) 

616 assert res.from_user_id == user1.id 

617 assert res.to_user_id == user2.id 

618 assert res.reference_type == references_pb2.REFERENCE_TYPE_FRIEND 

619 assert res.text == "A test reference" 

620 assert now() - timedelta(hours=24) <= to_aware_datetime(res.written_time) <= now() 

621 assert not res.host_request_id 

622 

623 moderator.approve_reference(res.reference_id) 

624 

625 with references_session(token3) as api: 

626 # check it shows up 

627 res = api.ListReferences( 

628 references_pb2.ListReferencesReq( 

629 from_user_id=user1.id, to_user_id=user2.id, reference_type_filter=[references_pb2.REFERENCE_TYPE_FRIEND] 

630 ) 

631 ) 

632 assert len(res.references) == 1 

633 ref = res.references[0] 

634 assert ref.from_user_id == user1.id 

635 assert ref.to_user_id == user2.id 

636 assert ref.reference_type == references_pb2.REFERENCE_TYPE_FRIEND 

637 assert ref.text == "A test reference" 

638 assert now() - timedelta(hours=24) <= to_aware_datetime(ref.written_time) <= now() 

639 assert not ref.host_request_id 

640 

641 with references_session(token1) as api: 

642 # can't write a second friend reference 

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

644 api.WriteFriendReference( 

645 references_pb2.WriteFriendReferenceReq( 

646 to_user_id=user2.id, 

647 text="A test reference", 

648 was_appropriate=True, 

649 rating=0.5, 

650 ) 

651 ) 

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

653 assert e.value.details() == "Reference already given." 

654 

655 with references_session(token2) as api: 

656 # can't write a reference about yourself 

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

658 api.WriteFriendReference( 

659 references_pb2.WriteFriendReferenceReq( 

660 to_user_id=user2.id, 

661 text="I'm really awesome", 

662 was_appropriate=True, 

663 rating=1.0, 

664 ) 

665 ) 

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

667 assert e.value.details() == "You can't refer yourself." 

668 

669 

670def test_WriteFriendReference_with_empty_text(db): 

671 user1, token1 = generate_user() 

672 user2, token2 = generate_user() 

673 

674 with references_session(token1) as api: 

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

676 api.WriteFriendReference( 

677 references_pb2.WriteFriendReferenceReq(to_user_id=user2.id, text=" ", was_appropriate=True, rating=0.8) 

678 ) 

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

680 assert e.value.details() == "The text of a reference must not be empty" 

681 

682 

683def test_WriteFriendReference_with_private_text( 

684 db, email_collector: EmailCollector, push_collector: PushCollector, moderator 

685): 

686 user1, token1 = generate_user() 

687 user2, token2 = generate_user() 

688 

689 # Make users friends 

690 make_friends(user1, user2) 

691 

692 with references_session(token1) as api: 

693 ref = api.WriteFriendReference( 

694 references_pb2.WriteFriendReferenceReq( 

695 to_user_id=user2.id, 

696 text="They were nice!", 

697 was_appropriate=True, 

698 rating=0.6, 

699 private_text="A bit of an odd ball, but a nice person nonetheless.", 

700 ) 

701 ) 

702 # Approve before patches/jobs exit so the pending notification is delivered while mocked. 

703 moderator.approve_reference(ref.reference_id) 

704 

705 # make sure an email was sent to the user receiving the ref as well as the mods 

706 email_collector.pop_for_reports(last=True) 

707 email = email_collector.pop_for_recipient(user2.email, last=True) 

708 assert email.subject == f"[TEST] You've received a friend reference from {user1.name}!" 

709 assert email.recipient == user2.email 

710 

711 push = push_collector.pop_for_user(user2.id, last=True) 

712 assert push.content.title == f"New friend reference from {user1.name}" 

713 assert push.content.body == "They were nice!" 

714 

715 

716def test_WriteFriendReference_requires_friendship(db): 

717 """Test that users must be friends to write friend references""" 

718 user1, token1 = generate_user() 

719 user2, token2 = generate_user() 

720 

721 # Try to write friend reference without being friends 

722 with references_session(token1) as api: 

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

724 api.WriteFriendReference( 

725 references_pb2.WriteFriendReferenceReq( 

726 to_user_id=user2.id, 

727 text="A test reference", 

728 was_appropriate=True, 

729 rating=0.5, 

730 ) 

731 ) 

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

733 assert e.value.details() == "You can only write friend references for confirmed friends." 

734 

735 # Now make them friends 

736 make_friends(user1, user2) 

737 

738 # Should now be able to write a reference 

739 with references_session(token1) as api: 

740 res = api.WriteFriendReference( 

741 references_pb2.WriteFriendReferenceReq( 

742 to_user_id=user2.id, 

743 text="A test reference", 

744 was_appropriate=True, 

745 rating=0.5, 

746 ) 

747 ) 

748 assert res.from_user_id == user1.id 

749 assert res.to_user_id == user2.id 

750 

751 # Test the unfriending scenario: delete the friendship 

752 with session_scope() as session: 

753 # Change the friendship status to cancelled (simulating unfriending) 

754 session.execute( 

755 update(FriendRelationship) 

756 .where(FriendRelationship.from_user_id == user1.id, FriendRelationship.to_user_id == user2.id) 

757 .values(status=FriendStatus.cancelled) 

758 ) 

759 

760 # Try to write another friend reference after unfriending 

761 # (Note: This assumes user1 didn't already write a reference, or we test with user2 writing to user1) 

762 with references_session(token2) as api: 

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

764 api.WriteFriendReference( 

765 references_pb2.WriteFriendReferenceReq( 

766 to_user_id=user1.id, 

767 text="Another test reference", 

768 was_appropriate=True, 

769 rating=0.8, 

770 ) 

771 ) 

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

773 assert e.value.details() == "You can only write friend references for confirmed friends." 

774 

775 

776def test_host_request_states_references(db, moderator): 

777 user1, token1 = generate_user() 

778 user2, token2 = generate_user() 

779 

780 with session_scope() as session: 

781 # can't write ref 

782 hr1 = create_host_request(session, user2.id, user1.id, timedelta(days=10), status=HostRequestStatus.pending) 

783 # can write ref 

784 hr2 = create_host_request(session, user2.id, user1.id, timedelta(days=10), status=HostRequestStatus.accepted) 

785 # can't write ref 

786 hr3 = create_host_request(session, user2.id, user1.id, timedelta(days=10), status=HostRequestStatus.rejected) 

787 # can write ref 

788 hr4 = create_host_request(session, user2.id, user1.id, timedelta(days=10), status=HostRequestStatus.confirmed) 

789 # can't write ref 

790 hr5 = create_host_request(session, user2.id, user1.id, timedelta(days=10), status=HostRequestStatus.cancelled) 

791 

792 # Approve host requests so both participants can see them 

793 moderator.approve_host_request(hr1) 

794 moderator.approve_host_request(hr2) 

795 moderator.approve_host_request(hr3) 

796 moderator.approve_host_request(hr4) 

797 moderator.approve_host_request(hr5) 

798 

799 with references_session(token1) as api: 

800 # pending 

801 api.WriteHostRequestReference( 

802 references_pb2.WriteHostRequestReferenceReq( 

803 host_request_id=hr2, 

804 text="Should work!", 

805 was_appropriate=True, 

806 rating=0.9, 

807 ) 

808 ) 

809 

810 # accepted 

811 api.WriteHostRequestReference( 

812 references_pb2.WriteHostRequestReferenceReq( 

813 host_request_id=hr4, 

814 text="Should work!", 

815 was_appropriate=True, 

816 rating=0.9, 

817 ) 

818 ) 

819 

820 # rejected 

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

822 api.WriteHostRequestReference( 

823 references_pb2.WriteHostRequestReferenceReq( 

824 host_request_id=hr1, 

825 text="Shouldn't work...", 

826 was_appropriate=True, 

827 rating=0.9, 

828 ) 

829 ) 

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

831 assert e.value.details() == "You can't write a reference for that host request, or it wasn't found." 

832 

833 # confirmed 

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

835 api.WriteHostRequestReference( 

836 references_pb2.WriteHostRequestReferenceReq( 

837 host_request_id=hr3, 

838 text="Shouldn't work...", 

839 was_appropriate=True, 

840 rating=0.9, 

841 ) 

842 ) 

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

844 assert e.value.details() == "You can't write a reference for that host request, or it wasn't found." 

845 

846 # cancelled 

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

848 api.WriteHostRequestReference( 

849 references_pb2.WriteHostRequestReferenceReq( 

850 host_request_id=hr5, 

851 text="Shouldn't work...", 

852 was_appropriate=True, 

853 rating=0.9, 

854 ) 

855 ) 

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

857 assert e.value.details() == "You can't write a reference for that host request, or it wasn't found." 

858 

859 

860def test_public_trip_offer_reference_roles(db, moderator): 

861 """An offer reverses initiator/recipient, so the reference types have to come from the stay role.""" 

862 surfer, surfer_token = generate_user() 

863 host, host_token = generate_user() 

864 

865 with session_scope() as session: 

866 host_request_id = create_host_request(session, surfer.id, host.id, timedelta(days=7), is_offer=True) 

867 

868 moderator.approve_host_request(host_request_id) 

869 # ListPendingReferencesToWrite reads the other user out of the LiteUser matview 

870 refresh_materialized_views_rapid(empty_pb2.Empty()) 

871 

872 with references_session(surfer_token) as api: 

873 available = api.AvailableWriteReferences( 

874 references_pb2.AvailableWriteReferencesReq(to_user_id=host.id) 

875 ).available_write_references 

876 assert [r.reference_type for r in available] == [references_pb2.REFERENCE_TYPE_SURFED] 

877 pending = api.ListPendingReferencesToWrite(empty_pb2.Empty()).pending_references 

878 assert [r.reference_type for r in pending] == [references_pb2.REFERENCE_TYPE_SURFED] 

879 

880 api.WriteHostRequestReference( 

881 references_pb2.WriteHostRequestReferenceReq( 

882 host_request_id=host_request_id, 

883 text="Thanks for having me!", 

884 was_appropriate=True, 

885 rating=0.9, 

886 ) 

887 ) 

888 

889 with references_session(host_token) as api: 

890 available = api.AvailableWriteReferences( 

891 references_pb2.AvailableWriteReferencesReq(to_user_id=surfer.id) 

892 ).available_write_references 

893 assert [r.reference_type for r in available] == [references_pb2.REFERENCE_TYPE_HOSTED] 

894 pending = api.ListPendingReferencesToWrite(empty_pb2.Empty()).pending_references 

895 assert [r.reference_type for r in pending] == [references_pb2.REFERENCE_TYPE_HOSTED] 

896 

897 api.WriteHostRequestReference( 

898 references_pb2.WriteHostRequestReferenceReq( 

899 host_request_id=host_request_id, 

900 text="Great guest!", 

901 was_appropriate=True, 

902 rating=0.9, 

903 ) 

904 ) 

905 

906 with session_scope() as session: 

907 references = { 

908 reference.from_user_id: reference 

909 for reference in session.execute( 

910 select(Reference).where(Reference.host_request_id == host_request_id) 

911 ).scalars() 

912 } 

913 assert references[surfer.id].reference_type == ReferenceType.surfed 

914 assert references[surfer.id].to_user_id == host.id 

915 assert references[host.id].reference_type == ReferenceType.hosted 

916 assert references[host.id].to_user_id == surfer.id 

917 

918 

919def test_WriteHostRequestReference(db, moderator): 

920 user1, token1 = generate_user() 

921 user2, token2 = generate_user() 

922 user3, token3 = generate_user() 

923 user4, token4 = generate_user() 

924 

925 with session_scope() as session: 

926 # too old 

927 hr1 = create_host_request(session, user3.id, user1.id, timedelta(days=20)) 

928 # valid host req, surfer said we didn't show up but we can still write a req 

929 hr2 = create_host_request(session, user3.id, user1.id, timedelta(days=10), surfer_reason_didnt_meetup="No show") 

930 # valid surfing req 

931 hr3 = create_host_request(session, user1.id, user3.id, timedelta(days=7)) 

932 # not yet complete 

933 hr4 = create_host_request(session, user2.id, user1.id, timedelta(days=1), status=HostRequestStatus.pending) 

934 # we indicated we didn't meet 

935 hr5 = create_host_request(session, user4.id, user1.id, timedelta(days=7), host_reason_didnt_meetup="") 

936 # we will indicate we didn't meet 

937 hr6 = create_host_request(session, user4.id, user1.id, timedelta(days=8)) 

938 

939 # Approve host requests so both participants can see them 

940 moderator.approve_host_request(hr1) 

941 moderator.approve_host_request(hr2) 

942 moderator.approve_host_request(hr3) 

943 moderator.approve_host_request(hr4) 

944 moderator.approve_host_request(hr5) 

945 moderator.approve_host_request(hr6) 

946 

947 with references_session(token3) as api: 

948 # can write for this one 

949 api.WriteHostRequestReference( 

950 references_pb2.WriteHostRequestReferenceReq( 

951 host_request_id=hr3, 

952 text="Should work!", 

953 was_appropriate=True, 

954 rating=0.9, 

955 ) 

956 ) 

957 

958 with references_session(token1) as api: 

959 # can't write reference for a HR that's not yet finished 

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

961 api.WriteHostRequestReference( 

962 references_pb2.WriteHostRequestReferenceReq( 

963 host_request_id=hr4, 

964 text="Shouldn't work...", 

965 was_appropriate=True, 

966 rating=0.9, 

967 ) 

968 ) 

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

970 assert e.value.details() == "You can't write a reference for that host request, or it wasn't found." 

971 

972 # can't write reference that's more than 2 weeks old 

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

974 api.WriteHostRequestReference( 

975 references_pb2.WriteHostRequestReferenceReq( 

976 host_request_id=hr1, 

977 text="Shouldn't work...", 

978 was_appropriate=True, 

979 rating=0.9, 

980 ) 

981 ) 

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

983 assert e.value.details() == "You can't write a reference for that host request, or it wasn't found." 

984 

985 # can write for this one 

986 api.WriteHostRequestReference( 

987 references_pb2.WriteHostRequestReferenceReq( 

988 host_request_id=hr2, 

989 text="Should work!", 

990 was_appropriate=True, 

991 rating=0.9, 

992 ) 

993 ) 

994 

995 # but can't write a second one for the same one 

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

997 api.WriteHostRequestReference( 

998 references_pb2.WriteHostRequestReferenceReq( 

999 host_request_id=hr2, 

1000 text="Shouldn't work...", 

1001 was_appropriate=True, 

1002 rating=0.9, 

1003 ) 

1004 ) 

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

1006 assert e.value.details() == "Reference already given." 

1007 

1008 # can write for this one too 

1009 api.WriteHostRequestReference( 

1010 references_pb2.WriteHostRequestReferenceReq( 

1011 host_request_id=hr3, 

1012 text="Should work!", 

1013 was_appropriate=True, 

1014 rating=0.9, 

1015 ) 

1016 ) 

1017 

1018 # can't write reference for a HR that we indicated we didn't show up 

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

1020 api.WriteHostRequestReference( 

1021 references_pb2.WriteHostRequestReferenceReq( 

1022 host_request_id=hr5, 

1023 text="Shouldn't work...", 

1024 was_appropriate=True, 

1025 rating=0.9, 

1026 ) 

1027 ) 

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

1029 assert ( 

1030 e.value.details() 

1031 == "You can't write a reference for that host request because you indicated that you didn't meet up." 

1032 ) 

1033 

1034 # can't write reference for a HR that we indicate we didn't show up for 

1035 api.HostRequestIndicateDidntMeetup( 

1036 references_pb2.HostRequestIndicateDidntMeetupReq( 

1037 host_request_id=hr6, 

1038 reason_didnt_meetup="No clue?", 

1039 ) 

1040 ) 

1041 

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

1043 api.WriteHostRequestReference( 

1044 references_pb2.WriteHostRequestReferenceReq( 

1045 host_request_id=hr6, 

1046 text="Shouldn't work...", 

1047 was_appropriate=True, 

1048 rating=0.9, 

1049 ) 

1050 ) 

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

1052 assert ( 

1053 e.value.details() 

1054 == "You can't write a reference for that host request because you indicated that you didn't meet up." 

1055 ) 

1056 

1057 with references_session(token4) as api: 

1058 # they can still write one 

1059 api.WriteHostRequestReference( 

1060 references_pb2.WriteHostRequestReferenceReq( 

1061 host_request_id=hr6, 

1062 text="Should work!", 

1063 was_appropriate=True, 

1064 rating=0.9, 

1065 ) 

1066 ) 

1067 

1068 

1069def test_WriteHostRequestReference_private_text( 

1070 db, email_collector: EmailCollector, push_collector: PushCollector, moderator 

1071): 

1072 user1, token1 = generate_user() 

1073 user2, token2 = generate_user() 

1074 

1075 with session_scope() as session: 

1076 hr = create_host_request(session, user1.id, user2.id, timedelta(days=10)) 

1077 moderator.approve_host_request(hr) 

1078 

1079 with references_session(token1) as api: 

1080 ref = api.WriteHostRequestReference( 

1081 references_pb2.WriteHostRequestReferenceReq( 

1082 host_request_id=hr, 

1083 text="Should work!", 

1084 was_appropriate=True, 

1085 rating=0.9, 

1086 private_text="Something", 

1087 ) 

1088 ) 

1089 # Approve before patches/jobs exit so the pending notification is delivered while mocked. 

1090 moderator.approve_reference(ref.reference_id) 

1091 

1092 # make sure an email was sent to the user receiving the ref as well as the mods 

1093 email_collector.pop_for_reports(last=True) 

1094 email = email_collector.pop_for_recipient(user2.email, last=True) 

1095 assert email.subject == f"[TEST] You've received a reference from {user1.name}!" 

1096 assert email.recipient == user2.email 

1097 

1098 push = push_collector.pop_for_user(user2.id, last=True) 

1099 assert push.content.title == f"New reference from {user1.name}" 

1100 assert push.content.body == f"{user1.name} left you a reference, now it's your turn to write theirs!" 

1101 

1102 

1103def test_GetHostRequestReferenceStatus(db, moderator): 

1104 user1, token1 = generate_user() 

1105 user2, token2 = generate_user() 

1106 

1107 # user1 writes; RPC returns has_given True 

1108 with session_scope() as session: 

1109 hr1 = create_host_request(session, user1.id, user2.id, timedelta(days=7)) 

1110 moderator.approve_host_request(hr1) 

1111 with references_session(token1) as api: 

1112 api.WriteHostRequestReference( 

1113 references_pb2.WriteHostRequestReferenceReq( 

1114 host_request_id=hr1, text="Great stay!", was_appropriate=True, rating=0.9 

1115 ) 

1116 ) 

1117 res = api.GetHostRequestReferenceStatus(references_pb2.GetHostRequestReferenceStatusReq(host_request_id=hr1)) 

1118 assert res.has_given is True 

1119 

1120 # false: no reference written yet 

1121 with session_scope() as session: 

1122 hr2 = create_host_request(session, user1.id, user2.id, timedelta(days=7)) 

1123 moderator.approve_host_request(hr2) 

1124 with references_session(token1) as api: 

1125 res = api.GetHostRequestReferenceStatus(references_pb2.GetHostRequestReferenceStatusReq(host_request_id=hr2)) 

1126 assert res.has_given is False 

1127 

1128 # false: other user wrote a reference 

1129 with session_scope() as session: 

1130 hr3 = create_host_request(session, user1.id, user2.id, timedelta(days=7)) 

1131 moderator.approve_host_request(hr3) 

1132 with references_session(token2) as api: 

1133 api.WriteHostRequestReference( 

1134 references_pb2.WriteHostRequestReferenceReq( 

1135 host_request_id=hr3, text="Lovely guest!", was_appropriate=True, rating=0.95 

1136 ) 

1137 ) 

1138 with references_session(token1) as api: 

1139 res = api.GetHostRequestReferenceStatus(references_pb2.GetHostRequestReferenceStatusReq(host_request_id=hr3)) 

1140 assert res.has_given is False 

1141 

1142 # false: nonexistent host request id 

1143 with references_session(token1) as api: 

1144 res = api.GetHostRequestReferenceStatus(references_pb2.GetHostRequestReferenceStatusReq(host_request_id=999999)) 

1145 assert res.has_given is False 

1146 

1147 # Additional status flags 

1148 with session_scope() as session: 

1149 # expired (too old) 

1150 hr_expired = create_host_request(session, user2.id, user1.id, timedelta(days=20)) 

1151 # current user (host) indicated didn't meet up 

1152 hr_didnt_stay_host = create_host_request( 

1153 session, user2.id, user1.id, timedelta(days=10), host_reason_didnt_meetup="" 

1154 ) 

1155 # other user (surfer) indicated didn't meet up 

1156 hr_other_didnt_stay = create_host_request( 

1157 session, user2.id, user1.id, timedelta(days=10), surfer_reason_didnt_meetup="No show" 

1158 ) 

1159 

1160 moderator.approve_host_request(hr_expired) 

1161 moderator.approve_host_request(hr_didnt_stay_host) 

1162 moderator.approve_host_request(hr_other_didnt_stay) 

1163 

1164 # expired: is_expired true, can_write false, didnt_stay false 

1165 with references_session(token1) as api: 

1166 res = api.GetHostRequestReferenceStatus( 

1167 references_pb2.GetHostRequestReferenceStatusReq(host_request_id=hr_expired) 

1168 ) 

1169 assert res.has_given is False 

1170 assert res.is_expired is True 

1171 assert res.can_write is False 

1172 assert res.didnt_stay is False 

1173 

1174 # current user indicated didn't meet up: didnt_stay true, can_write false, not expired 

1175 with references_session(token1) as api: 

1176 res = api.GetHostRequestReferenceStatus( 

1177 references_pb2.GetHostRequestReferenceStatusReq(host_request_id=hr_didnt_stay_host) 

1178 ) 

1179 assert res.has_given is False 

1180 assert res.is_expired is False 

1181 assert res.didnt_stay is True 

1182 assert res.can_write is False 

1183 

1184 # other party indicated didn't meet up: didnt_stay false, can_write true (within window), not expired 

1185 with references_session(token1) as api: 

1186 res = api.GetHostRequestReferenceStatus( 

1187 references_pb2.GetHostRequestReferenceStatusReq(host_request_id=hr_other_didnt_stay) 

1188 ) 

1189 assert res.has_given is False 

1190 assert res.is_expired is False 

1191 assert res.didnt_stay is False 

1192 assert res.can_write is True 

1193 

1194 

1195def test_AvailableWriteReferences_and_ListPendingReferencesToWrite(db, moderator): 

1196 user1, token1 = generate_user() 

1197 user2, token2 = generate_user() 

1198 user3, token3 = generate_user() 

1199 user4, token4 = generate_user() 

1200 user5, token5 = generate_user(delete_user=True) 

1201 user6, token6 = generate_user() 

1202 user7, token7 = generate_user() 

1203 user8, token8 = generate_user() 

1204 user9, token9 = generate_user() 

1205 user10, token10 = generate_user() 

1206 user11, token11 = generate_user() 

1207 make_user_block(user1, user6) 

1208 make_user_block(user7, user1) 

1209 

1210 with session_scope() as session: 

1211 # too old 

1212 hr1 = create_host_request(session, user3.id, user1.id, timedelta(days=20)) 

1213 

1214 # already wrote friend ref to user3 

1215 create_friend_reference(session, user1.id, user3.id, timedelta(days=15, seconds=70)) 

1216 

1217 # already given 

1218 _, hr2 = create_host_reference(session, user2.id, user1.id, timedelta(days=10, seconds=110), surfing=True) 

1219 create_host_reference(session, user1.id, user2.id, timedelta(days=10, seconds=100), host_request_id=hr2) 

1220 

1221 # valid hosted 

1222 hr3 = create_host_request(session, user3.id, user1.id, timedelta(days=8)) 

1223 

1224 # valid surfed 

1225 hr4 = create_host_request(session, user1.id, user4.id, timedelta(days=5)) 

1226 

1227 # not yet complete 

1228 hr5 = create_host_request(session, user2.id, user1.id, timedelta(days=2), status=HostRequestStatus.pending) 

1229 

1230 # already wrote friend ref to user2 

1231 create_friend_reference(session, user1.id, user2.id, timedelta(days=1)) 

1232 

1233 # user5 deleted, reference won't show up as pending 

1234 hr_user5 = create_host_request(session, user1.id, user5.id, timedelta(days=5)) 

1235 

1236 # user6 blocked, reference won't show up as pending 

1237 hr_user6 = create_host_request(session, user1.id, user6.id, timedelta(days=5)) 

1238 

1239 # user7 blocking, reference won't show up as pending 

1240 hr_user7 = create_host_request(session, user1.id, user7.id, timedelta(days=5)) 

1241 

1242 # hosted but we indicated we didn't meet up, no reason; should not show up 

1243 hr_user8 = create_host_request(session, user8.id, user1.id, timedelta(days=11), host_reason_didnt_meetup="") 

1244 

1245 # surfed but we indicated we didn't meet up, has reason; should not show up 

1246 hr_user9 = create_host_request( 

1247 session, user1.id, user9.id, timedelta(days=10), surfer_reason_didnt_meetup="They never showed up!" 

1248 ) 

1249 

1250 # surfed but they indicated we didn't meet up, no reason; should show up 

1251 hr6 = create_host_request(session, user1.id, user10.id, timedelta(days=4), host_reason_didnt_meetup="") 

1252 

1253 # hosted but they indicated we didn't meet up, has reason; should show up 

1254 hr7 = create_host_request( 

1255 session, user11.id, user1.id, timedelta(days=3), surfer_reason_didnt_meetup="They never showed up!!" 

1256 ) 

1257 

1258 # Approve all host requests so both participants can see them 

1259 moderator.approve_host_request(hr1) 

1260 moderator.approve_host_request(hr2) 

1261 moderator.approve_host_request(hr3) 

1262 moderator.approve_host_request(hr4) 

1263 moderator.approve_host_request(hr5) 

1264 moderator.approve_host_request(hr_user5) 

1265 moderator.approve_host_request(hr_user6) 

1266 moderator.approve_host_request(hr_user7) 

1267 moderator.approve_host_request(hr_user8) 

1268 moderator.approve_host_request(hr_user9) 

1269 moderator.approve_host_request(hr6) 

1270 moderator.approve_host_request(hr7) 

1271 

1272 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1273 

1274 with references_session(token1) as api: 

1275 # can't write reference for invisible user 

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

1277 api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user5.id)) 

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

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

1280 

1281 # can't write reference for blocking user 

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

1283 api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user7.id)) 

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

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

1286 

1287 # can't write reference for blocked user 

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

1289 api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user6.id)) 

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

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

1292 

1293 # can't write anything to myself 

1294 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user1.id)) 

1295 assert not res.can_write_friend_reference 

1296 assert len(res.available_write_references) == 0 

1297 

1298 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user2.id)) 

1299 # can't write friend ref to user2 

1300 assert not res.can_write_friend_reference 

1301 # none we can write for user2 

1302 assert len(res.available_write_references) == 0 

1303 

1304 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user3.id)) 

1305 # can't write friend ref to user3 

1306 assert not res.can_write_friend_reference 

1307 # can write one reference because we hosted user3 

1308 assert len(res.available_write_references) == 1 

1309 w = res.available_write_references[0] 

1310 assert w.host_request_id == hr3 

1311 assert w.reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

1312 assert now() + timedelta(days=6) <= to_aware_datetime(w.time_expires) <= now() + timedelta(days=7) 

1313 

1314 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user4.id)) 

1315 # can write friend ref to user4 

1316 assert res.can_write_friend_reference 

1317 # can write one reference because we surfed with user4 

1318 assert len(res.available_write_references) == 1 

1319 w = res.available_write_references[0] 

1320 assert w.host_request_id == hr4 

1321 assert w.reference_type == references_pb2.REFERENCE_TYPE_SURFED 

1322 assert now() + timedelta(days=9) <= to_aware_datetime(w.time_expires) <= now() + timedelta(days=10) 

1323 

1324 # can't write a req if we indicated we didn't meet up 

1325 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user8.id)) 

1326 assert len(res.available_write_references) == 0 

1327 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user9.id)) 

1328 assert len(res.available_write_references) == 0 

1329 

1330 # can still write ref if the other person indicated we didn't meet up 

1331 # surfed with them 

1332 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user10.id)) 

1333 assert len(res.available_write_references) == 1 

1334 w = res.available_write_references[0] 

1335 assert w.host_request_id == hr6 

1336 assert w.reference_type == references_pb2.REFERENCE_TYPE_SURFED 

1337 assert now() + timedelta(days=10) <= to_aware_datetime(w.time_expires) <= now() + timedelta(days=11) 

1338 # hosted them 

1339 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user11.id)) 

1340 assert len(res.available_write_references) == 1 

1341 w = res.available_write_references[0] 

1342 assert w.host_request_id == hr7 

1343 assert w.reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

1344 assert now() + timedelta(days=11) <= to_aware_datetime(w.time_expires) <= now() + timedelta(days=12) 

1345 

1346 # finally check the general list 

1347 res = api.ListPendingReferencesToWrite(empty_pb2.Empty()) 

1348 assert len(res.pending_references) == 4 

1349 w = res.pending_references[0] 

1350 assert w.host_request_id == hr3 

1351 assert w.reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

1352 assert now() + timedelta(days=6) <= to_aware_datetime(w.time_expires) <= now() + timedelta(days=7) 

1353 w = res.pending_references[1] 

1354 assert w.host_request_id == hr4 

1355 assert w.reference_type == references_pb2.REFERENCE_TYPE_SURFED 

1356 assert now() + timedelta(days=9) <= to_aware_datetime(w.time_expires) <= now() + timedelta(days=10) 

1357 w = res.pending_references[2] 

1358 assert w.host_request_id == hr6 

1359 assert w.reference_type == references_pb2.REFERENCE_TYPE_SURFED 

1360 assert now() + timedelta(days=10) <= to_aware_datetime(w.time_expires) <= now() + timedelta(days=11) 

1361 w = res.pending_references[3] 

1362 assert w.host_request_id == hr7 

1363 assert w.reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

1364 assert now() + timedelta(days=11) <= to_aware_datetime(w.time_expires) <= now() + timedelta(days=12) 

1365 

1366 with account_session(token1) as account: 

1367 reminders = account.GetReminders(empty_pb2.Empty()).reminders 

1368 assert [reminder.WhichOneof("reminder") for reminder in reminders] == [ 

1369 "write_reference_reminder", 

1370 "write_reference_reminder", 

1371 "write_reference_reminder", 

1372 "write_reference_reminder", 

1373 ] 

1374 assert reminders[0].write_reference_reminder.host_request_id == hr3 

1375 assert reminders[0].write_reference_reminder.reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

1376 assert reminders[0].write_reference_reminder.other_user.user_id == user3.id 

1377 assert reminders[1].write_reference_reminder.host_request_id == hr4 

1378 assert reminders[1].write_reference_reminder.reference_type == references_pb2.REFERENCE_TYPE_SURFED 

1379 assert reminders[1].write_reference_reminder.other_user.user_id == user4.id 

1380 assert reminders[2].write_reference_reminder.host_request_id == hr6 

1381 assert reminders[2].write_reference_reminder.reference_type == references_pb2.REFERENCE_TYPE_SURFED 

1382 assert reminders[2].write_reference_reminder.other_user.user_id == user10.id 

1383 assert reminders[3].write_reference_reminder.host_request_id == hr7 

1384 assert reminders[3].write_reference_reminder.reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

1385 assert reminders[3].write_reference_reminder.other_user.user_id == user11.id 

1386 

1387 

1388@pytest.mark.parametrize("hs", ["host", "surfer"]) 

1389def test_regression_disappearing_refs(db, hs, moderator): 

1390 """ 

1391 Roughly the reproduction steps are: 

1392 * Send a host request, then have both host and surfer accept 

1393 * Wait for it to elapse (or hack it with SQL like what you told me to do) 

1394 * On the surfer account, leave a reference 

1395 * Then on the host account, the option to leave a reference is then not available 

1396 """ 

1397 user1, token1 = generate_user() 

1398 user2, token2 = generate_user() 

1399 req_start = (today() + timedelta(days=2)).isoformat() 

1400 req_end = (today() + timedelta(days=3)).isoformat() 

1401 with requests_session(token1) as api: 

1402 res = api.CreateHostRequest( 

1403 requests_pb2.CreateHostRequestReq( 

1404 host_user_id=user2.id, from_date=req_start, to_date=req_end, text=valid_request_text() 

1405 ) 

1406 ) 

1407 host_request_id = res.host_request_id 

1408 

1409 moderator.approve_host_request(host_request_id) 

1410 

1411 assert ( 

1412 api.ListHostRequests(requests_pb2.ListHostRequestsReq(only_sent=True)) 

1413 .host_requests[0] 

1414 .latest_message.text.text 

1415 == valid_request_text() 

1416 ) 

1417 

1418 with requests_session(token2) as api: 

1419 api.RespondHostRequest( 

1420 requests_pb2.RespondHostRequestReq( 

1421 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1422 ) 

1423 ) 

1424 

1425 with requests_session(token1) as api: 

1426 api.RespondHostRequest( 

1427 requests_pb2.RespondHostRequestReq( 

1428 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1429 ) 

1430 ) 

1431 

1432 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1433 

1434 with references_session(token1) as api: 

1435 res = api.ListPendingReferencesToWrite(empty_pb2.Empty()) 

1436 assert len(res.pending_references) == 0 

1437 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user2.id)) 

1438 assert len(res.available_write_references) == 0 

1439 

1440 with references_session(token2) as api: 

1441 res = api.ListPendingReferencesToWrite(empty_pb2.Empty()) 

1442 assert len(res.pending_references) == 0 

1443 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user1.id)) 

1444 assert len(res.available_write_references) == 0 

1445 

1446 # hack the time backwards 

1447 hack_req_start = today() - timedelta(days=10) + timedelta(days=2) 

1448 hack_req_end = today() - timedelta(days=10) + timedelta(days=3) 

1449 with session_scope() as session: 

1450 host_request = session.execute(select(HostRequest)).scalar_one() 

1451 assert host_request.conversation_id == host_request_id 

1452 host_request.from_date = hack_req_start 

1453 host_request.to_date = hack_req_end 

1454 

1455 with references_session(token1) as api: 

1456 res = api.ListPendingReferencesToWrite(empty_pb2.Empty()) 

1457 assert len(res.pending_references) == 1 

1458 assert res.pending_references[0].host_request_id == host_request_id 

1459 assert res.pending_references[0].reference_type == references_pb2.REFERENCE_TYPE_SURFED 

1460 

1461 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user2.id)) 

1462 assert len(res.available_write_references) == 1 

1463 assert res.available_write_references[0].host_request_id == host_request_id 

1464 assert res.available_write_references[0].reference_type == references_pb2.REFERENCE_TYPE_SURFED 

1465 

1466 with references_session(token2) as api: 

1467 res = api.ListPendingReferencesToWrite(empty_pb2.Empty()) 

1468 assert len(res.pending_references) == 1 

1469 assert res.pending_references[0].host_request_id == host_request_id 

1470 assert res.pending_references[0].reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

1471 

1472 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user1.id)) 

1473 assert len(res.available_write_references) == 1 

1474 assert res.available_write_references[0].host_request_id == host_request_id 

1475 assert res.available_write_references[0].reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

1476 

1477 if hs == "host": 

1478 with references_session(token2) as api: 

1479 api.WriteHostRequestReference( 

1480 references_pb2.WriteHostRequestReferenceReq( 

1481 host_request_id=host_request_id, 

1482 text="Good stuff", 

1483 was_appropriate=True, 

1484 rating=0.86, 

1485 ) 

1486 ) 

1487 

1488 with references_session(token2) as api: 

1489 res = api.ListPendingReferencesToWrite(empty_pb2.Empty()) 

1490 assert len(res.pending_references) == 0 

1491 

1492 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user2.id)) 

1493 assert len(res.available_write_references) == 0 

1494 

1495 with references_session(token1) as api: 

1496 res = api.ListPendingReferencesToWrite(empty_pb2.Empty()) 

1497 assert len(res.pending_references) == 1 

1498 assert res.pending_references[0].host_request_id == host_request_id 

1499 assert res.pending_references[0].reference_type == references_pb2.REFERENCE_TYPE_SURFED 

1500 

1501 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user2.id)) 

1502 assert len(res.available_write_references) == 1 

1503 assert res.available_write_references[0].host_request_id == host_request_id 

1504 assert res.available_write_references[0].reference_type == references_pb2.REFERENCE_TYPE_SURFED 

1505 else: 

1506 with references_session(token1) as api: 

1507 api.WriteHostRequestReference( 

1508 references_pb2.WriteHostRequestReferenceReq( 

1509 host_request_id=host_request_id, 

1510 text="Good stuff", 

1511 was_appropriate=True, 

1512 rating=0.86, 

1513 ) 

1514 ) 

1515 

1516 with references_session(token1) as api: 

1517 res = api.ListPendingReferencesToWrite(empty_pb2.Empty()) 

1518 assert len(res.pending_references) == 0 

1519 

1520 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user1.id)) 

1521 assert len(res.available_write_references) == 0 

1522 

1523 with references_session(token2) as api: 

1524 res = api.ListPendingReferencesToWrite(empty_pb2.Empty()) 

1525 assert len(res.pending_references) == 1 

1526 assert res.pending_references[0].host_request_id == host_request_id 

1527 assert res.pending_references[0].reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

1528 

1529 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user1.id)) 

1530 assert len(res.available_write_references) == 1 

1531 assert res.available_write_references[0].host_request_id == host_request_id 

1532 assert res.available_write_references[0].reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

1533 

1534 

1535def test_WriteFriendReference_creates_shadowed_moderation_state(db): 

1536 """New friend references start out shadowed and are not visible to non-authors.""" 

1537 user1, token1 = generate_user() 

1538 user2, token2 = generate_user() 

1539 user3, token3 = generate_user() 

1540 make_friends(user1, user2) 

1541 

1542 with references_session(token1) as api: 

1543 ref = api.WriteFriendReference( 

1544 references_pb2.WriteFriendReferenceReq( 

1545 to_user_id=user2.id, text="Nice friend", was_appropriate=True, rating=0.9 

1546 ) 

1547 ) 

1548 

1549 with session_scope() as session: 

1550 reference = session.execute(select(Reference).where(Reference.id == ref.reference_id)).scalar_one() 

1551 assert reference.moderation_state.visibility == ModerationVisibility.shadowed 

1552 assert reference.moderation_state.object_type == ModerationObjectType.reference 

1553 assert reference.moderation_state.object_id == reference.id 

1554 

1555 # Author can see their own shadowed reference. 

1556 with references_session(token1) as api: 

1557 res = api.ListReferences(references_pb2.ListReferencesReq(from_user_id=user1.id)) 

1558 assert [r.reference_id for r in res.references] == [ref.reference_id] 

1559 

1560 # Non-author user cannot see it while shadowed. 

1561 with references_session(token3) as api: 

1562 res = api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user2.id)) 

1563 assert [r.reference_id for r in res.references] == [] 

1564 

1565 

1566def test_reference_hidden_via_ums_disappears_from_listings(db, moderator): 

1567 """Hiding a reference through UMS removes it from listings, including for the author.""" 

1568 user1, token1 = generate_user() 

1569 user2, token2 = generate_user() 

1570 user3, token3 = generate_user() 

1571 make_friends(user1, user2) 

1572 

1573 with references_session(token1) as api: 

1574 ref = api.WriteFriendReference( 

1575 references_pb2.WriteFriendReferenceReq( 

1576 to_user_id=user2.id, text="Visible for now", was_appropriate=True, rating=0.9 

1577 ) 

1578 ) 

1579 moderator.approve_reference(ref.reference_id) 

1580 

1581 with references_session(token3) as api: 

1582 assert api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user2.id)).references 

1583 

1584 # Hide the reference via the moderation API. 

1585 with real_moderation_session(moderator.token) as mod_api: 

1586 state_res = mod_api.GetModerationState( 

1587 moderation_pb2.GetModerationStateReq( 

1588 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_REFERENCE, 

1589 object_id=ref.reference_id, 

1590 ) 

1591 ) 

1592 mod_api.ModerateContent( 

1593 moderation_pb2.ModerateContentReq( 

1594 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1595 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1596 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

1597 reason="test", 

1598 ) 

1599 ) 

1600 

1601 # Hidden references are invisible to the author and to other users. 

1602 with references_session(token1) as api: 

1603 assert not api.ListReferences(references_pb2.ListReferencesReq(from_user_id=user1.id)).references 

1604 with references_session(token3) as api: 

1605 assert not api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user2.id)).references