Coverage for app / backend / src / tests / test_api.py: 100%
1164 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 09:44 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 09:44 +0000
1from datetime import timedelta
3import grpc
4import pytest
5from google.protobuf import empty_pb2, wrappers_pb2
6from sqlalchemy import func, select, update
8from couchers.db import session_scope
9from couchers.helpers.badges import user_add_badge
10from couchers.jobs.handlers import update_badges
11from couchers.materialized_views import refresh_materialized_views_rapid
12from couchers.models import (
13 FriendRelationship,
14 FriendStatus,
15 LanguageFluency,
16 ModerationObjectType,
17 ModerationState,
18 ModerationVisibility,
19 RateLimitAction,
20 User,
21 UserBadge,
22)
23from couchers.models.notifications import Notification
24from couchers.proto import admin_pb2, api_pb2, blocking_pb2, jail_pb2, notifications_pb2
25from couchers.rate_limits.definitions import RATE_LIMIT_DEFINITIONS, RATE_LIMIT_HOURS
26from couchers.resources import get_badge_dict
27from couchers.utils import create_coordinate, now, to_aware_datetime
28from tests.fixtures.db import generate_user, make_friends, make_user_block
29from tests.fixtures.misc import PushCollector, email_fields, mock_notification_email
30from tests.fixtures.sessions import (
31 api_session,
32 blocking_session,
33 notifications_session,
34 real_api_session,
35 real_jail_session,
36)
37from tests.fixtures.sessions import (
38 real_admin_session as admin_session,
39)
42@pytest.fixture(autouse=True)
43def _(testconfig):
44 pass
47def test_ping(db):
48 user, token = generate_user(
49 regions_lived=["ESP", "FRA", "EST"],
50 regions_visited=["CHE", "REU", "FIN"],
51 language_abilities=[
52 ("fin", LanguageFluency.fluent),
53 ("fra", LanguageFluency.beginner),
54 ],
55 )
57 with real_api_session(token) as api:
58 res = api.Ping(api_pb2.PingReq())
60 assert res.user.user_id == user.id
61 assert res.user.username == user.username
62 assert res.user.name == user.name
63 assert res.user.city == user.city
64 assert res.user.hometown == user.hometown
65 assert res.user.verification == 0.0
66 assert res.user.community_standing == user.community_standing
67 assert res.user.num_references == 0
68 assert res.user.gender == user.gender
69 assert res.user.pronouns == user.pronouns
70 assert res.user.age == user.age
72 assert (res.user.lat, res.user.lng) == user.coordinates
74 # the joined time is fuzzed
75 # but shouldn't be before actual joined time, or more than one hour behind
76 assert user.joined - timedelta(hours=1) <= to_aware_datetime(res.user.joined) <= user.joined
77 # same for last_active
78 assert user.last_active - timedelta(hours=1) <= to_aware_datetime(res.user.last_active) <= user.last_active
80 assert res.user.hosting_status == api_pb2.HOSTING_STATUS_CANT_HOST
81 assert res.user.meetup_status == api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP
83 assert res.user.occupation == user.occupation
84 assert res.user.education == user.education
85 assert res.user.about_me == user.about_me
86 assert res.user.things_i_like == user.things_i_like
87 assert {language_ability.code for language_ability in res.user.language_abilities} == {"fin", "fra"}
88 assert res.user.about_place == user.about_place
89 assert res.user.regions_visited == ["FIN", "REU", "CHE"] # Tests alphabetization by region name
90 assert res.user.regions_lived == ["EST", "FRA", "ESP"] # Ditto
91 assert res.user.additional_information == user.additional_information
93 assert res.user.friends == api_pb2.User.FriendshipStatus.NA
94 assert not res.user.HasField("pending_friend_request")
97def test_coords(db):
98 # make them need to update location
99 user1, token1 = generate_user(geom=create_coordinate(1, 0), geom_radius=2000, needs_to_update_location=True)
100 user2, token2 = generate_user()
102 with api_session(token2) as api:
103 res = api.Ping(api_pb2.PingReq())
104 assert res.user.city == user2.city
105 lat, lng = user2.coordinates
106 assert res.user.lat == lat
107 assert res.user.lng == lng
108 assert res.user.radius == user2.geom_radius
110 with api_session(token2) as api:
111 res = api.GetUser(api_pb2.GetUserReq(user=user1.username))
112 assert res.city == user1.city
113 assert res.lat == 1.0
114 assert res.lng == 0.0
115 assert res.radius == 2000.0
117 # Check coordinate wrapping
118 user3, token3 = generate_user(geom=create_coordinate(40.0, -180.5))
119 user4, token4 = generate_user(geom=create_coordinate(40.0, 20.0))
120 user5, token5 = generate_user(geom=create_coordinate(90.5, 20.0))
122 with api_session(token3) as api:
123 res = api.GetUser(api_pb2.GetUserReq(user=user3.username))
124 assert res.lat == 40.0
125 assert res.lng == 179.5
127 with api_session(token4) as api:
128 res = api.GetUser(api_pb2.GetUserReq(user=user4.username))
129 assert res.lat == 40.0
130 assert res.lng == 20.0
132 # PostGIS does not wrap longitude for latitude overflow
133 with api_session(token5) as api:
134 res = api.GetUser(api_pb2.GetUserReq(user=user5.username))
135 assert res.lat == 89.5
136 assert res.lng == 20.0
138 with real_jail_session(token1) as jail:
139 res = jail.JailInfo(empty_pb2.Empty())
140 assert res.jailed
141 assert res.needs_to_update_location
143 res = jail.SetLocation(
144 jail_pb2.SetLocationReq(
145 city="New York City",
146 lat=40.7812,
147 lng=-73.9647,
148 radius=250,
149 )
150 )
152 assert not res.jailed
153 assert not res.needs_to_update_location
155 res = jail.JailInfo(empty_pb2.Empty())
156 assert not res.jailed
157 assert not res.needs_to_update_location
159 with api_session(token2) as api:
160 res = api.GetUser(api_pb2.GetUserReq(user=user1.username))
161 assert res.city == "New York City"
162 assert res.lat == 40.7812
163 assert res.lng == -73.9647
164 assert res.radius == 250
167def test_get_user(db):
168 user1, token1 = generate_user()
169 user2, token2 = generate_user()
171 with api_session(token1) as api:
172 res = api.GetUser(api_pb2.GetUserReq(user=user2.username))
173 assert res.user_id == user2.id
174 assert res.username == user2.username
175 assert res.name == user2.name
177 with api_session(token1) as api:
178 res = api.GetUser(api_pb2.GetUserReq(user=str(user2.id)))
179 assert res.user_id == user2.id
180 assert res.username == user2.username
181 assert res.name == user2.name
184@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"])
185def test_user_model_to_pb_ghost_user(db, flag):
186 user1, token1 = generate_user()
187 user2, _ = generate_user()
189 with session_scope() as session:
190 session.execute(update(User).where(User.id == user2.id).values(**{flag: func.now()}))
192 refresh_materialized_views_rapid(empty_pb2.Empty())
194 with api_session(token1) as api:
195 user_pb = api.GetUser(api_pb2.GetUserReq(user=user2.username))
197 assert user_pb.user_id == user2.id
198 assert user_pb.is_ghost
199 assert user_pb.username == "ghost"
200 assert user_pb.name == "Deactivated Account"
201 assert (
202 user_pb.about_me
203 == "This user is no longer on the platform. They may have deleted their account, been blocked, or been banned. We recommend exercising caution with any further interaction with this user off the platform. You can always reach out to support if you need any help."
204 )
206 assert user_pb.lat == 0
207 assert user_pb.lng == 0
208 assert user_pb.radius == 0
209 assert user_pb.verification == 0.0
210 assert user_pb.community_standing == 0.0
211 assert user_pb.num_references == 0
212 assert user_pb.age == 0
213 assert user_pb.hosting_status == 0
214 assert user_pb.meetup_status == 0
215 assert user_pb.city == ""
216 assert user_pb.hometown == ""
217 assert user_pb.timezone == ""
218 assert user_pb.gender == ""
219 assert user_pb.pronouns == ""
220 assert user_pb.occupation == ""
221 assert user_pb.education == ""
222 assert user_pb.things_i_like == ""
223 assert user_pb.about_place == ""
224 assert user_pb.additional_information == ""
225 assert list(user_pb.language_abilities) == []
226 assert list(user_pb.regions_visited) == []
227 assert list(user_pb.regions_lived) == []
228 assert list(user_pb.badges) == []
229 assert user_pb.friends == api_pb2.User.FriendshipStatus.NOT_FRIENDS
230 assert user_pb.avatar_url == ""
231 assert user_pb.avatar_thumbnail_url == ""
232 assert not user_pb.has_strong_verification
234 with api_session(token1) as api:
235 lite_user_pb = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username))
237 assert lite_user_pb.user_id == user2.id
238 assert lite_user_pb.is_ghost
239 assert lite_user_pb.username == "ghost"
240 assert lite_user_pb.name == "Deactivated Account"
241 assert lite_user_pb.city == ""
242 assert lite_user_pb.age == 0
243 assert lite_user_pb.avatar_url == ""
244 assert lite_user_pb.avatar_thumbnail_url == ""
245 assert lite_user_pb.lat == 0
246 assert lite_user_pb.lng == 0
247 assert lite_user_pb.radius == 0
248 assert not lite_user_pb.has_strong_verification
251def test_user_model_to_pb_ghost_user_blocked(db):
252 user1, token1 = generate_user()
253 user2, _ = generate_user()
255 with blocking_session(token1) as user_blocks:
256 user_blocks.BlockUser(blocking_pb2.BlockUserReq(username=user2.username))
258 refresh_materialized_views_rapid(empty_pb2.Empty())
260 with api_session(token1) as api:
261 user_pb = api.GetUser(api_pb2.GetUserReq(user=user2.username))
263 assert user_pb.user_id == user2.id
264 assert user_pb.is_ghost
265 assert user_pb.username == "ghost"
266 assert user_pb.name == "Deactivated Account"
267 assert (
268 user_pb.about_me
269 == "This user is no longer on the platform. They may have deleted their account, been blocked, or been banned. We recommend exercising caution with any further interaction with this user off the platform. You can always reach out to support if you need any help."
270 )
272 assert user_pb.lat == 0
273 assert user_pb.lng == 0
274 assert user_pb.radius == 0
275 assert user_pb.verification == 0.0
276 assert user_pb.community_standing == 0.0
277 assert user_pb.num_references == 0
278 assert user_pb.age == 0
279 assert user_pb.hosting_status == 0
280 assert user_pb.meetup_status == 0
281 assert user_pb.city == ""
282 assert user_pb.hometown == ""
283 assert user_pb.timezone == ""
284 assert user_pb.gender == ""
285 assert user_pb.pronouns == ""
286 assert user_pb.occupation == ""
287 assert user_pb.education == ""
288 assert user_pb.things_i_like == ""
289 assert user_pb.about_place == ""
290 assert user_pb.additional_information == ""
291 assert list(user_pb.language_abilities) == []
292 assert list(user_pb.regions_visited) == []
293 assert list(user_pb.regions_lived) == []
294 assert list(user_pb.badges) == []
295 assert user_pb.friends == api_pb2.User.FriendshipStatus.NOT_FRIENDS
296 assert user_pb.avatar_url == ""
297 assert user_pb.avatar_thumbnail_url == ""
298 assert not user_pb.has_strong_verification
300 with api_session(token1) as api:
301 lite_user_pb = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username))
303 assert lite_user_pb.user_id == user2.id
304 assert lite_user_pb.is_ghost
305 assert lite_user_pb.username == "ghost"
306 assert lite_user_pb.name == "Deactivated Account"
307 assert lite_user_pb.city == ""
308 assert lite_user_pb.age == 0
309 assert lite_user_pb.avatar_url == ""
310 assert lite_user_pb.avatar_thumbnail_url == ""
311 assert lite_user_pb.lat == 0
312 assert lite_user_pb.lng == 0
313 assert lite_user_pb.radius == 0
314 assert not lite_user_pb.has_strong_verification
317@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"])
318def test_admin_viewing_ghost_users_sees_full_profile(db, flag):
319 admin, token_admin = generate_user(is_superuser=True)
320 user, _ = generate_user()
322 with session_scope() as session:
323 session.execute(update(User).where(User.id == user.id).values(**{flag: func.now()}))
325 with admin_session(token_admin) as api:
326 user_pb = api.GetUser(admin_pb2.GetUserReq(user=user.username))
328 assert user_pb.user_id == user.id
329 assert user_pb.username == user.username
330 assert user_pb.name == user.name
331 assert user_pb.city == user.city
332 assert user_pb.name != "Deactivated Account"
333 assert user_pb.username != "ghost"
334 assert user_pb.hosting_status in (
335 api_pb2.HOSTING_STATUS_UNKNOWN,
336 api_pb2.HOSTING_STATUS_CAN_HOST,
337 api_pb2.HOSTING_STATUS_MAYBE,
338 api_pb2.HOSTING_STATUS_CANT_HOST,
339 )
342def test_lite_coords(db):
343 # make them need to update location
344 user1, token1 = generate_user(geom=create_coordinate(0, 0), geom_radius=0, needs_to_update_location=True)
345 user2, token2 = generate_user()
347 refresh_materialized_views_rapid(empty_pb2.Empty())
349 with api_session(token2) as api:
350 res = api.Ping(api_pb2.PingReq())
351 assert res.user.city == user2.city
352 lat, lng = user2.coordinates
353 assert res.user.lat == lat
354 assert res.user.lng == lng
355 assert res.user.radius == user2.geom_radius
357 with api_session(token2) as api:
358 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user1.username))
359 assert res.city == user1.city
360 assert res.lat == 0.0
361 assert res.lng == 0.0
362 assert res.radius == 0.0
364 # Check coordinate wrapping
365 user3, token3 = generate_user(geom=create_coordinate(40.0, -180.5))
366 user4, token4 = generate_user(geom=create_coordinate(40.0, 20.0))
367 user5, token5 = generate_user(geom=create_coordinate(90.5, 20.0))
369 refresh_materialized_views_rapid(empty_pb2.Empty())
371 with api_session(token3) as api:
372 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user3.username))
373 assert res.lat == 40.0
374 assert res.lng == 179.5
376 with api_session(token4) as api:
377 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user4.username))
378 assert res.lat == 40.0
379 assert res.lng == 20.0
381 # PostGIS does not wrap longitude for latitude overflow
382 with api_session(token5) as api:
383 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user5.username))
384 assert res.lat == 89.5
385 assert res.lng == 20.0
387 with real_jail_session(token1) as jail:
388 res = jail.JailInfo(empty_pb2.Empty())
389 assert res.jailed
390 assert res.needs_to_update_location
392 res = jail.SetLocation(
393 jail_pb2.SetLocationReq(
394 city="New York City",
395 lat=40.7812,
396 lng=-73.9647,
397 radius=250,
398 )
399 )
401 assert not res.jailed
402 assert not res.needs_to_update_location
404 res = jail.JailInfo(empty_pb2.Empty())
405 assert not res.jailed
406 assert not res.needs_to_update_location
408 refresh_materialized_views_rapid(empty_pb2.Empty())
410 with api_session(token2) as api:
411 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user1.username))
412 assert res.city == "New York City"
413 assert res.lat == 40.7812
414 assert res.lng == -73.9647
415 assert res.radius == 250
418def test_lite_get_user(db):
419 user1, token1 = generate_user()
420 user2, token2 = generate_user()
422 refresh_materialized_views_rapid(empty_pb2.Empty())
424 with api_session(token1) as api:
425 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username))
426 assert res.user_id == user2.id
427 assert res.username == user2.username
428 assert res.name == user2.name
430 with api_session(token1) as api:
431 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=str(user2.id)))
432 assert res.user_id == user2.id
433 assert res.username == user2.username
434 assert res.name == user2.name
437def test_GetLiteUsers(db):
438 user1, token1 = generate_user()
439 user2, _ = generate_user()
440 user3, _ = generate_user()
441 user4, _ = generate_user()
442 user5, _ = generate_user()
443 user6, _ = generate_user()
445 make_user_block(user4, user1)
447 refresh_materialized_views_rapid(empty_pb2.Empty())
449 with api_session(token1) as api:
450 res = api.GetLiteUsers(
451 api_pb2.GetLiteUsersReq(
452 users=[
453 user1.username,
454 str(user1.id),
455 "nonexistent",
456 str(user2.id),
457 "9994",
458 user6.username,
459 str(user5.id),
460 "notreal",
461 user4.username,
462 ]
463 )
464 )
466 assert len(res.responses) == 9
467 assert res.responses[0].query == user1.username
468 assert res.responses[0].user.user_id == user1.id
470 assert res.responses[1].query == str(user1.id)
471 assert res.responses[1].user.user_id == user1.id
473 assert res.responses[2].query == "nonexistent"
474 assert res.responses[2].not_found
476 assert res.responses[3].query == str(user2.id)
477 assert res.responses[3].user.user_id == user2.id
479 assert res.responses[4].query == "9994"
480 assert res.responses[4].not_found
482 assert res.responses[5].query == user6.username
483 assert res.responses[5].user.user_id == user6.id
485 assert res.responses[6].query == str(user5.id)
486 assert res.responses[6].user.user_id == user5.id
488 assert res.responses[7].query == "notreal"
489 assert res.responses[7].not_found
491 # blocked - should return ghost profile
492 assert res.responses[8].query == user4.username
493 assert not res.responses[8].not_found
494 assert res.responses[8].user.user_id == user4.id
495 assert res.responses[8].user.is_ghost
496 assert res.responses[8].user.username == "ghost"
497 assert res.responses[8].user.name == "Deactivated Account"
499 with api_session(token1) as api:
500 with pytest.raises(grpc.RpcError) as e:
501 api.GetLiteUsers(api_pb2.GetLiteUsersReq(users=201 * [user1.username]))
502 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
503 assert e.value.details() == "You can't request that many users at a time."
506def test_update_profile(db):
507 user, token = generate_user()
509 with api_session(token) as api:
510 with pytest.raises(grpc.RpcError) as e:
511 api.UpdateProfile(api_pb2.UpdateProfileReq(name=wrappers_pb2.StringValue(value=" ")))
512 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
513 assert e.value.details() == "Name not supported."
515 with pytest.raises(grpc.RpcError) as e:
516 api.UpdateProfile(
517 api_pb2.UpdateProfileReq(lat=wrappers_pb2.DoubleValue(value=0), lng=wrappers_pb2.DoubleValue(value=0))
518 )
519 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
520 assert e.value.details() == "Invalid coordinate."
522 with pytest.raises(grpc.RpcError) as e:
523 api.UpdateProfile(
524 api_pb2.UpdateProfileReq(regions_visited=api_pb2.RepeatedStringValue(value=["United States"]))
525 )
526 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
527 assert e.value.details() == "Invalid region."
529 with pytest.raises(grpc.RpcError) as e:
530 api.UpdateProfile(
531 api_pb2.UpdateProfileReq(regions_lived=api_pb2.RepeatedStringValue(value=["United Kingdom"]))
532 )
533 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
534 assert e.value.details() == "Invalid region."
536 api.UpdateProfile(
537 api_pb2.UpdateProfileReq(
538 name=wrappers_pb2.StringValue(value="New name"),
539 city=wrappers_pb2.StringValue(value="Timbuktu"),
540 hometown=api_pb2.NullableStringValue(value="Walla Walla"),
541 lat=wrappers_pb2.DoubleValue(value=0.01),
542 lng=wrappers_pb2.DoubleValue(value=-2),
543 radius=wrappers_pb2.DoubleValue(value=321),
544 pronouns=api_pb2.NullableStringValue(value="Ro, Robo, Robots"),
545 occupation=api_pb2.NullableStringValue(value="Testing"),
546 education=api_pb2.NullableStringValue(value="Couchers U"),
547 about_me=api_pb2.NullableStringValue(value="I rule"),
548 things_i_like=api_pb2.NullableStringValue(value="Couchers"),
549 about_place=api_pb2.NullableStringValue(value="My place"),
550 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST,
551 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
552 language_abilities=api_pb2.RepeatedLanguageAbilityValue(
553 value=[
554 api_pb2.LanguageAbility(
555 code="eng",
556 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
557 )
558 ],
559 ),
560 regions_visited=api_pb2.RepeatedStringValue(value=["CXR", "FIN"]),
561 regions_lived=api_pb2.RepeatedStringValue(value=["USA", "EST"]),
562 additional_information=api_pb2.NullableStringValue(value="I <3 Couchers"),
563 )
564 )
566 user_details = api.GetUser(api_pb2.GetUserReq(user=user.username))
567 assert user_details.name == "New name"
568 assert user_details.city == "Timbuktu"
569 assert user_details.hometown == "Walla Walla"
570 assert user_details.pronouns == "Ro, Robo, Robots"
571 assert user_details.education == "Couchers U"
572 assert user_details.things_i_like == "Couchers"
573 assert user_details.lat == 0.01
574 assert user_details.lng == -2
575 assert user_details.radius == 321
576 assert user_details.occupation == "Testing"
577 assert user_details.about_me == "I rule"
578 assert user_details.about_place == "My place"
579 assert user_details.hosting_status == api_pb2.HOSTING_STATUS_CAN_HOST
580 assert user_details.meetup_status == api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP
581 assert user_details.language_abilities[0].code == "eng"
582 assert user_details.language_abilities[0].fluency == api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT
583 assert user_details.additional_information == "I <3 Couchers"
584 assert user_details.regions_visited == ["CXR", "FIN"]
585 assert user_details.regions_lived == ["EST", "USA"]
587 # Test unset values
588 api.UpdateProfile(
589 api_pb2.UpdateProfileReq(
590 hometown=api_pb2.NullableStringValue(is_null=True),
591 radius=wrappers_pb2.DoubleValue(value=0),
592 pronouns=api_pb2.NullableStringValue(is_null=True),
593 occupation=api_pb2.NullableStringValue(is_null=True),
594 education=api_pb2.NullableStringValue(is_null=True),
595 about_me=api_pb2.NullableStringValue(is_null=True),
596 things_i_like=api_pb2.NullableStringValue(is_null=True),
597 about_place=api_pb2.NullableStringValue(is_null=True),
598 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST,
599 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
600 language_abilities=api_pb2.RepeatedLanguageAbilityValue(value=[]),
601 regions_visited=api_pb2.RepeatedStringValue(value=[]),
602 regions_lived=api_pb2.RepeatedStringValue(value=[]),
603 additional_information=api_pb2.NullableStringValue(is_null=True),
604 )
605 )
607 user_details = api.GetUser(api_pb2.GetUserReq(user=user.username))
608 assert not user_details.hometown
609 assert not user_details.radius
610 assert not user_details.pronouns
611 assert not user_details.occupation
612 assert not user_details.education
613 assert not user_details.about_me
614 assert not user_details.things_i_like
615 assert not user_details.about_place
616 assert user_details.hosting_status == api_pb2.HOSTING_STATUS_CAN_HOST
617 assert user_details.meetup_status == api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP
618 assert not user_details.language_abilities
619 assert not user_details.regions_visited
620 assert not user_details.regions_lived
621 assert not user_details.additional_information
624def test_update_profile_do_not_email(db):
625 user, token = generate_user()
627 with notifications_session(token) as notifications:
628 notifications.SetNotificationSettings(notifications_pb2.SetNotificationSettingsReq(enable_do_not_email=True))
630 with api_session(token) as api:
631 with pytest.raises(grpc.RpcError) as e:
632 api.UpdateProfile(api_pb2.UpdateProfileReq(hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST))
633 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
634 assert e.value.details() == "You cannot enable hosting while you have emails turned off in your settings."
636 with pytest.raises(grpc.RpcError) as e:
637 api.UpdateProfile(api_pb2.UpdateProfileReq(meetup_status=api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP))
638 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
639 assert e.value.details() == "You cannot enable meeting up while you have emails turned off in your settings."
642def test_language_abilities(db):
643 user, token = generate_user(
644 language_abilities=[
645 ("fin", LanguageFluency.fluent),
646 ("fra", LanguageFluency.beginner),
647 ],
648 )
650 with api_session(token) as api:
651 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
652 assert len(res.language_abilities) == 2
654 # can't add non-existent languages
655 with pytest.raises(grpc.RpcError) as err:
656 api.UpdateProfile(
657 api_pb2.UpdateProfileReq(
658 language_abilities=api_pb2.RepeatedLanguageAbilityValue(
659 value=[
660 api_pb2.LanguageAbility(
661 code="QQQ",
662 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
663 )
664 ],
665 ),
666 )
667 )
668 assert err.value.code() == grpc.StatusCode.INVALID_ARGUMENT
669 assert err.value.details() == "Invalid language."
671 # can't have multiple languages of the same type
672 with pytest.raises(Exception) as err2:
673 api.UpdateProfile(
674 api_pb2.UpdateProfileReq(
675 language_abilities=api_pb2.RepeatedLanguageAbilityValue(
676 value=[
677 api_pb2.LanguageAbility(
678 code="eng",
679 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
680 ),
681 api_pb2.LanguageAbility(
682 code="eng",
683 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
684 ),
685 ],
686 ),
687 )
688 )
689 assert "violates unique constraint" in str(err2.value)
691 # nothing changed
692 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
693 assert len(res.language_abilities) == 2
695 # now actually add a value
696 api.UpdateProfile(
697 api_pb2.UpdateProfileReq(
698 language_abilities=api_pb2.RepeatedLanguageAbilityValue(
699 value=[
700 api_pb2.LanguageAbility(
701 code="eng",
702 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
703 )
704 ],
705 ),
706 )
707 )
709 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
710 assert len(res.language_abilities) == 1
711 assert res.language_abilities[0].code == "eng"
712 assert res.language_abilities[0].fluency == api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT
714 # change the value to a new one
715 api.UpdateProfile(
716 api_pb2.UpdateProfileReq(
717 language_abilities=api_pb2.RepeatedLanguageAbilityValue(
718 value=[
719 api_pb2.LanguageAbility(
720 code="fin",
721 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
722 )
723 ],
724 ),
725 )
726 )
728 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
729 assert len(res.language_abilities) == 1
730 assert res.language_abilities[0].code == "fin"
731 assert res.language_abilities[0].fluency == api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER
733 # should be able to set to same value still
734 api.UpdateProfile(
735 api_pb2.UpdateProfileReq(
736 language_abilities=api_pb2.RepeatedLanguageAbilityValue(
737 value=[
738 api_pb2.LanguageAbility(
739 code="fin",
740 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
741 )
742 ],
743 ),
744 )
745 )
747 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
748 assert len(res.language_abilities) == 1
749 assert res.language_abilities[0].code == "fin"
750 assert res.language_abilities[0].fluency == api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER
752 # don't change it
753 api.UpdateProfile(api_pb2.UpdateProfileReq())
755 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
756 assert len(res.language_abilities) == 1
757 assert res.language_abilities[0].code == "fin"
758 assert res.language_abilities[0].fluency == api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER
760 # remove value
761 api.UpdateProfile(
762 api_pb2.UpdateProfileReq(
763 language_abilities=api_pb2.RepeatedLanguageAbilityValue(
764 value=[],
765 ),
766 )
767 )
769 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
770 assert len(res.language_abilities) == 0
773def test_pending_friend_request_count(db, moderator):
774 user1, token1 = generate_user()
775 user2, token2 = generate_user()
776 user3, token3 = generate_user()
778 with api_session(token2) as api:
779 res = api.Ping(api_pb2.PingReq())
780 assert res.pending_friend_request_count == 0
782 with api_session(token1) as api:
783 res = api.Ping(api_pb2.PingReq())
784 assert res.pending_friend_request_count == 0
785 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
786 # Sender can still see their own sent requests (even while SHADOWED)
787 res = api.Ping(api_pb2.PingReq())
788 assert res.pending_friend_request_count == 0
790 # Get friend request ID from sender's view (author can see SHADOWED)
791 with api_session(token1) as api:
792 res = api.ListFriendRequests(empty_pb2.Empty())
793 assert len(res.sent) == 1
794 fr_id = res.sent[0].friend_request_id
796 # Recipient cannot see SHADOWED friend requests before mod approval
797 with api_session(token2) as api:
798 res = api.Ping(api_pb2.PingReq())
799 assert res.pending_friend_request_count == 0
801 # Moderator approves the friend request
802 moderator.approve_friend_request(fr_id)
804 # Now recipient can see the approved friend request
805 with api_session(token2) as api:
806 res = api.Ping(api_pb2.PingReq())
807 assert res.pending_friend_request_count == 1
809 with api_session(token2) as api:
810 # check it's there
811 res = api.ListFriendRequests(empty_pb2.Empty())
812 assert len(res.sent) == 0
813 assert len(res.received) == 1
815 assert res.received[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING
816 assert res.received[0].user_id == user1.id
818 fr_id = res.received[0].friend_request_id
820 # accept it
821 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=True))
823 res = api.Ping(api_pb2.PingReq())
824 assert res.pending_friend_request_count == 0
827def test_friend_request_flow(db, push_collector: PushCollector, moderator):
828 user1, token1 = generate_user(complete_profile=True)
829 user2, token2 = generate_user(complete_profile=True)
830 user3, token3 = generate_user()
832 # send a friend request from user1 to user2
833 with api_session(token1) as api:
834 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
836 with session_scope() as session:
837 friend_request = session.execute(
838 select(FriendRelationship).where(
839 FriendRelationship.from_user_id == user1.id, FriendRelationship.to_user_id == user2.id
840 )
841 ).scalar_one()
842 friend_request_id = friend_request.id
844 # Notification is deferred while content is SHADOWED
845 # No push notification sent yet
846 assert push_collector.count_for_user(user2.id) == 0
848 with api_session(token1) as api:
849 # Sender can see their own sent requests (even while SHADOWED)
850 res = api.ListFriendRequests(empty_pb2.Empty())
851 assert len(res.sent) == 1
852 assert len(res.received) == 0
854 assert res.sent[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING
855 assert res.sent[0].user_id == user2.id
856 assert res.sent[0].friend_request_id == friend_request_id
858 # Recipient cannot see SHADOWED friend requests
859 with api_session(token2) as api:
860 res = api.ListFriendRequests(empty_pb2.Empty())
861 assert len(res.sent) == 0
862 assert len(res.received) == 0
864 # Moderator approves the friend request - this triggers the notification
865 with mock_notification_email() as mock:
866 moderator.approve_friend_request(friend_request_id)
868 push = push_collector.pop_for_user(user2.id, last=True)
869 assert push.content.title == f"Friend request from {user1.name}"
870 assert push.content.body == f"{user1.name} wants to be your friend."
872 mock.assert_called_once()
873 e = email_fields(mock)
874 assert e.recipient == user2.email
875 assert e.subject == f"[TEST] {user1.name} wants to be your friend on Couchers.org!"
876 assert user2.name in e.plain
877 assert user2.name in e.html
878 assert user1.name in e.plain
879 assert user1.name in e.html
880 assert "http://localhost:5001/img/thumbnail/" not in e.plain
881 assert "http://localhost:5001/img/thumbnail/" in e.html
882 assert "http://localhost:3000/connections/friends/" in e.plain
883 assert "http://localhost:3000/connections/friends/" in e.html
885 # Now recipient can see the approved friend request
886 with api_session(token2) as api:
887 # check it's there
888 res = api.ListFriendRequests(empty_pb2.Empty())
889 assert len(res.sent) == 0
890 assert len(res.received) == 1
892 assert res.received[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING
893 assert res.received[0].user_id == user1.id
895 fr_id = res.received[0].friend_request_id
897 # accept it
898 with mock_notification_email() as mock:
899 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=True))
901 # check it's gone
902 res = api.ListFriendRequests(empty_pb2.Empty())
903 assert len(res.sent) == 0
904 assert len(res.received) == 0
906 # check we're friends now
907 res = api.ListFriends(empty_pb2.Empty())
908 assert len(res.user_ids) == 1
909 assert res.user_ids[0] == user1.id
911 # user2 got one push (from the friend request creation)
912 # user1 should now have one push (from the friend request acceptance)
913 push = push_collector.pop_for_user(user1.id, last=True)
914 assert push.content.title == f"{user2.name} accepted your friend request"
915 assert push.content.body == f"You are now friends with {user2.name}."
917 mock.assert_called_once()
918 e = email_fields(mock)
919 assert e.recipient == user1.email
920 assert e.subject == f"[TEST] {user2.name} accepted your friend request!"
921 assert user1.name in e.plain
922 assert user1.name in e.html
923 assert user2.name in e.plain
924 assert user2.name in e.html
925 assert "http://localhost:5001/img/thumbnail/" not in e.plain
926 assert "http://localhost:5001/img/thumbnail/" in e.html
927 assert f"http://localhost:3000/user/{user2.username}" in e.plain
928 assert f"http://localhost:3000/user/{user2.username}" in e.html
930 with api_session(token1) as api:
931 # check it's gone
932 res = api.ListFriendRequests(empty_pb2.Empty())
933 assert len(res.sent) == 0
934 assert len(res.received) == 0
936 # check we're friends now
937 res = api.ListFriends(empty_pb2.Empty())
938 assert len(res.user_ids) == 1
939 assert res.user_ids[0] == user2.id
941 with api_session(token1) as api:
942 # we can't unfriend if we aren't friends
943 with pytest.raises(grpc.RpcError) as err:
944 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user3.id))
945 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
946 assert err.value.details() == "You aren't friends with that user!"
948 # we can unfriend
949 res = api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user2.id))
951 res = api.ListFriends(empty_pb2.Empty())
952 assert len(res.user_ids) == 0
955def test_RemoveFriend_regression(db, push_collector: PushCollector, moderator):
956 user1, token1 = generate_user(complete_profile=True)
957 user2, token2 = generate_user(complete_profile=True)
958 user3, token3 = generate_user()
959 user4, token4 = generate_user()
960 user5, token5 = generate_user()
961 user6, token6 = generate_user()
963 # Send friend requests
964 with api_session(token4) as api:
965 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user1.id))
966 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
968 with api_session(token5) as api:
969 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user1.id))
971 with api_session(token1) as api:
972 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
973 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user3.id))
975 # Approve all friend requests via moderation
976 with session_scope() as session:
977 friend_requests = session.execute(select(FriendRelationship)).scalars().all()
978 for fr in friend_requests:
979 moderator.approve_friend_request(fr.id)
981 # Now recipients can respond
982 with api_session(token1) as api:
983 api.RespondFriendRequest(
984 api_pb2.RespondFriendRequestReq(
985 friend_request_id=api.ListFriendRequests(empty_pb2.Empty()).received[0].friend_request_id, accept=True
986 )
987 )
989 with api_session(token2) as api:
990 for fr in api.ListFriendRequests(empty_pb2.Empty()).received:
991 api.RespondFriendRequest(
992 api_pb2.RespondFriendRequestReq(friend_request_id=fr.friend_request_id, accept=True)
993 )
995 with api_session(token1) as api:
996 res = api.ListFriends(empty_pb2.Empty())
997 assert sorted(res.user_ids) == sorted([user2.id, user4.id])
999 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user2.id))
1001 res = api.ListFriends(empty_pb2.Empty())
1002 assert sorted(res.user_ids) == [user4.id]
1004 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user4.id))
1006 res = api.ListFriends(empty_pb2.Empty())
1007 assert not res.user_ids
1010def test_cant_friend_request_twice(db):
1011 user1, token1 = generate_user()
1012 user2, token2 = generate_user()
1014 # send friend request from user1 to user2
1015 with api_session(token1) as api:
1016 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
1018 with pytest.raises(grpc.RpcError) as e:
1019 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
1020 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1021 assert e.value.details() == "You are already friends with or have sent a friend request to that user."
1024def test_cant_friend_request_pending(db):
1025 user1, token1 = generate_user()
1026 user2, token2 = generate_user()
1028 # send friend request from user1 to user2
1029 with api_session(token1) as api:
1030 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
1032 with api_session(token2) as api:
1033 with pytest.raises(grpc.RpcError) as e:
1034 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user1.id))
1035 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1036 assert e.value.details() == "You are already friends with or have sent a friend request to that user."
1039def test_cant_friend_request_already_friends(db):
1040 user1, token1 = generate_user()
1041 user2, token2 = generate_user()
1042 make_friends(user1, user2)
1044 with api_session(token1) as api:
1045 with pytest.raises(grpc.RpcError) as e:
1046 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
1047 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1048 assert e.value.details() == "You are already friends with or have sent a friend request to that user."
1050 with api_session(token2) as api:
1051 with pytest.raises(grpc.RpcError) as e:
1052 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user1.id))
1053 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1054 assert e.value.details() == "You are already friends with or have sent a friend request to that user."
1057def test_excessive_friend_requests_are_reported(db):
1058 """Test that excessive friend requests are first reported in a warning email and finally lead blocking of further requests."""
1059 user, token = generate_user()
1060 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.friend_request]
1061 with api_session(token) as api:
1062 # Test warning email
1063 with mock_notification_email() as mock_email:
1064 for _ in range(rate_limit_definition.warning_limit):
1065 friend_user, _ = generate_user()
1066 _ = api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=friend_user.id))
1068 assert mock_email.call_count == 0
1069 friend_user, _ = generate_user()
1070 _ = api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=friend_user.id))
1072 assert mock_email.call_count == 1
1073 email = email_fields(mock_email).plain
1074 assert email.startswith(
1075 f"User {user.username} has sent {rate_limit_definition.warning_limit} friend requests in the past {RATE_LIMIT_HOURS} hours."
1076 )
1078 # Test ban after exceeding FRIEND_REQUEST_HARD_LIMIT
1079 with mock_notification_email() as mock_email:
1080 for _ in range(rate_limit_definition.hard_limit - rate_limit_definition.warning_limit - 1):
1081 friend_user, _ = generate_user()
1082 _ = api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=friend_user.id))
1084 assert mock_email.call_count == 0
1085 friend_user, _ = generate_user()
1086 with pytest.raises(grpc.RpcError) as exc_info:
1087 _ = api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=friend_user.id))
1088 assert exc_info.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
1089 assert (
1090 exc_info.value.details()
1091 == "You have sent a lot of friend requests in the past 24 hours. To avoid spam, you can't send any more for now."
1092 )
1094 assert mock_email.call_count == 1
1095 email = email_fields(mock_email).plain
1096 assert email.startswith(
1097 f"User {user.username} has sent {rate_limit_definition.hard_limit} friend requests in the past {RATE_LIMIT_HOURS} hours."
1098 )
1099 assert "The user has been blocked from sending further friend requests for now." in email
1102def test_ListFriends(db, moderator):
1103 user1, token1 = generate_user()
1104 user2, token2 = generate_user()
1105 user3, token3 = generate_user()
1107 # send friend request from user1 to user2 and user3
1108 with api_session(token1) as api:
1109 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
1110 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user3.id))
1111 # sender can see their sent requests (they are the author)
1112 res = api.ListFriendRequests(empty_pb2.Empty())
1113 assert len(res.sent) == 2
1114 user1_to_user2_id = [req for req in res.sent if req.user_id == user2.id][0].friend_request_id
1115 user1_to_user3_id = [req for req in res.sent if req.user_id == user3.id][0].friend_request_id
1117 with api_session(token3) as api:
1118 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
1119 res = api.ListFriendRequests(empty_pb2.Empty())
1120 user3_to_user2_id = res.sent[0].friend_request_id
1122 # Moderator approves the friend requests so recipients can see them
1123 moderator.approve_friend_request(user1_to_user2_id)
1124 moderator.approve_friend_request(user3_to_user2_id)
1126 with api_session(token2) as api:
1127 res = api.ListFriendRequests(empty_pb2.Empty())
1128 assert len(res.received) == 2
1130 # order is an implementation detail
1131 user1_req = [req for req in res.received if req.user_id == user1.id][0]
1132 user3_req = [req for req in res.received if req.user_id == user3.id][0]
1134 assert user1_req.state == api_pb2.FriendRequest.FriendRequestStatus.PENDING
1135 assert user1_req.user_id == user1.id
1136 api.RespondFriendRequest(
1137 api_pb2.RespondFriendRequestReq(friend_request_id=user1_req.friend_request_id, accept=True)
1138 )
1140 assert user3_req.state == api_pb2.FriendRequest.FriendRequestStatus.PENDING
1141 assert user3_req.user_id == user3.id
1142 api.RespondFriendRequest(
1143 api_pb2.RespondFriendRequestReq(friend_request_id=user3_req.friend_request_id, accept=True)
1144 )
1146 # check we now have two friends
1147 res = api.ListFriends(empty_pb2.Empty())
1148 assert len(res.user_ids) == 2
1149 assert user1.id in res.user_ids
1150 assert user3.id in res.user_ids
1152 # Moderator approves user1's friend request to user3 so user3 can see it
1153 moderator.approve_friend_request(user1_to_user3_id)
1155 with api_session(token3) as api:
1156 res = api.ListFriends(empty_pb2.Empty())
1157 assert len(res.user_ids) == 1
1158 assert user2.id in res.user_ids
1160 res = api.ListFriendRequests(empty_pb2.Empty())
1161 assert len(res.received) == 1
1162 assert res.received[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING
1163 assert res.received[0].user_id == user1.id
1164 fr_id = res.received[0].friend_request_id
1165 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=True))
1167 res = api.ListFriends(empty_pb2.Empty())
1168 assert len(res.user_ids) == 2
1169 assert user1.id in res.user_ids
1170 assert user2.id in res.user_ids
1172 with api_session(token1) as api:
1173 res = api.ListFriends(empty_pb2.Empty())
1174 assert len(res.user_ids) == 2
1175 assert user2.id in res.user_ids
1176 assert user3.id in res.user_ids
1179def test_ListMutualFriends(db):
1180 user1, token1 = generate_user()
1181 user2, token2 = generate_user()
1182 user3, token3 = generate_user()
1183 user4, token4 = generate_user()
1184 user5, token5 = generate_user()
1186 # arrange friends like this: 1<->2, 1<->3, 1<->4, 1<->5, 3<->2, 3<->4,
1187 # so 1 and 2 should have mutual friend 3 only
1188 make_friends(user1, user2)
1189 make_friends(user1, user3)
1190 make_friends(user1, user4)
1191 make_friends(user1, user5)
1192 make_friends(user3, user2)
1193 make_friends(user3, user4)
1195 with api_session(token1) as api:
1196 mutual_friends = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user2.id)).mutual_friends
1197 assert len(mutual_friends) == 1
1198 assert mutual_friends[0].user_id == user3.id
1200 # and other way around same
1201 with api_session(token2) as api:
1202 mutual_friends = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user1.id)).mutual_friends
1203 assert len(mutual_friends) == 1
1204 assert mutual_friends[0].user_id == user3.id
1206 # Check pending request doesn't have effect
1207 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user5.id))
1209 mutual_friends = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user1.id)).mutual_friends
1210 assert len(mutual_friends) == 1
1211 assert mutual_friends[0].user_id == user3.id
1213 # both ways
1214 with api_session(token1) as api:
1215 mutual_friends = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user2.id)).mutual_friends
1216 assert len(mutual_friends) == 1
1217 assert mutual_friends[0].user_id == user3.id
1220def test_mutual_friends_self(db):
1221 user1, token1 = generate_user()
1222 user2, token2 = generate_user()
1223 user3, token3 = generate_user()
1224 user4, token4 = generate_user()
1226 make_friends(user1, user2)
1227 make_friends(user2, user3)
1228 make_friends(user1, user4)
1230 with api_session(token1) as api:
1231 res = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user1.id))
1232 assert len(res.mutual_friends) == 0
1234 with api_session(token2) as api:
1235 res = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user2.id))
1236 assert len(res.mutual_friends) == 0
1238 with api_session(token3) as api:
1239 res = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user3.id))
1240 assert len(res.mutual_friends) == 0
1242 with api_session(token4) as api:
1243 res = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user4.id))
1244 assert len(res.mutual_friends) == 0
1247def test_CancelFriendRequest(db):
1248 user1, token1 = generate_user()
1249 user2, token2 = generate_user()
1251 with api_session(token1) as api:
1252 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
1254 res = api.ListFriendRequests(empty_pb2.Empty())
1255 assert res.sent[0].user_id == user2.id
1256 fr_id = res.sent[0].friend_request_id
1258 api.CancelFriendRequest(api_pb2.CancelFriendRequestReq(friend_request_id=fr_id))
1260 # check it's gone
1261 res = api.ListFriendRequests(empty_pb2.Empty())
1262 assert len(res.sent) == 0
1263 assert len(res.received) == 0
1265 # check not friends
1266 res = api.ListFriends(empty_pb2.Empty())
1267 assert len(res.user_ids) == 0
1269 with api_session(token2) as api:
1270 # check it's gone
1271 res = api.ListFriendRequests(empty_pb2.Empty())
1272 assert len(res.sent) == 0
1273 assert len(res.received) == 0
1275 # check we're not friends
1276 res = api.ListFriends(empty_pb2.Empty())
1277 assert len(res.user_ids) == 0
1279 with api_session(token1) as api:
1280 # check we can send another friend req
1281 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
1283 res = api.ListFriendRequests(empty_pb2.Empty())
1284 assert res.sent[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING
1285 assert res.sent[0].user_id == user2.id
1288def test_accept_friend_request(db, moderator):
1289 user1, token1 = generate_user()
1290 user2, token2 = generate_user()
1292 with session_scope() as session:
1293 moderation_state = ModerationState(
1294 object_type=ModerationObjectType.friend_request,
1295 object_id=0,
1296 visibility=ModerationVisibility.visible,
1297 )
1298 session.add(moderation_state)
1299 session.flush()
1300 friend_request = FriendRelationship(
1301 from_user_id=user1.id,
1302 to_user_id=user2.id,
1303 status=FriendStatus.pending,
1304 moderation_state_id=moderation_state.id,
1305 )
1306 session.add(friend_request)
1307 session.flush()
1308 moderation_state.object_id = friend_request.id
1309 session.commit()
1310 friend_request_id = friend_request.id
1312 with api_session(token2) as api:
1313 # check request pending
1314 res = api.ListFriendRequests(empty_pb2.Empty())
1315 assert len(res.received) == 1
1316 assert res.received[0].user_id == user1.id
1318 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=friend_request_id, accept=True))
1320 # check request is gone
1321 res = api.ListFriendRequests(empty_pb2.Empty())
1322 assert len(res.sent) == 0
1323 assert len(res.received) == 0
1325 # check now friends
1326 res = api.ListFriends(empty_pb2.Empty())
1327 assert len(res.user_ids) == 1
1328 assert res.user_ids[0] == user1.id
1330 with api_session(token1) as api:
1331 # check request gone
1332 res = api.ListFriendRequests(empty_pb2.Empty())
1333 assert len(res.sent) == 0
1334 assert len(res.received) == 0
1336 # check now friends
1337 res = api.ListFriends(empty_pb2.Empty())
1338 assert len(res.user_ids) == 1
1339 assert res.user_ids[0] == user2.id
1342def test_reject_friend_request(db, moderator):
1343 user1, token1 = generate_user()
1344 user2, token2 = generate_user()
1346 with api_session(token1) as api:
1347 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
1349 res = api.ListFriendRequests(empty_pb2.Empty())
1350 assert res.sent[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING
1351 assert res.sent[0].user_id == user2.id
1352 fr_id = res.sent[0].friend_request_id
1354 # Moderator approves the friend request so recipient can see it
1355 moderator.approve_friend_request(fr_id)
1357 with api_session(token2) as api:
1358 res = api.ListFriendRequests(empty_pb2.Empty())
1359 assert res.received[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING
1360 assert res.received[0].user_id == user1.id
1362 fr_id = res.received[0].friend_request_id
1364 # reject it
1365 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=False))
1367 # check it's gone
1368 res = api.ListFriendRequests(empty_pb2.Empty())
1369 assert len(res.sent) == 0
1370 assert len(res.received) == 0
1372 # check not friends
1373 res = api.ListFriends(empty_pb2.Empty())
1374 assert len(res.user_ids) == 0
1376 with api_session(token1) as api:
1377 # check it's gone
1378 res = api.ListFriendRequests(empty_pb2.Empty())
1379 assert len(res.sent) == 0
1380 assert len(res.received) == 0
1382 # check we're not friends
1383 res = api.ListFriends(empty_pb2.Empty())
1384 assert len(res.user_ids) == 0
1386 # check we can send another friend req
1387 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
1389 res = api.ListFriendRequests(empty_pb2.Empty())
1390 assert res.sent[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING
1391 assert res.sent[0].user_id == user2.id
1394def test_hosting_preferences(db):
1395 user1, token1 = generate_user()
1396 user2, token2 = generate_user()
1398 with api_session(token1) as api:
1399 res = api.GetUser(api_pb2.GetUserReq(user=user1.username))
1400 assert not res.HasField("max_guests")
1401 assert not res.HasField("last_minute")
1402 assert not res.HasField("has_pets")
1403 assert not res.HasField("accepts_pets")
1404 assert not res.HasField("pet_details")
1405 assert not res.HasField("has_kids")
1406 assert not res.HasField("accepts_kids")
1407 assert not res.HasField("kid_details")
1408 assert not res.HasField("has_housemates")
1409 assert not res.HasField("housemate_details")
1410 assert not res.HasField("wheelchair_accessible")
1411 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_UNKNOWN
1412 assert not res.HasField("smokes_at_home")
1413 assert not res.HasField("drinking_allowed")
1414 assert not res.HasField("drinks_at_home")
1415 assert not res.HasField("other_host_info")
1416 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN
1417 assert not res.HasField("sleeping_details")
1418 assert not res.HasField("area")
1419 assert not res.HasField("house_rules")
1420 assert not res.HasField("parking")
1421 assert res.parking_details == api_pb2.PARKING_DETAILS_UNKNOWN
1422 assert not res.HasField("camping_ok")
1424 api.UpdateProfile(
1425 api_pb2.UpdateProfileReq(
1426 max_guests=api_pb2.NullableUInt32Value(value=3),
1427 last_minute=api_pb2.NullableBoolValue(value=True),
1428 has_pets=api_pb2.NullableBoolValue(value=False),
1429 accepts_pets=api_pb2.NullableBoolValue(value=True),
1430 pet_details=api_pb2.NullableStringValue(value="I love dogs"),
1431 has_kids=api_pb2.NullableBoolValue(value=False),
1432 accepts_kids=api_pb2.NullableBoolValue(value=True),
1433 kid_details=api_pb2.NullableStringValue(value="I hate kids"),
1434 has_housemates=api_pb2.NullableBoolValue(value=False),
1435 housemate_details=api_pb2.NullableStringValue(value="I have 7 housemates"),
1436 wheelchair_accessible=api_pb2.NullableBoolValue(value=True),
1437 smoking_allowed=api_pb2.SMOKING_LOCATION_WINDOW,
1438 area=api_pb2.NullableStringValue(value="area!"),
1439 smokes_at_home=api_pb2.NullableBoolValue(value=False),
1440 drinking_allowed=api_pb2.NullableBoolValue(value=True),
1441 drinks_at_home=api_pb2.NullableBoolValue(value=False),
1442 other_host_info=api_pb2.NullableStringValue(value="I'm pretty swell"),
1443 sleeping_arrangement=api_pb2.SLEEPING_ARRANGEMENT_COMMON,
1444 sleeping_details=api_pb2.NullableStringValue(value="Couch in living room"),
1445 house_rules=api_pb2.NullableStringValue(value="RULES!"),
1446 parking=api_pb2.NullableBoolValue(value=True),
1447 parking_details=api_pb2.PARKING_DETAILS_PAID_ONSITE,
1448 camping_ok=api_pb2.NullableBoolValue(value=False),
1449 )
1450 )
1452 # Use a second user to view the hosting preferences just to check
1453 # that it is public information.
1454 with api_session(token2) as api:
1455 res = api.GetUser(api_pb2.GetUserReq(user=user1.username))
1456 assert res.max_guests.value == 3
1457 assert res.last_minute.value
1458 assert not res.has_pets.value
1459 assert res.accepts_pets.value
1460 assert res.pet_details.value == "I love dogs"
1461 assert not res.has_kids.value
1462 assert res.accepts_kids.value
1463 assert res.kid_details.value == "I hate kids"
1464 assert not res.has_housemates.value
1465 assert res.housemate_details.value == "I have 7 housemates"
1466 assert res.wheelchair_accessible.value
1467 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_WINDOW
1468 assert not res.smokes_at_home.value
1469 assert res.drinking_allowed.value
1470 assert not res.drinks_at_home.value
1471 assert res.other_host_info.value == "I'm pretty swell"
1472 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_COMMON
1473 assert res.sleeping_details.value == "Couch in living room"
1474 assert res.area.value == "area!"
1475 assert res.house_rules.value == "RULES!"
1476 assert res.parking.value
1477 assert res.parking_details == api_pb2.PARKING_DETAILS_PAID_ONSITE
1478 assert not res.camping_ok.value
1480 # test unsetting
1481 with api_session(token1) as api:
1482 api.UpdateProfile(
1483 api_pb2.UpdateProfileReq(
1484 max_guests=api_pb2.NullableUInt32Value(is_null=True),
1485 last_minute=api_pb2.NullableBoolValue(is_null=True),
1486 has_pets=api_pb2.NullableBoolValue(is_null=True),
1487 accepts_pets=api_pb2.NullableBoolValue(is_null=True),
1488 pet_details=api_pb2.NullableStringValue(is_null=True),
1489 has_kids=api_pb2.NullableBoolValue(is_null=True),
1490 accepts_kids=api_pb2.NullableBoolValue(is_null=True),
1491 kid_details=api_pb2.NullableStringValue(is_null=True),
1492 has_housemates=api_pb2.NullableBoolValue(is_null=True),
1493 housemate_details=api_pb2.NullableStringValue(is_null=True),
1494 wheelchair_accessible=api_pb2.NullableBoolValue(is_null=True),
1495 smoking_allowed=api_pb2.SMOKING_LOCATION_UNKNOWN,
1496 area=api_pb2.NullableStringValue(is_null=True),
1497 smokes_at_home=api_pb2.NullableBoolValue(is_null=True),
1498 drinking_allowed=api_pb2.NullableBoolValue(is_null=True),
1499 drinks_at_home=api_pb2.NullableBoolValue(is_null=True),
1500 other_host_info=api_pb2.NullableStringValue(is_null=True),
1501 sleeping_arrangement=api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
1502 sleeping_details=api_pb2.NullableStringValue(is_null=True),
1503 house_rules=api_pb2.NullableStringValue(is_null=True),
1504 parking=api_pb2.NullableBoolValue(is_null=True),
1505 parking_details=api_pb2.PARKING_DETAILS_UNKNOWN,
1506 camping_ok=api_pb2.NullableBoolValue(is_null=True),
1507 )
1508 )
1510 res = api.GetUser(api_pb2.GetUserReq(user=user1.username))
1511 assert not res.HasField("max_guests")
1512 assert not res.HasField("last_minute")
1513 assert not res.HasField("has_pets")
1514 assert not res.HasField("accepts_pets")
1515 assert not res.HasField("pet_details")
1516 assert not res.HasField("has_kids")
1517 assert not res.HasField("accepts_kids")
1518 assert not res.HasField("kid_details")
1519 assert not res.HasField("has_housemates")
1520 assert not res.HasField("housemate_details")
1521 assert not res.HasField("wheelchair_accessible")
1522 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_UNKNOWN
1523 assert not res.HasField("smokes_at_home")
1524 assert not res.HasField("drinking_allowed")
1525 assert not res.HasField("drinks_at_home")
1526 assert not res.HasField("other_host_info")
1527 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN
1528 assert not res.HasField("sleeping_details")
1529 assert not res.HasField("area")
1530 assert not res.HasField("house_rules")
1531 assert not res.HasField("parking")
1532 assert res.parking_details == api_pb2.PARKING_DETAILS_UNKNOWN
1533 assert not res.HasField("camping_ok")
1536def test_badges(db):
1537 user1, _ = generate_user(last_donated=None)
1538 user2, _ = generate_user(last_donated=None)
1539 user3, _ = generate_user(last_donated=None)
1540 user4, token = generate_user(last_donated=None)
1542 update_badges(empty_pb2.Empty())
1544 founder_badge = get_badge_dict()["founder"]
1545 board_member_badge = get_badge_dict()["board_member"]
1547 with api_session(token) as api:
1548 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).badges == ["founder", "board_member"]
1549 assert api.GetUser(api_pb2.GetUserReq(user=user2.username)).badges == ["founder", "board_member"]
1550 assert api.GetUser(api_pb2.GetUserReq(user=user3.username)).badges == []
1552 assert api.ListBadgeUsers(api_pb2.ListBadgeUsersReq(badge_id=founder_badge.id)).user_ids == [1, 2]
1553 res = api.ListBadgeUsers(api_pb2.ListBadgeUsersReq(badge_id=board_member_badge.id, page_size=1))
1554 assert res.user_ids == [1]
1555 res2 = api.ListBadgeUsers(
1556 api_pb2.ListBadgeUsersReq(badge_id=board_member_badge.id, page_token=res.next_page_token)
1557 )
1558 assert res2.user_ids == [2]
1561def test_user_add_badge_is_idempotent(db):
1562 """Test that adding a badge a user already has is a no-op and doesn't send a duplicate notification."""
1563 user, _ = generate_user()
1565 with session_scope() as session:
1566 user_add_badge(session, user.id, "volunteer")
1568 # one badge row, one notification
1569 with session_scope() as session:
1570 badge_count = session.execute(
1571 select(func.count())
1572 .select_from(UserBadge)
1573 .where(UserBadge.user_id == user.id, UserBadge.badge_id == "volunteer")
1574 ).scalar()
1575 assert badge_count == 1
1576 notification_count = session.execute(
1577 select(func.count()).select_from(Notification).where(Notification.user_id == user.id)
1578 ).scalar()
1579 assert notification_count == 1
1581 # add the same badge again
1582 with session_scope() as session:
1583 user_add_badge(session, user.id, "volunteer")
1585 # still one badge row, no new notification
1586 with session_scope() as session:
1587 badge_count = session.execute(
1588 select(func.count())
1589 .select_from(UserBadge)
1590 .where(UserBadge.user_id == user.id, UserBadge.badge_id == "volunteer")
1591 ).scalar()
1592 assert badge_count == 1
1593 notification_count = session.execute(
1594 select(func.count()).select_from(Notification).where(Notification.user_id == user.id)
1595 ).scalar()
1596 assert notification_count == 1
1599@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"])
1600def test_ListBadgeUsers_excludes_ghost_users(db, flag):
1601 """Test that ListBadgeUsers does not return deleted/banned users."""
1602 user1, token1 = generate_user()
1603 user2, _ = generate_user()
1604 user3, _ = generate_user()
1606 volunteer_badge = get_badge_dict()["volunteer"]
1608 # Give all three users the volunteer badge
1609 with session_scope() as session:
1610 user_add_badge(session, user1.id, "volunteer", do_notify=False)
1611 user_add_badge(session, user2.id, "volunteer", do_notify=False)
1612 user_add_badge(session, user3.id, "volunteer", do_notify=False)
1614 # Verify all three users appear in the badge list
1615 with api_session(token1) as api:
1616 res = api.ListBadgeUsers(api_pb2.ListBadgeUsersReq(badge_id=volunteer_badge.id))
1617 assert set(res.user_ids) == {user1.id, user2.id, user3.id}
1619 # Make user2 invisible (deleted or banned)
1620 with session_scope() as session:
1621 db_user2 = session.execute(select(User).where(User.id == user2.id)).scalar_one()
1622 setattr(db_user2, flag, now())
1624 # Now user2 should not appear in the badge list
1625 with api_session(token1) as api:
1626 res = api.ListBadgeUsers(api_pb2.ListBadgeUsersReq(badge_id=volunteer_badge.id))
1627 assert set(res.user_ids) == {user1.id, user3.id}
1630@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"])
1631def test_GetLiteUser_ghost_user_by_username(db, flag):
1632 """Test that GetLiteUser returns a ghost profile for deleted/banned users when querying by username."""
1633 user1, token1 = generate_user()
1634 user2, _ = generate_user()
1636 # Make user2 invisible
1637 with session_scope() as session:
1638 db_user2 = session.merge(user2)
1639 setattr(db_user2, flag, now())
1640 session.commit()
1642 # Refresh the materialized view
1643 refresh_materialized_views_rapid(empty_pb2.Empty())
1645 with api_session(token1) as api:
1646 # Query by username
1647 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username))
1649 assert lite_user.user_id == user2.id
1650 assert lite_user.username == "ghost"
1651 assert lite_user.name == "Deactivated Account"
1652 assert lite_user.lat == 0
1653 assert lite_user.lng == 0
1654 assert lite_user.radius == 0
1655 assert lite_user.city == ""
1656 assert lite_user.age == 0
1657 assert lite_user.avatar_url == ""
1658 assert lite_user.avatar_thumbnail_url == ""
1659 assert not lite_user.has_strong_verification
1662@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"])
1663def test_GetLiteUser_ghost_user_by_id(db, flag):
1664 """Test that GetLiteUser returns a ghost profile for deleted/banned users when querying by ID."""
1665 user1, token1 = generate_user()
1666 user2, _ = generate_user()
1668 # Make user2 invisible
1669 with session_scope() as session:
1670 db_user2 = session.merge(user2)
1671 setattr(db_user2, flag, now())
1672 session.commit()
1674 # Refresh the materialized view
1675 refresh_materialized_views_rapid(empty_pb2.Empty())
1677 with api_session(token1) as api:
1678 # Query by ID
1679 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=str(user2.id)))
1681 assert lite_user.user_id == user2.id
1682 assert lite_user.username == "ghost"
1683 assert lite_user.name == "Deactivated Account"
1684 assert lite_user.lat == 0
1685 assert lite_user.lng == 0
1686 assert lite_user.radius == 0
1687 assert lite_user.city == ""
1688 assert lite_user.age == 0
1689 assert lite_user.avatar_url == ""
1690 assert lite_user.avatar_thumbnail_url == ""
1691 assert not lite_user.has_strong_verification
1694def test_GetLiteUser_blocked_user(db):
1695 """Test that GetLiteUser returns a ghost profile for blocked users."""
1696 user1, token1 = generate_user()
1697 user2, _ = generate_user()
1699 # User1 blocks user2
1700 make_user_block(user1, user2)
1702 # Refresh the materialized view
1703 refresh_materialized_views_rapid(empty_pb2.Empty())
1705 with api_session(token1) as api:
1706 # Query by username
1707 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username))
1709 assert lite_user.user_id == user2.id
1710 assert lite_user.is_ghost
1711 assert lite_user.username == "ghost"
1712 assert lite_user.name == "Deactivated Account"
1714 # Query by ID
1715 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=str(user2.id)))
1717 assert lite_user.user_id == user2.id
1718 assert lite_user.is_ghost
1719 assert lite_user.username == "ghost"
1720 assert lite_user.name == "Deactivated Account"
1723def test_GetLiteUser_blocking_user(db):
1724 """Test that GetLiteUser returns a ghost profile when the target user has blocked the requester."""
1725 user1, token1 = generate_user()
1726 user2, _ = generate_user()
1728 # User2 blocks user1
1729 make_user_block(user2, user1)
1731 # Refresh the materialized view
1732 refresh_materialized_views_rapid(empty_pb2.Empty())
1734 with api_session(token1) as api:
1735 # Query by username
1736 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username))
1738 assert lite_user.user_id == user2.id
1739 assert lite_user.is_ghost
1740 assert lite_user.username == "ghost"
1741 assert lite_user.name == "Deactivated Account"
1743 # Query by ID
1744 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=str(user2.id)))
1746 assert lite_user.user_id == user2.id
1747 assert lite_user.is_ghost
1748 assert lite_user.username == "ghost"
1749 assert lite_user.name == "Deactivated Account"
1752@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"])
1753def test_GetLiteUsers_ghost_users(db, flag):
1754 """Test that GetLiteUsers returns ghost profiles for deleted/banned users."""
1755 user1, token1 = generate_user()
1756 user2, _ = generate_user()
1757 user3, _ = generate_user()
1758 user4, _ = generate_user()
1760 # Make user2 and user4 invisible
1761 with session_scope() as session:
1762 db_user2 = session.merge(user2)
1763 setattr(db_user2, flag, now())
1764 db_user4 = session.merge(user4)
1765 setattr(db_user4, flag, now())
1766 session.commit()
1768 # Refresh the materialized view
1769 refresh_materialized_views_rapid(empty_pb2.Empty())
1771 with api_session(token1) as api:
1772 res = api.GetLiteUsers(
1773 api_pb2.GetLiteUsersReq(
1774 users=[
1775 user1.username, # visible
1776 user2.username, # ghost
1777 str(user3.id), # visible
1778 str(user4.id), # ghost
1779 ]
1780 )
1781 )
1783 assert len(res.responses) == 4
1785 # user1 - visible, normal profile
1786 assert res.responses[0].query == user1.username
1787 assert not res.responses[0].not_found
1788 assert res.responses[0].user.user_id == user1.id
1789 assert res.responses[0].user.username == user1.username
1790 assert res.responses[0].user.name == user1.name
1792 # user2 - ghost by username
1793 assert res.responses[1].query == user2.username
1794 assert not res.responses[1].not_found
1795 assert res.responses[1].user.user_id == user2.id
1796 assert res.responses[1].user.is_ghost
1797 assert res.responses[1].user.username == "ghost"
1798 assert res.responses[1].user.name == "Deactivated Account"
1800 # user3 - visible, normal profile
1801 assert res.responses[2].query == str(user3.id)
1802 assert not res.responses[2].not_found
1803 assert res.responses[2].user.user_id == user3.id
1804 assert res.responses[2].user.username == user3.username
1805 assert res.responses[2].user.name == user3.name
1807 # user4 - ghost by ID
1808 assert res.responses[3].query == str(user4.id)
1809 assert not res.responses[3].not_found
1810 assert res.responses[3].user.user_id == user4.id
1811 assert res.responses[3].user.is_ghost
1812 assert res.responses[3].user.username == "ghost"
1813 assert res.responses[3].user.name == "Deactivated Account"
1816def test_GetLiteUsers_blocked_users(db):
1817 """Test that GetLiteUsers returns ghost profiles for blocked users."""
1818 user1, token1 = generate_user()
1819 user2, _ = generate_user()
1820 user3, _ = generate_user()
1821 user4, _ = generate_user()
1822 user5, _ = generate_user()
1824 # User1 blocks user2
1825 make_user_block(user1, user2)
1826 # User4 blocks user1
1827 make_user_block(user4, user1)
1829 # Refresh the materialized view
1830 refresh_materialized_views_rapid(empty_pb2.Empty())
1832 with api_session(token1) as api:
1833 res = api.GetLiteUsers(
1834 api_pb2.GetLiteUsersReq(
1835 users=[
1836 user2.username, # user1 blocked user2
1837 str(user3.id), # visible
1838 user4.username, # user4 blocked user1
1839 str(user5.id), # visible
1840 ]
1841 )
1842 )
1844 assert len(res.responses) == 4
1846 # user2 - blocked by user1, should be ghost
1847 assert res.responses[0].query == user2.username
1848 assert not res.responses[0].not_found
1849 assert res.responses[0].user.user_id == user2.id
1850 assert res.responses[0].user.is_ghost
1851 assert res.responses[0].user.username == "ghost"
1852 assert res.responses[0].user.name == "Deactivated Account"
1854 # user3 - visible
1855 assert res.responses[1].query == str(user3.id)
1856 assert not res.responses[1].not_found
1857 assert res.responses[1].user.user_id == user3.id
1858 assert res.responses[1].user.username == user3.username
1860 # user4 - user4 blocked user1, should be ghost
1861 assert res.responses[2].query == user4.username
1862 assert not res.responses[2].not_found
1863 assert res.responses[2].user.user_id == user4.id
1864 assert res.responses[2].user.is_ghost
1865 assert res.responses[2].user.username == "ghost"
1866 assert res.responses[2].user.name == "Deactivated Account"
1868 # user5 - visible
1869 assert res.responses[3].query == str(user5.id)
1870 assert not res.responses[3].not_found
1871 assert res.responses[3].user.user_id == user5.id
1872 assert res.responses[3].user.username == user5.username
1875@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"])
1876def test_GetUser_ghost_user_by_id(db, flag):
1877 """Test that GetUser returns a ghost profile for deleted/banned users when querying by ID."""
1878 user1, token1 = generate_user()
1879 user2, _ = generate_user()
1881 # Make user2 invisible
1882 with session_scope() as session:
1883 db_user2 = session.merge(user2)
1884 setattr(db_user2, flag, now())
1885 session.commit()
1887 with api_session(token1) as api:
1888 # Query by ID
1889 user_pb = api.GetUser(api_pb2.GetUserReq(user=str(user2.id)))
1891 assert user_pb.user_id == user2.id
1892 assert user_pb.username == "ghost"
1893 assert user_pb.name == "Deactivated Account"
1894 assert user_pb.city == ""
1895 assert user_pb.hosting_status == 0
1896 assert user_pb.meetup_status == 0
1899def test_GetUser_blocked_user(db):
1900 """Test that GetUser returns a ghost profile for blocked users."""
1901 user1, token1 = generate_user()
1902 user2, _ = generate_user()
1904 # User1 blocks user2
1905 make_user_block(user1, user2)
1907 with api_session(token1) as api:
1908 # Query by username
1909 user_pb = api.GetUser(api_pb2.GetUserReq(user=user2.username))
1911 assert user_pb.user_id == user2.id
1912 assert user_pb.username == "ghost"
1913 assert user_pb.name == "Deactivated Account"
1915 # Query by ID
1916 user_pb = api.GetUser(api_pb2.GetUserReq(user=str(user2.id)))
1918 assert user_pb.user_id == user2.id
1919 assert user_pb.username == "ghost"
1920 assert user_pb.name == "Deactivated Account"
1923def test_GetUser_blocking_user(db):
1924 """Test that GetUser returns a ghost profile when the target user has blocked the requester."""
1925 user1, token1 = generate_user()
1926 user2, _ = generate_user()
1928 # User2 blocks user1
1929 make_user_block(user2, user1)
1931 with api_session(token1) as api:
1932 # Query by username
1933 user_pb = api.GetUser(api_pb2.GetUserReq(user=user2.username))
1935 assert user_pb.user_id == user2.id
1936 assert user_pb.username == "ghost"
1937 assert user_pb.name == "Deactivated Account"
1939 # Query by ID
1940 user_pb = api.GetUser(api_pb2.GetUserReq(user=str(user2.id)))
1942 assert user_pb.user_id == user2.id
1943 assert user_pb.username == "ghost"
1944 assert user_pb.name == "Deactivated Account"