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
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-03 11:56 +0000
1from datetime import date, datetime, timedelta
3import grpc
4import pytest
5from google.protobuf import empty_pb2
6from sqlalchemy import func, select, update
7from sqlalchemy.orm import Session
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
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
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
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)
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
102 public_trip_id = create_public_trip(session, surfer_user_id, from_date, to_date) if is_offer else None
104 conversation = Conversation()
105 session.add(conversation)
106 session.flush()
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)
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()
126 moderation_state = create_moderation(
127 session,
128 ModerationObjectType.host_request,
129 conversation.id,
130 initiator_user_id,
131 )
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
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()
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)
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()
188 moderation_state = create_moderation(
189 session,
190 ModerationObjectType.host_request,
191 conversation.id,
192 surfer_user_id,
193 )
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
211 session.add(host_request)
212 session.commit()
213 return host_request.conversation_id
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 )
237 host_request = session.execute(
238 select(HostRequest).where(HostRequest.conversation_id == actual_host_request_id)
239 ).scalar_one()
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
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()
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
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
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()
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
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()
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 )
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))
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 )
330 ref6, _ = create_host_reference(session, user6.id, user1.id, timedelta(days=16, seconds=40), surfing=True)
332 ref7 = create_friend_reference(session, user7.id, user1.id, timedelta(days=16, seconds=30))
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)
337 # should be visible even under 2 weeks
338 ref7b = create_friend_reference(session, user1.id, user7.id, timedelta(days=9))
340 # hidden because it's less than 2 weeks
341 ref6hidden, _ = create_host_reference(session, user6.id, user1.id, timedelta(days=5), surfing=False)
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 )
349 # note that visibility tests don't really test real logic
351 # these check the right refs are in the right requests and appear in the right order (latest first)
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]
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]
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
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]
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
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]
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
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]
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
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
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
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."
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
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
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
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()
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)
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
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
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)
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
505 with api_session(token2) as api:
506 res = api.GetUser(api_pb2.GetUserReq(user=user1.username))
507 assert res.num_references == 1
510def test_ListReference_banned_deleted_users(db):
511 user1, token1 = generate_user()
512 user2, token2 = generate_user()
513 user3, token3 = generate_user()
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))
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
527 with api_session(token1) as api:
528 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 2
530 # ban user2
531 with session_scope() as session:
532 session.execute(update(User).where(User.username == user2.username).values(banned_at=func.now()))
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
541 with api_session(token1) as api:
542 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 1
544 # delete user3
545 with session_scope() as session:
546 session.execute(update(User).where(User.username == user3.username).values(deleted_at=func.now()))
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
555 with api_session(token1) as api:
556 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 1
559def test_ListReference_blocked_shadowed_users(db):
560 user1, token1 = generate_user()
561 user2, token2 = generate_user()
562 user3, token3 = generate_user()
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))
568 # user1 blocks user2: reference from blocked user remains in both list and count
569 make_user_block(user1, user2)
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
575 with api_session(token1) as api:
576 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 2
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()))
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
586 with api_session(token1) as api:
587 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 1
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
594 with api_session(token3) as api:
595 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).num_references == 2
598def test_WriteFriendReference(db, moderator):
599 user1, token1 = generate_user()
600 user2, token2 = generate_user()
601 user3, token3 = generate_user()
603 # Make user1 and user2 friends
604 make_friends(user1, user2)
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
623 moderator.approve_reference(res.reference_id)
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
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."
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."
670def test_WriteFriendReference_with_empty_text(db):
671 user1, token1 = generate_user()
672 user2, token2 = generate_user()
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"
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()
689 # Make users friends
690 make_friends(user1, user2)
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)
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
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!"
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()
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."
735 # Now make them friends
736 make_friends(user1, user2)
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
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 )
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."
776def test_host_request_states_references(db, moderator):
777 user1, token1 = generate_user()
778 user2, token2 = generate_user()
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)
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)
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 )
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 )
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."
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."
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."
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()
865 with session_scope() as session:
866 host_request_id = create_host_request(session, surfer.id, host.id, timedelta(days=7), is_offer=True)
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())
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]
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 )
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]
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 )
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
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()
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))
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)
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 )
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."
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."
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 )
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."
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 )
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 )
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 )
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 )
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 )
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()
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)
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)
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
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!"
1103def test_GetHostRequestReferenceStatus(db, moderator):
1104 user1, token1 = generate_user()
1105 user2, token2 = generate_user()
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
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
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
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
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 )
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)
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
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
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
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)
1210 with session_scope() as session:
1211 # too old
1212 hr1 = create_host_request(session, user3.id, user1.id, timedelta(days=20))
1214 # already wrote friend ref to user3
1215 create_friend_reference(session, user1.id, user3.id, timedelta(days=15, seconds=70))
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)
1221 # valid hosted
1222 hr3 = create_host_request(session, user3.id, user1.id, timedelta(days=8))
1224 # valid surfed
1225 hr4 = create_host_request(session, user1.id, user4.id, timedelta(days=5))
1227 # not yet complete
1228 hr5 = create_host_request(session, user2.id, user1.id, timedelta(days=2), status=HostRequestStatus.pending)
1230 # already wrote friend ref to user2
1231 create_friend_reference(session, user1.id, user2.id, timedelta(days=1))
1233 # user5 deleted, reference won't show up as pending
1234 hr_user5 = create_host_request(session, user1.id, user5.id, timedelta(days=5))
1236 # user6 blocked, reference won't show up as pending
1237 hr_user6 = create_host_request(session, user1.id, user6.id, timedelta(days=5))
1239 # user7 blocking, reference won't show up as pending
1240 hr_user7 = create_host_request(session, user1.id, user7.id, timedelta(days=5))
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="")
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 )
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="")
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 )
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)
1272 refresh_materialized_views_rapid(empty_pb2.Empty())
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."
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."
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."
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
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
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)
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)
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
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)
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)
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
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
1409 moderator.approve_host_request(host_request_id)
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 )
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 )
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 )
1432 refresh_materialized_views_rapid(empty_pb2.Empty())
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
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
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
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
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
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
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
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 )
1488 with references_session(token2) as api:
1489 res = api.ListPendingReferencesToWrite(empty_pb2.Empty())
1490 assert len(res.pending_references) == 0
1492 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user2.id))
1493 assert len(res.available_write_references) == 0
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
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 )
1516 with references_session(token1) as api:
1517 res = api.ListPendingReferencesToWrite(empty_pb2.Empty())
1518 assert len(res.pending_references) == 0
1520 res = api.AvailableWriteReferences(references_pb2.AvailableWriteReferencesReq(to_user_id=user1.id))
1521 assert len(res.available_write_references) == 0
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
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
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)
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 )
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
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]
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] == []
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)
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)
1581 with references_session(token3) as api:
1582 assert api.ListReferences(references_pb2.ListReferencesReq(to_user_id=user2.id)).references
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 )
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