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

1from datetime import timedelta 

2 

3import grpc 

4import pytest 

5from google.protobuf import empty_pb2, wrappers_pb2 

6from sqlalchemy import func, select, update 

7 

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) 

40 

41 

42@pytest.fixture(autouse=True) 

43def _(testconfig): 

44 pass 

45 

46 

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 ) 

56 

57 with real_api_session(token) as api: 

58 res = api.Ping(api_pb2.PingReq()) 

59 

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 

71 

72 assert (res.user.lat, res.user.lng) == user.coordinates 

73 

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 

79 

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 

82 

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 

92 

93 assert res.user.friends == api_pb2.User.FriendshipStatus.NA 

94 assert not res.user.HasField("pending_friend_request") 

95 

96 

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() 

101 

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 

109 

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 

116 

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)) 

121 

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 

126 

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 

131 

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 

137 

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 

142 

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 ) 

151 

152 assert not res.jailed 

153 assert not res.needs_to_update_location 

154 

155 res = jail.JailInfo(empty_pb2.Empty()) 

156 assert not res.jailed 

157 assert not res.needs_to_update_location 

158 

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 

165 

166 

167def test_get_user(db): 

168 user1, token1 = generate_user() 

169 user2, token2 = generate_user() 

170 

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 

176 

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 

182 

183 

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() 

188 

189 with session_scope() as session: 

190 session.execute(update(User).where(User.id == user2.id).values(**{flag: func.now()})) 

191 

192 refresh_materialized_views_rapid(empty_pb2.Empty()) 

193 

194 with api_session(token1) as api: 

195 user_pb = api.GetUser(api_pb2.GetUserReq(user=user2.username)) 

196 

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 ) 

205 

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 

233 

234 with api_session(token1) as api: 

235 lite_user_pb = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username)) 

236 

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 

249 

250 

251def test_user_model_to_pb_ghost_user_blocked(db): 

252 user1, token1 = generate_user() 

253 user2, _ = generate_user() 

254 

255 with blocking_session(token1) as user_blocks: 

256 user_blocks.BlockUser(blocking_pb2.BlockUserReq(username=user2.username)) 

257 

258 refresh_materialized_views_rapid(empty_pb2.Empty()) 

259 

260 with api_session(token1) as api: 

261 user_pb = api.GetUser(api_pb2.GetUserReq(user=user2.username)) 

262 

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 ) 

271 

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 

299 

300 with api_session(token1) as api: 

301 lite_user_pb = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username)) 

302 

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 

315 

316 

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() 

321 

322 with session_scope() as session: 

323 session.execute(update(User).where(User.id == user.id).values(**{flag: func.now()})) 

324 

325 with admin_session(token_admin) as api: 

326 user_pb = api.GetUser(admin_pb2.GetUserReq(user=user.username)) 

327 

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 ) 

340 

341 

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() 

346 

347 refresh_materialized_views_rapid(empty_pb2.Empty()) 

348 

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 

356 

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 

363 

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)) 

368 

369 refresh_materialized_views_rapid(empty_pb2.Empty()) 

370 

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 

375 

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 

380 

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 

386 

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 

391 

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 ) 

400 

401 assert not res.jailed 

402 assert not res.needs_to_update_location 

403 

404 res = jail.JailInfo(empty_pb2.Empty()) 

405 assert not res.jailed 

406 assert not res.needs_to_update_location 

407 

408 refresh_materialized_views_rapid(empty_pb2.Empty()) 

409 

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 

416 

417 

418def test_lite_get_user(db): 

419 user1, token1 = generate_user() 

420 user2, token2 = generate_user() 

421 

422 refresh_materialized_views_rapid(empty_pb2.Empty()) 

423 

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 

429 

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 

435 

436 

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() 

444 

445 make_user_block(user4, user1) 

446 

447 refresh_materialized_views_rapid(empty_pb2.Empty()) 

448 

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 ) 

465 

466 assert len(res.responses) == 9 

467 assert res.responses[0].query == user1.username 

468 assert res.responses[0].user.user_id == user1.id 

469 

470 assert res.responses[1].query == str(user1.id) 

471 assert res.responses[1].user.user_id == user1.id 

472 

473 assert res.responses[2].query == "nonexistent" 

474 assert res.responses[2].not_found 

475 

476 assert res.responses[3].query == str(user2.id) 

477 assert res.responses[3].user.user_id == user2.id 

478 

479 assert res.responses[4].query == "9994" 

480 assert res.responses[4].not_found 

481 

482 assert res.responses[5].query == user6.username 

483 assert res.responses[5].user.user_id == user6.id 

484 

485 assert res.responses[6].query == str(user5.id) 

486 assert res.responses[6].user.user_id == user5.id 

487 

488 assert res.responses[7].query == "notreal" 

489 assert res.responses[7].not_found 

490 

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" 

498 

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." 

504 

505 

506def test_update_profile(db): 

507 user, token = generate_user() 

508 

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." 

514 

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." 

521 

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." 

528 

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." 

535 

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 ) 

565 

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"] 

586 

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 ) 

606 

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 

622 

623 

624def test_update_profile_do_not_email(db): 

625 user, token = generate_user() 

626 

627 with notifications_session(token) as notifications: 

628 notifications.SetNotificationSettings(notifications_pb2.SetNotificationSettingsReq(enable_do_not_email=True)) 

629 

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." 

635 

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." 

640 

641 

642def test_language_abilities(db): 

643 user, token = generate_user( 

644 language_abilities=[ 

645 ("fin", LanguageFluency.fluent), 

646 ("fra", LanguageFluency.beginner), 

647 ], 

648 ) 

649 

650 with api_session(token) as api: 

651 res = api.GetUser(api_pb2.GetUserReq(user=user.username)) 

652 assert len(res.language_abilities) == 2 

653 

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." 

670 

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) 

690 

691 # nothing changed 

692 res = api.GetUser(api_pb2.GetUserReq(user=user.username)) 

693 assert len(res.language_abilities) == 2 

694 

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 ) 

708 

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 

713 

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 ) 

727 

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 

732 

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 ) 

746 

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 

751 

752 # don't change it 

753 api.UpdateProfile(api_pb2.UpdateProfileReq()) 

754 

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 

759 

760 # remove value 

761 api.UpdateProfile( 

762 api_pb2.UpdateProfileReq( 

763 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

764 value=[], 

765 ), 

766 ) 

767 ) 

768 

769 res = api.GetUser(api_pb2.GetUserReq(user=user.username)) 

770 assert len(res.language_abilities) == 0 

771 

772 

773def test_pending_friend_request_count(db, moderator): 

774 user1, token1 = generate_user() 

775 user2, token2 = generate_user() 

776 user3, token3 = generate_user() 

777 

778 with api_session(token2) as api: 

779 res = api.Ping(api_pb2.PingReq()) 

780 assert res.pending_friend_request_count == 0 

781 

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 

789 

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 

795 

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 

800 

801 # Moderator approves the friend request 

802 moderator.approve_friend_request(fr_id) 

803 

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 

808 

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 

814 

815 assert res.received[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING 

816 assert res.received[0].user_id == user1.id 

817 

818 fr_id = res.received[0].friend_request_id 

819 

820 # accept it 

821 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=True)) 

822 

823 res = api.Ping(api_pb2.PingReq()) 

824 assert res.pending_friend_request_count == 0 

825 

826 

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() 

831 

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)) 

835 

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 

843 

844 # Notification is deferred while content is SHADOWED 

845 # No push notification sent yet 

846 assert push_collector.count_for_user(user2.id) == 0 

847 

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 

853 

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 

857 

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 

863 

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) 

867 

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." 

871 

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 

884 

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 

891 

892 assert res.received[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING 

893 assert res.received[0].user_id == user1.id 

894 

895 fr_id = res.received[0].friend_request_id 

896 

897 # accept it 

898 with mock_notification_email() as mock: 

899 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=True)) 

900 

901 # check it's gone 

902 res = api.ListFriendRequests(empty_pb2.Empty()) 

903 assert len(res.sent) == 0 

904 assert len(res.received) == 0 

905 

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 

910 

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}." 

916 

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 

929 

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 

935 

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 

940 

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!" 

947 

948 # we can unfriend 

949 res = api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user2.id)) 

950 

951 res = api.ListFriends(empty_pb2.Empty()) 

952 assert len(res.user_ids) == 0 

953 

954 

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() 

962 

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)) 

967 

968 with api_session(token5) as api: 

969 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user1.id)) 

970 

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)) 

974 

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) 

980 

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 ) 

988 

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 ) 

994 

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]) 

998 

999 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user2.id)) 

1000 

1001 res = api.ListFriends(empty_pb2.Empty()) 

1002 assert sorted(res.user_ids) == [user4.id] 

1003 

1004 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user4.id)) 

1005 

1006 res = api.ListFriends(empty_pb2.Empty()) 

1007 assert not res.user_ids 

1008 

1009 

1010def test_cant_friend_request_twice(db): 

1011 user1, token1 = generate_user() 

1012 user2, token2 = generate_user() 

1013 

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)) 

1017 

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." 

1022 

1023 

1024def test_cant_friend_request_pending(db): 

1025 user1, token1 = generate_user() 

1026 user2, token2 = generate_user() 

1027 

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)) 

1031 

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." 

1037 

1038 

1039def test_cant_friend_request_already_friends(db): 

1040 user1, token1 = generate_user() 

1041 user2, token2 = generate_user() 

1042 make_friends(user1, user2) 

1043 

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." 

1049 

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." 

1055 

1056 

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)) 

1067 

1068 assert mock_email.call_count == 0 

1069 friend_user, _ = generate_user() 

1070 _ = api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=friend_user.id)) 

1071 

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 ) 

1077 

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)) 

1083 

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 ) 

1093 

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 

1100 

1101 

1102def test_ListFriends(db, moderator): 

1103 user1, token1 = generate_user() 

1104 user2, token2 = generate_user() 

1105 user3, token3 = generate_user() 

1106 

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 

1116 

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 

1121 

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) 

1125 

1126 with api_session(token2) as api: 

1127 res = api.ListFriendRequests(empty_pb2.Empty()) 

1128 assert len(res.received) == 2 

1129 

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] 

1133 

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 ) 

1139 

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 ) 

1145 

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 

1151 

1152 # Moderator approves user1's friend request to user3 so user3 can see it 

1153 moderator.approve_friend_request(user1_to_user3_id) 

1154 

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 

1159 

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)) 

1166 

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 

1171 

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 

1177 

1178 

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() 

1185 

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) 

1194 

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 

1199 

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 

1205 

1206 # Check pending request doesn't have effect 

1207 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user5.id)) 

1208 

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 

1212 

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 

1218 

1219 

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() 

1225 

1226 make_friends(user1, user2) 

1227 make_friends(user2, user3) 

1228 make_friends(user1, user4) 

1229 

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 

1233 

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 

1237 

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 

1241 

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 

1245 

1246 

1247def test_CancelFriendRequest(db): 

1248 user1, token1 = generate_user() 

1249 user2, token2 = generate_user() 

1250 

1251 with api_session(token1) as api: 

1252 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id)) 

1253 

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 

1257 

1258 api.CancelFriendRequest(api_pb2.CancelFriendRequestReq(friend_request_id=fr_id)) 

1259 

1260 # check it's gone 

1261 res = api.ListFriendRequests(empty_pb2.Empty()) 

1262 assert len(res.sent) == 0 

1263 assert len(res.received) == 0 

1264 

1265 # check not friends 

1266 res = api.ListFriends(empty_pb2.Empty()) 

1267 assert len(res.user_ids) == 0 

1268 

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 

1274 

1275 # check we're not friends 

1276 res = api.ListFriends(empty_pb2.Empty()) 

1277 assert len(res.user_ids) == 0 

1278 

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)) 

1282 

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 

1286 

1287 

1288def test_accept_friend_request(db, moderator): 

1289 user1, token1 = generate_user() 

1290 user2, token2 = generate_user() 

1291 

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 

1311 

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 

1317 

1318 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=friend_request_id, accept=True)) 

1319 

1320 # check request is gone 

1321 res = api.ListFriendRequests(empty_pb2.Empty()) 

1322 assert len(res.sent) == 0 

1323 assert len(res.received) == 0 

1324 

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 

1329 

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 

1335 

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 

1340 

1341 

1342def test_reject_friend_request(db, moderator): 

1343 user1, token1 = generate_user() 

1344 user2, token2 = generate_user() 

1345 

1346 with api_session(token1) as api: 

1347 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id)) 

1348 

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 

1353 

1354 # Moderator approves the friend request so recipient can see it 

1355 moderator.approve_friend_request(fr_id) 

1356 

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 

1361 

1362 fr_id = res.received[0].friend_request_id 

1363 

1364 # reject it 

1365 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=False)) 

1366 

1367 # check it's gone 

1368 res = api.ListFriendRequests(empty_pb2.Empty()) 

1369 assert len(res.sent) == 0 

1370 assert len(res.received) == 0 

1371 

1372 # check not friends 

1373 res = api.ListFriends(empty_pb2.Empty()) 

1374 assert len(res.user_ids) == 0 

1375 

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 

1381 

1382 # check we're not friends 

1383 res = api.ListFriends(empty_pb2.Empty()) 

1384 assert len(res.user_ids) == 0 

1385 

1386 # check we can send another friend req 

1387 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id)) 

1388 

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 

1392 

1393 

1394def test_hosting_preferences(db): 

1395 user1, token1 = generate_user() 

1396 user2, token2 = generate_user() 

1397 

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") 

1423 

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 ) 

1451 

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 

1479 

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 ) 

1509 

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") 

1534 

1535 

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) 

1541 

1542 update_badges(empty_pb2.Empty()) 

1543 

1544 founder_badge = get_badge_dict()["founder"] 

1545 board_member_badge = get_badge_dict()["board_member"] 

1546 

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 == [] 

1551 

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] 

1559 

1560 

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() 

1564 

1565 with session_scope() as session: 

1566 user_add_badge(session, user.id, "volunteer") 

1567 

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 

1580 

1581 # add the same badge again 

1582 with session_scope() as session: 

1583 user_add_badge(session, user.id, "volunteer") 

1584 

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 

1597 

1598 

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() 

1605 

1606 volunteer_badge = get_badge_dict()["volunteer"] 

1607 

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) 

1613 

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} 

1618 

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()) 

1623 

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} 

1628 

1629 

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() 

1635 

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() 

1641 

1642 # Refresh the materialized view 

1643 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1644 

1645 with api_session(token1) as api: 

1646 # Query by username 

1647 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username)) 

1648 

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 

1660 

1661 

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() 

1667 

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() 

1673 

1674 # Refresh the materialized view 

1675 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1676 

1677 with api_session(token1) as api: 

1678 # Query by ID 

1679 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=str(user2.id))) 

1680 

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 

1692 

1693 

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() 

1698 

1699 # User1 blocks user2 

1700 make_user_block(user1, user2) 

1701 

1702 # Refresh the materialized view 

1703 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1704 

1705 with api_session(token1) as api: 

1706 # Query by username 

1707 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username)) 

1708 

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" 

1713 

1714 # Query by ID 

1715 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=str(user2.id))) 

1716 

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" 

1721 

1722 

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() 

1727 

1728 # User2 blocks user1 

1729 make_user_block(user2, user1) 

1730 

1731 # Refresh the materialized view 

1732 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1733 

1734 with api_session(token1) as api: 

1735 # Query by username 

1736 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username)) 

1737 

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" 

1742 

1743 # Query by ID 

1744 lite_user = api.GetLiteUser(api_pb2.GetLiteUserReq(user=str(user2.id))) 

1745 

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" 

1750 

1751 

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() 

1759 

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() 

1767 

1768 # Refresh the materialized view 

1769 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1770 

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 ) 

1782 

1783 assert len(res.responses) == 4 

1784 

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 

1791 

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" 

1799 

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 

1806 

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" 

1814 

1815 

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() 

1823 

1824 # User1 blocks user2 

1825 make_user_block(user1, user2) 

1826 # User4 blocks user1 

1827 make_user_block(user4, user1) 

1828 

1829 # Refresh the materialized view 

1830 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1831 

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 ) 

1843 

1844 assert len(res.responses) == 4 

1845 

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" 

1853 

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 

1859 

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" 

1867 

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 

1873 

1874 

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() 

1880 

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() 

1886 

1887 with api_session(token1) as api: 

1888 # Query by ID 

1889 user_pb = api.GetUser(api_pb2.GetUserReq(user=str(user2.id))) 

1890 

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 

1897 

1898 

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() 

1903 

1904 # User1 blocks user2 

1905 make_user_block(user1, user2) 

1906 

1907 with api_session(token1) as api: 

1908 # Query by username 

1909 user_pb = api.GetUser(api_pb2.GetUserReq(user=user2.username)) 

1910 

1911 assert user_pb.user_id == user2.id 

1912 assert user_pb.username == "ghost" 

1913 assert user_pb.name == "Deactivated Account" 

1914 

1915 # Query by ID 

1916 user_pb = api.GetUser(api_pb2.GetUserReq(user=str(user2.id))) 

1917 

1918 assert user_pb.user_id == user2.id 

1919 assert user_pb.username == "ghost" 

1920 assert user_pb.name == "Deactivated Account" 

1921 

1922 

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() 

1927 

1928 # User2 blocks user1 

1929 make_user_block(user2, user1) 

1930 

1931 with api_session(token1) as api: 

1932 # Query by username 

1933 user_pb = api.GetUser(api_pb2.GetUserReq(user=user2.username)) 

1934 

1935 assert user_pb.user_id == user2.id 

1936 assert user_pb.username == "ghost" 

1937 assert user_pb.name == "Deactivated Account" 

1938 

1939 # Query by ID 

1940 user_pb = api.GetUser(api_pb2.GetUserReq(user=str(user2.id))) 

1941 

1942 assert user_pb.user_id == user2.id 

1943 assert user_pb.username == "ghost" 

1944 assert user_pb.name == "Deactivated Account"