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

740 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-10 12:25 +0000

1from datetime import UTC, date, datetime, timedelta 

2from unittest.mock import patch 

3 

4import grpc 

5import pytest 

6from google.protobuf import empty_pb2, wrappers_pb2 

7from sqlalchemy import select, update 

8from sqlalchemy.sql import func 

9 

10from couchers import urls 

11from couchers.crypto import hash_password, random_hex 

12from couchers.db import session_scope 

13from couchers.materialized_views import refresh_materialized_views_rapid 

14from couchers.models import ( 

15 AccountDeletionReason, 

16 AccountDeletionToken, 

17 BackgroundJob, 

18 HostingStatus, 

19 InviteCode, 

20 PhotoGalleryItem, 

21 SleepingArrangement, 

22 Upload, 

23 User, 

24) 

25from couchers.proto import account_pb2, api_pb2, auth_pb2, messages_pb2, requests_pb2 

26from couchers.utils import now, today 

27from tests.fixtures.db import backdate_conversations, generate_user, make_volunteer 

28from tests.fixtures.misc import EmailCollector, PushCollector, process_jobs 

29from tests.fixtures.sessions import ( 

30 account_session, 

31 auth_api_session, 

32 public_session, 

33 real_account_session, 

34 requests_session, 

35) 

36from tests.fixtures.timewarp import Timewarp 

37from tests.test_requests import valid_request_text 

38 

39 

40@pytest.fixture(autouse=True) 

41def _(testconfig): 

42 pass 

43 

44 

45def test_GetAccountInfo(db, fast_passwords): 

46 # with password 

47 user1, token1 = generate_user(hashed_password=hash_password(random_hex()), email="user@couchers.invalid") 

48 

49 with account_session(token1) as account: 

50 res = account.GetAccountInfo(empty_pb2.Empty()) 

51 assert res.email == "user@couchers.invalid" 

52 assert res.username == user1.username 

53 assert not res.has_strong_verification 

54 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_UNVERIFIED 

55 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_UNVERIFIED 

56 assert not res.is_superuser 

57 assert res.ui_language_preference == "" 

58 assert not res.is_volunteer 

59 

60 

61def test_donation_banner_no_drive(db): 

62 """Test that the banner is not shown when no drive is configured (flag unset)""" 

63 # User has donated, but there's no drive, so the banner should not show 

64 user, token = generate_user() 

65 

66 with account_session(token) as account: 

67 res = account.GetAccountInfo(empty_pb2.Empty()) 

68 assert not res.should_show_donation_banner 

69 

70 

71def test_donation_banner_never_donated(db, feature_flags): 

72 """Test that banner is shown when user has never donated and drive is active""" 

73 # Explicitly set last_donated=None since generate_user defaults to now() 

74 user, token = generate_user(last_donated=None) 

75 

76 drive_start = datetime(2025, 11, 1, tzinfo=UTC) 

77 feature_flags.set("donation_drive_start", int(drive_start.timestamp())) 

78 with account_session(token) as account: 

79 res = account.GetAccountInfo(empty_pb2.Empty()) 

80 assert res.should_show_donation_banner 

81 

82 

83def test_donation_banner_donated_before_drive(db, feature_flags): 

84 """Test that banner is shown when user donated before drive start""" 

85 user, token = generate_user() 

86 

87 # Set donation before drive start 

88 with session_scope() as session: 

89 last_donated = datetime(2025, 10, 15, tzinfo=UTC) # Before Nov 1 

90 session.execute(update(User).where(User.id == user.id).values(last_donated=last_donated)) 

91 

92 drive_start = datetime(2025, 11, 1, tzinfo=UTC) 

93 feature_flags.set("donation_drive_start", int(drive_start.timestamp())) 

94 with account_session(token) as account: 

95 res = account.GetAccountInfo(empty_pb2.Empty()) 

96 assert res.should_show_donation_banner 

97 

98 

99def test_donation_banner_donated_after_drive(db, feature_flags): 

100 """Test that banner is not shown when user donated after drive start""" 

101 user, token = generate_user() 

102 

103 # Set donation after drive start 

104 with session_scope() as session: 

105 last_donated = datetime(2025, 11, 15, tzinfo=UTC) # After Nov 1 

106 session.execute(update(User).where(User.id == user.id).values(last_donated=last_donated)) 

107 

108 drive_start = datetime(2025, 11, 1, tzinfo=UTC) 

109 feature_flags.set("donation_drive_start", int(drive_start.timestamp())) 

110 with account_session(token) as account: 

111 res = account.GetAccountInfo(empty_pb2.Empty()) 

112 assert not res.should_show_donation_banner 

113 

114 

115def test_donation_banner_donated_exactly_at_drive_start(db, feature_flags): 

116 """Test that banner is not shown when user donated exactly at drive start time""" 

117 drive_start = datetime(2025, 11, 1, tzinfo=UTC) 

118 

119 user, token = generate_user() 

120 

121 # Set donation exactly at drive start 

122 with session_scope() as session: 

123 session.execute(update(User).where(User.id == user.id).values(last_donated=drive_start)) 

124 

125 feature_flags.set("donation_drive_start", int(drive_start.timestamp())) 

126 with account_session(token) as account: 

127 res = account.GetAccountInfo(empty_pb2.Empty()) 

128 assert not res.should_show_donation_banner 

129 

130 

131def test_GetAccountInfo_regression(db): 

132 # there was a bug in evaluating `has_completed_profile` on the backend (in python) 

133 # when about_me is None but the user has a key, it was failing because len(about_me) doesn't work on None 

134 user, token = generate_user(about_me=None, complete_profile=False) 

135 

136 # add an avatar photo to the user's profile gallery 

137 with session_scope() as session: 

138 key = random_hex(32) 

139 filename = random_hex(32) + ".jpg" 

140 session.add( 

141 Upload( 

142 key=key, 

143 filename=filename, 

144 creator_user_id=user.id, 

145 ) 

146 ) 

147 session.flush() 

148 assert user.profile_gallery_id is not None 

149 session.add( 

150 PhotoGalleryItem( 

151 gallery_id=user.profile_gallery_id, 

152 upload_key=key, 

153 position=0, 

154 ) 

155 ) 

156 

157 with account_session(token) as account: 

158 res = account.GetAccountInfo(empty_pb2.Empty()) 

159 

160 

161def test_ChangePasswordV2_normal(db, fast_passwords, email_collector: EmailCollector, push_collector: PushCollector): 

162 # user has old password and is changing to new password 

163 old_password = random_hex() 

164 new_password = random_hex() 

165 user, token = generate_user(hashed_password=hash_password(old_password)) 

166 

167 with account_session(token) as account: 

168 account.ChangePasswordV2( 

169 account_pb2.ChangePasswordV2Req( 

170 old_password=old_password, 

171 new_password=new_password, 

172 ) 

173 ) 

174 

175 email = email_collector.pop_for_recipient(user.email, last=True) 

176 assert email.subject == "[TEST] Your password was changed" 

177 

178 push = push_collector.pop_for_user(user.id, last=True) 

179 assert push.content.title == "Password changed" 

180 assert push.content.body == "Your password was changed." 

181 

182 with session_scope() as session: 

183 updated_user = session.execute(select(User).where(User.id == user.id)).scalar_one() 

184 assert updated_user.hashed_password == hash_password(new_password) 

185 

186 

187def test_ChangePasswordV2_regression(db, fast_passwords): 

188 # send_password_changed_email wasn't working 

189 # user has old password and is changing to new password 

190 old_password = random_hex() 

191 new_password = random_hex() 

192 user, token = generate_user(hashed_password=hash_password(old_password)) 

193 

194 with account_session(token) as account: 

195 account.ChangePasswordV2( 

196 account_pb2.ChangePasswordV2Req( 

197 old_password=old_password, 

198 new_password=new_password, 

199 ) 

200 ) 

201 

202 with session_scope() as session: 

203 updated_user = session.execute(select(User).where(User.id == user.id)).scalar_one() 

204 assert updated_user.hashed_password == hash_password(new_password) 

205 

206 

207def test_ChangePasswordV2_normal_short_password(db, fast_passwords): 

208 # user has old password and is changing to new password, but used short password 

209 old_password = random_hex() 

210 new_password = random_hex(length=1) 

211 user, token = generate_user(hashed_password=hash_password(old_password)) 

212 

213 with account_session(token) as account: 

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

215 account.ChangePasswordV2( 

216 account_pb2.ChangePasswordV2Req( 

217 old_password=old_password, 

218 new_password=new_password, 

219 ) 

220 ) 

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

222 assert e.value.details() == "The password must be 8 or more characters long." 

223 

224 with session_scope() as session: 

225 updated_user = session.execute(select(User).where(User.id == user.id)).scalar_one() 

226 assert updated_user.hashed_password == hash_password(old_password) 

227 

228 

229def test_ChangePasswordV2_normal_long_password(db, fast_passwords): 

230 # user has old password and is changing to new password, but used short password 

231 old_password = random_hex() 

232 new_password = random_hex(length=1000) 

233 user, token = generate_user(hashed_password=hash_password(old_password)) 

234 

235 with account_session(token) as account: 

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

237 account.ChangePasswordV2( 

238 account_pb2.ChangePasswordV2Req( 

239 old_password=old_password, 

240 new_password=new_password, 

241 ) 

242 ) 

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

244 assert e.value.details() == "The password must be less than 256 characters." 

245 

246 with session_scope() as session: 

247 updated_user = session.execute(select(User).where(User.id == user.id)).scalar_one() 

248 assert updated_user.hashed_password == hash_password(old_password) 

249 

250 

251def test_ChangePasswordV2_normal_insecure_password(db, fast_passwords): 

252 # user has old password and is changing to new password, but used insecure password 

253 old_password = random_hex() 

254 new_password = "12345678" 

255 user, token = generate_user(hashed_password=hash_password(old_password)) 

256 

257 with account_session(token) as account: 

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

259 account.ChangePasswordV2( 

260 account_pb2.ChangePasswordV2Req( 

261 old_password=old_password, 

262 new_password=new_password, 

263 ) 

264 ) 

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

266 assert e.value.details() == "The password is insecure. Please use one that is not easily guessable." 

267 

268 with session_scope() as session: 

269 updated_user = session.execute(select(User).where(User.id == user.id)).scalar_one() 

270 assert updated_user.hashed_password == hash_password(old_password) 

271 

272 

273def test_ChangePasswordV2_normal_wrong_password(db, fast_passwords): 

274 # user has old password and is changing to new password, but used wrong old password 

275 old_password = random_hex() 

276 new_password = random_hex() 

277 user, token = generate_user(hashed_password=hash_password(old_password)) 

278 

279 with account_session(token) as account: 

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

281 account.ChangePasswordV2( 

282 account_pb2.ChangePasswordV2Req( 

283 old_password="Wrong password", 

284 new_password=new_password, 

285 ) 

286 ) 

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

288 assert e.value.details() == "Wrong password." 

289 

290 with session_scope() as session: 

291 updated_user = session.execute(select(User).where(User.id == user.id)).scalar_one() 

292 assert updated_user.hashed_password == hash_password(old_password) 

293 

294 

295def test_ChangePasswordV2_normal_no_passwords(db, fast_passwords): 

296 # user has old password and called with empty body 

297 old_password = random_hex() 

298 user, token = generate_user(hashed_password=hash_password(old_password)) 

299 

300 with account_session(token) as account: 

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

302 account.ChangePasswordV2(account_pb2.ChangePasswordV2Req(old_password=old_password)) 

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

304 assert e.value.details() == "The password must be 8 or more characters long." 

305 

306 with session_scope() as session: 

307 updated_user = session.execute(select(User).where(User.id == user.id)).scalar_one() 

308 assert updated_user.hashed_password == hash_password(old_password) 

309 

310 

311def test_ChangeEmailV2_wrong_password(db, fast_passwords): 

312 password = random_hex() 

313 new_email = f"{random_hex()}@couchers.org.invalid" 

314 user, token = generate_user(hashed_password=hash_password(password)) 

315 

316 with account_session(token) as account: 

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

318 account.ChangeEmailV2( 

319 account_pb2.ChangeEmailV2Req( 

320 password="Wrong password", 

321 new_email=new_email, 

322 ) 

323 ) 

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

325 assert e.value.details() == "Wrong password." 

326 

327 with session_scope() as session: 

328 assert ( 

329 session.execute( 

330 select(func.count()) 

331 .select_from(User) 

332 .where(User.new_email_token_created <= func.now()) 

333 .where(User.new_email_token_expiry >= func.now()) 

334 ) 

335 ).scalar_one() == 0 

336 

337 

338def test_ChangeEmailV2_wrong_email(db, fast_passwords): 

339 password = random_hex() 

340 new_email = f"{random_hex()}@couchers.org.invalid" 

341 user, token = generate_user(hashed_password=hash_password(password)) 

342 

343 with account_session(token) as account: 

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

345 account.ChangeEmailV2( 

346 account_pb2.ChangeEmailV2Req( 

347 password="Wrong password", 

348 new_email=new_email, 

349 ) 

350 ) 

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

352 assert e.value.details() == "Wrong password." 

353 

354 with session_scope() as session: 

355 assert ( 

356 session.execute( 

357 select(func.count()) 

358 .select_from(User) 

359 .where(User.new_email_token_created <= func.now()) 

360 .where(User.new_email_token_expiry >= func.now()) 

361 ) 

362 ).scalar_one() == 0 

363 

364 

365def test_ChangeEmailV2_invalid_email(db, fast_passwords): 

366 password = random_hex() 

367 user, token = generate_user(hashed_password=hash_password(password)) 

368 

369 with account_session(token) as account: 

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

371 account.ChangeEmailV2( 

372 account_pb2.ChangeEmailV2Req( 

373 password=password, 

374 new_email="not a real email", 

375 ) 

376 ) 

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

378 assert e.value.details() == "Invalid email." 

379 

380 with session_scope() as session: 

381 assert ( 

382 session.execute( 

383 select(func.count()) 

384 .select_from(User) 

385 .where(User.new_email_token_created <= func.now()) 

386 .where(User.new_email_token_expiry >= func.now()) 

387 ) 

388 ).scalar_one() == 0 

389 

390 

391def test_ChangeEmailV2_email_in_use(db, fast_passwords): 

392 password = random_hex() 

393 user, token = generate_user(hashed_password=hash_password(password)) 

394 user2, token2 = generate_user(hashed_password=hash_password(password)) 

395 

396 with account_session(token) as account: 

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

398 account.ChangeEmailV2( 

399 account_pb2.ChangeEmailV2Req( 

400 password=password, 

401 new_email=user2.email, 

402 ) 

403 ) 

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

405 assert e.value.details() == "Invalid email." 

406 

407 with session_scope() as session: 

408 assert ( 

409 session.execute( 

410 select(func.count()) 

411 .select_from(User) 

412 .where(User.new_email_token_created <= func.now()) 

413 .where(User.new_email_token_expiry >= func.now()) 

414 ) 

415 ).scalar_one() == 0 

416 

417 

418def test_ChangeEmailV2_no_change(db, fast_passwords): 

419 password = random_hex() 

420 user, token = generate_user(hashed_password=hash_password(password)) 

421 

422 with account_session(token) as account: 

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

424 account.ChangeEmailV2( 

425 account_pb2.ChangeEmailV2Req( 

426 password=password, 

427 new_email=user.email, 

428 ) 

429 ) 

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

431 assert e.value.details() == "Invalid email." 

432 

433 with session_scope() as session: 

434 assert ( 

435 session.execute( 

436 select(func.count()) 

437 .select_from(User) 

438 .where(User.new_email_token_created <= func.now()) 

439 .where(User.new_email_token_expiry >= func.now()) 

440 ) 

441 ).scalar_one() == 0 

442 

443 

444def test_ChangeEmailV2_wrong_token(db, fast_passwords): 

445 password = random_hex() 

446 new_email = f"{random_hex()}@couchers.org.invalid" 

447 user, token = generate_user(hashed_password=hash_password(password)) 

448 

449 with account_session(token) as account: 

450 account.ChangeEmailV2( 

451 account_pb2.ChangeEmailV2Req( 

452 password=password, 

453 new_email=new_email, 

454 ) 

455 ) 

456 

457 with auth_api_session() as (auth_api, metadata_interceptor): 

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

459 res = auth_api.ConfirmChangeEmailV2( 

460 auth_pb2.ConfirmChangeEmailV2Req( 

461 change_email_token="wrongtoken", 

462 ) 

463 ) 

464 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

465 assert e.value.details() == "Invalid token." 

466 

467 with session_scope() as session: 

468 user_updated = session.execute(select(User).where(User.id == user.id)).scalar_one() 

469 assert user_updated.email == user.email 

470 

471 

472def test_ChangeEmailV2_tokens_two_hour_window(db, fast_passwords, timewarp: Timewarp): 

473 password = random_hex() 

474 new_email = f"{random_hex()}@couchers.org.invalid" 

475 user, token = generate_user(hashed_password=hash_password(password)) 

476 

477 with account_session(token) as account: 

478 account.ChangeEmailV2( 

479 account_pb2.ChangeEmailV2Req( 

480 password=password, 

481 new_email=new_email, 

482 ) 

483 ) 

484 

485 with session_scope() as session: 

486 new_email_token = session.execute(select(User.new_email_token).where(User.id == user.id)).scalar_one() 

487 

488 # before the token was issued 

489 timewarp.advance(-timedelta(minutes=1)) 

490 with auth_api_session() as (auth_api, metadata_interceptor): 

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

492 auth_api.ConfirmChangeEmailV2(auth_pb2.ConfirmChangeEmailV2Req()) 

493 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

494 assert e.value.details() == "Invalid token." 

495 

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

497 auth_api.ConfirmChangeEmailV2( 

498 auth_pb2.ConfirmChangeEmailV2Req( 

499 change_email_token=new_email_token, 

500 ) 

501 ) 

502 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

503 assert e.value.details() == "Invalid token." 

504 

505 # and a minute after the two hour window closed 

506 timewarp.advance(timedelta(hours=2, minutes=1)) 

507 with auth_api_session() as (auth_api, metadata_interceptor): 

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

509 auth_api.ConfirmChangeEmailV2(auth_pb2.ConfirmChangeEmailV2Req()) 

510 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

511 assert e.value.details() == "Invalid token." 

512 

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

514 auth_api.ConfirmChangeEmailV2( 

515 auth_pb2.ConfirmChangeEmailV2Req( 

516 change_email_token=new_email_token, 

517 ) 

518 ) 

519 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

520 assert e.value.details() == "Invalid token." 

521 

522 

523def test_ChangeEmailV2(db, fast_passwords, push_collector: PushCollector): 

524 password = random_hex() 

525 new_email = f"{random_hex()}@couchers.org.invalid" 

526 user, token = generate_user(hashed_password=hash_password(password)) 

527 user_id = user.id 

528 

529 with account_session(token) as account: 

530 account.ChangeEmailV2( 

531 account_pb2.ChangeEmailV2Req( 

532 password=password, 

533 new_email=new_email, 

534 ) 

535 ) 

536 

537 with session_scope() as session: 

538 user_updated = session.execute(select(User).where(User.id == user_id)).scalar_one() 

539 assert user_updated.email == user.email 

540 assert user_updated.new_email == new_email 

541 assert user_updated.new_email_token is not None 

542 assert user_updated.new_email_token_created 

543 assert user_updated.new_email_token_created <= now() 

544 assert user_updated.new_email_token_expiry 

545 assert user_updated.new_email_token_expiry >= now() 

546 

547 token = user_updated.new_email_token 

548 

549 process_jobs() 

550 push = push_collector.pop_for_user(user_id, last=True) 

551 assert push.content.title == "Email change requested" 

552 assert push.content.body == f"Use the link we sent to {new_email} to confirm your new address." 

553 

554 with auth_api_session() as (auth_api, metadata_interceptor): 

555 auth_api.ConfirmChangeEmailV2( 

556 auth_pb2.ConfirmChangeEmailV2Req( 

557 change_email_token=token, 

558 ) 

559 ) 

560 

561 with session_scope() as session: 

562 user = session.execute(select(User).where(User.id == user_id)).scalar_one() 

563 assert user.email == new_email 

564 assert user.new_email is None 

565 assert user.new_email_token is None 

566 assert user.new_email_token_created is None 

567 assert user.new_email_token_expiry is None 

568 

569 process_jobs() 

570 push = push_collector.pop_for_user(user_id, last=True) 

571 assert push.content.title == "Email verified" 

572 assert push.content.body == "Your new email address has been verified." 

573 

574 

575def test_ChangeEmailV2_sends_proper_emails(db, fast_passwords, push_collector: PushCollector): 

576 password = random_hex() 

577 new_email = f"{random_hex()}@couchers.org.invalid" 

578 user, token = generate_user(hashed_password=hash_password(password)) 

579 

580 with account_session(token) as account: 

581 account.ChangeEmailV2( 

582 account_pb2.ChangeEmailV2Req( 

583 password=password, 

584 new_email=new_email, 

585 ) 

586 ) 

587 

588 process_jobs() 

589 

590 with session_scope() as session: 

591 jobs = session.execute(select(BackgroundJob).where(BackgroundJob.job_type == "send_email")).scalars().all() 

592 assert len(jobs) == 2 

593 uq_str1 = b"Email address change initiated" 

594 uq_str2 = b"You requested that your email be changed from" 

595 assert (uq_str1 in jobs[0].payload and uq_str2 in jobs[1].payload) or ( 

596 uq_str2 in jobs[0].payload and uq_str1 in jobs[1].payload 

597 ) 

598 

599 push = push_collector.pop_for_user(user.id, last=True) 

600 assert push.content.title == "Email change requested" 

601 assert push.content.body == f"Use the link we sent to {new_email} to confirm your new address." 

602 

603 

604def test_ChangeLanguagePreference(db, fast_passwords): 

605 # user changes from default to ISO 639-1 language code 

606 new_lang = "zh" 

607 user, token = generate_user() 

608 

609 with real_account_session(token) as account: 

610 res = account.GetAccountInfo(empty_pb2.Empty()) 

611 assert res.ui_language_preference == "" 

612 

613 # call will have info about the request 

614 res, call = account.ChangeLanguagePreference.with_call( 

615 account_pb2.ChangeLanguagePreferenceReq(ui_language_preference=new_lang) 

616 ) 

617 

618 # cookies are sent via initial metadata, so we check for it there 

619 # the value of "set-cookie" will be the full cookie string, pull the key value from the string 

620 cookie_values = [v.split(";")[0] for k, v in call.initial_metadata() if k == "set-cookie"] 

621 assert any(val == "NEXT_LOCALE=zh" for val in cookie_values), ( 

622 f"Didn't find the right cookie, got {call.initial_metadata()}" 

623 ) 

624 

625 # the changed language preference should also be sent to the backend 

626 res = account.GetAccountInfo(empty_pb2.Empty()) 

627 assert res.ui_language_preference == "zh" 

628 

629 

630def test_contributor_form(db): 

631 user, token = generate_user() 

632 

633 with account_session(token) as account: 

634 res = account.GetContributorFormInfo(empty_pb2.Empty()) 

635 assert not res.filled_contributor_form 

636 

637 account.FillContributorForm(account_pb2.FillContributorFormReq(contributor_form=auth_pb2.ContributorForm())) 

638 

639 res = account.GetContributorFormInfo(empty_pb2.Empty()) 

640 assert res.filled_contributor_form 

641 

642 

643def test_DeleteAccount_start(db, email_collector: EmailCollector): 

644 user, token = generate_user() 

645 

646 with account_session(token) as account: 

647 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True, reason=None)) 

648 email = email_collector.pop_for_recipient(user.email, last=True) 

649 assert email.subject == "[TEST] Confirm your account deletion" 

650 

651 with session_scope() as session: 

652 deletion_token: AccountDeletionToken = session.execute( 

653 select(AccountDeletionToken).where(AccountDeletionToken.user_id == user.id) 

654 ).scalar_one() 

655 

656 assert deletion_token.is_valid 

657 assert session.execute(select(User).where(User.id == user.id)).scalar_one().deleted_at is None 

658 

659 

660def test_DeleteAccount_message_storage(db): 

661 user, token = generate_user() 

662 

663 with account_session(token) as account: 

664 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True, reason=None)) # not stored 

665 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True, reason="")) # not stored 

666 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True, reason="Reason")) 

667 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True, reason="0192#(&!&#)*@//)(8")) 

668 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True, reason="\n\n\t")) # not stored 

669 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True, reason="1337")) 

670 

671 with session_scope() as session: 

672 assert session.execute(select(func.count()).select_from(AccountDeletionReason)).scalar_one() == 3 

673 

674 

675def test_full_delete_account_with_recovery(db, email_collector: EmailCollector, push_collector: PushCollector): 

676 user, token = generate_user() 

677 user_id = user.id 

678 

679 with account_session(token) as account: 

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

681 account.DeleteAccount(account_pb2.DeleteAccountReq()) 

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

683 assert err.value.details() == "Please confirm your account deletion." 

684 

685 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True)) 

686 

687 email = email_collector.pop_for_recipient(user.email, last=True) 

688 assert email.subject == "[TEST] Confirm your account deletion" 

689 assert email.recipient == user.email 

690 assert "account deletion" in email.subject.lower() 

691 unique_string = "You requested that we delete your Couchers.org account." 

692 assert unique_string in email.plain 

693 assert unique_string in email.html 

694 assert "support@couchers.org" in email.plain 

695 assert "support@couchers.org" in email.html 

696 

697 push = push_collector.pop_for_user(user_id, last=True) 

698 assert push.content.title == "Account deletion requested" 

699 assert push.content.body == "Use the link we emailed you to confirm." 

700 

701 with session_scope() as session: 

702 token_o = session.execute(select(AccountDeletionToken)).scalar_one() 

703 delete_token = token_o.token 

704 

705 user_ = session.execute(select(User).where(User.id == user_id)).scalar_one() 

706 assert token_o.user == user_ 

707 assert user_.deleted_at is None 

708 assert not user_.undelete_token 

709 assert not user_.undelete_until 

710 

711 assert delete_token in email.plain 

712 assert delete_token in email.html 

713 delete_url = f"http://localhost:3000/delete-account?token={delete_token}" 

714 assert delete_url in email.plain 

715 assert delete_url in email.html 

716 

717 with auth_api_session() as (auth_api, metadata_interceptor): 

718 auth_api.ConfirmDeleteAccount( 

719 auth_pb2.ConfirmDeleteAccountReq( 

720 token=delete_token, 

721 ) 

722 ) 

723 

724 email = email_collector.pop_for_recipient(user.email, last=True) 

725 assert email.recipient == user.email 

726 assert "account has been deleted" in email.subject.lower() 

727 unique_string = "You have successfully deleted your Couchers.org account." 

728 assert unique_string in email.plain 

729 assert unique_string in email.html 

730 assert "7 days" in email.plain 

731 assert "7 days" in email.html 

732 assert "support@couchers.org" in email.plain 

733 assert "support@couchers.org" in email.html 

734 

735 push = push_collector.pop_for_user(user_id, last=True) 

736 assert push.content.title == "Account deleted" 

737 assert push.content.body == "You can restore it within 7 days using the link we emailed you." 

738 

739 with session_scope() as session: 

740 assert not session.execute(select(AccountDeletionToken)).scalar_one_or_none() 

741 

742 user_ = session.execute(select(User).where(User.id == user_id)).scalar_one() 

743 assert user_.deleted_at is not None 

744 assert user_.undelete_token 

745 assert user_.undelete_until 

746 assert user_.undelete_until > now() 

747 

748 undelete_token = user_.undelete_token 

749 

750 undelete_url = f"http://localhost:3000/recover-account?token={undelete_token}" 

751 assert undelete_url in email.plain 

752 assert undelete_url in email.html 

753 

754 with auth_api_session() as (auth_api, metadata_interceptor): 

755 auth_api.RecoverAccount( 

756 auth_pb2.RecoverAccountReq( 

757 token=undelete_token, 

758 ) 

759 ) 

760 

761 email = email_collector.pop_for_recipient(user.email, last=True) 

762 assert email.recipient == user.email 

763 assert "account has been recovered" in email.subject.lower() 

764 unique_string = "Your Couchers.org account has been successfully recovered." 

765 assert unique_string in email.plain 

766 assert unique_string in email.html 

767 assert "support@couchers.org" in email.plain 

768 assert "support@couchers.org" in email.html 

769 

770 push = push_collector.pop_for_user(user_id, last=True) 

771 assert push.content.title == "Account restored" 

772 assert push.content.body == "Welcome back!" 

773 

774 with session_scope() as session: 

775 assert not session.execute(select(AccountDeletionToken)).scalar_one_or_none() 

776 

777 user = session.execute(select(User).where(User.id == user_id)).scalar_one() 

778 assert user.deleted_at is None 

779 assert not user.undelete_token 

780 assert not user.undelete_until 

781 

782 

783def test_multiple_delete_tokens(db): 

784 """ 

785 Make sure deletion tokens are deleted on delete 

786 """ 

787 user, token = generate_user() 

788 

789 with account_session(token) as account: 

790 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True)) 

791 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True)) 

792 account.DeleteAccount(account_pb2.DeleteAccountReq(confirm=True)) 

793 

794 with session_scope() as session: 

795 assert session.execute(select(func.count()).select_from(AccountDeletionToken)).scalar_one() == 3 

796 token = session.execute(select(AccountDeletionToken.token).limit(1)).scalar_one() 

797 

798 with auth_api_session() as (auth_api, metadata_interceptor): 

799 auth_api.ConfirmDeleteAccount( 

800 auth_pb2.ConfirmDeleteAccountReq( 

801 token=token, 

802 ) 

803 ) 

804 

805 with session_scope() as session: 

806 assert not session.execute(select(AccountDeletionToken.token)).scalar_one_or_none() 

807 

808 

809def test_ListActiveSessions_pagination(db, fast_passwords): 

810 password = random_hex() 

811 user, token = generate_user(hashed_password=hash_password(password)) 

812 

813 with auth_api_session() as (auth_api, metadata_interceptor): 

814 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

815 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

816 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

817 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

818 

819 with real_account_session(token) as account: 

820 res = account.ListActiveSessions(account_pb2.ListActiveSessionsReq(page_size=3)) 

821 assert len(res.active_sessions) == 3 

822 res = account.ListActiveSessions(account_pb2.ListActiveSessionsReq(page_token=res.next_page_token, page_size=3)) 

823 assert len(res.active_sessions) == 2 

824 assert not res.next_page_token 

825 

826 

827def test_ListActiveSessions_details(db, fast_passwords): 

828 password = random_hex() 

829 user, token = generate_user(hashed_password=hash_password(password)) 

830 

831 ips_user_agents = [ 

832 ( 

833 "108.123.33.162", 

834 "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1", 

835 ), 

836 ( 

837 "8.245.212.28", 

838 "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/26.0 Chrome/122.0.0.0 Mobile Safari/537.36", 

839 ), 

840 ( 

841 "95.254.140.156", 

842 "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0", 

843 ), 

844 ] 

845 

846 for ip, user_agent in ips_user_agents: 

847 options = (("grpc.primary_user_agent", user_agent),) 

848 with auth_api_session(grpc_channel_options=options) as (auth_api, metadata_interceptor): 

849 auth_api.Authenticate( 

850 auth_pb2.AuthReq(user=user.username, password=password), metadata=(("x-couchers-real-ip", ip),) 

851 ) 

852 

853 def dummy_geoip(ip_address): 

854 return { 

855 "108.123.33.162": "Chicago, United States", 

856 "8.245.212.28": "Sydney, Australia", 

857 }.get(ip_address) 

858 

859 with real_account_session(token) as account: 

860 with patch("couchers.servicers.account.geoip_approximate_location", dummy_geoip): 

861 res = account.ListActiveSessions(account_pb2.ListActiveSessionsReq()) 

862 print(res) 

863 assert len(res.active_sessions) == 4 

864 

865 # this one currently making the API call 

866 assert res.active_sessions[0].operating_system == "Other" 

867 assert res.active_sessions[0].browser == "Other" 

868 assert res.active_sessions[0].device == "Other" 

869 assert res.active_sessions[0].approximate_location == "Unknown" 

870 assert res.active_sessions[0].is_current_session 

871 

872 assert res.active_sessions[1].operating_system == "Ubuntu" 

873 assert res.active_sessions[1].browser == "Firefox" 

874 assert res.active_sessions[1].device == "Other" 

875 assert res.active_sessions[1].approximate_location == "Unknown" 

876 assert not res.active_sessions[1].is_current_session 

877 

878 assert res.active_sessions[2].operating_system == "Android" 

879 assert res.active_sessions[2].browser == "Samsung Internet" 

880 assert res.active_sessions[2].device == "K" 

881 assert res.active_sessions[2].approximate_location == "Sydney, Australia" 

882 assert not res.active_sessions[2].is_current_session 

883 

884 assert res.active_sessions[3].operating_system == "iOS" 

885 assert res.active_sessions[3].browser == "Mobile Safari" 

886 assert res.active_sessions[3].device == "iPhone" 

887 assert res.active_sessions[3].approximate_location == "Chicago, United States" 

888 assert not res.active_sessions[3].is_current_session 

889 

890 

891def test_LogOutSession(db, fast_passwords): 

892 password = random_hex() 

893 user, token = generate_user(hashed_password=hash_password(password)) 

894 

895 with auth_api_session() as (auth_api, metadata_interceptor): 

896 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

897 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

898 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

899 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

900 

901 with real_account_session(token) as account: 

902 res = account.ListActiveSessions(account_pb2.ListActiveSessionsReq()) 

903 assert len(res.active_sessions) == 5 

904 account.LogOutSession(account_pb2.LogOutSessionReq(created=res.active_sessions[3].created)) 

905 

906 res2 = account.ListActiveSessions(account_pb2.ListActiveSessionsReq()) 

907 assert len(res2.active_sessions) == 4 

908 

909 # ignore the first session as it changes 

910 assert res.active_sessions[1:3] + res.active_sessions[4:] == res2.active_sessions[1:] 

911 

912 

913def test_LogOutOtherSessions(db, fast_passwords): 

914 password = random_hex() 

915 user, token = generate_user(hashed_password=hash_password(password)) 

916 

917 with auth_api_session() as (auth_api, metadata_interceptor): 

918 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

919 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

920 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

921 auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password=password)) 

922 

923 with real_account_session(token) as account: 

924 res = account.ListActiveSessions(account_pb2.ListActiveSessionsReq()) 

925 assert len(res.active_sessions) == 5 

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

927 account.LogOutOtherSessions(account_pb2.LogOutOtherSessionsReq(confirm=False)) 

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

929 assert e.value.details() == "Please confirm you want to log out of other sessions." 

930 

931 account.LogOutOtherSessions(account_pb2.LogOutOtherSessionsReq(confirm=True)) 

932 res = account.ListActiveSessions(account_pb2.ListActiveSessionsReq()) 

933 assert len(res.active_sessions) == 1 

934 

935 

936def test_CreateInviteCode(db): 

937 user, token = generate_user() 

938 

939 with account_session(token) as account: 

940 res = account.CreateInviteCode(account_pb2.CreateInviteCodeReq()) 

941 code = res.code 

942 assert len(code) == 8 

943 

944 with session_scope() as session: 

945 invite = session.execute(select(InviteCode).where(InviteCode.id == code)).scalar_one() 

946 assert invite.creator_user_id == user.id 

947 assert invite.disabled is None 

948 assert res.url == urls.invite_code_link(code=res.code) 

949 

950 

951def test_DisableInviteCode(db): 

952 user, token = generate_user() 

953 

954 with account_session(token) as account: 

955 code = account.CreateInviteCode(account_pb2.CreateInviteCodeReq()).code 

956 account.DisableInviteCode(account_pb2.DisableInviteCodeReq(code=code)) 

957 

958 with session_scope() as session: 

959 invite = session.execute(select(InviteCode).where(InviteCode.id == code)).scalar_one() 

960 assert invite.disabled is not None 

961 

962 

963def test_ListInviteCodes(db): 

964 user, token = generate_user() 

965 another_user, _ = generate_user() 

966 

967 with account_session(token) as account: 

968 code = account.CreateInviteCode(account_pb2.CreateInviteCodeReq()).code 

969 

970 # simulate another_user having signed up with this invite code 

971 with session_scope() as session: 

972 session.execute(update(User).where(User.id == another_user.id).values(invite_code_id=code)) 

973 

974 with account_session(token) as account: 

975 res = account.ListInviteCodes(empty_pb2.Empty()) 

976 assert len(res.invite_codes) == 1 

977 assert res.invite_codes[0].code == code 

978 assert res.invite_codes[0].uses == 1 

979 assert res.invite_codes[0].url == urls.invite_code_link(code=code) 

980 

981 

982def test_reminders(db, moderator): 

983 # reference writing reminders tested in test_AvailableWriteReferences_and_ListPendingReferencesToWrite 

984 # we use LiteUser, so remember to refresh materialized views 

985 user, token = generate_user(complete_profile=False) 

986 complete_user, complete_token = generate_user(complete_profile=True) 

987 req_user1, req_user_token1 = generate_user(complete_profile=True) 

988 req_user2, req_user_token2 = generate_user(complete_profile=True) 

989 

990 refresh_materialized_views_rapid(empty_pb2.Empty()) 

991 with account_session(complete_token) as account: 

992 assert [reminder.WhichOneof("reminder") for reminder in account.GetReminders(empty_pb2.Empty()).reminders] == [] 

993 with account_session(token) as account: 

994 assert [reminder.WhichOneof("reminder") for reminder in account.GetReminders(empty_pb2.Empty()).reminders] == [ 

995 "complete_profile_reminder", 

996 ] 

997 

998 today_plus_2 = (today() + timedelta(days=2)).isoformat() 

999 today_plus_3 = (today() + timedelta(days=3)).isoformat() 

1000 with requests_session(req_user_token1) as api: 

1001 host_request1_id = api.CreateHostRequest( 

1002 requests_pb2.CreateHostRequestReq( 

1003 host_user_id=user.id, 

1004 from_date=today_plus_2, 

1005 to_date=today_plus_3, 

1006 text=valid_request_text("Test request 1"), 

1007 ) 

1008 ).host_request_id 

1009 moderator.approve_host_request(host_request1_id) 

1010 

1011 with account_session(token) as account: 

1012 reminders = account.GetReminders(empty_pb2.Empty()).reminders 

1013 assert [reminder.WhichOneof("reminder") for reminder in reminders] == [ 

1014 "respond_to_host_request_reminder", 

1015 "complete_profile_reminder", 

1016 ] 

1017 assert reminders[0].respond_to_host_request_reminder.host_request_id == host_request1_id 

1018 assert reminders[0].respond_to_host_request_reminder.surfer_user.user_id == req_user1.id 

1019 

1020 with requests_session(req_user_token2) as api: 

1021 host_request2_id = api.CreateHostRequest( 

1022 requests_pb2.CreateHostRequestReq( 

1023 host_user_id=user.id, 

1024 from_date=today_plus_2, 

1025 to_date=today_plus_3, 

1026 text=valid_request_text("Test request 2"), 

1027 ) 

1028 ).host_request_id 

1029 moderator.approve_host_request(host_request2_id) 

1030 

1031 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1032 with account_session(token) as account: 

1033 reminders = account.GetReminders(empty_pb2.Empty()).reminders 

1034 assert [reminder.WhichOneof("reminder") for reminder in reminders] == [ 

1035 "respond_to_host_request_reminder", 

1036 "respond_to_host_request_reminder", 

1037 "complete_profile_reminder", 

1038 ] 

1039 assert reminders[0].respond_to_host_request_reminder.host_request_id == host_request1_id 

1040 assert reminders[0].respond_to_host_request_reminder.surfer_user.user_id == req_user1.id 

1041 assert reminders[1].respond_to_host_request_reminder.host_request_id == host_request2_id 

1042 assert reminders[1].respond_to_host_request_reminder.surfer_user.user_id == req_user2.id 

1043 

1044 backdate_conversations() 

1045 with requests_session(req_user_token1) as api: 

1046 host_request3_id = api.CreateHostRequest( 

1047 requests_pb2.CreateHostRequestReq( 

1048 host_user_id=user.id, 

1049 from_date=today_plus_2, 

1050 to_date=today_plus_3, 

1051 text=valid_request_text("Test request 3"), 

1052 ) 

1053 ).host_request_id 

1054 moderator.approve_host_request(host_request3_id) 

1055 

1056 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1057 with account_session(token) as account: 

1058 reminders = account.GetReminders(empty_pb2.Empty()).reminders 

1059 assert [reminder.WhichOneof("reminder") for reminder in reminders] == [ 

1060 "respond_to_host_request_reminder", 

1061 "respond_to_host_request_reminder", 

1062 "respond_to_host_request_reminder", 

1063 "complete_profile_reminder", 

1064 ] 

1065 assert reminders[0].respond_to_host_request_reminder.host_request_id == host_request1_id 

1066 assert reminders[0].respond_to_host_request_reminder.surfer_user.user_id == req_user1.id 

1067 assert reminders[1].respond_to_host_request_reminder.host_request_id == host_request2_id 

1068 assert reminders[1].respond_to_host_request_reminder.surfer_user.user_id == req_user2.id 

1069 assert reminders[2].respond_to_host_request_reminder.host_request_id == host_request3_id 

1070 assert reminders[2].respond_to_host_request_reminder.surfer_user.user_id == req_user1.id 

1071 

1072 # accept req 

1073 with requests_session(token) as api: 

1074 api.RespondHostRequest( 

1075 requests_pb2.RespondHostRequestReq( 

1076 host_request_id=host_request1_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1077 ) 

1078 ) 

1079 

1080 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1081 with account_session(token) as account: 

1082 reminders = account.GetReminders(empty_pb2.Empty()).reminders 

1083 assert [reminder.WhichOneof("reminder") for reminder in reminders] == [ 

1084 "respond_to_host_request_reminder", 

1085 "respond_to_host_request_reminder", 

1086 "complete_profile_reminder", 

1087 ] 

1088 assert reminders[0].respond_to_host_request_reminder.host_request_id == host_request2_id 

1089 assert reminders[0].respond_to_host_request_reminder.surfer_user.user_id == req_user2.id 

1090 assert reminders[1].respond_to_host_request_reminder.host_request_id == host_request3_id 

1091 assert reminders[1].respond_to_host_request_reminder.surfer_user.user_id == req_user1.id 

1092 

1093 # host replies to req2 with a message: reminder should clear even though it's still pending 

1094 with requests_session(token) as api: 

1095 api.SendHostRequestMessage( 

1096 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request2_id, text="Let me think about it") 

1097 ) 

1098 

1099 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1100 with account_session(token) as account: 

1101 reminders = account.GetReminders(empty_pb2.Empty()).reminders 

1102 assert [reminder.WhichOneof("reminder") for reminder in reminders] == [ 

1103 "respond_to_host_request_reminder", 

1104 "complete_profile_reminder", 

1105 ] 

1106 assert reminders[0].respond_to_host_request_reminder.host_request_id == host_request3_id 

1107 assert reminders[0].respond_to_host_request_reminder.surfer_user.user_id == req_user1.id 

1108 

1109 # surfer sending a message should not clear the reminder 

1110 with requests_session(req_user_token1) as api: 

1111 api.SendHostRequestMessage( 

1112 requests_pb2.SendHostRequestMessageReq(host_request_id=host_request3_id, text="Any update?") 

1113 ) 

1114 

1115 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1116 with account_session(token) as account: 

1117 reminders = account.GetReminders(empty_pb2.Empty()).reminders 

1118 assert [reminder.WhichOneof("reminder") for reminder in reminders] == [ 

1119 "respond_to_host_request_reminder", 

1120 "complete_profile_reminder", 

1121 ] 

1122 assert reminders[0].respond_to_host_request_reminder.host_request_id == host_request3_id 

1123 assert reminders[0].respond_to_host_request_reminder.surfer_user.user_id == req_user1.id 

1124 

1125 

1126def test_confirm_host_request_reminder(db, moderator): 

1127 host, host_token = generate_user(complete_profile=True) 

1128 surfer, surfer_token = generate_user(complete_profile=True) 

1129 

1130 today_plus_10 = (today() + timedelta(days=10)).isoformat() 

1131 today_plus_12 = (today() + timedelta(days=12)).isoformat() 

1132 

1133 with requests_session(surfer_token) as api: 

1134 host_request_id = api.CreateHostRequest( 

1135 requests_pb2.CreateHostRequestReq( 

1136 host_user_id=host.id, 

1137 from_date=today_plus_10, 

1138 to_date=today_plus_12, 

1139 text=valid_request_text("Please host me"), 

1140 ) 

1141 ).host_request_id 

1142 moderator.approve_host_request(host_request_id) 

1143 

1144 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1145 with account_session(surfer_token) as account: 

1146 assert [reminder.WhichOneof("reminder") for reminder in account.GetReminders(empty_pb2.Empty()).reminders] == [] 

1147 

1148 with requests_session(host_token) as api: 

1149 api.RespondHostRequest( 

1150 requests_pb2.RespondHostRequestReq( 

1151 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1152 ) 

1153 ) 

1154 

1155 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1156 with account_session(surfer_token) as account: 

1157 reminders = account.GetReminders(empty_pb2.Empty()).reminders 

1158 assert [reminder.WhichOneof("reminder") for reminder in reminders] == ["confirm_host_request_reminder"] 

1159 assert reminders[0].confirm_host_request_reminder.host_request_id == host_request_id 

1160 assert reminders[0].confirm_host_request_reminder.host_user.user_id == host.id 

1161 

1162 # after surfer confirms, reminder should clear 

1163 with requests_session(surfer_token) as api: 

1164 api.RespondHostRequest( 

1165 requests_pb2.RespondHostRequestReq( 

1166 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1167 ) 

1168 ) 

1169 

1170 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1171 with account_session(surfer_token) as account: 

1172 assert [reminder.WhichOneof("reminder") for reminder in account.GetReminders(empty_pb2.Empty()).reminders] == [] 

1173 

1174 

1175def test_my_home_reminder(db): 

1176 # can_host with incomplete my home (max_guests not set) → reminder shown 

1177 can_host_incomplete, token1 = generate_user(hosting_status=HostingStatus.can_host) 

1178 # maybe with incomplete my home → reminder shown 

1179 maybe_incomplete, token2 = generate_user(hosting_status=HostingStatus.maybe) 

1180 # cant_host → no reminder regardless of my home completion 

1181 cant_host, token3 = generate_user(hosting_status=HostingStatus.cant_host) 

1182 # can_host with fully completed my home → no reminder 

1183 can_host_complete, token4 = generate_user( 

1184 hosting_status=HostingStatus.can_host, 

1185 max_guests=2, 

1186 sleeping_arrangement=SleepingArrangement.private, 

1187 # about_place is set by default in make_user 

1188 ) 

1189 

1190 with account_session(token1) as account: 

1191 assert [reminder.WhichOneof("reminder") for reminder in account.GetReminders(empty_pb2.Empty()).reminders] == [ 

1192 "complete_my_home_reminder", 

1193 ] 

1194 

1195 with account_session(token2) as account: 

1196 assert [reminder.WhichOneof("reminder") for reminder in account.GetReminders(empty_pb2.Empty()).reminders] == [ 

1197 "complete_my_home_reminder", 

1198 ] 

1199 

1200 with account_session(token3) as account: 

1201 assert [reminder.WhichOneof("reminder") for reminder in account.GetReminders(empty_pb2.Empty()).reminders] == [] 

1202 

1203 with account_session(token4) as account: 

1204 assert [reminder.WhichOneof("reminder") for reminder in account.GetReminders(empty_pb2.Empty()).reminders] == [] 

1205 

1206 

1207def test_volunteer_stuff(db): 

1208 # taken from couchers/app/backend/resources/badges.json 

1209 board_member_id = 8347 

1210 

1211 # with password 

1212 user, token = generate_user(name="Von Tester", username="tester", city="Amsterdam", id=board_member_id) 

1213 

1214 with account_session(token) as account: 

1215 res = account.GetAccountInfo(empty_pb2.Empty()) 

1216 assert not res.is_volunteer 

1217 

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

1219 account.GetMyVolunteerInfo(empty_pb2.Empty()) 

1220 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

1221 assert ( 

1222 e.value.details() == "You are currently not registered as a volunteer, if this is wrong, please contact us." 

1223 ) 

1224 

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

1226 account.UpdateMyVolunteerInfo(account_pb2.UpdateMyVolunteerInfoReq()) 

1227 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

1228 assert ( 

1229 e.value.details() == "You are currently not registered as a volunteer, if this is wrong, please contact us." 

1230 ) 

1231 

1232 with session_scope() as session: 

1233 session.add( 

1234 make_volunteer( 

1235 user_id=user.id, 

1236 display_name="Great Volunteer", 

1237 display_location="The Bitbucket", 

1238 role="Lead Tester", 

1239 started_volunteering=date(2020, 6, 1), 

1240 ) 

1241 ) 

1242 

1243 with account_session(token) as account: 

1244 res = account.GetAccountInfo(empty_pb2.Empty()) 

1245 assert res.is_volunteer 

1246 

1247 res = account.GetMyVolunteerInfo(empty_pb2.Empty()) 

1248 

1249 assert res.display_name == "Great Volunteer" 

1250 assert res.display_location == "The Bitbucket" 

1251 assert res.role == "Lead Tester" 

1252 assert res.started_volunteering == "2020-06-01" 

1253 assert not res.stopped_volunteering 

1254 assert res.show_on_team_page 

1255 assert res.link_type == "couchers" 

1256 assert res.link_text == "@tester" 

1257 assert res.link_url == "http://localhost:3000/user/tester" 

1258 

1259 res = account.UpdateMyVolunteerInfo( 

1260 account_pb2.UpdateMyVolunteerInfoReq( 

1261 display_name=wrappers_pb2.StringValue(value=""), 

1262 link_type=wrappers_pb2.StringValue(value="website"), 

1263 link_text=wrappers_pb2.StringValue(value="testervontester.com.invalid"), 

1264 link_url=wrappers_pb2.StringValue(value="https://www.testervontester.com.invalid/"), 

1265 ) 

1266 ) 

1267 

1268 assert res.display_name == "" 

1269 assert res.display_location == "The Bitbucket" 

1270 assert res.role == "Lead Tester" 

1271 assert res.started_volunteering == "2020-06-01" 

1272 assert not res.stopped_volunteering 

1273 assert res.show_on_team_page 

1274 assert res.link_type == "website" 

1275 assert res.link_text == "testervontester.com.invalid" 

1276 assert res.link_url == "https://www.testervontester.com.invalid/" 

1277 res = account.UpdateMyVolunteerInfo( 

1278 account_pb2.UpdateMyVolunteerInfoReq( 

1279 display_name=wrappers_pb2.StringValue(value=""), 

1280 link_type=wrappers_pb2.StringValue(value="linkedin"), 

1281 link_text=wrappers_pb2.StringValue(value="tester-vontester"), 

1282 ) 

1283 ) 

1284 assert res.display_name == "" 

1285 assert res.display_location == "The Bitbucket" 

1286 assert res.role == "Lead Tester" 

1287 assert res.started_volunteering == "2020-06-01" 

1288 assert not res.stopped_volunteering 

1289 assert res.show_on_team_page 

1290 assert res.link_type == "linkedin" 

1291 assert res.link_text == "tester-vontester" 

1292 assert res.link_url == "https://www.linkedin.com/in/tester-vontester/" 

1293 

1294 res = account.UpdateMyVolunteerInfo( 

1295 account_pb2.UpdateMyVolunteerInfoReq( 

1296 display_name=wrappers_pb2.StringValue(value="Tester"), 

1297 display_location=wrappers_pb2.StringValue(value=""), 

1298 link_type=wrappers_pb2.StringValue(value="email"), 

1299 link_text=wrappers_pb2.StringValue(value="tester@vontester.com.invalid"), 

1300 ) 

1301 ) 

1302 assert res.display_name == "Tester" 

1303 assert res.display_location == "" 

1304 assert res.role == "Lead Tester" 

1305 assert res.started_volunteering == "2020-06-01" 

1306 assert not res.stopped_volunteering 

1307 assert res.show_on_team_page 

1308 assert res.link_type == "email" 

1309 assert res.link_text == "tester@vontester.com.invalid" 

1310 assert res.link_url == "mailto:tester@vontester.com.invalid" 

1311 

1312 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1313 

1314 with public_session() as public: 

1315 res = public.GetVolunteers(empty_pb2.Empty()) 

1316 assert len(res.current_volunteers) == 1 

1317 v = res.current_volunteers[0] 

1318 assert v.name == "Tester" 

1319 assert v.username == "tester" 

1320 assert v.is_board_member 

1321 assert v.role == "Lead Tester" 

1322 assert v.location == "Amsterdam" 

1323 assert v.img.startswith("http://localhost:5001/img/thumbnail/") 

1324 assert v.link_type == "email" 

1325 assert v.link_text == "tester@vontester.com.invalid" 

1326 assert v.link_url == "mailto:tester@vontester.com.invalid"