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

1197 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-21 09:29 +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 

45@pytest.fixture(autouse=True) 

46def _(testconfig): 

47 pass 

48 

49 

50def test_ping(db): 

51 user, token = generate_user( 

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

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

54 language_abilities=[ 

55 ("fin", LanguageFluency.fluent), 

56 ("fra", LanguageFluency.beginner), 

57 ], 

58 ) 

59 

60 with real_api_session(token) as api: 

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

62 

63 assert res.user.user_id == user.id 

64 assert res.user.username == user.username 

65 assert res.user.name == user.name 

66 assert res.user.city == user.city 

67 assert res.user.hometown == user.hometown 

68 assert res.user.verification == 0.0 

69 assert res.user.community_standing == user.community_standing 

70 assert res.user.num_references == 0 

71 assert res.user.gender == user.gender 

72 assert res.user.pronouns == user.pronouns 

73 assert res.user.age == user.age 

74 

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

76 

77 # the joined time is fuzzed 

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

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

80 # same for last_active 

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

82 

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

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

85 

86 assert res.user.occupation == user.occupation 

87 assert res.user.education == user.education 

88 assert res.user.about_me == user.about_me 

89 assert res.user.things_i_like == user.things_i_like 

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

91 assert res.user.about_place == user.about_place 

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

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

94 assert res.user.additional_information == user.additional_information 

95 

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

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

98 

99 

100def test_coords(db): 

101 # make them need to update location 

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

103 user2, token2 = generate_user() 

104 

105 with api_session(token2) as api: 

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

107 assert res.user.city == user2.city 

108 lat, lng = user2.coordinates 

109 assert res.user.lat == lat 

110 assert res.user.lng == lng 

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

112 

113 with api_session(token2) as api: 

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

115 assert res.city == user1.city 

116 assert res.lat == 1.0 

117 assert res.lng == 0.0 

118 assert res.radius == 2000.0 

119 

120 # Check coordinate wrapping 

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

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

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

124 

125 with api_session(token3) as api: 

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

127 assert res.lat == 40.0 

128 assert res.lng == 179.5 

129 

130 with api_session(token4) as api: 

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

132 assert res.lat == 40.0 

133 assert res.lng == 20.0 

134 

135 # PostGIS does not wrap longitude for latitude overflow 

136 with api_session(token5) as api: 

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

138 assert res.lat == 89.5 

139 assert res.lng == 20.0 

140 

141 with real_jail_session(token1) as jail: 

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

143 assert res.jailed 

144 assert res.needs_to_update_location 

145 

146 res = jail.SetLocation( 

147 jail_pb2.SetLocationReq( 

148 city="New York City", 

149 lat=40.7812, 

150 lng=-73.9647, 

151 radius=250, 

152 ) 

153 ) 

154 

155 assert not res.jailed 

156 assert not res.needs_to_update_location 

157 

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

159 assert not res.jailed 

160 assert not res.needs_to_update_location 

161 

162 with api_session(token2) as api: 

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

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

165 assert res.lat == 40.7812 

166 assert res.lng == -73.9647 

167 assert res.radius == 250 

168 

169 

170def test_get_user(db): 

171 user1, token1 = generate_user() 

172 user2, token2 = generate_user() 

173 

174 with api_session(token1) as api: 

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

176 assert res.user_id == user2.id 

177 assert res.username == user2.username 

178 assert res.name == user2.name 

179 

180 with api_session(token1) as api: 

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

182 assert res.user_id == user2.id 

183 assert res.username == user2.username 

184 assert res.name == user2.name 

185 

186 

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

188def test_user_model_to_pb_ghost_user(db, flag): 

189 user1, token1 = generate_user() 

190 user2, _ = generate_user() 

191 

192 with session_scope() as session: 

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

194 

195 refresh_materialized_views_rapid(empty_pb2.Empty()) 

196 

197 with api_session(token1) as api: 

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

199 

200 assert user_pb.user_id == user2.id 

201 assert user_pb.is_ghost 

202 assert user_pb.username == "ghost" 

203 assert user_pb.name == "Deactivated Account" 

204 assert ( 

205 user_pb.about_me 

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

207 ) 

208 

209 assert user_pb.lat == 0 

210 assert user_pb.lng == 0 

211 assert user_pb.radius == 0 

212 assert user_pb.verification == 0.0 

213 assert user_pb.community_standing == 0.0 

214 assert user_pb.num_references == 0 

215 assert user_pb.age == 0 

216 assert user_pb.hosting_status == 0 

217 assert user_pb.meetup_status == 0 

218 assert user_pb.city == "" 

219 assert user_pb.hometown == "" 

220 assert user_pb.timezone == "" 

221 assert user_pb.gender == "" 

222 assert user_pb.pronouns == "" 

223 assert user_pb.occupation == "" 

224 assert user_pb.education == "" 

225 assert user_pb.things_i_like == "" 

226 assert user_pb.about_place == "" 

227 assert user_pb.additional_information == "" 

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

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

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

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

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

233 assert user_pb.avatar_url == "" 

234 assert user_pb.avatar_thumbnail_url == "" 

235 assert not user_pb.has_strong_verification 

236 

237 with api_session(token1) as api: 

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

239 

240 assert lite_user_pb.user_id == user2.id 

241 assert lite_user_pb.is_ghost 

242 assert lite_user_pb.username == "ghost" 

243 assert lite_user_pb.name == "Deactivated Account" 

244 assert lite_user_pb.city == "" 

245 assert lite_user_pb.age == 0 

246 assert lite_user_pb.avatar_url == "" 

247 assert lite_user_pb.avatar_thumbnail_url == "" 

248 assert lite_user_pb.lat == 0 

249 assert lite_user_pb.lng == 0 

250 assert lite_user_pb.radius == 0 

251 assert not lite_user_pb.has_strong_verification 

252 

253 

254def test_user_model_to_pb_ghost_user_blocked(db): 

255 user1, token1 = generate_user() 

256 user2, _ = generate_user() 

257 

258 with blocking_session(token1) as user_blocks: 

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

260 

261 refresh_materialized_views_rapid(empty_pb2.Empty()) 

262 

263 with api_session(token1) as api: 

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

265 

266 assert user_pb.user_id == user2.id 

267 assert user_pb.is_ghost 

268 assert user_pb.username == "ghost" 

269 assert user_pb.name == "Deactivated Account" 

270 assert ( 

271 user_pb.about_me 

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

273 ) 

274 

275 assert user_pb.lat == 0 

276 assert user_pb.lng == 0 

277 assert user_pb.radius == 0 

278 assert user_pb.verification == 0.0 

279 assert user_pb.community_standing == 0.0 

280 assert user_pb.num_references == 0 

281 assert user_pb.age == 0 

282 assert user_pb.hosting_status == 0 

283 assert user_pb.meetup_status == 0 

284 assert user_pb.city == "" 

285 assert user_pb.hometown == "" 

286 assert user_pb.timezone == "" 

287 assert user_pb.gender == "" 

288 assert user_pb.pronouns == "" 

289 assert user_pb.occupation == "" 

290 assert user_pb.education == "" 

291 assert user_pb.things_i_like == "" 

292 assert user_pb.about_place == "" 

293 assert user_pb.additional_information == "" 

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

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

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

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

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

299 assert user_pb.avatar_url == "" 

300 assert user_pb.avatar_thumbnail_url == "" 

301 assert not user_pb.has_strong_verification 

302 

303 with api_session(token1) as api: 

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

305 

306 assert lite_user_pb.user_id == user2.id 

307 assert lite_user_pb.is_ghost 

308 assert lite_user_pb.username == "ghost" 

309 assert lite_user_pb.name == "Deactivated Account" 

310 assert lite_user_pb.city == "" 

311 assert lite_user_pb.age == 0 

312 assert lite_user_pb.avatar_url == "" 

313 assert lite_user_pb.avatar_thumbnail_url == "" 

314 assert lite_user_pb.lat == 0 

315 assert lite_user_pb.lng == 0 

316 assert lite_user_pb.radius == 0 

317 assert not lite_user_pb.has_strong_verification 

318 

319 

320@pytest.mark.parametrize( 

321 "flag,state", 

322 [ 

323 ("banned_at", NonvisibleUserState.banned), 

324 ("shadowed_at", NonvisibleUserState.shadowed), 

325 ("deleted_at", NonvisibleUserState.deleted), 

326 ], 

327) 

328def test_viewing_nonvisible_user_profile_is_logged(db, flag, state): 

329 user1, token1 = generate_user() 

330 user2, _ = generate_user() 

331 

332 with session_scope() as session: 

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

334 

335 refresh_materialized_views_rapid(empty_pb2.Empty()) 

336 

337 with api_session(token1) as api: 

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

339 assert user_pb.is_ghost 

340 

341 with session_scope() as session: 

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

343 assert access.access_type == NonvisibleUserAccessType.ghost_served 

344 assert access.target_state == state 

345 assert access.target_user_id == user2.id 

346 assert access.actor_user_id == user1.id 

347 assert access.ip_address is None 

348 assert access.user_agent is None 

349 assert access.sofa is None 

350 

351 

352def test_viewing_blocked_user_profile_is_not_logged(db): 

353 user1, token1 = generate_user() 

354 user2, _ = generate_user() 

355 

356 with blocking_session(token1) as user_blocks: 

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

358 

359 refresh_materialized_views_rapid(empty_pb2.Empty()) 

360 

361 with api_session(token1) as api: 

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

363 assert user_pb.is_ghost 

364 

365 with session_scope() as session: 

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

367 

368 

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

370def test_admin_viewing_ghost_users_sees_full_profile(db, flag): 

371 admin, token_admin = generate_user(is_superuser=True) 

372 user, _ = generate_user() 

373 

374 with session_scope() as session: 

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

376 

377 with admin_session(token_admin) as api: 

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

379 

380 assert user_pb.user_id == user.id 

381 assert user_pb.username == user.username 

382 assert user_pb.name == user.name 

383 assert user_pb.city == user.city 

384 assert user_pb.name != "Deactivated Account" 

385 assert user_pb.username != "ghost" 

386 assert user_pb.hosting_status in ( 

387 api_pb2.HOSTING_STATUS_UNKNOWN, 

388 api_pb2.HOSTING_STATUS_CAN_HOST, 

389 api_pb2.HOSTING_STATUS_MAYBE, 

390 api_pb2.HOSTING_STATUS_CANT_HOST, 

391 ) 

392 

393 

394def test_lite_coords(db): 

395 # make them need to update location 

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

397 user2, token2 = generate_user() 

398 

399 refresh_materialized_views_rapid(empty_pb2.Empty()) 

400 

401 with api_session(token2) as api: 

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

403 assert res.user.city == user2.city 

404 lat, lng = user2.coordinates 

405 assert res.user.lat == lat 

406 assert res.user.lng == lng 

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

408 

409 with api_session(token2) as api: 

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

411 assert res.city == user1.city 

412 assert res.lat == 0.0 

413 assert res.lng == 0.0 

414 assert res.radius == 0.0 

415 

416 # Check coordinate wrapping 

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

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

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

420 

421 refresh_materialized_views_rapid(empty_pb2.Empty()) 

422 

423 with api_session(token3) as api: 

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

425 assert res.lat == 40.0 

426 assert res.lng == 179.5 

427 

428 with api_session(token4) as api: 

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

430 assert res.lat == 40.0 

431 assert res.lng == 20.0 

432 

433 # PostGIS does not wrap longitude for latitude overflow 

434 with api_session(token5) as api: 

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

436 assert res.lat == 89.5 

437 assert res.lng == 20.0 

438 

439 with real_jail_session(token1) as jail: 

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

441 assert res.jailed 

442 assert res.needs_to_update_location 

443 

444 res = jail.SetLocation( 

445 jail_pb2.SetLocationReq( 

446 city="New York City", 

447 lat=40.7812, 

448 lng=-73.9647, 

449 radius=250, 

450 ) 

451 ) 

452 

453 assert not res.jailed 

454 assert not res.needs_to_update_location 

455 

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

457 assert not res.jailed 

458 assert not res.needs_to_update_location 

459 

460 refresh_materialized_views_rapid(empty_pb2.Empty()) 

461 

462 with api_session(token2) as api: 

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

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

465 assert res.lat == 40.7812 

466 assert res.lng == -73.9647 

467 assert res.radius == 250 

468 

469 

470def test_lite_get_user(db): 

471 user1, token1 = generate_user() 

472 user2, token2 = generate_user() 

473 

474 refresh_materialized_views_rapid(empty_pb2.Empty()) 

475 

476 with api_session(token1) as api: 

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

478 assert res.user_id == user2.id 

479 assert res.username == user2.username 

480 assert res.name == user2.name 

481 

482 with api_session(token1) as api: 

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

484 assert res.user_id == user2.id 

485 assert res.username == user2.username 

486 assert res.name == user2.name 

487 

488 

489def test_GetLiteUsers(db): 

490 user1, token1 = generate_user() 

491 user2, _ = generate_user() 

492 user3, _ = generate_user() 

493 user4, _ = generate_user() 

494 user5, _ = generate_user() 

495 user6, _ = generate_user() 

496 

497 make_user_block(user4, user1) 

498 

499 refresh_materialized_views_rapid(empty_pb2.Empty()) 

500 

501 with api_session(token1) as api: 

502 res = api.GetLiteUsers( 

503 api_pb2.GetLiteUsersReq( 

504 users=[ 

505 user1.username, 

506 str(user1.id), 

507 "nonexistent", 

508 str(user2.id), 

509 "9994", 

510 user6.username, 

511 str(user5.id), 

512 "notreal", 

513 user4.username, 

514 ] 

515 ) 

516 ) 

517 

518 assert len(res.responses) == 9 

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

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

521 

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

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

524 

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

526 assert res.responses[2].not_found 

527 

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

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

530 

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

532 assert res.responses[4].not_found 

533 

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

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

536 

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

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

539 

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

541 assert res.responses[7].not_found 

542 

543 # blocked - should return ghost profile 

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

545 assert not res.responses[8].not_found 

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

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

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

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

550 

551 with api_session(token1) as api: 

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

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

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

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

556 

557 

558def test_update_profile(db): 

559 user, token = generate_user() 

560 

561 with api_session(token) as api: 

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

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

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

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

566 

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

568 api.UpdateProfile( 

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

570 ) 

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

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

573 

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

575 api.UpdateProfile( 

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

577 ) 

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

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

580 

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

582 api.UpdateProfile( 

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

584 ) 

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

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

587 

588 api.UpdateProfile( 

589 api_pb2.UpdateProfileReq( 

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

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

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

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

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

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

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

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

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

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

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

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

602 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

603 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

604 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

605 value=[ 

606 api_pb2.LanguageAbility( 

607 code="eng", 

608 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

609 ) 

610 ], 

611 ), 

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

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

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

615 ) 

616 ) 

617 

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

619 assert user_details.name == "New name" 

620 assert user_details.city == "Timbuktu" 

621 assert user_details.hometown == "Walla Walla" 

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

623 assert user_details.education == "Couchers U" 

624 assert user_details.things_i_like == "Couchers" 

625 assert user_details.lat == 0.01 

626 assert user_details.lng == -2 

627 assert user_details.radius == 321 

628 assert user_details.occupation == "Testing" 

629 assert user_details.about_me == "I rule" 

630 assert user_details.about_place == "My place" 

631 assert user_details.hosting_status == api_pb2.HOSTING_STATUS_CAN_HOST 

632 assert user_details.meetup_status == api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP 

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

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

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

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

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

638 

639 # Test unset values 

640 api.UpdateProfile( 

641 api_pb2.UpdateProfileReq( 

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

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

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

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

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

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

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

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

650 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

651 meetup_status=api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

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

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

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

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

656 ) 

657 ) 

658 

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

660 assert not user_details.hometown 

661 assert not user_details.radius 

662 assert not user_details.pronouns 

663 assert not user_details.occupation 

664 assert not user_details.education 

665 assert not user_details.about_me 

666 assert not user_details.things_i_like 

667 assert not user_details.about_place 

668 assert user_details.hosting_status == api_pb2.HOSTING_STATUS_CAN_HOST 

669 assert user_details.meetup_status == api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP 

670 assert not user_details.language_abilities 

671 assert not user_details.regions_visited 

672 assert not user_details.regions_lived 

673 assert not user_details.additional_information 

674 

675 

676def test_update_profile_do_not_email(db): 

677 user, token = generate_user() 

678 

679 with notifications_session(token) as notifications: 

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

681 

682 with api_session(token) as api: 

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

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

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

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

687 

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

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

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

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

692 

693 

694def test_language_abilities(db): 

695 user, token = generate_user( 

696 language_abilities=[ 

697 ("fin", LanguageFluency.fluent), 

698 ("fra", LanguageFluency.beginner), 

699 ], 

700 ) 

701 

702 with api_session(token) as api: 

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

704 assert len(res.language_abilities) == 2 

705 

706 # can't add non-existent languages 

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

708 api.UpdateProfile( 

709 api_pb2.UpdateProfileReq( 

710 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

711 value=[ 

712 api_pb2.LanguageAbility( 

713 code="QQQ", 

714 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

715 ) 

716 ], 

717 ), 

718 ) 

719 ) 

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

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

722 

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

724 with pytest.raises(Exception) as err2: 

725 api.UpdateProfile( 

726 api_pb2.UpdateProfileReq( 

727 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

728 value=[ 

729 api_pb2.LanguageAbility( 

730 code="eng", 

731 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

732 ), 

733 api_pb2.LanguageAbility( 

734 code="eng", 

735 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

736 ), 

737 ], 

738 ), 

739 ) 

740 ) 

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

742 

743 # nothing changed 

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

745 assert len(res.language_abilities) == 2 

746 

747 # now actually add a value 

748 api.UpdateProfile( 

749 api_pb2.UpdateProfileReq( 

750 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

751 value=[ 

752 api_pb2.LanguageAbility( 

753 code="eng", 

754 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

755 ) 

756 ], 

757 ), 

758 ) 

759 ) 

760 

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

762 assert len(res.language_abilities) == 1 

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

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

765 

766 # change the value to a new one 

767 api.UpdateProfile( 

768 api_pb2.UpdateProfileReq( 

769 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

770 value=[ 

771 api_pb2.LanguageAbility( 

772 code="fin", 

773 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER, 

774 ) 

775 ], 

776 ), 

777 ) 

778 ) 

779 

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

781 assert len(res.language_abilities) == 1 

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

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

784 

785 # should be able to set to same value still 

786 api.UpdateProfile( 

787 api_pb2.UpdateProfileReq( 

788 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

789 value=[ 

790 api_pb2.LanguageAbility( 

791 code="fin", 

792 fluency=api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER, 

793 ) 

794 ], 

795 ), 

796 ) 

797 ) 

798 

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

800 assert len(res.language_abilities) == 1 

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

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

803 

804 # don't change it 

805 api.UpdateProfile(api_pb2.UpdateProfileReq()) 

806 

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

808 assert len(res.language_abilities) == 1 

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

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

811 

812 # remove value 

813 api.UpdateProfile( 

814 api_pb2.UpdateProfileReq( 

815 language_abilities=api_pb2.RepeatedLanguageAbilityValue( 

816 value=[], 

817 ), 

818 ) 

819 ) 

820 

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

822 assert len(res.language_abilities) == 0 

823 

824 

825def test_pending_friend_request_count(db, moderator): 

826 user1, token1 = generate_user() 

827 user2, token2 = generate_user() 

828 user3, token3 = generate_user() 

829 

830 with api_session(token2) as api: 

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

832 assert res.pending_friend_request_count == 0 

833 

834 with api_session(token1) as api: 

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

836 assert res.pending_friend_request_count == 0 

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

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

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

840 assert res.pending_friend_request_count == 0 

841 

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

843 with api_session(token1) as api: 

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

845 assert len(res.sent) == 1 

846 fr_id = res.sent[0].friend_request_id 

847 

848 # Recipient cannot see SHADOWED friend requests before mod approval 

849 with api_session(token2) as api: 

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

851 assert res.pending_friend_request_count == 0 

852 

853 # Moderator approves the friend request 

854 moderator.approve_friend_request(fr_id) 

855 

856 # Now recipient can see the approved friend request 

857 with api_session(token2) as api: 

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

859 assert res.pending_friend_request_count == 1 

860 

861 with api_session(token2) as api: 

862 # check it's there 

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

864 assert len(res.sent) == 0 

865 assert len(res.received) == 1 

866 

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

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

869 

870 fr_id = res.received[0].friend_request_id 

871 

872 # accept it 

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

874 

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

876 assert res.pending_friend_request_count == 0 

877 

878 

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

880 user1, token1 = generate_user(complete_profile=True) 

881 user2, token2 = generate_user(complete_profile=True) 

882 user3, token3 = generate_user() 

883 

884 # send a friend request from user1 to user2 

885 with api_session(token1) as api: 

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

887 

888 with session_scope() as session: 

889 friend_request = session.execute( 

890 select(FriendRelationship).where( 

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

892 ) 

893 ).scalar_one() 

894 friend_request_id = friend_request.id 

895 

896 # Notification is deferred while content is SHADOWED 

897 # No push notification sent yet 

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

899 

900 with api_session(token1) as api: 

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

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

903 assert len(res.sent) == 1 

904 assert len(res.received) == 0 

905 

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

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

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

909 

910 # Recipient cannot see SHADOWED friend requests 

911 with api_session(token2) as api: 

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

913 assert len(res.sent) == 0 

914 assert len(res.received) == 0 

915 

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

917 moderator.approve_friend_request(friend_request_id) 

918 

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

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

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

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

923 

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

925 assert email.recipient == user2.email 

926 assert email.subject == f"[TEST] {user1.name} wants to be your friend on Couchers.org!" 

927 assert user2.name in email.plain 

928 assert user2.name in email.html 

929 assert user1.name in email.plain 

930 assert user1.name in email.html 

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

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

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

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

935 

936 # Now recipient can see the approved friend request 

937 with api_session(token2) as api: 

938 # check it's there 

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

940 assert len(res.sent) == 0 

941 assert len(res.received) == 1 

942 

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

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

945 

946 fr_id = res.received[0].friend_request_id 

947 

948 # accept it 

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

950 

951 # check it's gone 

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

953 assert len(res.sent) == 0 

954 assert len(res.received) == 0 

955 

956 # check we're friends now 

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

958 assert len(res.user_ids) == 1 

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

960 

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

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

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

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

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

966 

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

968 assert email.recipient == user1.email 

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

970 assert user1.name in email.plain 

971 assert user1.name in email.html 

972 assert user2.name in email.plain 

973 assert user2.name in email.html 

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

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

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

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

978 

979 with api_session(token1) as api: 

980 # check it's gone 

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

982 assert len(res.sent) == 0 

983 assert len(res.received) == 0 

984 

985 # check we're friends now 

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

987 assert len(res.user_ids) == 1 

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

989 

990 with api_session(token1) as api: 

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

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

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

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

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

996 

997 # we can unfriend 

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

999 

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

1001 assert len(res.user_ids) == 0 

1002 

1003 

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

1005 user1, token1 = generate_user(complete_profile=True) 

1006 user2, token2 = generate_user(complete_profile=True) 

1007 user3, token3 = generate_user() 

1008 user4, token4 = generate_user() 

1009 user5, token5 = generate_user() 

1010 user6, token6 = generate_user() 

1011 

1012 # Send friend requests 

1013 with api_session(token4) as api: 

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

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

1016 

1017 with api_session(token5) as api: 

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

1019 

1020 with api_session(token1) as api: 

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

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

1023 

1024 # Approve all friend requests via moderation 

1025 with session_scope() as session: 

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

1027 for fr in friend_requests: 

1028 moderator.approve_friend_request(fr.id) 

1029 

1030 # Now recipients can respond 

1031 with api_session(token1) as api: 

1032 api.RespondFriendRequest( 

1033 api_pb2.RespondFriendRequestReq( 

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

1035 ) 

1036 ) 

1037 

1038 with api_session(token2) as api: 

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

1040 api.RespondFriendRequest( 

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

1042 ) 

1043 

1044 with api_session(token1) as api: 

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

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

1047 

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

1049 

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

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

1052 

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

1054 

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

1056 assert not res.user_ids 

1057 

1058 

1059def test_cant_friend_request_twice(db): 

1060 user1, token1 = generate_user() 

1061 user2, token2 = generate_user() 

1062 

1063 # send friend request from user1 to user2 

1064 with api_session(token1) as api: 

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

1066 

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

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

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

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

1071 

1072 

1073def test_cant_friend_request_pending(db): 

1074 user1, token1 = generate_user() 

1075 user2, token2 = generate_user() 

1076 

1077 # send friend request from user1 to user2 

1078 with api_session(token1) as api: 

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

1080 

1081 with api_session(token2) as api: 

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

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

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

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

1086 

1087 

1088def test_cant_friend_request_already_friends(db): 

1089 user1, token1 = generate_user() 

1090 user2, token2 = generate_user() 

1091 make_friends(user1, user2) 

1092 

1093 with api_session(token1) as api: 

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

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

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

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

1098 

1099 with api_session(token2) as api: 

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

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

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

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

1104 

1105 

1106def test_cant_friend_request_incomplete_profile(db): 

1107 user1, token1 = generate_user(complete_profile=False) 

1108 user2, token2 = generate_user(complete_profile=True) 

1109 

1110 with api_session(token1) as api: 

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

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

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

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

1115 

1116 # the other direction should still work 

1117 with api_session(token2) as api: 

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

1119 

1120 

1121def test_excessive_friend_requests_are_reported(db, email_collector: EmailCollector): 

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

1123 user, token = generate_user() 

1124 rate_limit_definition = RATE_LIMIT_DEFINITIONS[RateLimitAction.friend_request] 

1125 with api_session(token) as api: 

1126 # Test warning email 

1127 for _ in range(rate_limit_definition.warning_limit): 

1128 friend_user, _ = generate_user() 

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

1130 

1131 assert email_collector.count_for_reports() == 0 

1132 friend_user, _ = generate_user() 

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

1134 

1135 email = email_collector.pop_for_reports(last=True) 

1136 assert email.plain.startswith( 

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

1138 ) 

1139 

1140 # Test ban after exceeding FRIEND_REQUEST_HARD_LIMIT 

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

1142 friend_user, _ = generate_user() 

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

1144 

1145 assert email_collector.count_for_reports() == 0 

1146 friend_user, _ = generate_user() 

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

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

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

1150 assert ( 

1151 exc_info.value.details() 

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

1153 ) 

1154 

1155 email = email_collector.pop_for_reports(last=True) 

1156 assert email.plain.startswith( 

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

1158 ) 

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

1160 

1161 

1162def test_ListFriends(db, moderator): 

1163 user1, token1 = generate_user() 

1164 user2, token2 = generate_user() 

1165 user3, token3 = generate_user() 

1166 

1167 # send friend request from user1 to user2 and user3 

1168 with api_session(token1) as api: 

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

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

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

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

1173 assert len(res.sent) == 2 

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

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

1176 

1177 with api_session(token3) as api: 

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

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

1180 user3_to_user2_id = res.sent[0].friend_request_id 

1181 

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

1183 moderator.approve_friend_request(user1_to_user2_id) 

1184 moderator.approve_friend_request(user3_to_user2_id) 

1185 

1186 with api_session(token2) as api: 

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

1188 assert len(res.received) == 2 

1189 

1190 # order is an implementation detail 

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

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

1193 

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

1195 assert user1_req.user_id == user1.id 

1196 api.RespondFriendRequest( 

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

1198 ) 

1199 

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

1201 assert user3_req.user_id == user3.id 

1202 api.RespondFriendRequest( 

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

1204 ) 

1205 

1206 # check we now have two friends 

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

1208 assert len(res.user_ids) == 2 

1209 assert user1.id in res.user_ids 

1210 assert user3.id in res.user_ids 

1211 

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

1213 moderator.approve_friend_request(user1_to_user3_id) 

1214 

1215 with api_session(token3) as api: 

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

1217 assert len(res.user_ids) == 1 

1218 assert user2.id in res.user_ids 

1219 

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

1221 assert len(res.received) == 1 

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

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

1224 fr_id = res.received[0].friend_request_id 

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

1226 

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

1228 assert len(res.user_ids) == 2 

1229 assert user1.id in res.user_ids 

1230 assert user2.id in res.user_ids 

1231 

1232 with api_session(token1) as api: 

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

1234 assert len(res.user_ids) == 2 

1235 assert user2.id in res.user_ids 

1236 assert user3.id in res.user_ids 

1237 

1238 

1239def test_ListMutualFriends(db): 

1240 user1, token1 = generate_user() 

1241 user2, token2 = generate_user() 

1242 user3, token3 = generate_user() 

1243 user4, token4 = generate_user() 

1244 user5, token5 = generate_user() 

1245 

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

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

1248 make_friends(user1, user2) 

1249 make_friends(user1, user3) 

1250 make_friends(user1, user4) 

1251 make_friends(user1, user5) 

1252 make_friends(user3, user2) 

1253 make_friends(user3, user4) 

1254 

1255 with api_session(token1) as api: 

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

1257 assert len(mutual_friends) == 1 

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

1259 

1260 # and other way around same 

1261 with api_session(token2) as api: 

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

1263 assert len(mutual_friends) == 1 

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

1265 

1266 # Check pending request doesn't have effect 

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

1268 

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

1270 assert len(mutual_friends) == 1 

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

1272 

1273 # both ways 

1274 with api_session(token1) as api: 

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

1276 assert len(mutual_friends) == 1 

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

1278 

1279 

1280def test_mutual_friends_self(db): 

1281 user1, token1 = generate_user() 

1282 user2, token2 = generate_user() 

1283 user3, token3 = generate_user() 

1284 user4, token4 = generate_user() 

1285 

1286 make_friends(user1, user2) 

1287 make_friends(user2, user3) 

1288 make_friends(user1, user4) 

1289 

1290 with api_session(token1) as api: 

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

1292 assert len(res.mutual_friends) == 0 

1293 

1294 with api_session(token2) as api: 

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

1296 assert len(res.mutual_friends) == 0 

1297 

1298 with api_session(token3) as api: 

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

1300 assert len(res.mutual_friends) == 0 

1301 

1302 with api_session(token4) as api: 

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

1304 assert len(res.mutual_friends) == 0 

1305 

1306 

1307def test_CancelFriendRequest(db): 

1308 user1, token1 = generate_user() 

1309 user2, token2 = generate_user() 

1310 

1311 with api_session(token1) as api: 

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

1313 

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

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

1316 fr_id = res.sent[0].friend_request_id 

1317 

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

1319 

1320 # check it's gone 

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

1322 assert len(res.sent) == 0 

1323 assert len(res.received) == 0 

1324 

1325 # check not friends 

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

1327 assert len(res.user_ids) == 0 

1328 

1329 with api_session(token2) as api: 

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 we're not friends 

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

1337 assert len(res.user_ids) == 0 

1338 

1339 with api_session(token1) as api: 

1340 # check we can send another friend req 

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

1342 

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

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

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

1346 

1347 

1348def test_accept_friend_request(db, moderator): 

1349 user1, token1 = generate_user() 

1350 user2, token2 = generate_user() 

1351 

1352 with session_scope() as session: 

1353 moderation_state = ModerationState( 

1354 object_type=ModerationObjectType.friend_request, 

1355 object_id=0, 

1356 visibility=ModerationVisibility.visible, 

1357 ) 

1358 session.add(moderation_state) 

1359 session.flush() 

1360 friend_request = FriendRelationship( 

1361 from_user_id=user1.id, 

1362 to_user_id=user2.id, 

1363 status=FriendStatus.pending, 

1364 moderation_state_id=moderation_state.id, 

1365 ) 

1366 session.add(friend_request) 

1367 session.flush() 

1368 moderation_state.object_id = friend_request.id 

1369 session.commit() 

1370 friend_request_id = friend_request.id 

1371 

1372 with api_session(token2) as api: 

1373 # check request pending 

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

1375 assert len(res.received) == 1 

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

1377 

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

1379 

1380 # check request is gone 

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

1382 assert len(res.sent) == 0 

1383 assert len(res.received) == 0 

1384 

1385 # check now friends 

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

1387 assert len(res.user_ids) == 1 

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

1389 

1390 with api_session(token1) as api: 

1391 # check request gone 

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

1393 assert len(res.sent) == 0 

1394 assert len(res.received) == 0 

1395 

1396 # check now friends 

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

1398 assert len(res.user_ids) == 1 

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

1400 

1401 

1402def test_reject_friend_request(db, moderator): 

1403 user1, token1 = generate_user() 

1404 user2, token2 = generate_user() 

1405 

1406 with api_session(token1) as api: 

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

1408 

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

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

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

1412 fr_id = res.sent[0].friend_request_id 

1413 

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

1415 moderator.approve_friend_request(fr_id) 

1416 

1417 with api_session(token2) as api: 

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

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

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

1421 

1422 fr_id = res.received[0].friend_request_id 

1423 

1424 # reject it 

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

1426 

1427 # check it's gone 

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

1429 assert len(res.sent) == 0 

1430 assert len(res.received) == 0 

1431 

1432 # check not friends 

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

1434 assert len(res.user_ids) == 0 

1435 

1436 with api_session(token1) as api: 

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 we're not friends 

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

1444 assert len(res.user_ids) == 0 

1445 

1446 # check we can send another friend req 

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

1448 

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

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

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

1452 

1453 

1454def test_hosting_preferences(db): 

1455 user1, token1 = generate_user() 

1456 user2, token2 = generate_user() 

1457 

1458 with api_session(token1) as api: 

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

1460 assert not res.HasField("max_guests") 

1461 assert not res.HasField("last_minute") 

1462 assert not res.HasField("has_pets") 

1463 assert not res.HasField("accepts_pets") 

1464 assert not res.HasField("pet_details") 

1465 assert not res.HasField("has_kids") 

1466 assert not res.HasField("accepts_kids") 

1467 assert not res.HasField("kid_details") 

1468 assert not res.HasField("has_housemates") 

1469 assert not res.HasField("housemate_details") 

1470 assert not res.HasField("wheelchair_accessible") 

1471 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_UNKNOWN 

1472 assert not res.HasField("smokes_at_home") 

1473 assert not res.HasField("drinking_allowed") 

1474 assert not res.HasField("drinks_at_home") 

1475 assert not res.HasField("other_host_info") 

1476 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN 

1477 assert not res.HasField("sleeping_details") 

1478 assert not res.HasField("area") 

1479 assert not res.HasField("house_rules") 

1480 assert not res.HasField("parking") 

1481 assert res.parking_details == api_pb2.PARKING_DETAILS_UNKNOWN 

1482 assert not res.HasField("camping_ok") 

1483 

1484 api.UpdateProfile( 

1485 api_pb2.UpdateProfileReq( 

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

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

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

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

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

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

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

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

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

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

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

1497 smoking_allowed=api_pb2.SMOKING_LOCATION_WINDOW, 

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

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

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

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

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

1503 sleeping_arrangement=api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

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

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

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

1507 parking_details=api_pb2.PARKING_DETAILS_PAID_ONSITE, 

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

1509 ) 

1510 ) 

1511 

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

1513 # that it is public information. 

1514 with api_session(token2) as api: 

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

1516 assert res.max_guests.value == 3 

1517 assert res.last_minute.value 

1518 assert not res.has_pets.value 

1519 assert res.accepts_pets.value 

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

1521 assert not res.has_kids.value 

1522 assert res.accepts_kids.value 

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

1524 assert not res.has_housemates.value 

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

1526 assert res.wheelchair_accessible.value 

1527 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_WINDOW 

1528 assert not res.smokes_at_home.value 

1529 assert res.drinking_allowed.value 

1530 assert not res.drinks_at_home.value 

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

1532 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_COMMON 

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

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

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

1536 assert res.parking.value 

1537 assert res.parking_details == api_pb2.PARKING_DETAILS_PAID_ONSITE 

1538 assert not res.camping_ok.value 

1539 

1540 # test unsetting 

1541 with api_session(token1) as api: 

1542 api.UpdateProfile( 

1543 api_pb2.UpdateProfileReq( 

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

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

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

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

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

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

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

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

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

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

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

1555 smoking_allowed=api_pb2.SMOKING_LOCATION_UNKNOWN, 

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

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

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

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

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

1561 sleeping_arrangement=api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

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

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

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

1565 parking_details=api_pb2.PARKING_DETAILS_UNKNOWN, 

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

1567 ) 

1568 ) 

1569 

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

1571 assert not res.HasField("max_guests") 

1572 assert not res.HasField("last_minute") 

1573 assert not res.HasField("has_pets") 

1574 assert not res.HasField("accepts_pets") 

1575 assert not res.HasField("pet_details") 

1576 assert not res.HasField("has_kids") 

1577 assert not res.HasField("accepts_kids") 

1578 assert not res.HasField("kid_details") 

1579 assert not res.HasField("has_housemates") 

1580 assert not res.HasField("housemate_details") 

1581 assert not res.HasField("wheelchair_accessible") 

1582 assert res.smoking_allowed == api_pb2.SMOKING_LOCATION_UNKNOWN 

1583 assert not res.HasField("smokes_at_home") 

1584 assert not res.HasField("drinking_allowed") 

1585 assert not res.HasField("drinks_at_home") 

1586 assert not res.HasField("other_host_info") 

1587 assert res.sleeping_arrangement == api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN 

1588 assert not res.HasField("sleeping_details") 

1589 assert not res.HasField("area") 

1590 assert not res.HasField("house_rules") 

1591 assert not res.HasField("parking") 

1592 assert res.parking_details == api_pb2.PARKING_DETAILS_UNKNOWN 

1593 assert not res.HasField("camping_ok") 

1594 

1595 

1596def test_badges(db): 

1597 user1, _ = generate_user(last_donated=None) 

1598 user2, _ = generate_user(last_donated=None) 

1599 user3, _ = generate_user(last_donated=None) 

1600 user4, token = generate_user(last_donated=None) 

1601 

1602 update_badges(empty_pb2.Empty()) 

1603 

1604 founder_badge = get_badge_dict()["founder"] 

1605 board_member_badge = get_badge_dict()["board_member"] 

1606 

1607 with api_session(token) as api: 

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

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

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

1611 

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

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

1614 assert res.user_ids == [1] 

1615 res2 = api.ListBadgeUsers( 

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

1617 ) 

1618 assert res2.user_ids == [2] 

1619 

1620 

1621def test_user_add_badge_is_idempotent(db): 

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

1623 user, _ = generate_user() 

1624 

1625 with session_scope() as session: 

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

1627 

1628 # one badge row, one notification 

1629 with session_scope() as session: 

1630 badge_count = session.execute( 

1631 select(func.count()) 

1632 .select_from(UserBadge) 

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

1634 ).scalar() 

1635 assert badge_count == 1 

1636 notification_count = session.execute( 

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

1638 ).scalar() 

1639 assert notification_count == 1 

1640 

1641 # add the same badge again 

1642 with session_scope() as session: 

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

1644 

1645 # still one badge row, no new notification 

1646 with session_scope() as session: 

1647 badge_count = session.execute( 

1648 select(func.count()) 

1649 .select_from(UserBadge) 

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

1651 ).scalar() 

1652 assert badge_count == 1 

1653 notification_count = session.execute( 

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

1655 ).scalar() 

1656 assert notification_count == 1 

1657 

1658 

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

1660def test_ListBadgeUsers_excludes_ghost_users(db, flag): 

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

1662 user1, token1 = generate_user() 

1663 user2, _ = generate_user() 

1664 user3, _ = generate_user() 

1665 

1666 volunteer_badge = get_badge_dict()["volunteer"] 

1667 

1668 # Give all three users the volunteer badge 

1669 with session_scope() as session: 

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

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

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

1673 

1674 # Verify all three users appear in the badge list 

1675 with api_session(token1) as api: 

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

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

1678 

1679 # Make user2 invisible (deleted or banned) 

1680 with session_scope() as session: 

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

1682 setattr(db_user2, flag, now()) 

1683 

1684 # Now user2 should not 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, user3.id} 

1688 

1689 

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

1691def test_GetLiteUser_ghost_user_by_username(db, flag): 

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

1693 user1, token1 = generate_user() 

1694 user2, _ = generate_user() 

1695 

1696 # Make user2 invisible 

1697 with session_scope() as session: 

1698 db_user2 = session.merge(user2) 

1699 setattr(db_user2, flag, now()) 

1700 session.commit() 

1701 

1702 # Refresh the materialized view 

1703 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1704 

1705 with api_session(token1) as api: 

1706 # Query by username 

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

1708 

1709 assert lite_user.user_id == user2.id 

1710 assert lite_user.username == "ghost" 

1711 assert lite_user.name == "Deactivated Account" 

1712 assert lite_user.lat == 0 

1713 assert lite_user.lng == 0 

1714 assert lite_user.radius == 0 

1715 assert lite_user.city == "" 

1716 assert lite_user.age == 0 

1717 assert lite_user.avatar_url == "" 

1718 assert lite_user.avatar_thumbnail_url == "" 

1719 assert not lite_user.has_strong_verification 

1720 

1721 

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

1723def test_GetLiteUser_ghost_user_by_id(db, flag): 

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

1725 user1, token1 = generate_user() 

1726 user2, _ = generate_user() 

1727 

1728 # Make user2 invisible 

1729 with session_scope() as session: 

1730 db_user2 = session.merge(user2) 

1731 setattr(db_user2, flag, now()) 

1732 session.commit() 

1733 

1734 # Refresh the materialized view 

1735 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1736 

1737 with api_session(token1) as api: 

1738 # Query by ID 

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

1740 

1741 assert lite_user.user_id == user2.id 

1742 assert lite_user.username == "ghost" 

1743 assert lite_user.name == "Deactivated Account" 

1744 assert lite_user.lat == 0 

1745 assert lite_user.lng == 0 

1746 assert lite_user.radius == 0 

1747 assert lite_user.city == "" 

1748 assert lite_user.age == 0 

1749 assert lite_user.avatar_url == "" 

1750 assert lite_user.avatar_thumbnail_url == "" 

1751 assert not lite_user.has_strong_verification 

1752 

1753 

1754def test_GetLiteUser_blocked_user(db): 

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

1756 user1, token1 = generate_user() 

1757 user2, _ = generate_user() 

1758 

1759 # User1 blocks user2 

1760 make_user_block(user1, user2) 

1761 

1762 # Refresh the materialized view 

1763 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1764 

1765 with api_session(token1) as api: 

1766 # Query by username 

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

1768 

1769 assert lite_user.user_id == user2.id 

1770 assert lite_user.is_ghost 

1771 assert lite_user.username == "ghost" 

1772 assert lite_user.name == "Deactivated Account" 

1773 

1774 # Query by ID 

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

1776 

1777 assert lite_user.user_id == user2.id 

1778 assert lite_user.is_ghost 

1779 assert lite_user.username == "ghost" 

1780 assert lite_user.name == "Deactivated Account" 

1781 

1782 

1783def test_GetLiteUser_blocking_user(db): 

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

1785 user1, token1 = generate_user() 

1786 user2, _ = generate_user() 

1787 

1788 # User2 blocks user1 

1789 make_user_block(user2, user1) 

1790 

1791 # Refresh the materialized view 

1792 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1793 

1794 with api_session(token1) as api: 

1795 # Query by username 

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

1797 

1798 assert lite_user.user_id == user2.id 

1799 assert lite_user.is_ghost 

1800 assert lite_user.username == "ghost" 

1801 assert lite_user.name == "Deactivated Account" 

1802 

1803 # Query by ID 

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

1805 

1806 assert lite_user.user_id == user2.id 

1807 assert lite_user.is_ghost 

1808 assert lite_user.username == "ghost" 

1809 assert lite_user.name == "Deactivated Account" 

1810 

1811 

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

1813def test_GetLiteUsers_ghost_users(db, flag): 

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

1815 user1, token1 = generate_user() 

1816 user2, _ = generate_user() 

1817 user3, _ = generate_user() 

1818 user4, _ = generate_user() 

1819 

1820 # Make user2 and user4 invisible 

1821 with session_scope() as session: 

1822 db_user2 = session.merge(user2) 

1823 setattr(db_user2, flag, now()) 

1824 db_user4 = session.merge(user4) 

1825 setattr(db_user4, flag, now()) 

1826 session.commit() 

1827 

1828 # Refresh the materialized view 

1829 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1830 

1831 with api_session(token1) as api: 

1832 res = api.GetLiteUsers( 

1833 api_pb2.GetLiteUsersReq( 

1834 users=[ 

1835 user1.username, # visible 

1836 user2.username, # ghost 

1837 str(user3.id), # visible 

1838 str(user4.id), # ghost 

1839 ] 

1840 ) 

1841 ) 

1842 

1843 assert len(res.responses) == 4 

1844 

1845 # user1 - visible, normal profile 

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

1847 assert not res.responses[0].not_found 

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

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

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

1851 

1852 # user2 - ghost by username 

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

1854 assert not res.responses[1].not_found 

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

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

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

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

1859 

1860 # user3 - visible, normal profile 

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

1862 assert not res.responses[2].not_found 

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

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

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

1866 

1867 # user4 - ghost by ID 

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

1869 assert not res.responses[3].not_found 

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

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

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

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

1874 

1875 

1876def test_GetLiteUsers_blocked_users(db): 

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

1878 user1, token1 = generate_user() 

1879 user2, _ = generate_user() 

1880 user3, _ = generate_user() 

1881 user4, _ = generate_user() 

1882 user5, _ = generate_user() 

1883 

1884 # User1 blocks user2 

1885 make_user_block(user1, user2) 

1886 # User4 blocks user1 

1887 make_user_block(user4, user1) 

1888 

1889 # Refresh the materialized view 

1890 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1891 

1892 with api_session(token1) as api: 

1893 res = api.GetLiteUsers( 

1894 api_pb2.GetLiteUsersReq( 

1895 users=[ 

1896 user2.username, # user1 blocked user2 

1897 str(user3.id), # visible 

1898 user4.username, # user4 blocked user1 

1899 str(user5.id), # visible 

1900 ] 

1901 ) 

1902 ) 

1903 

1904 assert len(res.responses) == 4 

1905 

1906 # user2 - blocked by user1, should be ghost 

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

1908 assert not res.responses[0].not_found 

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

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

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

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

1913 

1914 # user3 - visible 

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

1916 assert not res.responses[1].not_found 

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

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

1919 

1920 # user4 - user4 blocked user1, should be ghost 

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

1922 assert not res.responses[2].not_found 

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

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

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

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

1927 

1928 # user5 - visible 

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

1930 assert not res.responses[3].not_found 

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

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

1933 

1934 

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

1936def test_GetUser_ghost_user_by_id(db, flag): 

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

1938 user1, token1 = generate_user() 

1939 user2, _ = generate_user() 

1940 

1941 # Make user2 invisible 

1942 with session_scope() as session: 

1943 db_user2 = session.merge(user2) 

1944 setattr(db_user2, flag, now()) 

1945 session.commit() 

1946 

1947 with api_session(token1) as api: 

1948 # Query by ID 

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

1950 

1951 assert user_pb.user_id == user2.id 

1952 assert user_pb.username == "ghost" 

1953 assert user_pb.name == "Deactivated Account" 

1954 assert user_pb.city == "" 

1955 assert user_pb.hosting_status == 0 

1956 assert user_pb.meetup_status == 0 

1957 

1958 

1959def test_GetUser_blocked_user(db): 

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

1961 user1, token1 = generate_user() 

1962 user2, _ = generate_user() 

1963 

1964 # User1 blocks user2 

1965 make_user_block(user1, user2) 

1966 

1967 with api_session(token1) as api: 

1968 # Query by username 

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

1970 

1971 assert user_pb.user_id == user2.id 

1972 assert user_pb.username == "ghost" 

1973 assert user_pb.name == "Deactivated Account" 

1974 

1975 # Query by ID 

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

1977 

1978 assert user_pb.user_id == user2.id 

1979 assert user_pb.username == "ghost" 

1980 assert user_pb.name == "Deactivated Account" 

1981 

1982 

1983def test_GetUser_blocking_user(db): 

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

1985 user1, token1 = generate_user() 

1986 user2, _ = generate_user() 

1987 

1988 # User2 blocks user1 

1989 make_user_block(user2, user1) 

1990 

1991 with api_session(token1) as api: 

1992 # Query by username 

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

1994 

1995 assert user_pb.user_id == user2.id 

1996 assert user_pb.username == "ghost" 

1997 assert user_pb.name == "Deactivated Account" 

1998 

1999 # Query by ID 

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

2001 

2002 assert user_pb.user_id == user2.id 

2003 assert user_pb.username == "ghost" 

2004 assert user_pb.name == "Deactivated Account"