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

737 statements  

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

40def test_GetAccountInfo(db, fast_passwords): 

41 # with password 

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

43 

44 with account_session(token1) as account: 

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

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

47 assert res.username == user1.username 

48 assert not res.has_strong_verification 

49 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_UNVERIFIED 

50 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_UNVERIFIED 

51 assert not res.is_superuser 

52 assert res.ui_language_preference == "" 

53 assert not res.is_volunteer 

54 

55 

56def test_donation_banner_no_drive(db): 

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

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

59 user, token = generate_user() 

60 

61 with account_session(token) as account: 

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

63 assert not res.should_show_donation_banner 

64 

65 

66def test_donation_banner_never_donated(db, feature_flags): 

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

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

69 user, token = generate_user(last_donated=None) 

70 

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

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

73 with account_session(token) as account: 

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

75 assert res.should_show_donation_banner 

76 

77 

78def test_donation_banner_donated_before_drive(db, feature_flags): 

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

80 user, token = generate_user() 

81 

82 # Set donation before drive start 

83 with session_scope() as session: 

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

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

86 

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

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

89 with account_session(token) as account: 

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

91 assert res.should_show_donation_banner 

92 

93 

94def test_donation_banner_donated_after_drive(db, feature_flags): 

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

96 user, token = generate_user() 

97 

98 # Set donation after drive start 

99 with session_scope() as session: 

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

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

102 

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

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

105 with account_session(token) as account: 

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

107 assert not res.should_show_donation_banner 

108 

109 

110def test_donation_banner_donated_exactly_at_drive_start(db, feature_flags): 

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

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

113 

114 user, token = generate_user() 

115 

116 # Set donation exactly at drive start 

117 with session_scope() as session: 

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

119 

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

121 with account_session(token) as account: 

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

123 assert not res.should_show_donation_banner 

124 

125 

126def test_GetAccountInfo_regression(db): 

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

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

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

130 

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

132 with session_scope() as session: 

133 key = random_hex(32) 

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

135 session.add( 

136 Upload( 

137 key=key, 

138 filename=filename, 

139 creator_user_id=user.id, 

140 ) 

141 ) 

142 session.flush() 

143 assert user.profile_gallery_id is not None 

144 session.add( 

145 PhotoGalleryItem( 

146 gallery_id=user.profile_gallery_id, 

147 upload_key=key, 

148 position=0, 

149 ) 

150 ) 

151 

152 with account_session(token) as account: 

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

154 

155 

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

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

158 old_password = random_hex() 

159 new_password = random_hex() 

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

161 

162 with account_session(token) as account: 

163 account.ChangePasswordV2( 

164 account_pb2.ChangePasswordV2Req( 

165 old_password=old_password, 

166 new_password=new_password, 

167 ) 

168 ) 

169 

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

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

172 

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

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

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

176 

177 with session_scope() as session: 

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

179 assert updated_user.hashed_password == hash_password(new_password) 

180 

181 

182def test_ChangePasswordV2_regression(db, fast_passwords): 

183 # send_password_changed_email wasn't working 

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

185 old_password = random_hex() 

186 new_password = random_hex() 

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

188 

189 with account_session(token) as account: 

190 account.ChangePasswordV2( 

191 account_pb2.ChangePasswordV2Req( 

192 old_password=old_password, 

193 new_password=new_password, 

194 ) 

195 ) 

196 

197 with session_scope() as session: 

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

199 assert updated_user.hashed_password == hash_password(new_password) 

200 

201 

202def test_ChangePasswordV2_normal_short_password(db, fast_passwords): 

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

204 old_password = random_hex() 

205 new_password = random_hex(length=1) 

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

207 

208 with account_session(token) as account: 

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

210 account.ChangePasswordV2( 

211 account_pb2.ChangePasswordV2Req( 

212 old_password=old_password, 

213 new_password=new_password, 

214 ) 

215 ) 

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

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

218 

219 with session_scope() as session: 

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

221 assert updated_user.hashed_password == hash_password(old_password) 

222 

223 

224def test_ChangePasswordV2_normal_long_password(db, fast_passwords): 

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

226 old_password = random_hex() 

227 new_password = random_hex(length=1000) 

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

229 

230 with account_session(token) as account: 

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

232 account.ChangePasswordV2( 

233 account_pb2.ChangePasswordV2Req( 

234 old_password=old_password, 

235 new_password=new_password, 

236 ) 

237 ) 

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

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

240 

241 with session_scope() as session: 

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

243 assert updated_user.hashed_password == hash_password(old_password) 

244 

245 

246def test_ChangePasswordV2_normal_insecure_password(db, fast_passwords): 

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

248 old_password = random_hex() 

249 new_password = "12345678" 

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

251 

252 with account_session(token) as account: 

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

254 account.ChangePasswordV2( 

255 account_pb2.ChangePasswordV2Req( 

256 old_password=old_password, 

257 new_password=new_password, 

258 ) 

259 ) 

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

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

262 

263 with session_scope() as session: 

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

265 assert updated_user.hashed_password == hash_password(old_password) 

266 

267 

268def test_ChangePasswordV2_normal_wrong_password(db, fast_passwords): 

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

270 old_password = random_hex() 

271 new_password = random_hex() 

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

273 

274 with account_session(token) as account: 

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

276 account.ChangePasswordV2( 

277 account_pb2.ChangePasswordV2Req( 

278 old_password="Wrong password", 

279 new_password=new_password, 

280 ) 

281 ) 

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

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

284 

285 with session_scope() as session: 

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

287 assert updated_user.hashed_password == hash_password(old_password) 

288 

289 

290def test_ChangePasswordV2_normal_no_passwords(db, fast_passwords): 

291 # user has old password and called with empty body 

292 old_password = random_hex() 

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

294 

295 with account_session(token) as account: 

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

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

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

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

300 

301 with session_scope() as session: 

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

303 assert updated_user.hashed_password == hash_password(old_password) 

304 

305 

306def test_ChangeEmailV2_wrong_password(db, fast_passwords): 

307 password = random_hex() 

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

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

310 

311 with account_session(token) as account: 

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

313 account.ChangeEmailV2( 

314 account_pb2.ChangeEmailV2Req( 

315 password="Wrong password", 

316 new_email=new_email, 

317 ) 

318 ) 

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

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

321 

322 with session_scope() as session: 

323 assert ( 

324 session.execute( 

325 select(func.count()) 

326 .select_from(User) 

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

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

329 ) 

330 ).scalar_one() == 0 

331 

332 

333def test_ChangeEmailV2_wrong_email(db, fast_passwords): 

334 password = random_hex() 

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

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

337 

338 with account_session(token) as account: 

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

340 account.ChangeEmailV2( 

341 account_pb2.ChangeEmailV2Req( 

342 password="Wrong password", 

343 new_email=new_email, 

344 ) 

345 ) 

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

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

348 

349 with session_scope() as session: 

350 assert ( 

351 session.execute( 

352 select(func.count()) 

353 .select_from(User) 

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

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

356 ) 

357 ).scalar_one() == 0 

358 

359 

360def test_ChangeEmailV2_invalid_email(db, fast_passwords): 

361 password = random_hex() 

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

363 

364 with account_session(token) as account: 

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

366 account.ChangeEmailV2( 

367 account_pb2.ChangeEmailV2Req( 

368 password=password, 

369 new_email="not a real email", 

370 ) 

371 ) 

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

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

374 

375 with session_scope() as session: 

376 assert ( 

377 session.execute( 

378 select(func.count()) 

379 .select_from(User) 

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

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

382 ) 

383 ).scalar_one() == 0 

384 

385 

386def test_ChangeEmailV2_email_in_use(db, fast_passwords): 

387 password = random_hex() 

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

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

390 

391 with account_session(token) as account: 

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

393 account.ChangeEmailV2( 

394 account_pb2.ChangeEmailV2Req( 

395 password=password, 

396 new_email=user2.email, 

397 ) 

398 ) 

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

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

401 

402 with session_scope() as session: 

403 assert ( 

404 session.execute( 

405 select(func.count()) 

406 .select_from(User) 

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

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

409 ) 

410 ).scalar_one() == 0 

411 

412 

413def test_ChangeEmailV2_no_change(db, fast_passwords): 

414 password = random_hex() 

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

416 

417 with account_session(token) as account: 

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

419 account.ChangeEmailV2( 

420 account_pb2.ChangeEmailV2Req( 

421 password=password, 

422 new_email=user.email, 

423 ) 

424 ) 

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

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

427 

428 with session_scope() as session: 

429 assert ( 

430 session.execute( 

431 select(func.count()) 

432 .select_from(User) 

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

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

435 ) 

436 ).scalar_one() == 0 

437 

438 

439def test_ChangeEmailV2_wrong_token(db, fast_passwords): 

440 password = random_hex() 

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

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

443 

444 with account_session(token) as account: 

445 account.ChangeEmailV2( 

446 account_pb2.ChangeEmailV2Req( 

447 password=password, 

448 new_email=new_email, 

449 ) 

450 ) 

451 

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

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

454 res = auth_api.ConfirmChangeEmailV2( 

455 auth_pb2.ConfirmChangeEmailV2Req( 

456 change_email_token="wrongtoken", 

457 ) 

458 ) 

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

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

461 

462 with session_scope() as session: 

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

464 assert user_updated.email == user.email 

465 

466 

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

468 password = random_hex() 

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

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

471 

472 with account_session(token) as account: 

473 account.ChangeEmailV2( 

474 account_pb2.ChangeEmailV2Req( 

475 password=password, 

476 new_email=new_email, 

477 ) 

478 ) 

479 

480 with session_scope() as session: 

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

482 

483 # before the token was issued 

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

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

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

487 auth_api.ConfirmChangeEmailV2(auth_pb2.ConfirmChangeEmailV2Req()) 

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

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

490 

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

492 auth_api.ConfirmChangeEmailV2( 

493 auth_pb2.ConfirmChangeEmailV2Req( 

494 change_email_token=new_email_token, 

495 ) 

496 ) 

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

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

499 

500 # and a minute after the two hour window closed 

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

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

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

504 auth_api.ConfirmChangeEmailV2(auth_pb2.ConfirmChangeEmailV2Req()) 

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

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

507 

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

509 auth_api.ConfirmChangeEmailV2( 

510 auth_pb2.ConfirmChangeEmailV2Req( 

511 change_email_token=new_email_token, 

512 ) 

513 ) 

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

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

516 

517 

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

519 password = random_hex() 

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

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

522 user_id = user.id 

523 

524 with account_session(token) as account: 

525 account.ChangeEmailV2( 

526 account_pb2.ChangeEmailV2Req( 

527 password=password, 

528 new_email=new_email, 

529 ) 

530 ) 

531 

532 with session_scope() as session: 

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

534 assert user_updated.email == user.email 

535 assert user_updated.new_email == new_email 

536 assert user_updated.new_email_token is not None 

537 assert user_updated.new_email_token_created 

538 assert user_updated.new_email_token_created <= now() 

539 assert user_updated.new_email_token_expiry 

540 assert user_updated.new_email_token_expiry >= now() 

541 

542 token = user_updated.new_email_token 

543 

544 process_jobs() 

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

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

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

548 

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

550 auth_api.ConfirmChangeEmailV2( 

551 auth_pb2.ConfirmChangeEmailV2Req( 

552 change_email_token=token, 

553 ) 

554 ) 

555 

556 with session_scope() as session: 

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

558 assert user.email == new_email 

559 assert user.new_email is None 

560 assert user.new_email_token is None 

561 assert user.new_email_token_created is None 

562 assert user.new_email_token_expiry is None 

563 

564 process_jobs() 

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

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

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

568 

569 

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

571 password = random_hex() 

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

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

574 

575 with account_session(token) as account: 

576 account.ChangeEmailV2( 

577 account_pb2.ChangeEmailV2Req( 

578 password=password, 

579 new_email=new_email, 

580 ) 

581 ) 

582 

583 process_jobs() 

584 

585 with session_scope() as session: 

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

587 assert len(jobs) == 2 

588 uq_str1 = b"Email address change initiated" 

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

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

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

592 ) 

593 

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

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

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

597 

598 

599def test_ChangeLanguagePreference(db, fast_passwords): 

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

601 new_lang = "zh" 

602 user, token = generate_user() 

603 

604 with real_account_session(token) as account: 

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

606 assert res.ui_language_preference == "" 

607 

608 # call will have info about the request 

609 res, call = account.ChangeLanguagePreference.with_call( 

610 account_pb2.ChangeLanguagePreferenceReq(ui_language_preference=new_lang) 

611 ) 

612 

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

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

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

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

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

618 ) 

619 

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

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

622 assert res.ui_language_preference == "zh" 

623 

624 

625def test_contributor_form(db): 

626 user, token = generate_user() 

627 

628 with account_session(token) as account: 

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

630 assert not res.filled_contributor_form 

631 

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

633 

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

635 assert res.filled_contributor_form 

636 

637 

638def test_DeleteAccount_start(db, email_collector: EmailCollector): 

639 user, token = generate_user() 

640 

641 with account_session(token) as account: 

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

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

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

645 

646 with session_scope() as session: 

647 deletion_token: AccountDeletionToken = session.execute( 

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

649 ).scalar_one() 

650 

651 assert deletion_token.is_valid 

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

653 

654 

655def test_DeleteAccount_message_storage(db): 

656 user, token = generate_user() 

657 

658 with account_session(token) as account: 

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

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

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

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

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

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

665 

666 with session_scope() as session: 

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

668 

669 

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

671 user, token = generate_user() 

672 user_id = user.id 

673 

674 with account_session(token) as account: 

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

676 account.DeleteAccount(account_pb2.DeleteAccountReq()) 

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

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

679 

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

681 

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

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

684 assert email.recipient == user.email 

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

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

687 assert unique_string in email.plain 

688 assert unique_string in email.html 

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

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

691 

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

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

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

695 

696 with session_scope() as session: 

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

698 delete_token = token_o.token 

699 

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

701 assert token_o.user == user_ 

702 assert user_.deleted_at is None 

703 assert not user_.undelete_token 

704 assert not user_.undelete_until 

705 

706 assert delete_token in email.plain 

707 assert delete_token in email.html 

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

709 assert delete_url in email.plain 

710 assert delete_url in email.html 

711 

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

713 auth_api.ConfirmDeleteAccount( 

714 auth_pb2.ConfirmDeleteAccountReq( 

715 token=delete_token, 

716 ) 

717 ) 

718 

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

720 assert email.recipient == user.email 

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

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

723 assert unique_string in email.plain 

724 assert unique_string in email.html 

725 assert "7 days" in email.plain 

726 assert "7 days" in email.html 

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

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

729 

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

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

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

733 

734 with session_scope() as session: 

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

736 

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

738 assert user_.deleted_at is not None 

739 assert user_.undelete_token 

740 assert user_.undelete_until 

741 assert user_.undelete_until > now() 

742 

743 undelete_token = user_.undelete_token 

744 

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

746 assert undelete_url in email.plain 

747 assert undelete_url in email.html 

748 

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

750 auth_api.RecoverAccount( 

751 auth_pb2.RecoverAccountReq( 

752 token=undelete_token, 

753 ) 

754 ) 

755 

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

757 assert email.recipient == user.email 

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

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

760 assert unique_string in email.plain 

761 assert unique_string in email.html 

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

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

764 

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

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

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

768 

769 with session_scope() as session: 

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

771 

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

773 assert user.deleted_at is None 

774 assert not user.undelete_token 

775 assert not user.undelete_until 

776 

777 

778def test_multiple_delete_tokens(db): 

779 """ 

780 Make sure deletion tokens are deleted on delete 

781 """ 

782 user, token = generate_user() 

783 

784 with account_session(token) as account: 

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

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

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

788 

789 with session_scope() as session: 

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

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

792 

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

794 auth_api.ConfirmDeleteAccount( 

795 auth_pb2.ConfirmDeleteAccountReq( 

796 token=token, 

797 ) 

798 ) 

799 

800 with session_scope() as session: 

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

802 

803 

804def test_ListActiveSessions_pagination(db, fast_passwords): 

805 password = random_hex() 

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

807 

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

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

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

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

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

813 

814 with real_account_session(token) as account: 

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

816 assert len(res.active_sessions) == 3 

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

818 assert len(res.active_sessions) == 2 

819 assert not res.next_page_token 

820 

821 

822def test_ListActiveSessions_details(db, fast_passwords): 

823 password = random_hex() 

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

825 

826 ips_user_agents = [ 

827 ( 

828 "108.123.33.162", 

829 "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", 

830 ), 

831 ( 

832 "8.245.212.28", 

833 "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", 

834 ), 

835 ( 

836 "95.254.140.156", 

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

838 ), 

839 ] 

840 

841 for ip, user_agent in ips_user_agents: 

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

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

844 auth_api.Authenticate( 

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

846 ) 

847 

848 def dummy_geoip(ip_address): 

849 return { 

850 "108.123.33.162": "Chicago, United States", 

851 "8.245.212.28": "Sydney, Australia", 

852 }.get(ip_address) 

853 

854 with real_account_session(token) as account: 

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

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

857 print(res) 

858 assert len(res.active_sessions) == 4 

859 

860 # this one currently making the API call 

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

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

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

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

865 assert res.active_sessions[0].is_current_session 

866 

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

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

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

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

871 assert not res.active_sessions[1].is_current_session 

872 

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

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

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

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

877 assert not res.active_sessions[2].is_current_session 

878 

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

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

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

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

883 assert not res.active_sessions[3].is_current_session 

884 

885 

886def test_LogOutSession(db, fast_passwords): 

887 password = random_hex() 

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

889 

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

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

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

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

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

895 

896 with real_account_session(token) as account: 

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

898 assert len(res.active_sessions) == 5 

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

900 

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

902 assert len(res2.active_sessions) == 4 

903 

904 # ignore the first session as it changes 

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

906 

907 

908def test_LogOutOtherSessions(db, fast_passwords): 

909 password = random_hex() 

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

911 

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

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

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

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

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

917 

918 with real_account_session(token) as account: 

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

920 assert len(res.active_sessions) == 5 

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

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

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

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

925 

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

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

928 assert len(res.active_sessions) == 1 

929 

930 

931def test_CreateInviteCode(db): 

932 user, token = generate_user() 

933 

934 with account_session(token) as account: 

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

936 code = res.code 

937 assert len(code) == 8 

938 

939 with session_scope() as session: 

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

941 assert invite.creator_user_id == user.id 

942 assert invite.disabled is None 

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

944 

945 

946def test_DisableInviteCode(db): 

947 user, token = generate_user() 

948 

949 with account_session(token) as account: 

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

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

952 

953 with session_scope() as session: 

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

955 assert invite.disabled is not None 

956 

957 

958def test_ListInviteCodes(db): 

959 user, token = generate_user() 

960 another_user, _ = generate_user() 

961 

962 with account_session(token) as account: 

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

964 

965 # simulate another_user having signed up with this invite code 

966 with session_scope() as session: 

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

968 

969 with account_session(token) as account: 

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

971 assert len(res.invite_codes) == 1 

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

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

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

975 

976 

977def test_reminders(db, moderator): 

978 # reference writing reminders tested in test_AvailableWriteReferences_and_ListPendingReferencesToWrite 

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

980 user, token = generate_user(complete_profile=False) 

981 complete_user, complete_token = generate_user(complete_profile=True) 

982 req_user1, req_user_token1 = generate_user(complete_profile=True) 

983 req_user2, req_user_token2 = generate_user(complete_profile=True) 

984 

985 refresh_materialized_views_rapid(empty_pb2.Empty()) 

986 with account_session(complete_token) as account: 

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

988 with account_session(token) as account: 

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

990 "complete_profile_reminder", 

991 ] 

992 

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

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

995 with requests_session(req_user_token1) as api: 

996 host_request1_id = api.CreateHostRequest( 

997 requests_pb2.CreateHostRequestReq( 

998 host_user_id=user.id, 

999 from_date=today_plus_2, 

1000 to_date=today_plus_3, 

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

1002 ) 

1003 ).host_request_id 

1004 moderator.approve_host_request(host_request1_id) 

1005 

1006 with account_session(token) as account: 

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

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

1009 "respond_to_host_request_reminder", 

1010 "complete_profile_reminder", 

1011 ] 

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

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

1014 

1015 with requests_session(req_user_token2) as api: 

1016 host_request2_id = api.CreateHostRequest( 

1017 requests_pb2.CreateHostRequestReq( 

1018 host_user_id=user.id, 

1019 from_date=today_plus_2, 

1020 to_date=today_plus_3, 

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

1022 ) 

1023 ).host_request_id 

1024 moderator.approve_host_request(host_request2_id) 

1025 

1026 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1027 with account_session(token) as account: 

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

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

1030 "respond_to_host_request_reminder", 

1031 "respond_to_host_request_reminder", 

1032 "complete_profile_reminder", 

1033 ] 

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

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

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

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

1038 

1039 backdate_conversations() 

1040 with requests_session(req_user_token1) as api: 

1041 host_request3_id = api.CreateHostRequest( 

1042 requests_pb2.CreateHostRequestReq( 

1043 host_user_id=user.id, 

1044 from_date=today_plus_2, 

1045 to_date=today_plus_3, 

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

1047 ) 

1048 ).host_request_id 

1049 moderator.approve_host_request(host_request3_id) 

1050 

1051 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1052 with account_session(token) as account: 

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

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

1055 "respond_to_host_request_reminder", 

1056 "respond_to_host_request_reminder", 

1057 "respond_to_host_request_reminder", 

1058 "complete_profile_reminder", 

1059 ] 

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

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

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

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

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

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

1066 

1067 # accept req 

1068 with requests_session(token) as api: 

1069 api.RespondHostRequest( 

1070 requests_pb2.RespondHostRequestReq( 

1071 host_request_id=host_request1_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1072 ) 

1073 ) 

1074 

1075 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1076 with account_session(token) as account: 

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

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

1079 "respond_to_host_request_reminder", 

1080 "respond_to_host_request_reminder", 

1081 "complete_profile_reminder", 

1082 ] 

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

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

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

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

1087 

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

1089 with requests_session(token) as api: 

1090 api.SendHostRequestMessage( 

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

1092 ) 

1093 

1094 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1095 with account_session(token) as account: 

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

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

1098 "respond_to_host_request_reminder", 

1099 "complete_profile_reminder", 

1100 ] 

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

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

1103 

1104 # surfer sending a message should not clear the reminder 

1105 with requests_session(req_user_token1) as api: 

1106 api.SendHostRequestMessage( 

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

1108 ) 

1109 

1110 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1111 with account_session(token) as account: 

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

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

1114 "respond_to_host_request_reminder", 

1115 "complete_profile_reminder", 

1116 ] 

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

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

1119 

1120 

1121def test_confirm_host_request_reminder(db, moderator): 

1122 host, host_token = generate_user(complete_profile=True) 

1123 surfer, surfer_token = generate_user(complete_profile=True) 

1124 

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

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

1127 

1128 with requests_session(surfer_token) as api: 

1129 host_request_id = api.CreateHostRequest( 

1130 requests_pb2.CreateHostRequestReq( 

1131 host_user_id=host.id, 

1132 from_date=today_plus_10, 

1133 to_date=today_plus_12, 

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

1135 ) 

1136 ).host_request_id 

1137 moderator.approve_host_request(host_request_id) 

1138 

1139 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1140 with account_session(surfer_token) as account: 

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

1142 

1143 with requests_session(host_token) as api: 

1144 api.RespondHostRequest( 

1145 requests_pb2.RespondHostRequestReq( 

1146 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

1147 ) 

1148 ) 

1149 

1150 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1151 with account_session(surfer_token) as account: 

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

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

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

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

1156 

1157 # after surfer confirms, reminder should clear 

1158 with requests_session(surfer_token) as api: 

1159 api.RespondHostRequest( 

1160 requests_pb2.RespondHostRequestReq( 

1161 host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

1162 ) 

1163 ) 

1164 

1165 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1166 with account_session(surfer_token) as account: 

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

1168 

1169 

1170def test_my_home_reminder(db): 

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

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

1173 # maybe with incomplete my home → reminder shown 

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

1175 # cant_host → no reminder regardless of my home completion 

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

1177 # can_host with fully completed my home → no reminder 

1178 can_host_complete, token4 = generate_user( 

1179 hosting_status=HostingStatus.can_host, 

1180 max_guests=2, 

1181 sleeping_arrangement=SleepingArrangement.private, 

1182 # about_place is set by default in make_user 

1183 ) 

1184 

1185 with account_session(token1) as account: 

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

1187 "complete_my_home_reminder", 

1188 ] 

1189 

1190 with account_session(token2) 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(token3) as account: 

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

1197 

1198 with account_session(token4) as account: 

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

1200 

1201 

1202def test_volunteer_stuff(db): 

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

1204 board_member_id = 8347 

1205 

1206 # with password 

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

1208 

1209 with account_session(token) as account: 

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

1211 assert not res.is_volunteer 

1212 

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

1214 account.GetMyVolunteerInfo(empty_pb2.Empty()) 

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

1216 assert ( 

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

1218 ) 

1219 

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

1221 account.UpdateMyVolunteerInfo(account_pb2.UpdateMyVolunteerInfoReq()) 

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

1223 assert ( 

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

1225 ) 

1226 

1227 with session_scope() as session: 

1228 session.add( 

1229 make_volunteer( 

1230 user_id=user.id, 

1231 display_name="Great Volunteer", 

1232 display_location="The Bitbucket", 

1233 role="Lead Tester", 

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

1235 ) 

1236 ) 

1237 

1238 with account_session(token) as account: 

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

1240 assert res.is_volunteer 

1241 

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

1243 

1244 assert res.display_name == "Great Volunteer" 

1245 assert res.display_location == "The Bitbucket" 

1246 assert res.role == "Lead Tester" 

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

1248 assert not res.stopped_volunteering 

1249 assert res.show_on_team_page 

1250 assert res.link_type == "couchers" 

1251 assert res.link_text == "@tester" 

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

1253 

1254 res = account.UpdateMyVolunteerInfo( 

1255 account_pb2.UpdateMyVolunteerInfoReq( 

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

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

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

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

1260 ) 

1261 ) 

1262 

1263 assert res.display_name == "" 

1264 assert res.display_location == "The Bitbucket" 

1265 assert res.role == "Lead Tester" 

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

1267 assert not res.stopped_volunteering 

1268 assert res.show_on_team_page 

1269 assert res.link_type == "website" 

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

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

1272 res = account.UpdateMyVolunteerInfo( 

1273 account_pb2.UpdateMyVolunteerInfoReq( 

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

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

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

1277 ) 

1278 ) 

1279 assert res.display_name == "" 

1280 assert res.display_location == "The Bitbucket" 

1281 assert res.role == "Lead Tester" 

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

1283 assert not res.stopped_volunteering 

1284 assert res.show_on_team_page 

1285 assert res.link_type == "linkedin" 

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

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

1288 

1289 res = account.UpdateMyVolunteerInfo( 

1290 account_pb2.UpdateMyVolunteerInfoReq( 

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

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

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

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

1295 ) 

1296 ) 

1297 assert res.display_name == "Tester" 

1298 assert res.display_location == "" 

1299 assert res.role == "Lead Tester" 

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

1301 assert not res.stopped_volunteering 

1302 assert res.show_on_team_page 

1303 assert res.link_type == "email" 

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

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

1306 

1307 refresh_materialized_views_rapid(empty_pb2.Empty()) 

1308 

1309 with public_session() as public: 

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

1311 assert len(res.current_volunteers) == 1 

1312 v = res.current_volunteers[0] 

1313 assert v.name == "Tester" 

1314 assert v.username == "tester" 

1315 assert v.is_board_member 

1316 assert v.role == "Lead Tester" 

1317 assert v.location == "Amsterdam" 

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

1319 assert v.link_type == "email" 

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

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