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

747 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-19 15:47 +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 User, 

27) 

28from couchers.models.public_trips import PublicTrip, PublicTripStatus 

29from couchers.moderation.utils import create_moderation 

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

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

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

33from tests.fixtures.misc import EmailCollector, PushCollector 

34from tests.fixtures.sessions import ( 

35 account_session, 

36 api_session, 

37 real_moderation_session, 

38 references_session, 

39 requests_session, 

40) 

41from tests.test_requests import valid_request_text 

42 

43 

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

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

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

47 node = Node( 

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

49 node_type=NodeType.locality, 

50 ) 

51 session.add(node) 

52 session.flush() 

53 moderation_state = ModerationState( 

54 object_type=ModerationObjectType.public_trip, 

55 object_id=0, 

56 visibility=ModerationVisibility.visible, 

57 ) 

58 session.add(moderation_state) 

59 session.flush() 

60 trip = PublicTrip( 

61 user_id=user_id, 

62 node_id=node.id, 

63 from_date=from_date, 

64 to_date=to_date, 

65 description="Looking for a host!", 

66 status=PublicTripStatus.searching_for_host, 

67 moderation_state_id=moderation_state.id, 

68 ) 

69 session.add(trip) 

70 session.flush() 

71 moderation_state.object_id = trip.id 

72 return trip.id 

73 

74 

75def create_host_request( 

76 session: Session, 

77 surfer_user_id: int, 

78 host_user_id: int, 

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

80 status: HostRequestStatus = HostRequestStatus.confirmed, 

81 host_reason_didnt_meetup: str | None = None, 

82 surfer_reason_didnt_meetup: str | None = None, 

83 is_offer: bool = False, 

84) -> int: 

85 """ 

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

87 

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

89 """ 

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

91 to_date = today() - host_request_age 

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

93 

94 if is_offer: 

95 initiator_user_id, recipient_user_id = host_user_id, surfer_user_id 

96 initiator_reason, recipient_reason = host_reason_didnt_meetup, surfer_reason_didnt_meetup 

97 else: 

98 initiator_user_id, recipient_user_id = surfer_user_id, host_user_id 

99 initiator_reason, recipient_reason = surfer_reason_didnt_meetup, host_reason_didnt_meetup 

100 

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

102 

103 conversation = Conversation() 

104 session.add(conversation) 

105 session.flush() 

106 

107 msg1 = Message( 

108 conversation_id=conversation.id, 

109 author_id=initiator_user_id, 

110 message_type=MessageType.chat_created, 

111 ) 

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

113 session.add(msg1) 

114 

115 msg2 = Message( 

116 conversation_id=conversation.id, 

117 author_id=initiator_user_id, 

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

119 message_type=MessageType.text, 

120 ) 

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

122 session.add(msg2) 

123 session.flush() 

124 

125 moderation_state = create_moderation( 

126 session, 

127 ModerationObjectType.host_request, 

128 conversation.id, 

129 initiator_user_id, 

130 ) 

131 

132 host_request = HostRequest( 

133 conversation_id=conversation.id, 

134 initiator_user_id=initiator_user_id, 

135 recipient_user_id=recipient_user_id, 

136 from_date=from_date, 

137 to_date=to_date, 

138 status=status, 

139 initiator_last_seen_message_id=msg2.id, 

140 recipient_reason_didnt_meetup=recipient_reason, 

141 initiator_reason_didnt_meetup=initiator_reason, 

142 hosting_city="Test City", 

143 hosting_location=create_coordinate(0, 0), 

144 hosting_radius=10, 

145 moderation_state_id=moderation_state.id, 

146 public_trip_id=public_trip_id, 

147 ) 

148 session.add(host_request) 

149 session.commit() 

150 return host_request.conversation_id 

151 

152 

153def create_host_request_by_date( 

154 session: Session, 

155 surfer_user_id: int, 

156 host_user_id: int, 

157 from_date: date, 

158 to_date: date, 

159 status: HostRequestStatus, 

160 host_sent_request_reminders: int, 

161 last_sent_request_reminder_time: datetime, 

162) -> int: 

163 conversation = Conversation() 

164 session.add(conversation) 

165 session.flush() 

166 

167 msg1 = Message( 

168 conversation_id=conversation.id, 

169 author_id=surfer_user_id, 

170 message_type=MessageType.chat_created, 

171 ) 

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

173 session.add(msg1) 

174 

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

176 msg2 = Message( 

177 conversation_id=conversation.id, 

178 author_id=surfer_user_id, 

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

180 message_type=MessageType.text, 

181 ) 

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

183 session.add(msg2) 

184 session.flush() 

185 

186 moderation_state = create_moderation( 

187 session, 

188 ModerationObjectType.host_request, 

189 conversation.id, 

190 surfer_user_id, 

191 ) 

192 

193 host_request = HostRequest( 

194 conversation_id=conversation.id, 

195 initiator_user_id=surfer_user_id, 

196 recipient_user_id=host_user_id, 

197 from_date=from_date, 

198 to_date=to_date, 

199 status=status, 

200 hosting_city="Test City", 

201 hosting_location=create_coordinate(0, 0), 

202 hosting_radius=10, 

203 moderation_state_id=moderation_state.id, 

204 ) 

205 host_request.recipient_sent_request_reminders = host_sent_request_reminders 

206 host_request.last_sent_request_reminder_time = last_sent_request_reminder_time 

207 

208 session.add(host_request) 

209 session.commit() 

210 return host_request.conversation_id 

211 

212 

213def create_host_reference( 

214 session: Session, 

215 from_user_id: int, 

216 to_user_id: int, 

217 reference_age: timedelta, 

218 *, 

219 surfing: bool = True, 

220 host_request_id: int | None = None, 

221) -> tuple[int, int]: 

222 if host_request_id: 

223 actual_host_request_id = host_request_id 

224 else: 

225 if surfing: 

226 actual_host_request_id = host_request_id or create_host_request( 

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

228 ) 

229 else: 

230 actual_host_request_id = host_request_id or create_host_request( 

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

232 ) 

233 

234 host_request = session.execute( 

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

236 ).scalar_one() 

237 

238 if host_request.initiator_user_id == from_user_id: 

239 reference_type = ReferenceType.surfed 

240 to_user_id = host_request.recipient_user_id 

241 assert from_user_id == host_request.initiator_user_id 

242 else: 

243 reference_type = ReferenceType.hosted 

244 to_user_id = host_request.initiator_user_id 

245 assert from_user_id == host_request.recipient_user_id 

246 

247 moderation_state = ModerationState( 

248 object_type=ModerationObjectType.reference, 

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

250 visibility=ModerationVisibility.visible, 

251 ) 

252 session.add(moderation_state) 

253 session.flush() 

254 

255 reference = Reference( 

256 from_user_id=from_user_id, 

257 to_user_id=to_user_id, 

258 host_request_id=host_request.conversation_id, 

259 text="Dummy reference", 

260 rating=0.5, 

261 was_appropriate=True, 

262 reference_type=reference_type, 

263 moderation_state_id=moderation_state.id, 

264 ) 

265 reference.time = now() - reference_age 

266 

267 session.add(reference) 

268 session.flush() 

269 moderation_state.object_id = reference.id 

270 session.commit() 

271 return reference.id, actual_host_request_id 

272 

273 

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

275 moderation_state = ModerationState( 

276 object_type=ModerationObjectType.reference, 

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

278 visibility=ModerationVisibility.visible, 

279 ) 

280 session.add(moderation_state) 

281 session.flush() 

282 

283 reference = Reference( 

284 from_user_id=from_user_id, 

285 to_user_id=to_user_id, 

286 reference_type=ReferenceType.friend, 

287 text="Test friend request", 

288 rating=0.4, 

289 was_appropriate=True, 

290 moderation_state_id=moderation_state.id, 

291 ) 

292 reference.time = now() - reference_age 

293 session.add(reference) 

294 session.flush() 

295 moderation_state.object_id = reference.id 

296 session.commit() 

297 return reference.id 

298 

299 

300def test_ListPagination(db): 

301 user1, token1 = generate_user() 

302 user2, token2 = generate_user() 

303 user3, token3 = generate_user() 

304 user4, token4 = generate_user() 

305 user5, token5 = generate_user() 

306 user6, token6 = generate_user() 

307 user7, token7 = generate_user() 

308 user8, token8 = generate_user() 

309 user9, token9 = generate_user() 

310 

311 with session_scope() as session: 

312 # bidirectional references 

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

314 ref2b, _ = create_host_reference( 

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

316 ) 

317 

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

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

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

321 

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

323 ref5b, _ = create_host_reference( 

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

325 ) 

326 

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

328 

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

330 

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

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

333 

334 # should be visible even under 2 weeks 

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

336 

337 # hidden because it's less than 2 weeks 

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

339 

340 # visible because both were written 

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

342 ref8c, _ = create_host_reference( 

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

344 ) 

345 

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

347 

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

349 

350 with references_session(token2) as api: 

351 # written by user1 

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

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

354 

355 res = api.ListReferences( 

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

357 ) 

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

359 

360 res = api.ListReferences( 

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

362 ) 

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

364 assert not res.next_page_token 

365 

366 # received by user1 

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

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

369 

370 res = api.ListReferences( 

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

372 ) 

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

374 assert not res.next_page_token 

375 

376 # same thing but with filters 

377 res = api.ListReferences( 

378 references_pb2.ListReferencesReq( 

379 to_user_id=user1.id, 

380 reference_type_filter=[ 

381 references_pb2.REFERENCE_TYPE_HOSTED, 

382 references_pb2.REFERENCE_TYPE_SURFED, 

383 references_pb2.REFERENCE_TYPE_FRIEND, 

384 ], 

385 page_size=5, 

386 ) 

387 ) 

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

389 

390 res = api.ListReferences( 

391 references_pb2.ListReferencesReq( 

392 to_user_id=user1.id, 

393 reference_type_filter=[ 

394 references_pb2.REFERENCE_TYPE_HOSTED, 

395 references_pb2.REFERENCE_TYPE_SURFED, 

396 references_pb2.REFERENCE_TYPE_FRIEND, 

397 ], 

398 page_token=res.next_page_token, 

399 page_size=5, 

400 ) 

401 ) 

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

403 assert not res.next_page_token 

404 

405 # received hosting references 

406 res = api.ListReferences( 

407 references_pb2.ListReferencesReq( 

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

409 ) 

410 ) 

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

412 

413 res = api.ListReferences( 

414 references_pb2.ListReferencesReq( 

415 to_user_id=user1.id, 

416 reference_type_filter=[references_pb2.REFERENCE_TYPE_HOSTED], 

417 page_token=res.next_page_token, 

418 page_size=3, 

419 ) 

420 ) 

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

422 assert not res.next_page_token 

423 

424 # written friend references 

425 res = api.ListReferences( 

426 references_pb2.ListReferencesReq( 

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

428 ) 

429 ) 

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

431 assert not res.next_page_token 

432 

433 # written surfing references 

434 res = api.ListReferences( 

435 references_pb2.ListReferencesReq( 

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

437 ) 

438 ) 

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

440 assert not res.next_page_token 

441 

442 with references_session(token7) as api: 

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

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

445 api.ListReferences( 

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

447 ) 

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

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

450 

451 with references_session(token5) as api: 

452 # from user1 to user2 

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

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

455 assert not res.next_page_token 

456 

457 # from user5 to user1 

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

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

460 assert not res.next_page_token 

461 

462 with api_session(token2) as api: 

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

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

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

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

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

468 # user6 wrote references but received none 

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

470 

471 

472def test_num_references_matches_visible_list(db): 

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

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

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

476 user1, token1 = generate_user() 

477 user2, token2 = generate_user() 

478 

479 with session_scope() as session: 

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

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

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

483 

484 with references_session(token2) as api: 

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

486 assert len(res.references) == 0 

487 

488 with api_session(token2) as api: 

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

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

491 assert res.num_references == 0 

492 

493 with session_scope() as session: 

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

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

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

497 

498 with references_session(token2) as api: 

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

500 assert len(res.references) == 1 

501 

502 with api_session(token2) as api: 

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

504 assert res.num_references == 1 

505 

506 

507def test_ListReference_banned_deleted_users(db): 

508 user1, token1 = generate_user() 

509 user2, token2 = generate_user() 

510 user3, token3 = generate_user() 

511 

512 with session_scope() as session: 

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

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

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

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

517 

518 with references_session(token1) as api: 

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

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

521 assert len(refs_rec) == 2 

522 assert len(refs_sent) == 2 

523 

524 with api_session(token1) as api: 

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

526 

527 # ban user2 

528 with session_scope() as session: 

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

530 

531 # reference to and from banned user is hidden 

532 with references_session(token1) as api: 

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

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

535 assert len(refs_rec) == 1 

536 assert len(refs_sent) == 1 

537 

538 with api_session(token1) as api: 

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

540 

541 # delete user3 

542 with session_scope() as session: 

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

544 

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

546 with references_session(token1) as api: 

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

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

549 assert len(refs_rec) == 1 

550 assert len(refs_sent) == 1 

551 

552 with api_session(token1) as api: 

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

554 

555 

556def test_ListReference_blocked_shadowed_users(db): 

557 user1, token1 = generate_user() 

558 user2, token2 = generate_user() 

559 user3, token3 = generate_user() 

560 

561 with session_scope() as session: 

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

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

564 

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

566 make_user_block(user1, user2) 

567 

568 with references_session(token1) as api: 

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

570 assert len(refs_rec) == 2 

571 

572 with api_session(token1) as api: 

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

574 

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

576 with session_scope() as session: 

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

578 

579 with references_session(token1) as api: 

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

581 assert len(refs_rec) == 1 

582 

583 with api_session(token1) as api: 

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

585 

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

587 with references_session(token3) as api: 

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

589 assert len(refs_rec) == 2 

590 

591 with api_session(token3) as api: 

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

593 

594 

595def test_WriteFriendReference(db, moderator): 

596 user1, token1 = generate_user() 

597 user2, token2 = generate_user() 

598 user3, token3 = generate_user() 

599 

600 # Make user1 and user2 friends 

601 make_friends(user1, user2) 

602 

603 with references_session(token1) as api: 

604 # can write normal friend reference 

605 res = api.WriteFriendReference( 

606 references_pb2.WriteFriendReferenceReq( 

607 to_user_id=user2.id, 

608 text="A test reference", 

609 was_appropriate=True, 

610 rating=0.5, 

611 ) 

612 ) 

613 assert res.from_user_id == user1.id 

614 assert res.to_user_id == user2.id 

615 assert res.reference_type == references_pb2.REFERENCE_TYPE_FRIEND 

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

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

618 assert not res.host_request_id 

619 

620 moderator.approve_reference(res.reference_id) 

621 

622 with references_session(token3) as api: 

623 # check it shows up 

624 res = api.ListReferences( 

625 references_pb2.ListReferencesReq( 

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

627 ) 

628 ) 

629 assert len(res.references) == 1 

630 ref = res.references[0] 

631 assert ref.from_user_id == user1.id 

632 assert ref.to_user_id == user2.id 

633 assert ref.reference_type == references_pb2.REFERENCE_TYPE_FRIEND 

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

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

636 assert not ref.host_request_id 

637 

638 with references_session(token1) as api: 

639 # can't write a second friend reference 

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

641 api.WriteFriendReference( 

642 references_pb2.WriteFriendReferenceReq( 

643 to_user_id=user2.id, 

644 text="A test reference", 

645 was_appropriate=True, 

646 rating=0.5, 

647 ) 

648 ) 

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

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

651 

652 with references_session(token2) as api: 

653 # can't write a reference about yourself 

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

655 api.WriteFriendReference( 

656 references_pb2.WriteFriendReferenceReq( 

657 to_user_id=user2.id, 

658 text="I'm really awesome", 

659 was_appropriate=True, 

660 rating=1.0, 

661 ) 

662 ) 

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

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

665 

666 

667def test_WriteFriendReference_with_empty_text(db): 

668 user1, token1 = generate_user() 

669 user2, token2 = generate_user() 

670 

671 with references_session(token1) as api: 

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

673 api.WriteFriendReference( 

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

675 ) 

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

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

678 

679 

680def test_WriteFriendReference_with_private_text( 

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

682): 

683 user1, token1 = generate_user() 

684 user2, token2 = generate_user() 

685 

686 # Make users friends 

687 make_friends(user1, user2) 

688 

689 with references_session(token1) as api: 

690 ref = api.WriteFriendReference( 

691 references_pb2.WriteFriendReferenceReq( 

692 to_user_id=user2.id, 

693 text="They were nice!", 

694 was_appropriate=True, 

695 rating=0.6, 

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

697 ) 

698 ) 

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

700 moderator.approve_reference(ref.reference_id) 

701 

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

703 email_collector.pop_for_reports(last=True) 

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

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

706 assert email.recipient == user2.email 

707 

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

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

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

711 

712 

713def test_WriteFriendReference_requires_friendship(db): 

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

715 user1, token1 = generate_user() 

716 user2, token2 = generate_user() 

717 

718 # Try to write friend reference without being friends 

719 with references_session(token1) as api: 

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

721 api.WriteFriendReference( 

722 references_pb2.WriteFriendReferenceReq( 

723 to_user_id=user2.id, 

724 text="A test reference", 

725 was_appropriate=True, 

726 rating=0.5, 

727 ) 

728 ) 

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

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

731 

732 # Now make them friends 

733 make_friends(user1, user2) 

734 

735 # Should now be able to write a reference 

736 with references_session(token1) as api: 

737 res = api.WriteFriendReference( 

738 references_pb2.WriteFriendReferenceReq( 

739 to_user_id=user2.id, 

740 text="A test reference", 

741 was_appropriate=True, 

742 rating=0.5, 

743 ) 

744 ) 

745 assert res.from_user_id == user1.id 

746 assert res.to_user_id == user2.id 

747 

748 # Test the unfriending scenario: delete the friendship 

749 with session_scope() as session: 

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

751 session.execute( 

752 update(FriendRelationship) 

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

754 .values(status=FriendStatus.cancelled) 

755 ) 

756 

757 # Try to write another friend reference after unfriending 

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

759 with references_session(token2) as api: 

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

761 api.WriteFriendReference( 

762 references_pb2.WriteFriendReferenceReq( 

763 to_user_id=user1.id, 

764 text="Another test reference", 

765 was_appropriate=True, 

766 rating=0.8, 

767 ) 

768 ) 

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

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

771 

772 

773def test_host_request_states_references(db, moderator): 

774 user1, token1 = generate_user() 

775 user2, token2 = generate_user() 

776 

777 with session_scope() as session: 

778 # can't write ref 

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

780 # can write ref 

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

782 # can't write ref 

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

784 # can write ref 

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

786 # can't write ref 

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

788 

789 # Approve host requests so both participants can see them 

790 moderator.approve_host_request(hr1) 

791 moderator.approve_host_request(hr2) 

792 moderator.approve_host_request(hr3) 

793 moderator.approve_host_request(hr4) 

794 moderator.approve_host_request(hr5) 

795 

796 with references_session(token1) as api: 

797 # pending 

798 api.WriteHostRequestReference( 

799 references_pb2.WriteHostRequestReferenceReq( 

800 host_request_id=hr2, 

801 text="Should work!", 

802 was_appropriate=True, 

803 rating=0.9, 

804 ) 

805 ) 

806 

807 # accepted 

808 api.WriteHostRequestReference( 

809 references_pb2.WriteHostRequestReferenceReq( 

810 host_request_id=hr4, 

811 text="Should work!", 

812 was_appropriate=True, 

813 rating=0.9, 

814 ) 

815 ) 

816 

817 # rejected 

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

819 api.WriteHostRequestReference( 

820 references_pb2.WriteHostRequestReferenceReq( 

821 host_request_id=hr1, 

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

823 was_appropriate=True, 

824 rating=0.9, 

825 ) 

826 ) 

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

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

829 

830 # confirmed 

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

832 api.WriteHostRequestReference( 

833 references_pb2.WriteHostRequestReferenceReq( 

834 host_request_id=hr3, 

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

836 was_appropriate=True, 

837 rating=0.9, 

838 ) 

839 ) 

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

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

842 

843 # cancelled 

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

845 api.WriteHostRequestReference( 

846 references_pb2.WriteHostRequestReferenceReq( 

847 host_request_id=hr5, 

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

849 was_appropriate=True, 

850 rating=0.9, 

851 ) 

852 ) 

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

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

855 

856 

857def test_public_trip_offer_reference_roles(db, moderator): 

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

859 surfer, surfer_token = generate_user() 

860 host, host_token = generate_user() 

861 

862 with session_scope() as session: 

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

864 

865 moderator.approve_host_request(host_request_id) 

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

867 refresh_materialized_views_rapid(empty_pb2.Empty()) 

868 

869 with references_session(surfer_token) as api: 

870 available = api.AvailableWriteReferences( 

871 references_pb2.AvailableWriteReferencesReq(to_user_id=host.id) 

872 ).available_write_references 

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

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

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

876 

877 api.WriteHostRequestReference( 

878 references_pb2.WriteHostRequestReferenceReq( 

879 host_request_id=host_request_id, 

880 text="Thanks for having me!", 

881 was_appropriate=True, 

882 rating=0.9, 

883 ) 

884 ) 

885 

886 with references_session(host_token) as api: 

887 available = api.AvailableWriteReferences( 

888 references_pb2.AvailableWriteReferencesReq(to_user_id=surfer.id) 

889 ).available_write_references 

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

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

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

893 

894 api.WriteHostRequestReference( 

895 references_pb2.WriteHostRequestReferenceReq( 

896 host_request_id=host_request_id, 

897 text="Great guest!", 

898 was_appropriate=True, 

899 rating=0.9, 

900 ) 

901 ) 

902 

903 with session_scope() as session: 

904 references = { 

905 reference.from_user_id: reference 

906 for reference in session.execute( 

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

908 ).scalars() 

909 } 

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

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

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

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

914 

915 

916def test_WriteHostRequestReference(db, moderator): 

917 user1, token1 = generate_user() 

918 user2, token2 = generate_user() 

919 user3, token3 = generate_user() 

920 user4, token4 = generate_user() 

921 

922 with session_scope() as session: 

923 # too old 

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

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

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

927 # valid surfing req 

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

929 # not yet complete 

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

931 # we indicated we didn't meet 

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

933 # we will indicate we didn't meet 

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

935 

936 # Approve host requests so both participants can see them 

937 moderator.approve_host_request(hr1) 

938 moderator.approve_host_request(hr2) 

939 moderator.approve_host_request(hr3) 

940 moderator.approve_host_request(hr4) 

941 moderator.approve_host_request(hr5) 

942 moderator.approve_host_request(hr6) 

943 

944 with references_session(token3) as api: 

945 # can write for this one 

946 api.WriteHostRequestReference( 

947 references_pb2.WriteHostRequestReferenceReq( 

948 host_request_id=hr3, 

949 text="Should work!", 

950 was_appropriate=True, 

951 rating=0.9, 

952 ) 

953 ) 

954 

955 with references_session(token1) as api: 

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

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

958 api.WriteHostRequestReference( 

959 references_pb2.WriteHostRequestReferenceReq( 

960 host_request_id=hr4, 

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

962 was_appropriate=True, 

963 rating=0.9, 

964 ) 

965 ) 

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

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

968 

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

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

971 api.WriteHostRequestReference( 

972 references_pb2.WriteHostRequestReferenceReq( 

973 host_request_id=hr1, 

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

975 was_appropriate=True, 

976 rating=0.9, 

977 ) 

978 ) 

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

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

981 

982 # can write for this one 

983 api.WriteHostRequestReference( 

984 references_pb2.WriteHostRequestReferenceReq( 

985 host_request_id=hr2, 

986 text="Should work!", 

987 was_appropriate=True, 

988 rating=0.9, 

989 ) 

990 ) 

991 

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

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

994 api.WriteHostRequestReference( 

995 references_pb2.WriteHostRequestReferenceReq( 

996 host_request_id=hr2, 

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

998 was_appropriate=True, 

999 rating=0.9, 

1000 ) 

1001 ) 

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

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

1004 

1005 # can write for this one too 

1006 api.WriteHostRequestReference( 

1007 references_pb2.WriteHostRequestReferenceReq( 

1008 host_request_id=hr3, 

1009 text="Should work!", 

1010 was_appropriate=True, 

1011 rating=0.9, 

1012 ) 

1013 ) 

1014 

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

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

1017 api.WriteHostRequestReference( 

1018 references_pb2.WriteHostRequestReferenceReq( 

1019 host_request_id=hr5, 

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

1021 was_appropriate=True, 

1022 rating=0.9, 

1023 ) 

1024 ) 

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

1026 assert ( 

1027 e.value.details() 

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

1029 ) 

1030 

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

1032 api.HostRequestIndicateDidntMeetup( 

1033 references_pb2.HostRequestIndicateDidntMeetupReq( 

1034 host_request_id=hr6, 

1035 reason_didnt_meetup="No clue?", 

1036 ) 

1037 ) 

1038 

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

1040 api.WriteHostRequestReference( 

1041 references_pb2.WriteHostRequestReferenceReq( 

1042 host_request_id=hr6, 

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

1044 was_appropriate=True, 

1045 rating=0.9, 

1046 ) 

1047 ) 

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

1049 assert ( 

1050 e.value.details() 

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

1052 ) 

1053 

1054 with references_session(token4) as api: 

1055 # they can still write one 

1056 api.WriteHostRequestReference( 

1057 references_pb2.WriteHostRequestReferenceReq( 

1058 host_request_id=hr6, 

1059 text="Should work!", 

1060 was_appropriate=True, 

1061 rating=0.9, 

1062 ) 

1063 ) 

1064 

1065 

1066def test_WriteHostRequestReference_private_text( 

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

1068): 

1069 user1, token1 = generate_user() 

1070 user2, token2 = generate_user() 

1071 

1072 with session_scope() as session: 

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

1074 moderator.approve_host_request(hr) 

1075 

1076 with references_session(token1) as api: 

1077 ref = api.WriteHostRequestReference( 

1078 references_pb2.WriteHostRequestReferenceReq( 

1079 host_request_id=hr, 

1080 text="Should work!", 

1081 was_appropriate=True, 

1082 rating=0.9, 

1083 private_text="Something", 

1084 ) 

1085 ) 

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

1087 moderator.approve_reference(ref.reference_id) 

1088 

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

1090 email_collector.pop_for_reports(last=True) 

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

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

1093 assert email.recipient == user2.email 

1094 

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

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

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

1098 

1099 

1100def test_GetHostRequestReferenceStatus(db, moderator): 

1101 user1, token1 = generate_user() 

1102 user2, token2 = generate_user() 

1103 

1104 # user1 writes; RPC returns has_given True 

1105 with session_scope() as session: 

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

1107 moderator.approve_host_request(hr1) 

1108 with references_session(token1) as api: 

1109 api.WriteHostRequestReference( 

1110 references_pb2.WriteHostRequestReferenceReq( 

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

1112 ) 

1113 ) 

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

1115 assert res.has_given is True 

1116 

1117 # false: no reference written yet 

1118 with session_scope() as session: 

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

1120 moderator.approve_host_request(hr2) 

1121 with references_session(token1) as api: 

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

1123 assert res.has_given is False 

1124 

1125 # false: other user wrote a reference 

1126 with session_scope() as session: 

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

1128 moderator.approve_host_request(hr3) 

1129 with references_session(token2) as api: 

1130 api.WriteHostRequestReference( 

1131 references_pb2.WriteHostRequestReferenceReq( 

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

1133 ) 

1134 ) 

1135 with references_session(token1) as api: 

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

1137 assert res.has_given is False 

1138 

1139 # false: nonexistent host request id 

1140 with references_session(token1) as api: 

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

1142 assert res.has_given is False 

1143 

1144 # Additional status flags 

1145 with session_scope() as session: 

1146 # expired (too old) 

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

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

1149 hr_didnt_stay_host = create_host_request( 

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

1151 ) 

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

1153 hr_other_didnt_stay = create_host_request( 

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

1155 ) 

1156 

1157 moderator.approve_host_request(hr_expired) 

1158 moderator.approve_host_request(hr_didnt_stay_host) 

1159 moderator.approve_host_request(hr_other_didnt_stay) 

1160 

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

1162 with references_session(token1) as api: 

1163 res = api.GetHostRequestReferenceStatus( 

1164 references_pb2.GetHostRequestReferenceStatusReq(host_request_id=hr_expired) 

1165 ) 

1166 assert res.has_given is False 

1167 assert res.is_expired is True 

1168 assert res.can_write is False 

1169 assert res.didnt_stay is False 

1170 

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

1172 with references_session(token1) as api: 

1173 res = api.GetHostRequestReferenceStatus( 

1174 references_pb2.GetHostRequestReferenceStatusReq(host_request_id=hr_didnt_stay_host) 

1175 ) 

1176 assert res.has_given is False 

1177 assert res.is_expired is False 

1178 assert res.didnt_stay is True 

1179 assert res.can_write is False 

1180 

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

1182 with references_session(token1) as api: 

1183 res = api.GetHostRequestReferenceStatus( 

1184 references_pb2.GetHostRequestReferenceStatusReq(host_request_id=hr_other_didnt_stay) 

1185 ) 

1186 assert res.has_given is False 

1187 assert res.is_expired is False 

1188 assert res.didnt_stay is False 

1189 assert res.can_write is True 

1190 

1191 

1192def test_AvailableWriteReferences_and_ListPendingReferencesToWrite(db, moderator): 

1193 user1, token1 = generate_user() 

1194 user2, token2 = generate_user() 

1195 user3, token3 = generate_user() 

1196 user4, token4 = generate_user() 

1197 user5, token5 = generate_user(delete_user=True) 

1198 user6, token6 = generate_user() 

1199 user7, token7 = generate_user() 

1200 user8, token8 = generate_user() 

1201 user9, token9 = generate_user() 

1202 user10, token10 = generate_user() 

1203 user11, token11 = generate_user() 

1204 make_user_block(user1, user6) 

1205 make_user_block(user7, user1) 

1206 

1207 with session_scope() as session: 

1208 # too old 

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

1210 

1211 # already wrote friend ref to user3 

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

1213 

1214 # already given 

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

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

1217 

1218 # valid hosted 

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

1220 

1221 # valid surfed 

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

1223 

1224 # not yet complete 

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

1226 

1227 # already wrote friend ref to user2 

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

1229 

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

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

1232 

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

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

1235 

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

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

1238 

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

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

1241 

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

1243 hr_user9 = create_host_request( 

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

1245 ) 

1246 

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

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

1249 

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

1251 hr7 = create_host_request( 

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

1253 ) 

1254 

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

1256 moderator.approve_host_request(hr1) 

1257 moderator.approve_host_request(hr2) 

1258 moderator.approve_host_request(hr3) 

1259 moderator.approve_host_request(hr4) 

1260 moderator.approve_host_request(hr5) 

1261 moderator.approve_host_request(hr_user5) 

1262 moderator.approve_host_request(hr_user6) 

1263 moderator.approve_host_request(hr_user7) 

1264 moderator.approve_host_request(hr_user8) 

1265 moderator.approve_host_request(hr_user9) 

1266 moderator.approve_host_request(hr6) 

1267 moderator.approve_host_request(hr7) 

1268 

1269 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1270 

1271 with references_session(token1) as api: 

1272 # can't write reference for invisible user 

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

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

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

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

1277 

1278 # can't write reference for blocking user 

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

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

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

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

1283 

1284 # can't write reference for blocked user 

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

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

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

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

1289 

1290 # can't write anything to myself 

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

1292 assert not res.can_write_friend_reference 

1293 assert len(res.available_write_references) == 0 

1294 

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

1296 # can't write friend ref to user2 

1297 assert not res.can_write_friend_reference 

1298 # none we can write for user2 

1299 assert len(res.available_write_references) == 0 

1300 

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

1302 # can't write friend ref to user3 

1303 assert not res.can_write_friend_reference 

1304 # can write one reference because we hosted user3 

1305 assert len(res.available_write_references) == 1 

1306 w = res.available_write_references[0] 

1307 assert w.host_request_id == hr3 

1308 assert w.reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

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

1310 

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

1312 # can write friend ref to user4 

1313 assert res.can_write_friend_reference 

1314 # can write one reference because we surfed with user4 

1315 assert len(res.available_write_references) == 1 

1316 w = res.available_write_references[0] 

1317 assert w.host_request_id == hr4 

1318 assert w.reference_type == references_pb2.REFERENCE_TYPE_SURFED 

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

1320 

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

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

1323 assert len(res.available_write_references) == 0 

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

1325 assert len(res.available_write_references) == 0 

1326 

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

1328 # surfed with them 

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

1330 assert len(res.available_write_references) == 1 

1331 w = res.available_write_references[0] 

1332 assert w.host_request_id == hr6 

1333 assert w.reference_type == references_pb2.REFERENCE_TYPE_SURFED 

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

1335 # hosted them 

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

1337 assert len(res.available_write_references) == 1 

1338 w = res.available_write_references[0] 

1339 assert w.host_request_id == hr7 

1340 assert w.reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

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

1342 

1343 # finally check the general list 

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

1345 assert len(res.pending_references) == 4 

1346 w = res.pending_references[0] 

1347 assert w.host_request_id == hr3 

1348 assert w.reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

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

1350 w = res.pending_references[1] 

1351 assert w.host_request_id == hr4 

1352 assert w.reference_type == references_pb2.REFERENCE_TYPE_SURFED 

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

1354 w = res.pending_references[2] 

1355 assert w.host_request_id == hr6 

1356 assert w.reference_type == references_pb2.REFERENCE_TYPE_SURFED 

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

1358 w = res.pending_references[3] 

1359 assert w.host_request_id == hr7 

1360 assert w.reference_type == references_pb2.REFERENCE_TYPE_HOSTED 

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

1362 

1363 with account_session(token1) as account: 

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

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

1366 "write_reference_reminder", 

1367 "write_reference_reminder", 

1368 "write_reference_reminder", 

1369 "write_reference_reminder", 

1370 ] 

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

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

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

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

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

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

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

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

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

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

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

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

1383 

1384 

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

1386def test_regression_disappearing_refs(db, hs, moderator): 

1387 """ 

1388 Roughly the reproduction steps are: 

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

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

1391 * On the surfer account, leave a reference 

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

1393 """ 

1394 user1, token1 = generate_user() 

1395 user2, token2 = generate_user() 

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

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

1398 with requests_session(token1) as api: 

1399 res = api.CreateHostRequest( 

1400 requests_pb2.CreateHostRequestReq( 

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

1402 ) 

1403 ) 

1404 host_request_id = res.host_request_id 

1405 

1406 moderator.approve_host_request(host_request_id) 

1407 

1408 assert ( 

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

1410 .host_requests[0] 

1411 .latest_message.text.text 

1412 == valid_request_text() 

1413 ) 

1414 

1415 with requests_session(token2) as api: 

1416 api.RespondHostRequest( 

1417 requests_pb2.RespondHostRequestReq( 

1418 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1419 ) 

1420 ) 

1421 

1422 with requests_session(token1) as api: 

1423 api.RespondHostRequest( 

1424 requests_pb2.RespondHostRequestReq( 

1425 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1426 ) 

1427 ) 

1428 

1429 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1430 

1431 with references_session(token1) as api: 

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

1433 assert len(res.pending_references) == 0 

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

1435 assert len(res.available_write_references) == 0 

1436 

1437 with references_session(token2) as api: 

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

1439 assert len(res.pending_references) == 0 

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

1441 assert len(res.available_write_references) == 0 

1442 

1443 # hack the time backwards 

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

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

1446 with session_scope() as session: 

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

1448 assert host_request.conversation_id == host_request_id 

1449 host_request.from_date = hack_req_start 

1450 host_request.to_date = hack_req_end 

1451 

1452 with references_session(token1) as api: 

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

1454 assert len(res.pending_references) == 1 

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

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

1457 

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

1459 assert len(res.available_write_references) == 1 

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

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

1462 

1463 with references_session(token2) as api: 

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

1465 assert len(res.pending_references) == 1 

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

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

1468 

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

1470 assert len(res.available_write_references) == 1 

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

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

1473 

1474 if hs == "host": 

1475 with references_session(token2) as api: 

1476 api.WriteHostRequestReference( 

1477 references_pb2.WriteHostRequestReferenceReq( 

1478 host_request_id=host_request_id, 

1479 text="Good stuff", 

1480 was_appropriate=True, 

1481 rating=0.86, 

1482 ) 

1483 ) 

1484 

1485 with references_session(token2) as api: 

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

1487 assert len(res.pending_references) == 0 

1488 

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

1490 assert len(res.available_write_references) == 0 

1491 

1492 with references_session(token1) as api: 

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

1494 assert len(res.pending_references) == 1 

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

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

1497 

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

1499 assert len(res.available_write_references) == 1 

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

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

1502 else: 

1503 with references_session(token1) as api: 

1504 api.WriteHostRequestReference( 

1505 references_pb2.WriteHostRequestReferenceReq( 

1506 host_request_id=host_request_id, 

1507 text="Good stuff", 

1508 was_appropriate=True, 

1509 rating=0.86, 

1510 ) 

1511 ) 

1512 

1513 with references_session(token1) as api: 

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

1515 assert len(res.pending_references) == 0 

1516 

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

1518 assert len(res.available_write_references) == 0 

1519 

1520 with references_session(token2) as api: 

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

1522 assert len(res.pending_references) == 1 

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

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

1525 

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

1527 assert len(res.available_write_references) == 1 

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

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

1530 

1531 

1532def test_WriteFriendReference_creates_shadowed_moderation_state(db): 

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

1534 user1, token1 = generate_user() 

1535 user2, token2 = generate_user() 

1536 user3, token3 = generate_user() 

1537 make_friends(user1, user2) 

1538 

1539 with references_session(token1) as api: 

1540 ref = api.WriteFriendReference( 

1541 references_pb2.WriteFriendReferenceReq( 

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

1543 ) 

1544 ) 

1545 

1546 with session_scope() as session: 

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

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

1549 assert reference.moderation_state.object_type == ModerationObjectType.reference 

1550 assert reference.moderation_state.object_id == reference.id 

1551 

1552 # Author can see their own shadowed reference. 

1553 with references_session(token1) as api: 

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

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

1556 

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

1558 with references_session(token3) as api: 

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

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

1561 

1562 

1563def test_reference_hidden_via_ums_disappears_from_listings(db, moderator): 

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

1565 user1, token1 = generate_user() 

1566 user2, token2 = generate_user() 

1567 user3, token3 = generate_user() 

1568 make_friends(user1, user2) 

1569 

1570 with references_session(token1) as api: 

1571 ref = api.WriteFriendReference( 

1572 references_pb2.WriteFriendReferenceReq( 

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

1574 ) 

1575 ) 

1576 moderator.approve_reference(ref.reference_id) 

1577 

1578 with references_session(token3) as api: 

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

1580 

1581 # Hide the reference via the moderation API. 

1582 with real_moderation_session(moderator.token) as mod_api: 

1583 state_res = mod_api.GetModerationState( 

1584 moderation_pb2.GetModerationStateReq( 

1585 object_type=moderation_pb2.MODERATION_OBJECT_TYPE_REFERENCE, 

1586 object_id=ref.reference_id, 

1587 ) 

1588 ) 

1589 mod_api.ModerateContent( 

1590 moderation_pb2.ModerateContentReq( 

1591 moderation_state_id=state_res.moderation_state.moderation_state_id, 

1592 action=moderation_pb2.MODERATION_ACTION_HIDE, 

1593 visibility=moderation_pb2.MODERATION_VISIBILITY_HIDDEN, 

1594 reason="test", 

1595 ) 

1596 ) 

1597 

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

1599 with references_session(token1) as api: 

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

1601 with references_session(token3) as api: 

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