Coverage for app/backend/src/tests/test_api.py: 100%

1201 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-03 11:56 +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 NonvisibleUserAccess, 

20 NonvisibleUserAccessType, 

21 NonvisibleUserState, 

22 RateLimitAction, 

23 RateLimitViolation, 

24 User, 

25 UserBadge, 

26) 

27from couchers.models.notifications import Notification 

28from couchers.proto import admin_pb2, api_pb2, blocking_pb2, jail_pb2, notifications_pb2 

29from couchers.rate_limits.definitions import RATE_LIMIT_DEFINITIONS, RATE_LIMIT_HOURS 

30from couchers.resources import get_badge_dict 

31from couchers.utils import create_coordinate, now, to_aware_datetime 

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

33from tests.fixtures.misc import EmailCollector, PushCollector 

34from tests.fixtures.sessions import ( 

35 api_session, 

36 blocking_session, 

37 notifications_session, 

38 real_api_session, 

39 real_jail_session, 

40) 

41from tests.fixtures.sessions import ( 

42 real_admin_session as admin_session, 

43) 

44 

45 

46def test_ping(db): 

47 user, token = generate_user( 

48 regions_lived=["ESP", "FRA", "EST"], 

49 regions_visited=["CHE", "REU", "FIN"], 

50 language_abilities=[ 

51 ("fin", LanguageFluency.fluent), 

52 ("fra", LanguageFluency.beginner), 

53 ], 

54 ) 

55 

56 with real_api_session(token) as api: 

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

58 

59 assert res.user.user_id == user.id 

60 assert res.user.username == user.username 

61 assert res.user.name == user.name 

62 assert res.user.city == user.city 

63 assert res.user.hometown == user.hometown 

64 assert res.user.verification == 0.0 

65 assert res.user.community_standing == user.community_standing 

66 assert res.user.num_references == 0 

67 assert res.user.gender == user.gender 

68 assert res.user.pronouns == user.pronouns 

69 assert res.user.age == user.age 

70 

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

72 

73 # the joined time is fuzzed 

74 # but shouldn't be before actual joined time, or more than one hour behind 

75 assert user.joined - timedelta(hours=1) <= to_aware_datetime(res.user.joined) <= user.joined 

76 # same for last_active 

77 assert user.last_active - timedelta(hours=1) <= to_aware_datetime(res.user.last_active) <= user.last_active 

78 

79 assert res.user.hosting_status == api_pb2.HOSTING_STATUS_CANT_HOST 

80 assert res.user.meetup_status == api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP 

81 

82 assert res.user.occupation == user.occupation 

83 assert res.user.education == user.education 

84 assert res.user.about_me == user.about_me 

85 assert res.user.things_i_like == user.things_i_like 

86 assert {language_ability.code for language_ability in res.user.language_abilities} == {"fin", "fra"} 

87 assert res.user.about_place == user.about_place 

88 assert res.user.regions_visited == ["FIN", "REU", "CHE"] # Tests alphabetization by region name 

89 assert res.user.regions_lived == ["EST", "FRA", "ESP"] # Ditto 

90 assert res.user.additional_information == user.additional_information 

91 

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

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

94 

95 

96def test_coords(db): 

97 # make them need to update location 

98 user1, token1 = generate_user(geom=create_coordinate(1, 0), geom_radius=2000, needs_to_update_location=True) 

99 user2, token2 = generate_user() 

100 

101 with api_session(token2) as api: 

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

103 assert res.user.city == user2.city 

104 lat, lng = user2.coordinates 

105 assert res.user.lat == lat 

106 assert res.user.lng == lng 

107 assert res.user.radius == user2.geom_radius 

108 

109 with api_session(token2) as api: 

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

111 assert res.city == user1.city 

112 assert res.lat == 1.0 

113 assert res.lng == 0.0 

114 assert res.radius == 2000.0 

115 

116 # Check coordinate wrapping 

117 user3, token3 = generate_user(geom=create_coordinate(40.0, -180.5)) 

118 user4, token4 = generate_user(geom=create_coordinate(40.0, 20.0)) 

119 user5, token5 = generate_user(geom=create_coordinate(90.5, 20.0)) 

120 

121 with api_session(token3) as api: 

122 res = api.GetUser(api_pb2.GetUserReq(user=user3.username)) 

123 assert res.lat == 40.0 

124 assert res.lng == 179.5 

125 

126 with api_session(token4) as api: 

127 res = api.GetUser(api_pb2.GetUserReq(user=user4.username)) 

128 assert res.lat == 40.0 

129 assert res.lng == 20.0 

130 

131 # PostGIS does not wrap longitude for latitude overflow 

132 with api_session(token5) as api: 

133 res = api.GetUser(api_pb2.GetUserReq(user=user5.username)) 

134 assert res.lat == 89.5 

135 assert res.lng == 20.0 

136 

137 with real_jail_session(token1) as jail: 

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

139 assert res.jailed 

140 assert res.needs_to_update_location 

141 

142 res = jail.SetLocation( 

143 jail_pb2.SetLocationReq( 

144 city="New York City", 

145 lat=40.7812, 

146 lng=-73.9647, 

147 radius=250, 

148 ) 

149 ) 

150 

151 assert not res.jailed 

152 assert not res.needs_to_update_location 

153 

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

155 assert not res.jailed 

156 assert not res.needs_to_update_location 

157 

158 with api_session(token2) as api: 

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

160 assert res.city == "New York City" 

161 assert res.lat == 40.7812 

162 assert res.lng == -73.9647 

163 assert res.radius == 250 

164 

165 

166def test_get_user(db): 

167 user1, token1 = generate_user() 

168 user2, token2 = generate_user() 

169 

170 with api_session(token1) as api: 

171 res = api.GetUser(api_pb2.GetUserReq(user=user2.username)) 

172 assert res.user_id == user2.id 

173 assert res.username == user2.username 

174 assert res.name == user2.name 

175 

176 with api_session(token1) as api: 

177 res = api.GetUser(api_pb2.GetUserReq(user=str(user2.id))) 

178 assert res.user_id == user2.id 

179 assert res.username == user2.username 

180 assert res.name == user2.name 

181 

182 

183@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"]) 

184def test_user_model_to_pb_ghost_user(db, flag): 

185 user1, token1 = generate_user() 

186 user2, _ = generate_user() 

187 

188 with session_scope() as session: 

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

190 

191 refresh_materialized_views_rapid(empty_pb2.Empty()) 

192 

193 with api_session(token1) as api: 

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

195 

196 assert user_pb.user_id == user2.id 

197 assert user_pb.is_ghost 

198 assert user_pb.username == "ghost" 

199 assert user_pb.name == "Deactivated Account" 

200 assert ( 

201 user_pb.about_me 

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

203 ) 

204 

205 assert user_pb.lat == 0 

206 assert user_pb.lng == 0 

207 assert user_pb.radius == 0 

208 assert user_pb.verification == 0.0 

209 assert user_pb.community_standing == 0.0 

210 assert user_pb.num_references == 0 

211 assert user_pb.age == 0 

212 assert user_pb.hosting_status == 0 

213 assert user_pb.meetup_status == 0 

214 assert user_pb.city == "" 

215 assert user_pb.hometown == "" 

216 assert user_pb.timezone == "" 

217 assert user_pb.gender == "" 

218 assert user_pb.pronouns == "" 

219 assert user_pb.occupation == "" 

220 assert user_pb.education == "" 

221 assert user_pb.things_i_like == "" 

222 assert user_pb.about_place == "" 

223 assert user_pb.additional_information == "" 

224 assert list(user_pb.language_abilities) == [] 

225 assert list(user_pb.regions_visited) == [] 

226 assert list(user_pb.regions_lived) == [] 

227 assert list(user_pb.badges) == [] 

228 assert user_pb.friends == api_pb2.User.FriendshipStatus.NOT_FRIENDS 

229 assert user_pb.avatar_url == "" 

230 assert user_pb.avatar_thumbnail_url == "" 

231 assert not user_pb.has_strong_verification 

232 

233 with api_session(token1) as api: 

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

235 

236 assert lite_user_pb.user_id == user2.id 

237 assert lite_user_pb.is_ghost 

238 assert lite_user_pb.username == "ghost" 

239 assert lite_user_pb.name == "Deactivated Account" 

240 assert lite_user_pb.city == "" 

241 assert lite_user_pb.age == 0 

242 assert lite_user_pb.avatar_url == "" 

243 assert lite_user_pb.avatar_thumbnail_url == "" 

244 assert lite_user_pb.lat == 0 

245 assert lite_user_pb.lng == 0 

246 assert lite_user_pb.radius == 0 

247 assert not lite_user_pb.has_strong_verification 

248 

249 

250def test_user_model_to_pb_ghost_user_blocked(db): 

251 user1, token1 = generate_user() 

252 user2, _ = generate_user() 

253 

254 with blocking_session(token1) as user_blocks: 

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

256 

257 refresh_materialized_views_rapid(empty_pb2.Empty()) 

258 

259 with api_session(token1) as api: 

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

261 

262 assert user_pb.user_id == user2.id 

263 assert user_pb.is_ghost 

264 assert user_pb.username == "ghost" 

265 assert user_pb.name == "Deactivated Account" 

266 assert ( 

267 user_pb.about_me 

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

269 ) 

270 

271 assert user_pb.lat == 0 

272 assert user_pb.lng == 0 

273 assert user_pb.radius == 0 

274 assert user_pb.verification == 0.0 

275 assert user_pb.community_standing == 0.0 

276 assert user_pb.num_references == 0 

277 assert user_pb.age == 0 

278 assert user_pb.hosting_status == 0 

279 assert user_pb.meetup_status == 0 

280 assert user_pb.city == "" 

281 assert user_pb.hometown == "" 

282 assert user_pb.timezone == "" 

283 assert user_pb.gender == "" 

284 assert user_pb.pronouns == "" 

285 assert user_pb.occupation == "" 

286 assert user_pb.education == "" 

287 assert user_pb.things_i_like == "" 

288 assert user_pb.about_place == "" 

289 assert user_pb.additional_information == "" 

290 assert list(user_pb.language_abilities) == [] 

291 assert list(user_pb.regions_visited) == [] 

292 assert list(user_pb.regions_lived) == [] 

293 assert list(user_pb.badges) == [] 

294 assert user_pb.friends == api_pb2.User.FriendshipStatus.NOT_FRIENDS 

295 assert user_pb.avatar_url == "" 

296 assert user_pb.avatar_thumbnail_url == "" 

297 assert not user_pb.has_strong_verification 

298 

299 with api_session(token1) as api: 

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

301 

302 assert lite_user_pb.user_id == user2.id 

303 assert lite_user_pb.is_ghost 

304 assert lite_user_pb.username == "ghost" 

305 assert lite_user_pb.name == "Deactivated Account" 

306 assert lite_user_pb.city == "" 

307 assert lite_user_pb.age == 0 

308 assert lite_user_pb.avatar_url == "" 

309 assert lite_user_pb.avatar_thumbnail_url == "" 

310 assert lite_user_pb.lat == 0 

311 assert lite_user_pb.lng == 0 

312 assert lite_user_pb.radius == 0 

313 assert not lite_user_pb.has_strong_verification 

314 

315 

316@pytest.mark.parametrize( 

317 "flag,state", 

318 [ 

319 ("banned_at", NonvisibleUserState.banned), 

320 ("shadowed_at", NonvisibleUserState.shadowed), 

321 ("deleted_at", NonvisibleUserState.deleted), 

322 ], 

323) 

324def test_viewing_nonvisible_user_profile_is_logged(db, flag, state): 

325 user1, token1 = generate_user() 

326 user2, _ = generate_user() 

327 

328 with session_scope() as session: 

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

330 

331 refresh_materialized_views_rapid(empty_pb2.Empty()) 

332 

333 with api_session(token1) as api: 

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

335 assert user_pb.is_ghost 

336 

337 with session_scope() as session: 

338 access = session.execute(select(NonvisibleUserAccess)).scalar_one() 

339 assert access.access_type == NonvisibleUserAccessType.ghost_served 

340 assert access.target_state == state 

341 assert access.target_user_id == user2.id 

342 assert access.actor_user_id == user1.id 

343 assert access.ip_address is None 

344 assert access.user_agent is None 

345 assert access.sofa is None 

346 

347 

348def test_viewing_blocked_user_profile_is_not_logged(db): 

349 user1, token1 = generate_user() 

350 user2, _ = generate_user() 

351 

352 with blocking_session(token1) as user_blocks: 

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

354 

355 refresh_materialized_views_rapid(empty_pb2.Empty()) 

356 

357 with api_session(token1) as api: 

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

359 assert user_pb.is_ghost 

360 

361 with session_scope() as session: 

362 assert session.execute(select(func.count()).select_from(NonvisibleUserAccess)).scalar_one() == 0 

363 

364 

365@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"]) 

366def test_admin_viewing_ghost_users_sees_full_profile(db, flag): 

367 admin, token_admin = generate_user(is_superuser=True) 

368 user, _ = generate_user() 

369 

370 with session_scope() as session: 

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

372 

373 with admin_session(token_admin) as api: 

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

375 

376 assert user_pb.user_id == user.id 

377 assert user_pb.username == user.username 

378 assert user_pb.name == user.name 

379 assert user_pb.city == user.city 

380 assert user_pb.name != "Deactivated Account" 

381 assert user_pb.username != "ghost" 

382 assert user_pb.hosting_status in ( 

383 api_pb2.HOSTING_STATUS_UNKNOWN, 

384 api_pb2.HOSTING_STATUS_CAN_HOST, 

385 api_pb2.HOSTING_STATUS_MAYBE, 

386 api_pb2.HOSTING_STATUS_CANT_HOST, 

387 ) 

388 

389 

390def test_lite_coords(db): 

391 # make them need to update location 

392 user1, token1 = generate_user(geom=create_coordinate(0, 0), geom_radius=0, needs_to_update_location=True) 

393 user2, token2 = generate_user() 

394 

395 refresh_materialized_views_rapid(empty_pb2.Empty()) 

396 

397 with api_session(token2) as api: 

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

399 assert res.user.city == user2.city 

400 lat, lng = user2.coordinates 

401 assert res.user.lat == lat 

402 assert res.user.lng == lng 

403 assert res.user.radius == user2.geom_radius 

404 

405 with api_session(token2) as api: 

406 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user1.username)) 

407 assert res.city == user1.city 

408 assert res.lat == 0.0 

409 assert res.lng == 0.0 

410 assert res.radius == 0.0 

411 

412 # Check coordinate wrapping 

413 user3, token3 = generate_user(geom=create_coordinate(40.0, -180.5)) 

414 user4, token4 = generate_user(geom=create_coordinate(40.0, 20.0)) 

415 user5, token5 = generate_user(geom=create_coordinate(90.5, 20.0)) 

416 

417 refresh_materialized_views_rapid(empty_pb2.Empty()) 

418 

419 with api_session(token3) as api: 

420 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user3.username)) 

421 assert res.lat == 40.0 

422 assert res.lng == 179.5 

423 

424 with api_session(token4) as api: 

425 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user4.username)) 

426 assert res.lat == 40.0 

427 assert res.lng == 20.0 

428 

429 # PostGIS does not wrap longitude for latitude overflow 

430 with api_session(token5) as api: 

431 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user5.username)) 

432 assert res.lat == 89.5 

433 assert res.lng == 20.0 

434 

435 with real_jail_session(token1) as jail: 

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

437 assert res.jailed 

438 assert res.needs_to_update_location 

439 

440 res = jail.SetLocation( 

441 jail_pb2.SetLocationReq( 

442 city="New York City", 

443 lat=40.7812, 

444 lng=-73.9647, 

445 radius=250, 

446 ) 

447 ) 

448 

449 assert not res.jailed 

450 assert not res.needs_to_update_location 

451 

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

453 assert not res.jailed 

454 assert not res.needs_to_update_location 

455 

456 refresh_materialized_views_rapid(empty_pb2.Empty()) 

457 

458 with api_session(token2) as api: 

459 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user1.username)) 

460 assert res.city == "New York City" 

461 assert res.lat == 40.7812 

462 assert res.lng == -73.9647 

463 assert res.radius == 250 

464 

465 

466def test_lite_get_user(db): 

467 user1, token1 = generate_user() 

468 user2, token2 = generate_user() 

469 

470 refresh_materialized_views_rapid(empty_pb2.Empty()) 

471 

472 with api_session(token1) as api: 

473 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=user2.username)) 

474 assert res.user_id == user2.id 

475 assert res.username == user2.username 

476 assert res.name == user2.name 

477 

478 with api_session(token1) as api: 

479 res = api.GetLiteUser(api_pb2.GetLiteUserReq(user=str(user2.id))) 

480 assert res.user_id == user2.id 

481 assert res.username == user2.username 

482 assert res.name == user2.name 

483 

484 

485def test_GetLiteUsers(db): 

486 user1, token1 = generate_user() 

487 user2, _ = generate_user() 

488 user3, _ = generate_user() 

489 user4, _ = generate_user() 

490 user5, _ = generate_user() 

491 user6, _ = generate_user() 

492 

493 make_user_block(user4, user1) 

494 

495 refresh_materialized_views_rapid(empty_pb2.Empty()) 

496 

497 with api_session(token1) as api: 

498 res = api.GetLiteUsers( 

499 api_pb2.GetLiteUsersReq( 

500 users=[ 

501 user1.username, 

502 str(user1.id), 

503 "nonexistent", 

504 str(user2.id), 

505 "9994", 

506 user6.username, 

507 str(user5.id), 

508 "notreal", 

509 user4.username, 

510 ] 

511 ) 

512 ) 

513 

514 assert len(res.responses) == 9 

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

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

517 

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

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

520 

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

522 assert res.responses[2].not_found 

523 

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

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

526 

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

528 assert res.responses[4].not_found 

529 

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

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

532 

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

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

535 

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

537 assert res.responses[7].not_found 

538 

539 # blocked - should return ghost profile 

540 assert res.responses[8].query == user4.username 

541 assert not res.responses[8].not_found 

542 assert res.responses[8].user.user_id == user4.id 

543 assert res.responses[8].user.is_ghost 

544 assert res.responses[8].user.username == "ghost" 

545 assert res.responses[8].user.name == "Deactivated Account" 

546 

547 with api_session(token1) as api: 

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

549 api.GetLiteUsers(api_pb2.GetLiteUsersReq(users=201 * [user1.username])) 

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

551 assert e.value.details() == "You can't request that many users at a time." 

552 

553 

554def test_update_profile(db): 

555 user, token = generate_user() 

556 

557 with api_session(token) as api: 

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

559 api.UpdateProfile(api_pb2.UpdateProfileReq(name=wrappers_pb2.StringValue(value=" "))) 

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

561 assert e.value.details() == "Name not supported." 

562 

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

564 api.UpdateProfile( 

565 api_pb2.UpdateProfileReq(lat=wrappers_pb2.DoubleValue(value=0), lng=wrappers_pb2.DoubleValue(value=0)) 

566 ) 

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

568 assert e.value.details() == "Invalid coordinate." 

569 

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

571 api.UpdateProfile( 

572 api_pb2.UpdateProfileReq(regions_visited=api_pb2.RepeatedStringValue(value=["United States"])) 

573 ) 

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

575 assert e.value.details() == "Invalid region." 

576 

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

578 api.UpdateProfile( 

579 api_pb2.UpdateProfileReq(regions_lived=api_pb2.RepeatedStringValue(value=["United Kingdom"])) 

580 ) 

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

582 assert e.value.details() == "Invalid region." 

583 

584 api.UpdateProfile( 

585 api_pb2.UpdateProfileReq( 

586 name=wrappers_pb2.StringValue(value="New name"), 

587 city=wrappers_pb2.StringValue(value="Timbuktu"), 

588 hometown=api_pb2.NullableStringValue(value="Walla Walla"), 

589 lat=wrappers_pb2.DoubleValue(value=0.01), 

590 lng=wrappers_pb2.DoubleValue(value=-2), 

591 radius=wrappers_pb2.DoubleValue(value=321), 

592 pronouns=api_pb2.NullableStringValue(value="Ro, Robo, Robots"), 

593 occupation=api_pb2.NullableStringValue(value="Testing"), 

594 education=api_pb2.NullableStringValue(value="Couchers U"), 

595 about_me=api_pb2.NullableStringValue(value="I rule"), 

596 things_i_like=api_pb2.NullableStringValue(value="Couchers"), 

597 about_place=api_pb2.NullableStringValue(value="My place"), 

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( 

601 value=[ 

602 api_pb2.LanguageAbility( 

603 code="eng", 

604 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

605 ) 

606 ], 

607 ), 

608 regions_visited=api_pb2.RepeatedStringValue(value=["CXR", "FIN"]), 

609 regions_lived=api_pb2.RepeatedStringValue(value=["USA", "EST"]), 

610 additional_information=api_pb2.NullableStringValue(value="I <3 Couchers"), 

611 ) 

612 ) 

613 

614 user_details = api.GetUser(api_pb2.GetUserReq(user=user.username)) 

615 assert user_details.name == "New name" 

616 assert user_details.city == "Timbuktu" 

617 assert user_details.hometown == "Walla Walla" 

618 assert user_details.pronouns == "Ro, Robo, Robots" 

619 assert user_details.education == "Couchers U" 

620 assert user_details.things_i_like == "Couchers" 

621 assert user_details.lat == 0.01 

622 assert user_details.lng == -2 

623 assert user_details.radius == 321 

624 assert user_details.occupation == "Testing" 

625 assert user_details.about_me == "I rule" 

626 assert user_details.about_place == "My place" 

627 assert user_details.hosting_status == api_pb2.HOSTING_STATUS_CAN_HOST 

628 assert user_details.meetup_status == api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP 

629 assert user_details.language_abilities[0].code == "eng" 

630 assert user_details.language_abilities[0].fluency == api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT 

631 assert user_details.additional_information == "I <3 Couchers" 

632 assert user_details.regions_visited == ["CXR", "FIN"] 

633 assert user_details.regions_lived == ["EST", "USA"] 

634 

635 # Test unset values 

636 api.UpdateProfile( 

637 api_pb2.UpdateProfileReq( 

638 hometown=api_pb2.NullableStringValue(is_null=True), 

639 radius=wrappers_pb2.DoubleValue(value=0), 

640 pronouns=api_pb2.NullableStringValue(is_null=True), 

641 occupation=api_pb2.NullableStringValue(is_null=True), 

642 education=api_pb2.NullableStringValue(is_null=True), 

643 about_me=api_pb2.NullableStringValue(is_null=True), 

644 things_i_like=api_pb2.NullableStringValue(is_null=True), 

645 about_place=api_pb2.NullableStringValue(is_null=True), 

646 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

647 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

648 language_abilities=api_pb2.RepeatedLanguageAbilityValue(value=[]), 

649 regions_visited=api_pb2.RepeatedStringValue(value=[]), 

650 regions_lived=api_pb2.RepeatedStringValue(value=[]), 

651 additional_information=api_pb2.NullableStringValue(is_null=True), 

652 ) 

653 ) 

654 

655 user_details = api.GetUser(api_pb2.GetUserReq(user=user.username)) 

656 assert not user_details.hometown 

657 assert not user_details.radius 

658 assert not user_details.pronouns 

659 assert not user_details.occupation 

660 assert not user_details.education 

661 assert not user_details.about_me 

662 assert not user_details.things_i_like 

663 assert not user_details.about_place 

664 assert user_details.hosting_status == api_pb2.HOSTING_STATUS_CAN_HOST 

665 assert user_details.meetup_status == api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP 

666 assert not user_details.language_abilities 

667 assert not user_details.regions_visited 

668 assert not user_details.regions_lived 

669 assert not user_details.additional_information 

670 

671 

672def test_update_profile_do_not_email(db): 

673 user, token = generate_user() 

674 

675 with notifications_session(token) as notifications: 

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

677 

678 with api_session(token) as api: 

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

680 api.UpdateProfile(api_pb2.UpdateProfileReq(hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST)) 

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

682 assert e.value.details() == "You cannot enable hosting while you have emails turned off in your settings." 

683 

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

685 api.UpdateProfile(api_pb2.UpdateProfileReq(meetup_status=api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP)) 

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

687 assert e.value.details() == "You cannot enable meeting up while you have emails turned off in your settings." 

688 

689 

690def test_language_abilities(db): 

691 user, token = generate_user( 

692 language_abilities=[ 

693 ("fin", LanguageFluency.fluent), 

694 ("fra", LanguageFluency.beginner), 

695 ], 

696 ) 

697 

698 with api_session(token) as api: 

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

700 assert len(res.language_abilities) == 2 

701 

702 # can't add non-existent languages 

703 with pytest.raises(grpc.RpcError) as err: 

704 api.UpdateProfile( 

705 api_pb2.UpdateProfileReq( 

706 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

707 value=[ 

708 api_pb2.LanguageAbility( 

709 code="QQQ", 

710 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

711 ) 

712 ], 

713 ), 

714 ) 

715 ) 

716 assert err.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

717 assert err.value.details() == "Invalid language." 

718 

719 # can't have multiple languages of the same type 

720 with pytest.raises(Exception) as err2: 

721 api.UpdateProfile( 

722 api_pb2.UpdateProfileReq( 

723 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

724 value=[ 

725 api_pb2.LanguageAbility( 

726 code="eng", 

727 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

728 ), 

729 api_pb2.LanguageAbility( 

730 code="eng", 

731 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

732 ), 

733 ], 

734 ), 

735 ) 

736 ) 

737 assert "violates unique constraint" in str(err2.value) 

738 

739 # nothing changed 

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

741 assert len(res.language_abilities) == 2 

742 

743 # now actually add a value 

744 api.UpdateProfile( 

745 api_pb2.UpdateProfileReq( 

746 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

747 value=[ 

748 api_pb2.LanguageAbility( 

749 code="eng", 

750 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

751 ) 

752 ], 

753 ), 

754 ) 

755 ) 

756 

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

758 assert len(res.language_abilities) == 1 

759 assert res.language_abilities[0].code == "eng" 

760 assert res.language_abilities[0].fluency == api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT 

761 

762 # change the value to a new one 

763 api.UpdateProfile( 

764 api_pb2.UpdateProfileReq( 

765 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

766 value=[ 

767 api_pb2.LanguageAbility( 

768 code="fin", 

769 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER, 

770 ) 

771 ], 

772 ), 

773 ) 

774 ) 

775 

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

777 assert len(res.language_abilities) == 1 

778 assert res.language_abilities[0].code == "fin" 

779 assert res.language_abilities[0].fluency == api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER 

780 

781 # should be able to set to same value still 

782 api.UpdateProfile( 

783 api_pb2.UpdateProfileReq( 

784 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

785 value=[ 

786 api_pb2.LanguageAbility( 

787 code="fin", 

788 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER, 

789 ) 

790 ], 

791 ), 

792 ) 

793 ) 

794 

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

796 assert len(res.language_abilities) == 1 

797 assert res.language_abilities[0].code == "fin" 

798 assert res.language_abilities[0].fluency == api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER 

799 

800 # don't change it 

801 api.UpdateProfile(api_pb2.UpdateProfileReq()) 

802 

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

804 assert len(res.language_abilities) == 1 

805 assert res.language_abilities[0].code == "fin" 

806 assert res.language_abilities[0].fluency == api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER 

807 

808 # remove value 

809 api.UpdateProfile( 

810 api_pb2.UpdateProfileReq( 

811 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

812 value=[], 

813 ), 

814 ) 

815 ) 

816 

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

818 assert len(res.language_abilities) == 0 

819 

820 

821def test_pending_friend_request_count(db, moderator): 

822 user1, token1 = generate_user() 

823 user2, token2 = generate_user() 

824 user3, token3 = generate_user() 

825 

826 with api_session(token2) as api: 

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

828 assert res.pending_friend_request_count == 0 

829 

830 with api_session(token1) as api: 

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

832 assert res.pending_friend_request_count == 0 

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

834 # Sender can still see their own sent requests (even while SHADOWED) 

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

836 assert res.pending_friend_request_count == 0 

837 

838 # Get friend request ID from sender's view (author can see SHADOWED) 

839 with api_session(token1) as api: 

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

841 assert len(res.sent) == 1 

842 fr_id = res.sent[0].friend_request_id 

843 

844 # Recipient cannot see SHADOWED friend requests before mod approval 

845 with api_session(token2) as api: 

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

847 assert res.pending_friend_request_count == 0 

848 

849 # Moderator approves the friend request 

850 moderator.approve_friend_request(fr_id) 

851 

852 # Now recipient can see the approved friend request 

853 with api_session(token2) as api: 

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

855 assert res.pending_friend_request_count == 1 

856 

857 with api_session(token2) as api: 

858 # check it's there 

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

860 assert len(res.sent) == 0 

861 assert len(res.received) == 1 

862 

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

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

865 

866 fr_id = res.received[0].friend_request_id 

867 

868 # accept it 

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

870 

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

872 assert res.pending_friend_request_count == 0 

873 

874 

875def test_friend_request_flow(db, email_collector: EmailCollector, push_collector: PushCollector, moderator): 

876 user1, token1 = generate_user(complete_profile=True) 

877 user2, token2 = generate_user(complete_profile=True) 

878 user3, token3 = generate_user() 

879 

880 # send a friend request from user1 to user2 

881 with api_session(token1) as api: 

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

883 

884 with session_scope() as session: 

885 friend_request = session.execute( 

886 select(FriendRelationship).where( 

887 FriendRelationship.from_user_id == user1.id, FriendRelationship.to_user_id == user2.id 

888 ) 

889 ).scalar_one() 

890 friend_request_id = friend_request.id 

891 

892 # Notification is deferred while content is SHADOWED 

893 # No push notification sent yet 

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

895 

896 with api_session(token1) as api: 

897 # Sender can see their own sent requests (even while SHADOWED) 

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

899 assert len(res.sent) == 1 

900 assert len(res.received) == 0 

901 

902 assert res.sent[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING 

903 assert res.sent[0].user_id == user2.id 

904 assert res.sent[0].friend_request_id == friend_request_id 

905 

906 # Recipient cannot see SHADOWED friend requests 

907 with api_session(token2) as api: 

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

909 assert len(res.sent) == 0 

910 assert len(res.received) == 0 

911 

912 # Moderator approves the friend request - this triggers the notification 

913 moderator.approve_friend_request(friend_request_id) 

914 

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

916 assert push.content.title == f"Friend request from {user1.name}" 

917 assert push.content.body == f"{user1.name} wants to be your friend." 

918 assert push.content.action_url == f"http://localhost:3000/connections/friends/?from={user1.id}" 

919 

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

921 assert email.recipient == user2.email 

922 assert email.subject == f"[TEST] {user1.name} wants to be your friend" 

923 assert user2.name in email.plain 

924 assert user2.name in email.html 

925 assert user1.name in email.plain 

926 assert user1.name in email.html 

927 assert "http://localhost:5001/img/thumbnail/" not in email.plain 

928 assert "http://localhost:5001/img/thumbnail/" in email.html 

929 assert "http://localhost:3000/connections/friends/" in email.plain 

930 assert "http://localhost:3000/connections/friends/" in email.html 

931 

932 # Now recipient can see the approved friend request 

933 with api_session(token2) as api: 

934 # check it's there 

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

936 assert len(res.sent) == 0 

937 assert len(res.received) == 1 

938 

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

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

941 

942 fr_id = res.received[0].friend_request_id 

943 

944 # accept it 

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

946 

947 # check it's gone 

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

949 assert len(res.sent) == 0 

950 assert len(res.received) == 0 

951 

952 # check we're friends now 

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

954 assert len(res.user_ids) == 1 

955 assert res.user_ids[0] == user1.id 

956 

957 # user2 got one push (from the friend request creation) 

958 # user1 should now have one push (from the friend request acceptance) 

959 push = push_collector.pop_for_user(user1.id, last=True) 

960 assert push.content.title == f"{user2.name} accepted your friend request" 

961 assert push.content.body == f"You are now friends with {user2.name}." 

962 

963 email = email_collector.pop_for_recipient(user1.email, last=True) 

964 assert email.recipient == user1.email 

965 assert email.subject == f"[TEST] {user2.name} accepted your friend request" 

966 assert user1.name in email.plain 

967 assert user1.name in email.html 

968 assert user2.name in email.plain 

969 assert user2.name in email.html 

970 assert "http://localhost:5001/img/thumbnail/" not in email.plain 

971 assert "http://localhost:5001/img/thumbnail/" in email.html 

972 assert f"http://localhost:3000/user/{user2.username}" in email.plain 

973 assert f"http://localhost:3000/user/{user2.username}" in email.html 

974 

975 with api_session(token1) as api: 

976 # check it's gone 

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

978 assert len(res.sent) == 0 

979 assert len(res.received) == 0 

980 

981 # check we're friends now 

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

983 assert len(res.user_ids) == 1 

984 assert res.user_ids[0] == user2.id 

985 

986 with api_session(token1) as api: 

987 # we can't unfriend if we aren't friends 

988 with pytest.raises(grpc.RpcError) as err: 

989 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user3.id)) 

990 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

991 assert err.value.details() == "You aren't friends with that user!" 

992 

993 # we can unfriend 

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

995 

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

997 assert len(res.user_ids) == 0 

998 

999 

1000def test_RemoveFriend_regression(db, push_collector: PushCollector, moderator): 

1001 user1, token1 = generate_user(complete_profile=True) 

1002 user2, token2 = generate_user(complete_profile=True) 

1003 user3, token3 = generate_user() 

1004 user4, token4 = generate_user() 

1005 user5, token5 = generate_user() 

1006 user6, token6 = generate_user() 

1007 

1008 # Send friend requests 

1009 with api_session(token4) as api: 

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

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

1012 

1013 with api_session(token5) as api: 

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

1015 

1016 with api_session(token1) as api: 

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

1018 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user3.id)) 

1019 

1020 # Approve all friend requests via moderation 

1021 with session_scope() as session: 

1022 friend_requests = session.execute(select(FriendRelationship)).scalars().all() 

1023 for fr in friend_requests: 

1024 moderator.approve_friend_request(fr.id) 

1025 

1026 # Now recipients can respond 

1027 with api_session(token1) as api: 

1028 api.RespondFriendRequest( 

1029 api_pb2.RespondFriendRequestReq( 

1030 friend_request_id=api.ListFriendRequests(empty_pb2.Empty()).received[0].friend_request_id, accept=True 

1031 ) 

1032 ) 

1033 

1034 with api_session(token2) as api: 

1035 for fr in api.ListFriendRequests(empty_pb2.Empty()).received: 

1036 api.RespondFriendRequest( 

1037 api_pb2.RespondFriendRequestReq(friend_request_id=fr.friend_request_id, accept=True) 

1038 ) 

1039 

1040 with api_session(token1) as api: 

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

1042 assert sorted(res.user_ids) == sorted([user2.id, user4.id]) 

1043 

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

1045 

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

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

1048 

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

1050 

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

1052 assert not res.user_ids 

1053 

1054 

1055def test_cant_friend_request_twice(db): 

1056 user1, token1 = generate_user() 

1057 user2, token2 = generate_user() 

1058 

1059 # send friend request from user1 to user2 

1060 with api_session(token1) as api: 

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

1062 

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

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

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

1066 assert e.value.details() == "You are already friends with or have sent a friend request to that user." 

1067 

1068 

1069def test_cant_friend_request_pending(db): 

1070 user1, token1 = generate_user() 

1071 user2, token2 = generate_user() 

1072 

1073 # send friend request from user1 to user2 

1074 with api_session(token1) as api: 

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

1076 

1077 with api_session(token2) as api: 

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

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

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

1081 assert e.value.details() == "You are already friends with or have sent a friend request to that user." 

1082 

1083 

1084def test_cant_friend_request_already_friends(db): 

1085 user1, token1 = generate_user() 

1086 user2, token2 = generate_user() 

1087 make_friends(user1, user2) 

1088 

1089 with api_session(token1) as api: 

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

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

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

1093 assert e.value.details() == "You are already friends with or have sent a friend request to that user." 

1094 

1095 with api_session(token2) as api: 

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

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

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

1099 assert e.value.details() == "You are already friends with or have sent a friend request to that user." 

1100 

1101 

1102def test_cant_friend_request_incomplete_profile(db): 

1103 user1, token1 = generate_user(complete_profile=False) 

1104 user2, token2 = generate_user(complete_profile=True) 

1105 

1106 with api_session(token1) as api: 

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

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

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

1110 assert e.value.details() == "You have to complete your profile before you can send a friend request." 

1111 

1112 # the other direction should still work 

1113 with api_session(token2) as api: 

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

1115 

1116 

1117def test_excessive_friend_requests_are_reported(db, low_rate_limits, email_collector: EmailCollector): 

1118 """Test that excessive friend requests are first reported in a warning email and finally lead blocking of further requests.""" 

1119 user, token = generate_user() 

1120 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.friend_request] 

1121 with api_session(token) as api: 

1122 # Test warning email 

1123 for _ in range(rate_limit_definition.warning_limit): 

1124 friend_user, _ = generate_user() 

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

1126 

1127 assert email_collector.count_for_reports() == 0 

1128 friend_user, _ = generate_user() 

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

1130 

1131 email = email_collector.pop_for_reports(last=True) 

1132 assert email.plain.startswith( 

1133 f"User {user.username} has sent {rate_limit_definition.warning_limit} friend requests in the past {RATE_LIMIT_HOURS} hours." 

1134 ) 

1135 

1136 # Test ban after exceeding FRIEND_REQUEST_HARD_LIMIT 

1137 for _ in range(rate_limit_definition.hard_limit - rate_limit_definition.warning_limit - 1): 

1138 friend_user, _ = generate_user() 

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

1140 

1141 assert email_collector.count_for_reports() == 0 

1142 friend_user, _ = generate_user() 

1143 with pytest.raises(grpc.RpcError) as exc_info: 

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

1145 assert exc_info.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED 

1146 assert ( 

1147 exc_info.value.details() 

1148 == "You have sent a lot of friend requests in the past 24 hours. To avoid spam, you can't send any more for now." 

1149 ) 

1150 

1151 email = email_collector.pop_for_reports(last=True) 

1152 assert email.plain.startswith( 

1153 f"User {user.username} has sent {rate_limit_definition.hard_limit} friend requests in the past {RATE_LIMIT_HOURS} hours." 

1154 ) 

1155 assert "The user has been blocked from sending further friend requests for now." in email.plain 

1156 

1157 with session_scope() as session: 

1158 assert session.execute( 

1159 select(RateLimitViolation.is_hard_limit) 

1160 .where(RateLimitViolation.user_id == user.id) 

1161 .order_by(RateLimitViolation.id) 

1162 ).scalars().all() == [False, True] 

1163 

1164 # still blocked once the hard violation is on record, without reporting again 

1165 friend_user, _ = generate_user() 

1166 with pytest.raises(grpc.RpcError) as exc_info: 

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

1168 assert exc_info.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED 

1169 assert email_collector.count_for_reports() == 0 

1170 

1171 

1172def test_ListFriends(db, moderator): 

1173 user1, token1 = generate_user() 

1174 user2, token2 = generate_user() 

1175 user3, token3 = generate_user() 

1176 

1177 # send friend request from user1 to user2 and user3 

1178 with api_session(token1) as api: 

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

1180 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user3.id)) 

1181 # sender can see their sent requests (they are the author) 

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

1183 assert len(res.sent) == 2 

1184 user1_to_user2_id = [req for req in res.sent if req.user_id == user2.id][0].friend_request_id 

1185 user1_to_user3_id = [req for req in res.sent if req.user_id == user3.id][0].friend_request_id 

1186 

1187 with api_session(token3) as api: 

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

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

1190 user3_to_user2_id = res.sent[0].friend_request_id 

1191 

1192 # Moderator approves the friend requests so recipients can see them 

1193 moderator.approve_friend_request(user1_to_user2_id) 

1194 moderator.approve_friend_request(user3_to_user2_id) 

1195 

1196 with api_session(token2) as api: 

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

1198 assert len(res.received) == 2 

1199 

1200 # order is an implementation detail 

1201 user1_req = [req for req in res.received if req.user_id == user1.id][0] 

1202 user3_req = [req for req in res.received if req.user_id == user3.id][0] 

1203 

1204 assert user1_req.state == api_pb2.FriendRequest.FriendRequestStatus.PENDING 

1205 assert user1_req.user_id == user1.id 

1206 api.RespondFriendRequest( 

1207 api_pb2.RespondFriendRequestReq(friend_request_id=user1_req.friend_request_id, accept=True) 

1208 ) 

1209 

1210 assert user3_req.state == api_pb2.FriendRequest.FriendRequestStatus.PENDING 

1211 assert user3_req.user_id == user3.id 

1212 api.RespondFriendRequest( 

1213 api_pb2.RespondFriendRequestReq(friend_request_id=user3_req.friend_request_id, accept=True) 

1214 ) 

1215 

1216 # check we now have two friends 

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

1218 assert len(res.user_ids) == 2 

1219 assert user1.id in res.user_ids 

1220 assert user3.id in res.user_ids 

1221 

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

1223 moderator.approve_friend_request(user1_to_user3_id) 

1224 

1225 with api_session(token3) as api: 

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

1227 assert len(res.user_ids) == 1 

1228 assert user2.id in res.user_ids 

1229 

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

1231 assert len(res.received) == 1 

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

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

1234 fr_id = res.received[0].friend_request_id 

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

1236 

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

1238 assert len(res.user_ids) == 2 

1239 assert user1.id in res.user_ids 

1240 assert user2.id in res.user_ids 

1241 

1242 with api_session(token1) as api: 

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

1244 assert len(res.user_ids) == 2 

1245 assert user2.id in res.user_ids 

1246 assert user3.id in res.user_ids 

1247 

1248 

1249def test_ListMutualFriends(db): 

1250 user1, token1 = generate_user() 

1251 user2, token2 = generate_user() 

1252 user3, token3 = generate_user() 

1253 user4, token4 = generate_user() 

1254 user5, token5 = generate_user() 

1255 

1256 # arrange friends like this: 1<->2, 1<->3, 1<->4, 1<->5, 3<->2, 3<->4, 

1257 # so 1 and 2 should have mutual friend 3 only 

1258 make_friends(user1, user2) 

1259 make_friends(user1, user3) 

1260 make_friends(user1, user4) 

1261 make_friends(user1, user5) 

1262 make_friends(user3, user2) 

1263 make_friends(user3, user4) 

1264 

1265 with api_session(token1) as api: 

1266 mutual_friends = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user2.id)).mutual_friends 

1267 assert len(mutual_friends) == 1 

1268 assert mutual_friends[0].user_id == user3.id 

1269 

1270 # and other way around same 

1271 with api_session(token2) as api: 

1272 mutual_friends = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user1.id)).mutual_friends 

1273 assert len(mutual_friends) == 1 

1274 assert mutual_friends[0].user_id == user3.id 

1275 

1276 # Check pending request doesn't have effect 

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

1278 

1279 mutual_friends = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user1.id)).mutual_friends 

1280 assert len(mutual_friends) == 1 

1281 assert mutual_friends[0].user_id == user3.id 

1282 

1283 # both ways 

1284 with api_session(token1) as api: 

1285 mutual_friends = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user2.id)).mutual_friends 

1286 assert len(mutual_friends) == 1 

1287 assert mutual_friends[0].user_id == user3.id 

1288 

1289 

1290def test_mutual_friends_self(db): 

1291 user1, token1 = generate_user() 

1292 user2, token2 = generate_user() 

1293 user3, token3 = generate_user() 

1294 user4, token4 = generate_user() 

1295 

1296 make_friends(user1, user2) 

1297 make_friends(user2, user3) 

1298 make_friends(user1, user4) 

1299 

1300 with api_session(token1) as api: 

1301 res = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user1.id)) 

1302 assert len(res.mutual_friends) == 0 

1303 

1304 with api_session(token2) as api: 

1305 res = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user2.id)) 

1306 assert len(res.mutual_friends) == 0 

1307 

1308 with api_session(token3) as api: 

1309 res = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user3.id)) 

1310 assert len(res.mutual_friends) == 0 

1311 

1312 with api_session(token4) as api: 

1313 res = api.ListMutualFriends(api_pb2.ListMutualFriendsReq(user_id=user4.id)) 

1314 assert len(res.mutual_friends) == 0 

1315 

1316 

1317def test_CancelFriendRequest(db): 

1318 user1, token1 = generate_user() 

1319 user2, token2 = generate_user() 

1320 

1321 with api_session(token1) as api: 

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

1323 

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

1325 assert res.sent[0].user_id == user2.id 

1326 fr_id = res.sent[0].friend_request_id 

1327 

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

1329 

1330 # check it's gone 

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

1332 assert len(res.sent) == 0 

1333 assert len(res.received) == 0 

1334 

1335 # check not friends 

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

1337 assert len(res.user_ids) == 0 

1338 

1339 with api_session(token2) as api: 

1340 # check it's gone 

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

1342 assert len(res.sent) == 0 

1343 assert len(res.received) == 0 

1344 

1345 # check we're not friends 

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

1347 assert len(res.user_ids) == 0 

1348 

1349 with api_session(token1) as api: 

1350 # check we can send another friend req 

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

1352 

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

1354 assert res.sent[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING 

1355 assert res.sent[0].user_id == user2.id 

1356 

1357 

1358def test_accept_friend_request(db, moderator): 

1359 user1, token1 = generate_user() 

1360 user2, token2 = generate_user() 

1361 

1362 with session_scope() as session: 

1363 moderation_state = ModerationState( 

1364 object_type=ModerationObjectType.friend_request, 

1365 object_id=0, 

1366 visibility=ModerationVisibility.visible, 

1367 ) 

1368 session.add(moderation_state) 

1369 session.flush() 

1370 friend_request = FriendRelationship( 

1371 from_user_id=user1.id, 

1372 to_user_id=user2.id, 

1373 status=FriendStatus.pending, 

1374 moderation_state_id=moderation_state.id, 

1375 ) 

1376 session.add(friend_request) 

1377 session.flush() 

1378 moderation_state.object_id = friend_request.id 

1379 session.commit() 

1380 friend_request_id = friend_request.id 

1381 

1382 with api_session(token2) as api: 

1383 # check request pending 

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

1385 assert len(res.received) == 1 

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

1387 

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

1389 

1390 # check request is gone 

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

1392 assert len(res.sent) == 0 

1393 assert len(res.received) == 0 

1394 

1395 # check now friends 

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

1397 assert len(res.user_ids) == 1 

1398 assert res.user_ids[0] == user1.id 

1399 

1400 with api_session(token1) as api: 

1401 # check request gone 

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

1403 assert len(res.sent) == 0 

1404 assert len(res.received) == 0 

1405 

1406 # check now friends 

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

1408 assert len(res.user_ids) == 1 

1409 assert res.user_ids[0] == user2.id 

1410 

1411 

1412def test_reject_friend_request(db, moderator): 

1413 user1, token1 = generate_user() 

1414 user2, token2 = generate_user() 

1415 

1416 with api_session(token1) as api: 

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

1418 

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

1420 assert res.sent[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING 

1421 assert res.sent[0].user_id == user2.id 

1422 fr_id = res.sent[0].friend_request_id 

1423 

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

1425 moderator.approve_friend_request(fr_id) 

1426 

1427 with api_session(token2) as api: 

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

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

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

1431 

1432 fr_id = res.received[0].friend_request_id 

1433 

1434 # reject it 

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

1436 

1437 # check it's gone 

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

1439 assert len(res.sent) == 0 

1440 assert len(res.received) == 0 

1441 

1442 # check not friends 

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

1444 assert len(res.user_ids) == 0 

1445 

1446 with api_session(token1) as api: 

1447 # check it's gone 

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

1449 assert len(res.sent) == 0 

1450 assert len(res.received) == 0 

1451 

1452 # check we're not friends 

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

1454 assert len(res.user_ids) == 0 

1455 

1456 # check we can send another friend req 

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

1458 

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

1460 assert res.sent[0].state == api_pb2.FriendRequest.FriendRequestStatus.PENDING 

1461 assert res.sent[0].user_id == user2.id 

1462 

1463 

1464def test_hosting_preferences(db): 

1465 user1, token1 = generate_user() 

1466 user2, token2 = generate_user() 

1467 

1468 with api_session(token1) as api: 

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

1470 assert not res.HasField("max_guests") 

1471 assert not res.HasField("last_minute") 

1472 assert not res.HasField("has_pets") 

1473 assert not res.HasField("accepts_pets") 

1474 assert not res.HasField("pet_details") 

1475 assert not res.HasField("has_kids") 

1476 assert not res.HasField("accepts_kids") 

1477 assert not res.HasField("kid_details") 

1478 assert not res.HasField("has_housemates") 

1479 assert not res.HasField("housemate_details") 

1480 assert not res.HasField("wheelchair_accessible") 

1481 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_UNKNOWN 

1482 assert not res.HasField("smokes_at_home") 

1483 assert not res.HasField("drinking_allowed") 

1484 assert not res.HasField("drinks_at_home") 

1485 assert not res.HasField("other_host_info") 

1486 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN 

1487 assert not res.HasField("sleeping_details") 

1488 assert not res.HasField("area") 

1489 assert not res.HasField("house_rules") 

1490 assert not res.HasField("parking") 

1491 assert res.parking_details == api_pb2.PARKING_DETAILS_UNKNOWN 

1492 assert not res.HasField("camping_ok") 

1493 

1494 api.UpdateProfile( 

1495 api_pb2.UpdateProfileReq( 

1496 max_guests=api_pb2.NullableUInt32Value(value=3), 

1497 last_minute=api_pb2.NullableBoolValue(value=True), 

1498 has_pets=api_pb2.NullableBoolValue(value=False), 

1499 accepts_pets=api_pb2.NullableBoolValue(value=True), 

1500 pet_details=api_pb2.NullableStringValue(value="I love dogs"), 

1501 has_kids=api_pb2.NullableBoolValue(value=False), 

1502 accepts_kids=api_pb2.NullableBoolValue(value=True), 

1503 kid_details=api_pb2.NullableStringValue(value="I hate kids"), 

1504 has_housemates=api_pb2.NullableBoolValue(value=False), 

1505 housemate_details=api_pb2.NullableStringValue(value="I have 7 housemates"), 

1506 wheelchair_accessible=api_pb2.NullableBoolValue(value=True), 

1507 smoking_allowed=api_pb2.SMOKING_LOCATION_WINDOW, 

1508 area=api_pb2.NullableStringValue(value="area!"), 

1509 smokes_at_home=api_pb2.NullableBoolValue(value=False), 

1510 drinking_allowed=api_pb2.NullableBoolValue(value=True), 

1511 drinks_at_home=api_pb2.NullableBoolValue(value=False), 

1512 other_host_info=api_pb2.NullableStringValue(value="I'm pretty swell"), 

1513 sleeping_arrangement=api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

1514 sleeping_details=api_pb2.NullableStringValue(value="Couch in living room"), 

1515 house_rules=api_pb2.NullableStringValue(value="RULES!"), 

1516 parking=api_pb2.NullableBoolValue(value=True), 

1517 parking_details=api_pb2.PARKING_DETAILS_PAID_ONSITE, 

1518 camping_ok=api_pb2.NullableBoolValue(value=False), 

1519 ) 

1520 ) 

1521 

1522 # Use a second user to view the hosting preferences just to check 

1523 # that it is public information. 

1524 with api_session(token2) as api: 

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

1526 assert res.max_guests.value == 3 

1527 assert res.last_minute.value 

1528 assert not res.has_pets.value 

1529 assert res.accepts_pets.value 

1530 assert res.pet_details.value == "I love dogs" 

1531 assert not res.has_kids.value 

1532 assert res.accepts_kids.value 

1533 assert res.kid_details.value == "I hate kids" 

1534 assert not res.has_housemates.value 

1535 assert res.housemate_details.value == "I have 7 housemates" 

1536 assert res.wheelchair_accessible.value 

1537 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_WINDOW 

1538 assert not res.smokes_at_home.value 

1539 assert res.drinking_allowed.value 

1540 assert not res.drinks_at_home.value 

1541 assert res.other_host_info.value == "I'm pretty swell" 

1542 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_COMMON 

1543 assert res.sleeping_details.value == "Couch in living room" 

1544 assert res.area.value == "area!" 

1545 assert res.house_rules.value == "RULES!" 

1546 assert res.parking.value 

1547 assert res.parking_details == api_pb2.PARKING_DETAILS_PAID_ONSITE 

1548 assert not res.camping_ok.value 

1549 

1550 # test unsetting 

1551 with api_session(token1) as api: 

1552 api.UpdateProfile( 

1553 api_pb2.UpdateProfileReq( 

1554 max_guests=api_pb2.NullableUInt32Value(is_null=True), 

1555 last_minute=api_pb2.NullableBoolValue(is_null=True), 

1556 has_pets=api_pb2.NullableBoolValue(is_null=True), 

1557 accepts_pets=api_pb2.NullableBoolValue(is_null=True), 

1558 pet_details=api_pb2.NullableStringValue(is_null=True), 

1559 has_kids=api_pb2.NullableBoolValue(is_null=True), 

1560 accepts_kids=api_pb2.NullableBoolValue(is_null=True), 

1561 kid_details=api_pb2.NullableStringValue(is_null=True), 

1562 has_housemates=api_pb2.NullableBoolValue(is_null=True), 

1563 housemate_details=api_pb2.NullableStringValue(is_null=True), 

1564 wheelchair_accessible=api_pb2.NullableBoolValue(is_null=True), 

1565 smoking_allowed=api_pb2.SMOKING_LOCATION_UNKNOWN, 

1566 area=api_pb2.NullableStringValue(is_null=True), 

1567 smokes_at_home=api_pb2.NullableBoolValue(is_null=True), 

1568 drinking_allowed=api_pb2.NullableBoolValue(is_null=True), 

1569 drinks_at_home=api_pb2.NullableBoolValue(is_null=True), 

1570 other_host_info=api_pb2.NullableStringValue(is_null=True), 

1571 sleeping_arrangement=api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

1572 sleeping_details=api_pb2.NullableStringValue(is_null=True), 

1573 house_rules=api_pb2.NullableStringValue(is_null=True), 

1574 parking=api_pb2.NullableBoolValue(is_null=True), 

1575 parking_details=api_pb2.PARKING_DETAILS_UNKNOWN, 

1576 camping_ok=api_pb2.NullableBoolValue(is_null=True), 

1577 ) 

1578 ) 

1579 

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

1581 assert not res.HasField("max_guests") 

1582 assert not res.HasField("last_minute") 

1583 assert not res.HasField("has_pets") 

1584 assert not res.HasField("accepts_pets") 

1585 assert not res.HasField("pet_details") 

1586 assert not res.HasField("has_kids") 

1587 assert not res.HasField("accepts_kids") 

1588 assert not res.HasField("kid_details") 

1589 assert not res.HasField("has_housemates") 

1590 assert not res.HasField("housemate_details") 

1591 assert not res.HasField("wheelchair_accessible") 

1592 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_UNKNOWN 

1593 assert not res.HasField("smokes_at_home") 

1594 assert not res.HasField("drinking_allowed") 

1595 assert not res.HasField("drinks_at_home") 

1596 assert not res.HasField("other_host_info") 

1597 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN 

1598 assert not res.HasField("sleeping_details") 

1599 assert not res.HasField("area") 

1600 assert not res.HasField("house_rules") 

1601 assert not res.HasField("parking") 

1602 assert res.parking_details == api_pb2.PARKING_DETAILS_UNKNOWN 

1603 assert not res.HasField("camping_ok") 

1604 

1605 

1606def test_badges(db): 

1607 user1, _ = generate_user(last_donated=None) 

1608 user2, _ = generate_user(last_donated=None) 

1609 user3, _ = generate_user(last_donated=None) 

1610 user4, token = generate_user(last_donated=None) 

1611 

1612 update_badges(empty_pb2.Empty()) 

1613 

1614 founder_badge = get_badge_dict()["founder"] 

1615 board_member_badge = get_badge_dict()["board_member"] 

1616 

1617 with api_session(token) as api: 

1618 assert api.GetUser(api_pb2.GetUserReq(user=user1.username)).badges == ["founder", "board_member"] 

1619 assert api.GetUser(api_pb2.GetUserReq(user=user2.username)).badges == ["founder", "board_member"] 

1620 assert api.GetUser(api_pb2.GetUserReq(user=user3.username)).badges == [] 

1621 

1622 assert api.ListBadgeUsers(api_pb2.ListBadgeUsersReq(badge_id=founder_badge.id)).user_ids == [1, 2] 

1623 res = api.ListBadgeUsers(api_pb2.ListBadgeUsersReq(badge_id=board_member_badge.id, page_size=1)) 

1624 assert res.user_ids == [1] 

1625 res2 = api.ListBadgeUsers( 

1626 api_pb2.ListBadgeUsersReq(badge_id=board_member_badge.id, page_token=res.next_page_token) 

1627 ) 

1628 assert res2.user_ids == [2] 

1629 

1630 

1631def test_user_add_badge_is_idempotent(db): 

1632 """Test that adding a badge a user already has is a no-op and doesn't send a duplicate notification.""" 

1633 user, _ = generate_user() 

1634 

1635 with session_scope() as session: 

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

1637 

1638 # one badge row, one notification 

1639 with session_scope() as session: 

1640 badge_count = session.execute( 

1641 select(func.count()) 

1642 .select_from(UserBadge) 

1643 .where(UserBadge.user_id == user.id, UserBadge.badge_id == "volunteer") 

1644 ).scalar() 

1645 assert badge_count == 1 

1646 notification_count = session.execute( 

1647 select(func.count()).select_from(Notification).where(Notification.user_id == user.id) 

1648 ).scalar() 

1649 assert notification_count == 1 

1650 

1651 # add the same badge again 

1652 with session_scope() as session: 

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

1654 

1655 # still one badge row, no new notification 

1656 with session_scope() as session: 

1657 badge_count = session.execute( 

1658 select(func.count()) 

1659 .select_from(UserBadge) 

1660 .where(UserBadge.user_id == user.id, UserBadge.badge_id == "volunteer") 

1661 ).scalar() 

1662 assert badge_count == 1 

1663 notification_count = session.execute( 

1664 select(func.count()).select_from(Notification).where(Notification.user_id == user.id) 

1665 ).scalar() 

1666 assert notification_count == 1 

1667 

1668 

1669@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"]) 

1670def test_ListBadgeUsers_excludes_ghost_users(db, flag): 

1671 """Test that ListBadgeUsers does not return deleted/banned users.""" 

1672 user1, token1 = generate_user() 

1673 user2, _ = generate_user() 

1674 user3, _ = generate_user() 

1675 

1676 volunteer_badge = get_badge_dict()["volunteer"] 

1677 

1678 # Give all three users the volunteer badge 

1679 with session_scope() as session: 

1680 user_add_badge(session, user1.id, "volunteer", do_notify=False) 

1681 user_add_badge(session, user2.id, "volunteer", do_notify=False) 

1682 user_add_badge(session, user3.id, "volunteer", do_notify=False) 

1683 

1684 # Verify all three users appear in the badge list 

1685 with api_session(token1) as api: 

1686 res = api.ListBadgeUsers(api_pb2.ListBadgeUsersReq(badge_id=volunteer_badge.id)) 

1687 assert set(res.user_ids) == {user1.id, user2.id, user3.id} 

1688 

1689 # Make user2 invisible (deleted or banned) 

1690 with session_scope() as session: 

1691 db_user2 = session.execute(select(User).where(User.id == user2.id)).scalar_one() 

1692 setattr(db_user2, flag, now()) 

1693 

1694 # Now user2 should not appear in the badge list 

1695 with api_session(token1) as api: 

1696 res = api.ListBadgeUsers(api_pb2.ListBadgeUsersReq(badge_id=volunteer_badge.id)) 

1697 assert set(res.user_ids) == {user1.id, user3.id} 

1698 

1699 

1700@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"]) 

1701def test_GetLiteUser_ghost_user_by_username(db, flag): 

1702 """Test that GetLiteUser returns a ghost profile for deleted/banned users when querying by username.""" 

1703 user1, token1 = generate_user() 

1704 user2, _ = generate_user() 

1705 

1706 # Make user2 invisible 

1707 with session_scope() as session: 

1708 db_user2 = session.merge(user2) 

1709 setattr(db_user2, flag, now()) 

1710 session.commit() 

1711 

1712 # Refresh the materialized view 

1713 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1714 

1715 with api_session(token1) as api: 

1716 # Query by username 

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

1718 

1719 assert lite_user.user_id == user2.id 

1720 assert lite_user.username == "ghost" 

1721 assert lite_user.name == "Deactivated Account" 

1722 assert lite_user.lat == 0 

1723 assert lite_user.lng == 0 

1724 assert lite_user.radius == 0 

1725 assert lite_user.city == "" 

1726 assert lite_user.age == 0 

1727 assert lite_user.avatar_url == "" 

1728 assert lite_user.avatar_thumbnail_url == "" 

1729 assert not lite_user.has_strong_verification 

1730 

1731 

1732@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"]) 

1733def test_GetLiteUser_ghost_user_by_id(db, flag): 

1734 """Test that GetLiteUser returns a ghost profile for deleted/banned users when querying by ID.""" 

1735 user1, token1 = generate_user() 

1736 user2, _ = generate_user() 

1737 

1738 # Make user2 invisible 

1739 with session_scope() as session: 

1740 db_user2 = session.merge(user2) 

1741 setattr(db_user2, flag, now()) 

1742 session.commit() 

1743 

1744 # Refresh the materialized view 

1745 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1746 

1747 with api_session(token1) as api: 

1748 # Query by ID 

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

1750 

1751 assert lite_user.user_id == user2.id 

1752 assert lite_user.username == "ghost" 

1753 assert lite_user.name == "Deactivated Account" 

1754 assert lite_user.lat == 0 

1755 assert lite_user.lng == 0 

1756 assert lite_user.radius == 0 

1757 assert lite_user.city == "" 

1758 assert lite_user.age == 0 

1759 assert lite_user.avatar_url == "" 

1760 assert lite_user.avatar_thumbnail_url == "" 

1761 assert not lite_user.has_strong_verification 

1762 

1763 

1764def test_GetLiteUser_blocked_user(db): 

1765 """Test that GetLiteUser returns a ghost profile for blocked users.""" 

1766 user1, token1 = generate_user() 

1767 user2, _ = generate_user() 

1768 

1769 # User1 blocks user2 

1770 make_user_block(user1, user2) 

1771 

1772 # Refresh the materialized view 

1773 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1774 

1775 with api_session(token1) as api: 

1776 # Query by username 

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

1778 

1779 assert lite_user.user_id == user2.id 

1780 assert lite_user.is_ghost 

1781 assert lite_user.username == "ghost" 

1782 assert lite_user.name == "Deactivated Account" 

1783 

1784 # Query by ID 

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

1786 

1787 assert lite_user.user_id == user2.id 

1788 assert lite_user.is_ghost 

1789 assert lite_user.username == "ghost" 

1790 assert lite_user.name == "Deactivated Account" 

1791 

1792 

1793def test_GetLiteUser_blocking_user(db): 

1794 """Test that GetLiteUser returns a ghost profile when the target user has blocked the requester.""" 

1795 user1, token1 = generate_user() 

1796 user2, _ = generate_user() 

1797 

1798 # User2 blocks user1 

1799 make_user_block(user2, user1) 

1800 

1801 # Refresh the materialized view 

1802 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1803 

1804 with api_session(token1) as api: 

1805 # Query by username 

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

1807 

1808 assert lite_user.user_id == user2.id 

1809 assert lite_user.is_ghost 

1810 assert lite_user.username == "ghost" 

1811 assert lite_user.name == "Deactivated Account" 

1812 

1813 # Query by ID 

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

1815 

1816 assert lite_user.user_id == user2.id 

1817 assert lite_user.is_ghost 

1818 assert lite_user.username == "ghost" 

1819 assert lite_user.name == "Deactivated Account" 

1820 

1821 

1822@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"]) 

1823def test_GetLiteUsers_ghost_users(db, flag): 

1824 """Test that GetLiteUsers returns ghost profiles for deleted/banned users.""" 

1825 user1, token1 = generate_user() 

1826 user2, _ = generate_user() 

1827 user3, _ = generate_user() 

1828 user4, _ = generate_user() 

1829 

1830 # Make user2 and user4 invisible 

1831 with session_scope() as session: 

1832 db_user2 = session.merge(user2) 

1833 setattr(db_user2, flag, now()) 

1834 db_user4 = session.merge(user4) 

1835 setattr(db_user4, flag, now()) 

1836 session.commit() 

1837 

1838 # Refresh the materialized view 

1839 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1840 

1841 with api_session(token1) as api: 

1842 res = api.GetLiteUsers( 

1843 api_pb2.GetLiteUsersReq( 

1844 users=[ 

1845 user1.username, # visible 

1846 user2.username, # ghost 

1847 str(user3.id), # visible 

1848 str(user4.id), # ghost 

1849 ] 

1850 ) 

1851 ) 

1852 

1853 assert len(res.responses) == 4 

1854 

1855 # user1 - visible, normal profile 

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

1857 assert not res.responses[0].not_found 

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

1859 assert res.responses[0].user.username == user1.username 

1860 assert res.responses[0].user.name == user1.name 

1861 

1862 # user2 - ghost by username 

1863 assert res.responses[1].query == user2.username 

1864 assert not res.responses[1].not_found 

1865 assert res.responses[1].user.user_id == user2.id 

1866 assert res.responses[1].user.is_ghost 

1867 assert res.responses[1].user.username == "ghost" 

1868 assert res.responses[1].user.name == "Deactivated Account" 

1869 

1870 # user3 - visible, normal profile 

1871 assert res.responses[2].query == str(user3.id) 

1872 assert not res.responses[2].not_found 

1873 assert res.responses[2].user.user_id == user3.id 

1874 assert res.responses[2].user.username == user3.username 

1875 assert res.responses[2].user.name == user3.name 

1876 

1877 # user4 - ghost by ID 

1878 assert res.responses[3].query == str(user4.id) 

1879 assert not res.responses[3].not_found 

1880 assert res.responses[3].user.user_id == user4.id 

1881 assert res.responses[3].user.is_ghost 

1882 assert res.responses[3].user.username == "ghost" 

1883 assert res.responses[3].user.name == "Deactivated Account" 

1884 

1885 

1886def test_GetLiteUsers_blocked_users(db): 

1887 """Test that GetLiteUsers returns ghost profiles for blocked users.""" 

1888 user1, token1 = generate_user() 

1889 user2, _ = generate_user() 

1890 user3, _ = generate_user() 

1891 user4, _ = generate_user() 

1892 user5, _ = generate_user() 

1893 

1894 # User1 blocks user2 

1895 make_user_block(user1, user2) 

1896 # User4 blocks user1 

1897 make_user_block(user4, user1) 

1898 

1899 # Refresh the materialized view 

1900 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1901 

1902 with api_session(token1) as api: 

1903 res = api.GetLiteUsers( 

1904 api_pb2.GetLiteUsersReq( 

1905 users=[ 

1906 user2.username, # user1 blocked user2 

1907 str(user3.id), # visible 

1908 user4.username, # user4 blocked user1 

1909 str(user5.id), # visible 

1910 ] 

1911 ) 

1912 ) 

1913 

1914 assert len(res.responses) == 4 

1915 

1916 # user2 - blocked by user1, should be ghost 

1917 assert res.responses[0].query == user2.username 

1918 assert not res.responses[0].not_found 

1919 assert res.responses[0].user.user_id == user2.id 

1920 assert res.responses[0].user.is_ghost 

1921 assert res.responses[0].user.username == "ghost" 

1922 assert res.responses[0].user.name == "Deactivated Account" 

1923 

1924 # user3 - visible 

1925 assert res.responses[1].query == str(user3.id) 

1926 assert not res.responses[1].not_found 

1927 assert res.responses[1].user.user_id == user3.id 

1928 assert res.responses[1].user.username == user3.username 

1929 

1930 # user4 - user4 blocked user1, should be ghost 

1931 assert res.responses[2].query == user4.username 

1932 assert not res.responses[2].not_found 

1933 assert res.responses[2].user.user_id == user4.id 

1934 assert res.responses[2].user.is_ghost 

1935 assert res.responses[2].user.username == "ghost" 

1936 assert res.responses[2].user.name == "Deactivated Account" 

1937 

1938 # user5 - visible 

1939 assert res.responses[3].query == str(user5.id) 

1940 assert not res.responses[3].not_found 

1941 assert res.responses[3].user.user_id == user5.id 

1942 assert res.responses[3].user.username == user5.username 

1943 

1944 

1945@pytest.mark.parametrize("flag", ["deleted_at", "banned_at"]) 

1946def test_GetUser_ghost_user_by_id(db, flag): 

1947 """Test that GetUser returns a ghost profile for deleted/banned users when querying by ID.""" 

1948 user1, token1 = generate_user() 

1949 user2, _ = generate_user() 

1950 

1951 # Make user2 invisible 

1952 with session_scope() as session: 

1953 db_user2 = session.merge(user2) 

1954 setattr(db_user2, flag, now()) 

1955 session.commit() 

1956 

1957 with api_session(token1) as api: 

1958 # Query by ID 

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

1960 

1961 assert user_pb.user_id == user2.id 

1962 assert user_pb.username == "ghost" 

1963 assert user_pb.name == "Deactivated Account" 

1964 assert user_pb.city == "" 

1965 assert user_pb.hosting_status == 0 

1966 assert user_pb.meetup_status == 0 

1967 

1968 

1969def test_GetUser_blocked_user(db): 

1970 """Test that GetUser returns a ghost profile for blocked users.""" 

1971 user1, token1 = generate_user() 

1972 user2, _ = generate_user() 

1973 

1974 # User1 blocks user2 

1975 make_user_block(user1, user2) 

1976 

1977 with api_session(token1) as api: 

1978 # Query by username 

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

1980 

1981 assert user_pb.user_id == user2.id 

1982 assert user_pb.username == "ghost" 

1983 assert user_pb.name == "Deactivated Account" 

1984 

1985 # Query by ID 

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

1987 

1988 assert user_pb.user_id == user2.id 

1989 assert user_pb.username == "ghost" 

1990 assert user_pb.name == "Deactivated Account" 

1991 

1992 

1993def test_GetUser_blocking_user(db): 

1994 """Test that GetUser returns a ghost profile when the target user has blocked the requester.""" 

1995 user1, token1 = generate_user() 

1996 user2, _ = generate_user() 

1997 

1998 # User2 blocks user1 

1999 make_user_block(user2, user1) 

2000 

2001 with api_session(token1) as api: 

2002 # Query by username 

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

2004 

2005 assert user_pb.user_id == user2.id 

2006 assert user_pb.username == "ghost" 

2007 assert user_pb.name == "Deactivated Account" 

2008 

2009 # Query by ID 

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

2011 

2012 assert user_pb.user_id == user2.id 

2013 assert user_pb.username == "ghost" 

2014 assert user_pb.name == "Deactivated Account"