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

1194 statements  

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

24 UserBadge, 

25) 

26from couchers.models.notifications import Notification 

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

28from couchers.rate_limits.definitions import RATE_LIMIT_DEFINITIONS, RATE_LIMIT_HOURS 

29from couchers.resources import get_badge_dict 

30from couchers.utils import create_coordinate, now, to_aware_datetime 

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

32from tests.fixtures.misc import EmailCollector, PushCollector 

33from tests.fixtures.sessions import ( 

34 api_session, 

35 blocking_session, 

36 notifications_session, 

37 real_api_session, 

38 real_jail_session, 

39) 

40from tests.fixtures.sessions import ( 

41 real_admin_session as admin_session, 

42) 

43 

44 

45def test_ping(db): 

46 user, token = generate_user( 

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

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

49 language_abilities=[ 

50 ("fin", LanguageFluency.fluent), 

51 ("fra", LanguageFluency.beginner), 

52 ], 

53 ) 

54 

55 with real_api_session(token) as api: 

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

57 

58 assert res.user.user_id == user.id 

59 assert res.user.username == user.username 

60 assert res.user.name == user.name 

61 assert res.user.city == user.city 

62 assert res.user.hometown == user.hometown 

63 assert res.user.verification == 0.0 

64 assert res.user.community_standing == user.community_standing 

65 assert res.user.num_references == 0 

66 assert res.user.gender == user.gender 

67 assert res.user.pronouns == user.pronouns 

68 assert res.user.age == user.age 

69 

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

71 

72 # the joined time is fuzzed 

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

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

75 # same for last_active 

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

77 

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

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

80 

81 assert res.user.occupation == user.occupation 

82 assert res.user.education == user.education 

83 assert res.user.about_me == user.about_me 

84 assert res.user.things_i_like == user.things_i_like 

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

86 assert res.user.about_place == user.about_place 

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

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

89 assert res.user.additional_information == user.additional_information 

90 

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

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

93 

94 

95def test_coords(db): 

96 # make them need to update location 

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

98 user2, token2 = generate_user() 

99 

100 with api_session(token2) as api: 

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

102 assert res.user.city == user2.city 

103 lat, lng = user2.coordinates 

104 assert res.user.lat == lat 

105 assert res.user.lng == lng 

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

107 

108 with api_session(token2) as api: 

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

110 assert res.city == user1.city 

111 assert res.lat == 1.0 

112 assert res.lng == 0.0 

113 assert res.radius == 2000.0 

114 

115 # Check coordinate wrapping 

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

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

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

119 

120 with api_session(token3) as api: 

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

122 assert res.lat == 40.0 

123 assert res.lng == 179.5 

124 

125 with api_session(token4) as api: 

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

127 assert res.lat == 40.0 

128 assert res.lng == 20.0 

129 

130 # PostGIS does not wrap longitude for latitude overflow 

131 with api_session(token5) as api: 

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

133 assert res.lat == 89.5 

134 assert res.lng == 20.0 

135 

136 with real_jail_session(token1) as jail: 

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

138 assert res.jailed 

139 assert res.needs_to_update_location 

140 

141 res = jail.SetLocation( 

142 jail_pb2.SetLocationReq( 

143 city="New York City", 

144 lat=40.7812, 

145 lng=-73.9647, 

146 radius=250, 

147 ) 

148 ) 

149 

150 assert not res.jailed 

151 assert not res.needs_to_update_location 

152 

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

154 assert not res.jailed 

155 assert not res.needs_to_update_location 

156 

157 with api_session(token2) as api: 

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

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

160 assert res.lat == 40.7812 

161 assert res.lng == -73.9647 

162 assert res.radius == 250 

163 

164 

165def test_get_user(db): 

166 user1, token1 = generate_user() 

167 user2, token2 = generate_user() 

168 

169 with api_session(token1) as api: 

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

171 assert res.user_id == user2.id 

172 assert res.username == user2.username 

173 assert res.name == user2.name 

174 

175 with api_session(token1) as api: 

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

177 assert res.user_id == user2.id 

178 assert res.username == user2.username 

179 assert res.name == user2.name 

180 

181 

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

183def test_user_model_to_pb_ghost_user(db, flag): 

184 user1, token1 = generate_user() 

185 user2, _ = generate_user() 

186 

187 with session_scope() as session: 

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

189 

190 refresh_materialized_views_rapid(empty_pb2.Empty()) 

191 

192 with api_session(token1) as api: 

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

194 

195 assert user_pb.user_id == user2.id 

196 assert user_pb.is_ghost 

197 assert user_pb.username == "ghost" 

198 assert user_pb.name == "Deactivated Account" 

199 assert ( 

200 user_pb.about_me 

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

202 ) 

203 

204 assert user_pb.lat == 0 

205 assert user_pb.lng == 0 

206 assert user_pb.radius == 0 

207 assert user_pb.verification == 0.0 

208 assert user_pb.community_standing == 0.0 

209 assert user_pb.num_references == 0 

210 assert user_pb.age == 0 

211 assert user_pb.hosting_status == 0 

212 assert user_pb.meetup_status == 0 

213 assert user_pb.city == "" 

214 assert user_pb.hometown == "" 

215 assert user_pb.timezone == "" 

216 assert user_pb.gender == "" 

217 assert user_pb.pronouns == "" 

218 assert user_pb.occupation == "" 

219 assert user_pb.education == "" 

220 assert user_pb.things_i_like == "" 

221 assert user_pb.about_place == "" 

222 assert user_pb.additional_information == "" 

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

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

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

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

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

228 assert user_pb.avatar_url == "" 

229 assert user_pb.avatar_thumbnail_url == "" 

230 assert not user_pb.has_strong_verification 

231 

232 with api_session(token1) as api: 

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

234 

235 assert lite_user_pb.user_id == user2.id 

236 assert lite_user_pb.is_ghost 

237 assert lite_user_pb.username == "ghost" 

238 assert lite_user_pb.name == "Deactivated Account" 

239 assert lite_user_pb.city == "" 

240 assert lite_user_pb.age == 0 

241 assert lite_user_pb.avatar_url == "" 

242 assert lite_user_pb.avatar_thumbnail_url == "" 

243 assert lite_user_pb.lat == 0 

244 assert lite_user_pb.lng == 0 

245 assert lite_user_pb.radius == 0 

246 assert not lite_user_pb.has_strong_verification 

247 

248 

249def test_user_model_to_pb_ghost_user_blocked(db): 

250 user1, token1 = generate_user() 

251 user2, _ = generate_user() 

252 

253 with blocking_session(token1) as user_blocks: 

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

255 

256 refresh_materialized_views_rapid(empty_pb2.Empty()) 

257 

258 with api_session(token1) as api: 

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

260 

261 assert user_pb.user_id == user2.id 

262 assert user_pb.is_ghost 

263 assert user_pb.username == "ghost" 

264 assert user_pb.name == "Deactivated Account" 

265 assert ( 

266 user_pb.about_me 

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

268 ) 

269 

270 assert user_pb.lat == 0 

271 assert user_pb.lng == 0 

272 assert user_pb.radius == 0 

273 assert user_pb.verification == 0.0 

274 assert user_pb.community_standing == 0.0 

275 assert user_pb.num_references == 0 

276 assert user_pb.age == 0 

277 assert user_pb.hosting_status == 0 

278 assert user_pb.meetup_status == 0 

279 assert user_pb.city == "" 

280 assert user_pb.hometown == "" 

281 assert user_pb.timezone == "" 

282 assert user_pb.gender == "" 

283 assert user_pb.pronouns == "" 

284 assert user_pb.occupation == "" 

285 assert user_pb.education == "" 

286 assert user_pb.things_i_like == "" 

287 assert user_pb.about_place == "" 

288 assert user_pb.additional_information == "" 

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

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

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

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

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

294 assert user_pb.avatar_url == "" 

295 assert user_pb.avatar_thumbnail_url == "" 

296 assert not user_pb.has_strong_verification 

297 

298 with api_session(token1) as api: 

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

300 

301 assert lite_user_pb.user_id == user2.id 

302 assert lite_user_pb.is_ghost 

303 assert lite_user_pb.username == "ghost" 

304 assert lite_user_pb.name == "Deactivated Account" 

305 assert lite_user_pb.city == "" 

306 assert lite_user_pb.age == 0 

307 assert lite_user_pb.avatar_url == "" 

308 assert lite_user_pb.avatar_thumbnail_url == "" 

309 assert lite_user_pb.lat == 0 

310 assert lite_user_pb.lng == 0 

311 assert lite_user_pb.radius == 0 

312 assert not lite_user_pb.has_strong_verification 

313 

314 

315@pytest.mark.parametrize( 

316 "flag,state", 

317 [ 

318 ("banned_at", NonvisibleUserState.banned), 

319 ("shadowed_at", NonvisibleUserState.shadowed), 

320 ("deleted_at", NonvisibleUserState.deleted), 

321 ], 

322) 

323def test_viewing_nonvisible_user_profile_is_logged(db, flag, state): 

324 user1, token1 = generate_user() 

325 user2, _ = generate_user() 

326 

327 with session_scope() as session: 

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

329 

330 refresh_materialized_views_rapid(empty_pb2.Empty()) 

331 

332 with api_session(token1) as api: 

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

334 assert user_pb.is_ghost 

335 

336 with session_scope() as session: 

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

338 assert access.access_type == NonvisibleUserAccessType.ghost_served 

339 assert access.target_state == state 

340 assert access.target_user_id == user2.id 

341 assert access.actor_user_id == user1.id 

342 assert access.ip_address is None 

343 assert access.user_agent is None 

344 assert access.sofa is None 

345 

346 

347def test_viewing_blocked_user_profile_is_not_logged(db): 

348 user1, token1 = generate_user() 

349 user2, _ = generate_user() 

350 

351 with blocking_session(token1) as user_blocks: 

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

353 

354 refresh_materialized_views_rapid(empty_pb2.Empty()) 

355 

356 with api_session(token1) as api: 

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

358 assert user_pb.is_ghost 

359 

360 with session_scope() as session: 

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

362 

363 

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

365def test_admin_viewing_ghost_users_sees_full_profile(db, flag): 

366 admin, token_admin = generate_user(is_superuser=True) 

367 user, _ = generate_user() 

368 

369 with session_scope() as session: 

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

371 

372 with admin_session(token_admin) as api: 

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

374 

375 assert user_pb.user_id == user.id 

376 assert user_pb.username == user.username 

377 assert user_pb.name == user.name 

378 assert user_pb.city == user.city 

379 assert user_pb.name != "Deactivated Account" 

380 assert user_pb.username != "ghost" 

381 assert user_pb.hosting_status in ( 

382 api_pb2.HOSTING_STATUS_UNKNOWN, 

383 api_pb2.HOSTING_STATUS_CAN_HOST, 

384 api_pb2.HOSTING_STATUS_MAYBE, 

385 api_pb2.HOSTING_STATUS_CANT_HOST, 

386 ) 

387 

388 

389def test_lite_coords(db): 

390 # make them need to update location 

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

392 user2, token2 = generate_user() 

393 

394 refresh_materialized_views_rapid(empty_pb2.Empty()) 

395 

396 with api_session(token2) as api: 

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

398 assert res.user.city == user2.city 

399 lat, lng = user2.coordinates 

400 assert res.user.lat == lat 

401 assert res.user.lng == lng 

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

403 

404 with api_session(token2) as api: 

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

406 assert res.city == user1.city 

407 assert res.lat == 0.0 

408 assert res.lng == 0.0 

409 assert res.radius == 0.0 

410 

411 # Check coordinate wrapping 

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

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

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

415 

416 refresh_materialized_views_rapid(empty_pb2.Empty()) 

417 

418 with api_session(token3) as api: 

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

420 assert res.lat == 40.0 

421 assert res.lng == 179.5 

422 

423 with api_session(token4) as api: 

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

425 assert res.lat == 40.0 

426 assert res.lng == 20.0 

427 

428 # PostGIS does not wrap longitude for latitude overflow 

429 with api_session(token5) as api: 

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

431 assert res.lat == 89.5 

432 assert res.lng == 20.0 

433 

434 with real_jail_session(token1) as jail: 

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

436 assert res.jailed 

437 assert res.needs_to_update_location 

438 

439 res = jail.SetLocation( 

440 jail_pb2.SetLocationReq( 

441 city="New York City", 

442 lat=40.7812, 

443 lng=-73.9647, 

444 radius=250, 

445 ) 

446 ) 

447 

448 assert not res.jailed 

449 assert not res.needs_to_update_location 

450 

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

452 assert not res.jailed 

453 assert not res.needs_to_update_location 

454 

455 refresh_materialized_views_rapid(empty_pb2.Empty()) 

456 

457 with api_session(token2) as api: 

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

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

460 assert res.lat == 40.7812 

461 assert res.lng == -73.9647 

462 assert res.radius == 250 

463 

464 

465def test_lite_get_user(db): 

466 user1, token1 = generate_user() 

467 user2, token2 = generate_user() 

468 

469 refresh_materialized_views_rapid(empty_pb2.Empty()) 

470 

471 with api_session(token1) as api: 

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

473 assert res.user_id == user2.id 

474 assert res.username == user2.username 

475 assert res.name == user2.name 

476 

477 with api_session(token1) as api: 

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

479 assert res.user_id == user2.id 

480 assert res.username == user2.username 

481 assert res.name == user2.name 

482 

483 

484def test_GetLiteUsers(db): 

485 user1, token1 = generate_user() 

486 user2, _ = generate_user() 

487 user3, _ = generate_user() 

488 user4, _ = generate_user() 

489 user5, _ = generate_user() 

490 user6, _ = generate_user() 

491 

492 make_user_block(user4, user1) 

493 

494 refresh_materialized_views_rapid(empty_pb2.Empty()) 

495 

496 with api_session(token1) as api: 

497 res = api.GetLiteUsers( 

498 api_pb2.GetLiteUsersReq( 

499 users=[ 

500 user1.username, 

501 str(user1.id), 

502 "nonexistent", 

503 str(user2.id), 

504 "9994", 

505 user6.username, 

506 str(user5.id), 

507 "notreal", 

508 user4.username, 

509 ] 

510 ) 

511 ) 

512 

513 assert len(res.responses) == 9 

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

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

516 

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

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

519 

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

521 assert res.responses[2].not_found 

522 

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

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

525 

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

527 assert res.responses[4].not_found 

528 

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

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

531 

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

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

534 

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

536 assert res.responses[7].not_found 

537 

538 # blocked - should return ghost profile 

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

540 assert not res.responses[8].not_found 

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

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

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

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

545 

546 with api_session(token1) as api: 

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

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

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

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

551 

552 

553def test_update_profile(db): 

554 user, token = generate_user() 

555 

556 with api_session(token) as api: 

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

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

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

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

561 

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

563 api.UpdateProfile( 

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

565 ) 

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

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

568 

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

570 api.UpdateProfile( 

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

572 ) 

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

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

575 

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

577 api.UpdateProfile( 

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

579 ) 

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

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

582 

583 api.UpdateProfile( 

584 api_pb2.UpdateProfileReq( 

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

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

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

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

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

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

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

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

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

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

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

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

597 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

598 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

599 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

600 value=[ 

601 api_pb2.LanguageAbility( 

602 code="eng", 

603 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

604 ) 

605 ], 

606 ), 

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

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

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

610 ) 

611 ) 

612 

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

614 assert user_details.name == "New name" 

615 assert user_details.city == "Timbuktu" 

616 assert user_details.hometown == "Walla Walla" 

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

618 assert user_details.education == "Couchers U" 

619 assert user_details.things_i_like == "Couchers" 

620 assert user_details.lat == 0.01 

621 assert user_details.lng == -2 

622 assert user_details.radius == 321 

623 assert user_details.occupation == "Testing" 

624 assert user_details.about_me == "I rule" 

625 assert user_details.about_place == "My place" 

626 assert user_details.hosting_status == api_pb2.HOSTING_STATUS_CAN_HOST 

627 assert user_details.meetup_status == api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP 

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

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

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

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

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

633 

634 # Test unset values 

635 api.UpdateProfile( 

636 api_pb2.UpdateProfileReq( 

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

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

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

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

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

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

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

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

645 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

646 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

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

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

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

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

651 ) 

652 ) 

653 

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

655 assert not user_details.hometown 

656 assert not user_details.radius 

657 assert not user_details.pronouns 

658 assert not user_details.occupation 

659 assert not user_details.education 

660 assert not user_details.about_me 

661 assert not user_details.things_i_like 

662 assert not user_details.about_place 

663 assert user_details.hosting_status == api_pb2.HOSTING_STATUS_CAN_HOST 

664 assert user_details.meetup_status == api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP 

665 assert not user_details.language_abilities 

666 assert not user_details.regions_visited 

667 assert not user_details.regions_lived 

668 assert not user_details.additional_information 

669 

670 

671def test_update_profile_do_not_email(db): 

672 user, token = generate_user() 

673 

674 with notifications_session(token) as notifications: 

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

676 

677 with api_session(token) as api: 

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

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

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

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

682 

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

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

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

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

687 

688 

689def test_language_abilities(db): 

690 user, token = generate_user( 

691 language_abilities=[ 

692 ("fin", LanguageFluency.fluent), 

693 ("fra", LanguageFluency.beginner), 

694 ], 

695 ) 

696 

697 with api_session(token) as api: 

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

699 assert len(res.language_abilities) == 2 

700 

701 # can't add non-existent languages 

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

703 api.UpdateProfile( 

704 api_pb2.UpdateProfileReq( 

705 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

706 value=[ 

707 api_pb2.LanguageAbility( 

708 code="QQQ", 

709 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

710 ) 

711 ], 

712 ), 

713 ) 

714 ) 

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

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

717 

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

719 with pytest.raises(Exception) as err2: 

720 api.UpdateProfile( 

721 api_pb2.UpdateProfileReq( 

722 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

723 value=[ 

724 api_pb2.LanguageAbility( 

725 code="eng", 

726 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

727 ), 

728 api_pb2.LanguageAbility( 

729 code="eng", 

730 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

731 ), 

732 ], 

733 ), 

734 ) 

735 ) 

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

737 

738 # nothing changed 

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

740 assert len(res.language_abilities) == 2 

741 

742 # now actually add a value 

743 api.UpdateProfile( 

744 api_pb2.UpdateProfileReq( 

745 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

746 value=[ 

747 api_pb2.LanguageAbility( 

748 code="eng", 

749 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

750 ) 

751 ], 

752 ), 

753 ) 

754 ) 

755 

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

757 assert len(res.language_abilities) == 1 

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

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

760 

761 # change the value to a new one 

762 api.UpdateProfile( 

763 api_pb2.UpdateProfileReq( 

764 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

765 value=[ 

766 api_pb2.LanguageAbility( 

767 code="fin", 

768 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER, 

769 ) 

770 ], 

771 ), 

772 ) 

773 ) 

774 

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

776 assert len(res.language_abilities) == 1 

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

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

779 

780 # should be able to set to same value still 

781 api.UpdateProfile( 

782 api_pb2.UpdateProfileReq( 

783 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

784 value=[ 

785 api_pb2.LanguageAbility( 

786 code="fin", 

787 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER, 

788 ) 

789 ], 

790 ), 

791 ) 

792 ) 

793 

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

795 assert len(res.language_abilities) == 1 

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

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

798 

799 # don't change it 

800 api.UpdateProfile(api_pb2.UpdateProfileReq()) 

801 

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

803 assert len(res.language_abilities) == 1 

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

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

806 

807 # remove value 

808 api.UpdateProfile( 

809 api_pb2.UpdateProfileReq( 

810 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

811 value=[], 

812 ), 

813 ) 

814 ) 

815 

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

817 assert len(res.language_abilities) == 0 

818 

819 

820def test_pending_friend_request_count(db, moderator): 

821 user1, token1 = generate_user() 

822 user2, token2 = generate_user() 

823 user3, token3 = generate_user() 

824 

825 with api_session(token2) as api: 

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

827 assert res.pending_friend_request_count == 0 

828 

829 with api_session(token1) as api: 

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

831 assert res.pending_friend_request_count == 0 

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

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

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

835 assert res.pending_friend_request_count == 0 

836 

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

838 with api_session(token1) as api: 

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

840 assert len(res.sent) == 1 

841 fr_id = res.sent[0].friend_request_id 

842 

843 # Recipient cannot see SHADOWED friend requests before mod approval 

844 with api_session(token2) as api: 

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

846 assert res.pending_friend_request_count == 0 

847 

848 # Moderator approves the friend request 

849 moderator.approve_friend_request(fr_id) 

850 

851 # Now recipient can see the approved friend request 

852 with api_session(token2) as api: 

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

854 assert res.pending_friend_request_count == 1 

855 

856 with api_session(token2) as api: 

857 # check it's there 

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

859 assert len(res.sent) == 0 

860 assert len(res.received) == 1 

861 

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

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

864 

865 fr_id = res.received[0].friend_request_id 

866 

867 # accept it 

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

869 

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

871 assert res.pending_friend_request_count == 0 

872 

873 

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

875 user1, token1 = generate_user(complete_profile=True) 

876 user2, token2 = generate_user(complete_profile=True) 

877 user3, token3 = generate_user() 

878 

879 # send a friend request from user1 to user2 

880 with api_session(token1) as api: 

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

882 

883 with session_scope() as session: 

884 friend_request = session.execute( 

885 select(FriendRelationship).where( 

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

887 ) 

888 ).scalar_one() 

889 friend_request_id = friend_request.id 

890 

891 # Notification is deferred while content is SHADOWED 

892 # No push notification sent yet 

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

894 

895 with api_session(token1) as api: 

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

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

898 assert len(res.sent) == 1 

899 assert len(res.received) == 0 

900 

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

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

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

904 

905 # Recipient cannot see SHADOWED friend requests 

906 with api_session(token2) as api: 

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

908 assert len(res.sent) == 0 

909 assert len(res.received) == 0 

910 

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

912 moderator.approve_friend_request(friend_request_id) 

913 

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

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

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

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

918 

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

920 assert email.recipient == user2.email 

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

922 assert user2.name in email.plain 

923 assert user2.name in email.html 

924 assert user1.name in email.plain 

925 assert user1.name in email.html 

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

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

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

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

930 

931 # Now recipient can see the approved friend request 

932 with api_session(token2) as api: 

933 # check it's there 

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

935 assert len(res.sent) == 0 

936 assert len(res.received) == 1 

937 

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

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

940 

941 fr_id = res.received[0].friend_request_id 

942 

943 # accept it 

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

945 

946 # check it's gone 

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

948 assert len(res.sent) == 0 

949 assert len(res.received) == 0 

950 

951 # check we're friends now 

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

953 assert len(res.user_ids) == 1 

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

955 

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

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

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

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

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

961 

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

963 assert email.recipient == user1.email 

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

965 assert user1.name in email.plain 

966 assert user1.name in email.html 

967 assert user2.name in email.plain 

968 assert user2.name in email.html 

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

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

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

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

973 

974 with api_session(token1) as api: 

975 # check it's gone 

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

977 assert len(res.sent) == 0 

978 assert len(res.received) == 0 

979 

980 # check we're friends now 

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

982 assert len(res.user_ids) == 1 

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

984 

985 with api_session(token1) as api: 

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

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

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

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

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

991 

992 # we can unfriend 

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

994 

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

996 assert len(res.user_ids) == 0 

997 

998 

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

1000 user1, token1 = generate_user(complete_profile=True) 

1001 user2, token2 = generate_user(complete_profile=True) 

1002 user3, token3 = generate_user() 

1003 user4, token4 = generate_user() 

1004 user5, token5 = generate_user() 

1005 user6, token6 = generate_user() 

1006 

1007 # Send friend requests 

1008 with api_session(token4) as api: 

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

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

1011 

1012 with api_session(token5) as api: 

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

1014 

1015 with api_session(token1) as api: 

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

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

1018 

1019 # Approve all friend requests via moderation 

1020 with session_scope() as session: 

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

1022 for fr in friend_requests: 

1023 moderator.approve_friend_request(fr.id) 

1024 

1025 # Now recipients can respond 

1026 with api_session(token1) as api: 

1027 api.RespondFriendRequest( 

1028 api_pb2.RespondFriendRequestReq( 

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

1030 ) 

1031 ) 

1032 

1033 with api_session(token2) as api: 

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

1035 api.RespondFriendRequest( 

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

1037 ) 

1038 

1039 with api_session(token1) as api: 

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

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

1042 

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

1044 

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

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

1047 

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

1049 

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

1051 assert not res.user_ids 

1052 

1053 

1054def test_cant_friend_request_twice(db): 

1055 user1, token1 = generate_user() 

1056 user2, token2 = generate_user() 

1057 

1058 # send friend request from user1 to user2 

1059 with api_session(token1) as api: 

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

1061 

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

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

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

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

1066 

1067 

1068def test_cant_friend_request_pending(db): 

1069 user1, token1 = generate_user() 

1070 user2, token2 = generate_user() 

1071 

1072 # send friend request from user1 to user2 

1073 with api_session(token1) as api: 

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

1075 

1076 with api_session(token2) as api: 

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

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

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

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

1081 

1082 

1083def test_cant_friend_request_already_friends(db): 

1084 user1, token1 = generate_user() 

1085 user2, token2 = generate_user() 

1086 make_friends(user1, user2) 

1087 

1088 with api_session(token1) as api: 

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

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

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

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

1093 

1094 with api_session(token2) as api: 

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

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

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

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

1099 

1100 

1101def test_cant_friend_request_incomplete_profile(db): 

1102 user1, token1 = generate_user(complete_profile=False) 

1103 user2, token2 = generate_user(complete_profile=True) 

1104 

1105 with api_session(token1) as api: 

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

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

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

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

1110 

1111 # the other direction should still work 

1112 with api_session(token2) as api: 

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

1114 

1115 

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

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

1118 user, token = generate_user() 

1119 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.friend_request] 

1120 with api_session(token) as api: 

1121 # Test warning email 

1122 for _ in range(rate_limit_definition.warning_limit): 

1123 friend_user, _ = generate_user() 

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

1125 

1126 assert email_collector.count_for_reports() == 0 

1127 friend_user, _ = generate_user() 

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

1129 

1130 email = email_collector.pop_for_reports(last=True) 

1131 assert email.plain.startswith( 

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

1133 ) 

1134 

1135 # Test ban after exceeding FRIEND_REQUEST_HARD_LIMIT 

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

1137 friend_user, _ = generate_user() 

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

1139 

1140 assert email_collector.count_for_reports() == 0 

1141 friend_user, _ = generate_user() 

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

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

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

1145 assert ( 

1146 exc_info.value.details() 

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

1148 ) 

1149 

1150 email = email_collector.pop_for_reports(last=True) 

1151 assert email.plain.startswith( 

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

1153 ) 

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

1155 

1156 

1157def test_ListFriends(db, moderator): 

1158 user1, token1 = generate_user() 

1159 user2, token2 = generate_user() 

1160 user3, token3 = generate_user() 

1161 

1162 # send friend request from user1 to user2 and user3 

1163 with api_session(token1) as api: 

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

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

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

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

1168 assert len(res.sent) == 2 

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

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

1171 

1172 with api_session(token3) as api: 

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

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

1175 user3_to_user2_id = res.sent[0].friend_request_id 

1176 

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

1178 moderator.approve_friend_request(user1_to_user2_id) 

1179 moderator.approve_friend_request(user3_to_user2_id) 

1180 

1181 with api_session(token2) as api: 

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

1183 assert len(res.received) == 2 

1184 

1185 # order is an implementation detail 

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

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

1188 

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

1190 assert user1_req.user_id == user1.id 

1191 api.RespondFriendRequest( 

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

1193 ) 

1194 

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

1196 assert user3_req.user_id == user3.id 

1197 api.RespondFriendRequest( 

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

1199 ) 

1200 

1201 # check we now have two friends 

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

1203 assert len(res.user_ids) == 2 

1204 assert user1.id in res.user_ids 

1205 assert user3.id in res.user_ids 

1206 

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

1208 moderator.approve_friend_request(user1_to_user3_id) 

1209 

1210 with api_session(token3) as api: 

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

1212 assert len(res.user_ids) == 1 

1213 assert user2.id in res.user_ids 

1214 

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

1216 assert len(res.received) == 1 

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

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

1219 fr_id = res.received[0].friend_request_id 

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

1221 

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

1223 assert len(res.user_ids) == 2 

1224 assert user1.id in res.user_ids 

1225 assert user2.id in res.user_ids 

1226 

1227 with api_session(token1) as api: 

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

1229 assert len(res.user_ids) == 2 

1230 assert user2.id in res.user_ids 

1231 assert user3.id in res.user_ids 

1232 

1233 

1234def test_ListMutualFriends(db): 

1235 user1, token1 = generate_user() 

1236 user2, token2 = generate_user() 

1237 user3, token3 = generate_user() 

1238 user4, token4 = generate_user() 

1239 user5, token5 = generate_user() 

1240 

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

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

1243 make_friends(user1, user2) 

1244 make_friends(user1, user3) 

1245 make_friends(user1, user4) 

1246 make_friends(user1, user5) 

1247 make_friends(user3, user2) 

1248 make_friends(user3, user4) 

1249 

1250 with api_session(token1) as api: 

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

1252 assert len(mutual_friends) == 1 

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

1254 

1255 # and other way around same 

1256 with api_session(token2) as api: 

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

1258 assert len(mutual_friends) == 1 

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

1260 

1261 # Check pending request doesn't have effect 

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

1263 

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

1265 assert len(mutual_friends) == 1 

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

1267 

1268 # both ways 

1269 with api_session(token1) as api: 

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

1271 assert len(mutual_friends) == 1 

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

1273 

1274 

1275def test_mutual_friends_self(db): 

1276 user1, token1 = generate_user() 

1277 user2, token2 = generate_user() 

1278 user3, token3 = generate_user() 

1279 user4, token4 = generate_user() 

1280 

1281 make_friends(user1, user2) 

1282 make_friends(user2, user3) 

1283 make_friends(user1, user4) 

1284 

1285 with api_session(token1) as api: 

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

1287 assert len(res.mutual_friends) == 0 

1288 

1289 with api_session(token2) as api: 

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

1291 assert len(res.mutual_friends) == 0 

1292 

1293 with api_session(token3) as api: 

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

1295 assert len(res.mutual_friends) == 0 

1296 

1297 with api_session(token4) as api: 

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

1299 assert len(res.mutual_friends) == 0 

1300 

1301 

1302def test_CancelFriendRequest(db): 

1303 user1, token1 = generate_user() 

1304 user2, token2 = generate_user() 

1305 

1306 with api_session(token1) as api: 

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

1308 

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

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

1311 fr_id = res.sent[0].friend_request_id 

1312 

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

1314 

1315 # check it's gone 

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

1317 assert len(res.sent) == 0 

1318 assert len(res.received) == 0 

1319 

1320 # check not friends 

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

1322 assert len(res.user_ids) == 0 

1323 

1324 with api_session(token2) as api: 

1325 # check it's gone 

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

1327 assert len(res.sent) == 0 

1328 assert len(res.received) == 0 

1329 

1330 # check we're not friends 

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

1332 assert len(res.user_ids) == 0 

1333 

1334 with api_session(token1) as api: 

1335 # check we can send another friend req 

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

1337 

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

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

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

1341 

1342 

1343def test_accept_friend_request(db, moderator): 

1344 user1, token1 = generate_user() 

1345 user2, token2 = generate_user() 

1346 

1347 with session_scope() as session: 

1348 moderation_state = ModerationState( 

1349 object_type=ModerationObjectType.friend_request, 

1350 object_id=0, 

1351 visibility=ModerationVisibility.visible, 

1352 ) 

1353 session.add(moderation_state) 

1354 session.flush() 

1355 friend_request = FriendRelationship( 

1356 from_user_id=user1.id, 

1357 to_user_id=user2.id, 

1358 status=FriendStatus.pending, 

1359 moderation_state_id=moderation_state.id, 

1360 ) 

1361 session.add(friend_request) 

1362 session.flush() 

1363 moderation_state.object_id = friend_request.id 

1364 session.commit() 

1365 friend_request_id = friend_request.id 

1366 

1367 with api_session(token2) as api: 

1368 # check request pending 

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

1370 assert len(res.received) == 1 

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

1372 

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

1374 

1375 # check request is gone 

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

1377 assert len(res.sent) == 0 

1378 assert len(res.received) == 0 

1379 

1380 # check now friends 

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

1382 assert len(res.user_ids) == 1 

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

1384 

1385 with api_session(token1) as api: 

1386 # check request gone 

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

1388 assert len(res.sent) == 0 

1389 assert len(res.received) == 0 

1390 

1391 # check now friends 

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

1393 assert len(res.user_ids) == 1 

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

1395 

1396 

1397def test_reject_friend_request(db, moderator): 

1398 user1, token1 = generate_user() 

1399 user2, token2 = generate_user() 

1400 

1401 with api_session(token1) as api: 

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

1403 

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

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

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

1407 fr_id = res.sent[0].friend_request_id 

1408 

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

1410 moderator.approve_friend_request(fr_id) 

1411 

1412 with api_session(token2) as api: 

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

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

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

1416 

1417 fr_id = res.received[0].friend_request_id 

1418 

1419 # reject it 

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

1421 

1422 # check it's gone 

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

1424 assert len(res.sent) == 0 

1425 assert len(res.received) == 0 

1426 

1427 # check not friends 

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

1429 assert len(res.user_ids) == 0 

1430 

1431 with api_session(token1) as api: 

1432 # check it's gone 

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

1434 assert len(res.sent) == 0 

1435 assert len(res.received) == 0 

1436 

1437 # check we're not friends 

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

1439 assert len(res.user_ids) == 0 

1440 

1441 # check we can send another friend req 

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

1443 

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

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

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

1447 

1448 

1449def test_hosting_preferences(db): 

1450 user1, token1 = generate_user() 

1451 user2, token2 = generate_user() 

1452 

1453 with api_session(token1) as api: 

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

1455 assert not res.HasField("max_guests") 

1456 assert not res.HasField("last_minute") 

1457 assert not res.HasField("has_pets") 

1458 assert not res.HasField("accepts_pets") 

1459 assert not res.HasField("pet_details") 

1460 assert not res.HasField("has_kids") 

1461 assert not res.HasField("accepts_kids") 

1462 assert not res.HasField("kid_details") 

1463 assert not res.HasField("has_housemates") 

1464 assert not res.HasField("housemate_details") 

1465 assert not res.HasField("wheelchair_accessible") 

1466 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_UNKNOWN 

1467 assert not res.HasField("smokes_at_home") 

1468 assert not res.HasField("drinking_allowed") 

1469 assert not res.HasField("drinks_at_home") 

1470 assert not res.HasField("other_host_info") 

1471 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN 

1472 assert not res.HasField("sleeping_details") 

1473 assert not res.HasField("area") 

1474 assert not res.HasField("house_rules") 

1475 assert not res.HasField("parking") 

1476 assert res.parking_details == api_pb2.PARKING_DETAILS_UNKNOWN 

1477 assert not res.HasField("camping_ok") 

1478 

1479 api.UpdateProfile( 

1480 api_pb2.UpdateProfileReq( 

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

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

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

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

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

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

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

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

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

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

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

1492 smoking_allowed=api_pb2.SMOKING_LOCATION_WINDOW, 

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

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

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

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

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

1498 sleeping_arrangement=api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

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

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

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

1502 parking_details=api_pb2.PARKING_DETAILS_PAID_ONSITE, 

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

1504 ) 

1505 ) 

1506 

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

1508 # that it is public information. 

1509 with api_session(token2) as api: 

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

1511 assert res.max_guests.value == 3 

1512 assert res.last_minute.value 

1513 assert not res.has_pets.value 

1514 assert res.accepts_pets.value 

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

1516 assert not res.has_kids.value 

1517 assert res.accepts_kids.value 

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

1519 assert not res.has_housemates.value 

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

1521 assert res.wheelchair_accessible.value 

1522 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_WINDOW 

1523 assert not res.smokes_at_home.value 

1524 assert res.drinking_allowed.value 

1525 assert not res.drinks_at_home.value 

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

1527 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_COMMON 

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

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

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

1531 assert res.parking.value 

1532 assert res.parking_details == api_pb2.PARKING_DETAILS_PAID_ONSITE 

1533 assert not res.camping_ok.value 

1534 

1535 # test unsetting 

1536 with api_session(token1) as api: 

1537 api.UpdateProfile( 

1538 api_pb2.UpdateProfileReq( 

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

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

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

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

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

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

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

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

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

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

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

1550 smoking_allowed=api_pb2.SMOKING_LOCATION_UNKNOWN, 

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

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

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

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

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

1556 sleeping_arrangement=api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

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

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

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

1560 parking_details=api_pb2.PARKING_DETAILS_UNKNOWN, 

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

1562 ) 

1563 ) 

1564 

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

1566 assert not res.HasField("max_guests") 

1567 assert not res.HasField("last_minute") 

1568 assert not res.HasField("has_pets") 

1569 assert not res.HasField("accepts_pets") 

1570 assert not res.HasField("pet_details") 

1571 assert not res.HasField("has_kids") 

1572 assert not res.HasField("accepts_kids") 

1573 assert not res.HasField("kid_details") 

1574 assert not res.HasField("has_housemates") 

1575 assert not res.HasField("housemate_details") 

1576 assert not res.HasField("wheelchair_accessible") 

1577 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_UNKNOWN 

1578 assert not res.HasField("smokes_at_home") 

1579 assert not res.HasField("drinking_allowed") 

1580 assert not res.HasField("drinks_at_home") 

1581 assert not res.HasField("other_host_info") 

1582 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN 

1583 assert not res.HasField("sleeping_details") 

1584 assert not res.HasField("area") 

1585 assert not res.HasField("house_rules") 

1586 assert not res.HasField("parking") 

1587 assert res.parking_details == api_pb2.PARKING_DETAILS_UNKNOWN 

1588 assert not res.HasField("camping_ok") 

1589 

1590 

1591def test_badges(db): 

1592 user1, _ = generate_user(last_donated=None) 

1593 user2, _ = generate_user(last_donated=None) 

1594 user3, _ = generate_user(last_donated=None) 

1595 user4, token = generate_user(last_donated=None) 

1596 

1597 update_badges(empty_pb2.Empty()) 

1598 

1599 founder_badge = get_badge_dict()["founder"] 

1600 board_member_badge = get_badge_dict()["board_member"] 

1601 

1602 with api_session(token) as api: 

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

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

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

1606 

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

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

1609 assert res.user_ids == [1] 

1610 res2 = api.ListBadgeUsers( 

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

1612 ) 

1613 assert res2.user_ids == [2] 

1614 

1615 

1616def test_user_add_badge_is_idempotent(db): 

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

1618 user, _ = generate_user() 

1619 

1620 with session_scope() as session: 

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

1622 

1623 # one badge row, one notification 

1624 with session_scope() as session: 

1625 badge_count = session.execute( 

1626 select(func.count()) 

1627 .select_from(UserBadge) 

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

1629 ).scalar() 

1630 assert badge_count == 1 

1631 notification_count = session.execute( 

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

1633 ).scalar() 

1634 assert notification_count == 1 

1635 

1636 # add the same badge again 

1637 with session_scope() as session: 

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

1639 

1640 # still one badge row, no new notification 

1641 with session_scope() as session: 

1642 badge_count = session.execute( 

1643 select(func.count()) 

1644 .select_from(UserBadge) 

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

1646 ).scalar() 

1647 assert badge_count == 1 

1648 notification_count = session.execute( 

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

1650 ).scalar() 

1651 assert notification_count == 1 

1652 

1653 

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

1655def test_ListBadgeUsers_excludes_ghost_users(db, flag): 

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

1657 user1, token1 = generate_user() 

1658 user2, _ = generate_user() 

1659 user3, _ = generate_user() 

1660 

1661 volunteer_badge = get_badge_dict()["volunteer"] 

1662 

1663 # Give all three users the volunteer badge 

1664 with session_scope() as session: 

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

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

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

1668 

1669 # Verify all three users appear in the badge list 

1670 with api_session(token1) as api: 

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

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

1673 

1674 # Make user2 invisible (deleted or banned) 

1675 with session_scope() as session: 

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

1677 setattr(db_user2, flag, now()) 

1678 

1679 # Now user2 should not appear in the badge list 

1680 with api_session(token1) as api: 

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

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

1683 

1684 

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

1686def test_GetLiteUser_ghost_user_by_username(db, flag): 

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

1688 user1, token1 = generate_user() 

1689 user2, _ = generate_user() 

1690 

1691 # Make user2 invisible 

1692 with session_scope() as session: 

1693 db_user2 = session.merge(user2) 

1694 setattr(db_user2, flag, now()) 

1695 session.commit() 

1696 

1697 # Refresh the materialized view 

1698 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1699 

1700 with api_session(token1) as api: 

1701 # Query by username 

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

1703 

1704 assert lite_user.user_id == user2.id 

1705 assert lite_user.username == "ghost" 

1706 assert lite_user.name == "Deactivated Account" 

1707 assert lite_user.lat == 0 

1708 assert lite_user.lng == 0 

1709 assert lite_user.radius == 0 

1710 assert lite_user.city == "" 

1711 assert lite_user.age == 0 

1712 assert lite_user.avatar_url == "" 

1713 assert lite_user.avatar_thumbnail_url == "" 

1714 assert not lite_user.has_strong_verification 

1715 

1716 

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

1718def test_GetLiteUser_ghost_user_by_id(db, flag): 

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

1720 user1, token1 = generate_user() 

1721 user2, _ = generate_user() 

1722 

1723 # Make user2 invisible 

1724 with session_scope() as session: 

1725 db_user2 = session.merge(user2) 

1726 setattr(db_user2, flag, now()) 

1727 session.commit() 

1728 

1729 # Refresh the materialized view 

1730 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1731 

1732 with api_session(token1) as api: 

1733 # Query by ID 

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

1735 

1736 assert lite_user.user_id == user2.id 

1737 assert lite_user.username == "ghost" 

1738 assert lite_user.name == "Deactivated Account" 

1739 assert lite_user.lat == 0 

1740 assert lite_user.lng == 0 

1741 assert lite_user.radius == 0 

1742 assert lite_user.city == "" 

1743 assert lite_user.age == 0 

1744 assert lite_user.avatar_url == "" 

1745 assert lite_user.avatar_thumbnail_url == "" 

1746 assert not lite_user.has_strong_verification 

1747 

1748 

1749def test_GetLiteUser_blocked_user(db): 

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

1751 user1, token1 = generate_user() 

1752 user2, _ = generate_user() 

1753 

1754 # User1 blocks user2 

1755 make_user_block(user1, user2) 

1756 

1757 # Refresh the materialized view 

1758 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1759 

1760 with api_session(token1) as api: 

1761 # Query by username 

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

1763 

1764 assert lite_user.user_id == user2.id 

1765 assert lite_user.is_ghost 

1766 assert lite_user.username == "ghost" 

1767 assert lite_user.name == "Deactivated Account" 

1768 

1769 # Query by ID 

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

1771 

1772 assert lite_user.user_id == user2.id 

1773 assert lite_user.is_ghost 

1774 assert lite_user.username == "ghost" 

1775 assert lite_user.name == "Deactivated Account" 

1776 

1777 

1778def test_GetLiteUser_blocking_user(db): 

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

1780 user1, token1 = generate_user() 

1781 user2, _ = generate_user() 

1782 

1783 # User2 blocks user1 

1784 make_user_block(user2, user1) 

1785 

1786 # Refresh the materialized view 

1787 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1788 

1789 with api_session(token1) as api: 

1790 # Query by username 

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

1792 

1793 assert lite_user.user_id == user2.id 

1794 assert lite_user.is_ghost 

1795 assert lite_user.username == "ghost" 

1796 assert lite_user.name == "Deactivated Account" 

1797 

1798 # Query by ID 

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

1800 

1801 assert lite_user.user_id == user2.id 

1802 assert lite_user.is_ghost 

1803 assert lite_user.username == "ghost" 

1804 assert lite_user.name == "Deactivated Account" 

1805 

1806 

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

1808def test_GetLiteUsers_ghost_users(db, flag): 

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

1810 user1, token1 = generate_user() 

1811 user2, _ = generate_user() 

1812 user3, _ = generate_user() 

1813 user4, _ = generate_user() 

1814 

1815 # Make user2 and user4 invisible 

1816 with session_scope() as session: 

1817 db_user2 = session.merge(user2) 

1818 setattr(db_user2, flag, now()) 

1819 db_user4 = session.merge(user4) 

1820 setattr(db_user4, flag, now()) 

1821 session.commit() 

1822 

1823 # Refresh the materialized view 

1824 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1825 

1826 with api_session(token1) as api: 

1827 res = api.GetLiteUsers( 

1828 api_pb2.GetLiteUsersReq( 

1829 users=[ 

1830 user1.username, # visible 

1831 user2.username, # ghost 

1832 str(user3.id), # visible 

1833 str(user4.id), # ghost 

1834 ] 

1835 ) 

1836 ) 

1837 

1838 assert len(res.responses) == 4 

1839 

1840 # user1 - visible, normal profile 

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

1842 assert not res.responses[0].not_found 

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

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

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

1846 

1847 # user2 - ghost by username 

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

1849 assert not res.responses[1].not_found 

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

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

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

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

1854 

1855 # user3 - visible, normal profile 

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

1857 assert not res.responses[2].not_found 

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

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

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

1861 

1862 # user4 - ghost by ID 

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

1864 assert not res.responses[3].not_found 

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

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

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

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

1869 

1870 

1871def test_GetLiteUsers_blocked_users(db): 

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

1873 user1, token1 = generate_user() 

1874 user2, _ = generate_user() 

1875 user3, _ = generate_user() 

1876 user4, _ = generate_user() 

1877 user5, _ = generate_user() 

1878 

1879 # User1 blocks user2 

1880 make_user_block(user1, user2) 

1881 # User4 blocks user1 

1882 make_user_block(user4, user1) 

1883 

1884 # Refresh the materialized view 

1885 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1886 

1887 with api_session(token1) as api: 

1888 res = api.GetLiteUsers( 

1889 api_pb2.GetLiteUsersReq( 

1890 users=[ 

1891 user2.username, # user1 blocked user2 

1892 str(user3.id), # visible 

1893 user4.username, # user4 blocked user1 

1894 str(user5.id), # visible 

1895 ] 

1896 ) 

1897 ) 

1898 

1899 assert len(res.responses) == 4 

1900 

1901 # user2 - blocked by user1, should be ghost 

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

1903 assert not res.responses[0].not_found 

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

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

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

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

1908 

1909 # user3 - visible 

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

1911 assert not res.responses[1].not_found 

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

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

1914 

1915 # user4 - user4 blocked user1, should be ghost 

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

1917 assert not res.responses[2].not_found 

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

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

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

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

1922 

1923 # user5 - visible 

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

1925 assert not res.responses[3].not_found 

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

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

1928 

1929 

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

1931def test_GetUser_ghost_user_by_id(db, flag): 

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

1933 user1, token1 = generate_user() 

1934 user2, _ = generate_user() 

1935 

1936 # Make user2 invisible 

1937 with session_scope() as session: 

1938 db_user2 = session.merge(user2) 

1939 setattr(db_user2, flag, now()) 

1940 session.commit() 

1941 

1942 with api_session(token1) as api: 

1943 # Query by ID 

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

1945 

1946 assert user_pb.user_id == user2.id 

1947 assert user_pb.username == "ghost" 

1948 assert user_pb.name == "Deactivated Account" 

1949 assert user_pb.city == "" 

1950 assert user_pb.hosting_status == 0 

1951 assert user_pb.meetup_status == 0 

1952 

1953 

1954def test_GetUser_blocked_user(db): 

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

1956 user1, token1 = generate_user() 

1957 user2, _ = generate_user() 

1958 

1959 # User1 blocks user2 

1960 make_user_block(user1, user2) 

1961 

1962 with api_session(token1) as api: 

1963 # Query by username 

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

1965 

1966 assert user_pb.user_id == user2.id 

1967 assert user_pb.username == "ghost" 

1968 assert user_pb.name == "Deactivated Account" 

1969 

1970 # Query by ID 

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

1972 

1973 assert user_pb.user_id == user2.id 

1974 assert user_pb.username == "ghost" 

1975 assert user_pb.name == "Deactivated Account" 

1976 

1977 

1978def test_GetUser_blocking_user(db): 

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

1980 user1, token1 = generate_user() 

1981 user2, _ = generate_user() 

1982 

1983 # User2 blocks user1 

1984 make_user_block(user2, user1) 

1985 

1986 with api_session(token1) as api: 

1987 # Query by username 

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

1989 

1990 assert user_pb.user_id == user2.id 

1991 assert user_pb.username == "ghost" 

1992 assert user_pb.name == "Deactivated Account" 

1993 

1994 # Query by ID 

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

1996 

1997 assert user_pb.user_id == user2.id 

1998 assert user_pb.username == "ghost" 

1999 assert user_pb.name == "Deactivated Account"