Coverage for src/tests/test_auth.py: 100%

453 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-07-22 17:19 +0000

1import http.cookies 

2 

3import grpc 

4import pytest 

5from google.protobuf import empty_pb2, wrappers_pb2 

6from sqlalchemy.sql import delete, func 

7 

8from couchers import errors 

9from couchers.crypto import hash_password, random_hex 

10from couchers.db import session_scope 

11from couchers.models import ( 

12 ContributeOption, 

13 ContributorForm, 

14 LoginToken, 

15 PasswordResetToken, 

16 SignupFlow, 

17 User, 

18 UserSession, 

19) 

20from couchers.sql import couchers_select as select 

21from proto import api_pb2, auth_pb2 

22from tests.test_fixtures import ( # noqa 

23 api_session, 

24 auth_api_session, 

25 db, 

26 email_fields, 

27 fast_passwords, 

28 generate_user, 

29 mock_notification_email, 

30 push_collector, 

31 real_api_session, 

32 testconfig, 

33) 

34 

35 

36@pytest.fixture(autouse=True) 

37def _(testconfig, fast_passwords): 

38 pass 

39 

40 

41def get_session_cookie_token(metadata_interceptor): 

42 return http.cookies.SimpleCookie(metadata_interceptor.latest_headers["set-cookie"])["couchers-sesh"].value 

43 

44 

45def test_UsernameValid(db): 

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

47 assert auth_api.UsernameValid(auth_pb2.UsernameValidReq(username="test")).valid 

48 

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

50 assert not auth_api.UsernameValid(auth_pb2.UsernameValidReq(username="")).valid 

51 

52 

53def test_signup_incremental(db): 

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

55 res = auth_api.SignupFlow( 

56 auth_pb2.SignupFlowReq( 

57 basic=auth_pb2.SignupBasic(name="testing", email="email@couchers.org.invalid"), 

58 ) 

59 ) 

60 

61 flow_token = res.flow_token 

62 assert res.flow_token 

63 assert not res.HasField("auth_res") 

64 assert not res.need_basic 

65 assert res.need_account 

66 assert res.need_feedback 

67 assert res.need_verify_email 

68 assert res.need_accept_community_guidelines 

69 

70 # read out the signup token directly from the database for now 

71 with session_scope() as session: 

72 flow = session.execute(select(SignupFlow).where(SignupFlow.flow_token == flow_token)).scalar_one() 

73 assert flow.email_sent 

74 assert not flow.email_verified 

75 email_token = flow.email_token 

76 

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

78 res = auth_api.SignupFlow(auth_pb2.SignupFlowReq(flow_token=flow_token)) 

79 

80 assert res.flow_token == flow_token 

81 assert not res.HasField("auth_res") 

82 assert not res.need_basic 

83 assert res.need_account 

84 assert res.need_feedback 

85 assert res.need_verify_email 

86 assert res.need_accept_community_guidelines 

87 

88 # Add feedback 

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

90 res = auth_api.SignupFlow( 

91 auth_pb2.SignupFlowReq( 

92 flow_token=flow_token, 

93 feedback=auth_pb2.ContributorForm( 

94 ideas="I'm a robot, incapable of original ideation", 

95 features="I love all your features", 

96 experience="I haven't done couch surfing before", 

97 contribute=auth_pb2.CONTRIBUTE_OPTION_YES, 

98 contribute_ways=["serving", "backend"], 

99 expertise="I'd love to be your server: I can compute very fast, but only simple opcodes", 

100 ), 

101 ) 

102 ) 

103 

104 assert res.flow_token == flow_token 

105 assert not res.HasField("auth_res") 

106 assert not res.need_basic 

107 assert res.need_account 

108 assert not res.need_feedback 

109 assert res.need_verify_email 

110 assert res.need_accept_community_guidelines 

111 

112 # Agree to community guidelines 

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

114 res = auth_api.SignupFlow( 

115 auth_pb2.SignupFlowReq( 

116 flow_token=flow_token, 

117 accept_community_guidelines=wrappers_pb2.BoolValue(value=True), 

118 ) 

119 ) 

120 

121 assert res.flow_token == flow_token 

122 assert not res.HasField("auth_res") 

123 assert not res.need_basic 

124 assert res.need_account 

125 assert not res.need_feedback 

126 assert res.need_verify_email 

127 assert not res.need_accept_community_guidelines 

128 

129 # Verify email 

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

131 res = auth_api.SignupFlow( 

132 auth_pb2.SignupFlowReq( 

133 flow_token=flow_token, 

134 email_token=email_token, 

135 ) 

136 ) 

137 

138 assert res.flow_token == flow_token 

139 assert not res.HasField("auth_res") 

140 assert not res.need_basic 

141 assert res.need_account 

142 assert not res.need_feedback 

143 assert not res.need_verify_email 

144 assert not res.need_accept_community_guidelines 

145 

146 # Finally finish off account info 

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

148 res = auth_api.SignupFlow( 

149 auth_pb2.SignupFlowReq( 

150 flow_token=flow_token, 

151 account=auth_pb2.SignupAccount( 

152 username="frodo", 

153 password="a very insecure password", 

154 birthdate="1970-01-01", 

155 gender="Bot", 

156 hosting_status=api_pb2.HOSTING_STATUS_MAYBE, 

157 city="New York City", 

158 lat=40.7331, 

159 lng=-73.9778, 

160 radius=500, 

161 accept_tos=True, 

162 ), 

163 ) 

164 ) 

165 

166 assert not res.flow_token 

167 assert res.HasField("auth_res") 

168 assert res.auth_res.user_id 

169 assert not res.auth_res.jailed 

170 assert not res.need_basic 

171 assert not res.need_account 

172 assert not res.need_feedback 

173 assert not res.need_verify_email 

174 assert not res.need_accept_community_guidelines 

175 

176 user_id = res.auth_res.user_id 

177 

178 sess_token = get_session_cookie_token(metadata_interceptor) 

179 

180 with api_session(sess_token) as api: 

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

182 

183 assert res.username == "frodo" 

184 assert res.gender == "Bot" 

185 assert res.hosting_status == api_pb2.HOSTING_STATUS_MAYBE 

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

187 assert res.lat == 40.7331 

188 assert res.lng == -73.9778 

189 assert res.radius == 500 

190 

191 with session_scope() as session: 

192 form = session.execute(select(ContributorForm)).scalar_one() 

193 

194 assert form.ideas == "I'm a robot, incapable of original ideation" 

195 assert form.features == "I love all your features" 

196 assert form.experience == "I haven't done couch surfing before" 

197 assert form.contribute == ContributeOption.yes 

198 assert form.contribute_ways == ["serving", "backend"] 

199 assert form.expertise == "I'd love to be your server: I can compute very fast, but only simple opcodes" 

200 

201 

202def _quick_signup(): 

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

204 res = auth_api.SignupFlow( 

205 auth_pb2.SignupFlowReq( 

206 basic=auth_pb2.SignupBasic(name="testing", email="email@couchers.org.invalid"), 

207 account=auth_pb2.SignupAccount( 

208 username="frodo", 

209 password="a very insecure password", 

210 birthdate="1970-01-01", 

211 gender="Bot", 

212 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

213 city="New York City", 

214 lat=40.7331, 

215 lng=-73.9778, 

216 radius=500, 

217 accept_tos=True, 

218 ), 

219 feedback=auth_pb2.ContributorForm(), 

220 accept_community_guidelines=wrappers_pb2.BoolValue(value=True), 

221 ) 

222 ) 

223 

224 flow_token = res.flow_token 

225 

226 assert res.flow_token 

227 assert not res.HasField("auth_res") 

228 assert not res.need_basic 

229 assert not res.need_account 

230 assert not res.need_feedback 

231 assert res.need_verify_email 

232 

233 # read out the signup token directly from the database for now 

234 with session_scope() as session: 

235 flow = session.execute(select(SignupFlow).where(SignupFlow.flow_token == flow_token)).scalar_one() 

236 assert flow.email_sent 

237 assert not flow.email_verified 

238 email_token = flow.email_token 

239 

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

241 res = auth_api.SignupFlow(auth_pb2.SignupFlowReq(email_token=email_token)) 

242 

243 assert not res.flow_token 

244 assert res.HasField("auth_res") 

245 assert res.auth_res.user_id 

246 assert not res.auth_res.jailed 

247 assert not res.need_basic 

248 assert not res.need_account 

249 assert not res.need_feedback 

250 assert not res.need_verify_email 

251 

252 # make sure we got the right token in a cookie 

253 with session_scope() as session: 

254 token = ( 

255 session.execute( 

256 select(UserSession).join(User, UserSession.user_id == User.id).where(User.username == "frodo") 

257 ).scalar_one() 

258 ).token 

259 assert get_session_cookie_token(metadata_interceptor) == token 

260 

261 

262def test_signup(db): 

263 _quick_signup() 

264 

265 

266def test_basic_login(db): 

267 # Create our test user using signup 

268 _quick_signup() 

269 

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

271 auth_api.Authenticate(auth_pb2.AuthReq(user="frodo", password="a very insecure password")) 

272 

273 reply_token = get_session_cookie_token(metadata_interceptor) 

274 

275 with session_scope() as session: 

276 token = ( 

277 session.execute( 

278 select(UserSession) 

279 .join(User, UserSession.user_id == User.id) 

280 .where(User.username == "frodo") 

281 .where(UserSession.token == reply_token) 

282 .where(UserSession.is_valid) 

283 ).scalar_one_or_none() 

284 ).token 

285 assert token 

286 

287 # log out 

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

289 auth_api.Deauthenticate(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={reply_token}"),)) 

290 

291 

292def test_login_part_signed_up_verified_email(db): 

293 """ 

294 If you try to log in but didn't finish singing up, we send you a new email and ask you to finish signing up. 

295 """ 

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

297 res = auth_api.SignupFlow( 

298 auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="testing", email="email@couchers.org.invalid")) 

299 ) 

300 

301 flow_token = res.flow_token 

302 assert res.need_verify_email 

303 

304 # verify the email 

305 with session_scope() as session: 

306 flow = session.execute(select(SignupFlow).where(SignupFlow.flow_token == flow_token)).scalar_one() 

307 flow_token = flow.flow_token 

308 email_token = flow.email_token 

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

310 auth_api.SignupFlow(auth_pb2.SignupFlowReq(email_token=email_token)) 

311 

312 with mock_notification_email() as mock: 

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

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

315 auth_api.Authenticate(auth_pb2.AuthReq(user="email@couchers.org.invalid", password="wrong pwd")) 

316 assert e.value.details() == errors.SIGNUP_FLOW_EMAIL_STARTED_SIGNUP 

317 

318 assert mock.call_count == 1 

319 e = email_fields(mock) 

320 assert e.recipient == "email@couchers.org.invalid" 

321 assert flow_token in e.plain 

322 assert flow_token in e.html 

323 

324 

325def test_login_part_signed_up_not_verified_email(db): 

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

327 res = auth_api.SignupFlow( 

328 auth_pb2.SignupFlowReq( 

329 basic=auth_pb2.SignupBasic(name="testing", email="email@couchers.org.invalid"), 

330 account=auth_pb2.SignupAccount( 

331 username="frodo", 

332 password="a very insecure password", 

333 birthdate="1999-01-01", 

334 gender="Bot", 

335 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

336 city="New York City", 

337 lat=40.7331, 

338 lng=-73.9778, 

339 radius=500, 

340 accept_tos=True, 

341 ), 

342 ) 

343 ) 

344 

345 flow_token = res.flow_token 

346 assert res.need_verify_email 

347 

348 with mock_notification_email() as mock: 

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

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

351 auth_api.Authenticate(auth_pb2.AuthReq(user="frodo", password="wrong pwd")) 

352 assert e.value.details() == errors.SIGNUP_FLOW_EMAIL_STARTED_SIGNUP 

353 

354 with session_scope() as session: 

355 flow = session.execute(select(SignupFlow).where(SignupFlow.flow_token == flow_token)).scalar_one() 

356 email_token = flow.email_token 

357 

358 assert mock.call_count == 1 

359 e = email_fields(mock) 

360 assert e.recipient == "email@couchers.org.invalid" 

361 assert email_token in e.plain 

362 assert email_token in e.html 

363 

364 

365def test_banned_user(db): 

366 _quick_signup() 

367 

368 with session_scope() as session: 

369 session.execute(select(User)).scalar_one().is_banned = True 

370 

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

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

373 auth_api.Authenticate(auth_pb2.AuthReq(user="frodo", password="a very insecure password")) 

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

375 assert e.value.details() == errors.ACCOUNT_SUSPENDED 

376 

377 

378def test_deleted_user(db): 

379 _quick_signup() 

380 

381 with session_scope() as session: 

382 session.execute(select(User)).scalar_one().is_deleted = True 

383 

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

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

386 auth_api.Authenticate(auth_pb2.AuthReq(user="frodo", password="a very insecure password")) 

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

388 assert e.value.details() == errors.ACCOUNT_NOT_FOUND 

389 

390 

391def test_invalid_token(db): 

392 user1, token1 = generate_user() 

393 user2, token2 = generate_user() 

394 

395 wrong_token = random_hex(32) 

396 

397 with real_api_session(wrong_token) as api, pytest.raises(grpc.RpcError) as e: 

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

399 

400 assert e.value.code() == grpc.StatusCode.UNAUTHENTICATED 

401 assert e.value.details() == "Unauthorized" 

402 

403 

404def test_password_reset_v2(db, push_collector): 

405 user, token = generate_user(hashed_password=hash_password("mypassword")) 

406 

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

408 with mock_notification_email() as mock: 

409 res = auth_api.ResetPassword(auth_pb2.ResetPasswordReq(user=user.username)) 

410 

411 with session_scope() as session: 

412 password_reset_token = session.execute(select(PasswordResetToken)).scalar_one().token 

413 

414 assert mock.call_count == 1 

415 e = email_fields(mock) 

416 assert e.recipient == user.email 

417 assert "reset" in e.subject.lower() 

418 assert password_reset_token in e.plain 

419 assert password_reset_token in e.html 

420 unique_string = "You asked for your password to be reset on Couchers.org." 

421 assert unique_string in e.plain 

422 assert unique_string in e.html 

423 assert f"http://localhost:3000/complete-password-reset?token={password_reset_token}" in e.plain 

424 assert f"http://localhost:3000/complete-password-reset?token={password_reset_token}" in e.html 

425 assert "support@couchers.org" in e.plain 

426 assert "support@couchers.org" in e.html 

427 

428 push_collector.assert_user_push_matches_fields( 

429 user.id, 

430 title="A password reset was initiated on your account", 

431 body="Someone initiated a password change on your account.", 

432 ) 

433 

434 # make sure bad password are caught 

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

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

437 auth_api.CompletePasswordResetV2( 

438 auth_pb2.CompletePasswordResetV2Req(password_reset_token=password_reset_token, new_password="password") 

439 ) 

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

441 assert e.value.details() == errors.INSECURE_PASSWORD 

442 

443 # make sure we can set a good password 

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

445 pwd = random_hex() 

446 with mock_notification_email() as mock: 

447 res = auth_api.CompletePasswordResetV2( 

448 auth_pb2.CompletePasswordResetV2Req(password_reset_token=password_reset_token, new_password=pwd) 

449 ) 

450 

451 push_collector.assert_user_push_matches_fields( 

452 user.id, 

453 ix=1, 

454 title="Your password was successfully reset", 

455 body="Your password on Couchers.org was changed. If that was you, then no further action is needed.", 

456 ) 

457 

458 session_token = get_session_cookie_token(metadata_interceptor) 

459 

460 with session_scope() as session: 

461 other_session_token = ( 

462 session.execute( 

463 select(UserSession) 

464 .join(User, UserSession.user_id == User.id) 

465 .where(User.username == user.username) 

466 .where(UserSession.token == session_token) 

467 .where(UserSession.is_valid) 

468 ).scalar_one_or_none() 

469 ).token 

470 assert other_session_token 

471 

472 # make sure we can't set a password again 

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

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

475 auth_api.CompletePasswordResetV2( 

476 auth_pb2.CompletePasswordResetV2Req( 

477 password_reset_token=password_reset_token, new_password=random_hex() 

478 ) 

479 ) 

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

481 assert e.value.details() == errors.INVALID_TOKEN 

482 

483 with session_scope() as session: 

484 user = session.execute(select(User)).scalar_one() 

485 assert user.hashed_password == hash_password(pwd) 

486 

487 

488def test_password_reset_no_such_user(db): 

489 user, token = generate_user() 

490 

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

492 res = auth_api.ResetPassword( 

493 auth_pb2.ResetPasswordReq( 

494 user="nonexistentuser", 

495 ) 

496 ) 

497 

498 with session_scope() as session: 

499 res = session.execute(select(PasswordResetToken)).scalar_one_or_none() 

500 

501 assert res is None 

502 

503 

504def test_password_reset_invalid_token_v2(db): 

505 password = random_hex() 

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

507 

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

509 res = auth_api.ResetPassword( 

510 auth_pb2.ResetPasswordReq( 

511 user=user.username, 

512 ) 

513 ) 

514 

515 with auth_api_session() as (auth_api, metadata_interceptor), pytest.raises(grpc.RpcError) as e: 

516 res = auth_api.CompletePasswordResetV2(auth_pb2.CompletePasswordResetV2Req(password_reset_token="wrongtoken")) 

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

518 assert e.value.details() == errors.INVALID_TOKEN 

519 

520 with session_scope() as session: 

521 user = session.execute(select(User)).scalar_one() 

522 assert user.hashed_password == hash_password(password) 

523 

524 

525def test_logout_invalid_token(db): 

526 # Create our test user using signup 

527 _quick_signup() 

528 

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

530 auth_api.Authenticate(auth_pb2.AuthReq(user="frodo", password="a very insecure password")) 

531 

532 reply_token = get_session_cookie_token(metadata_interceptor) 

533 

534 # delete all login tokens 

535 with session_scope() as session: 

536 session.execute(delete(LoginToken)) 

537 

538 # log out with non-existent token should still return a valid result 

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

540 auth_api.Deauthenticate(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={reply_token}"),)) 

541 

542 reply_token = get_session_cookie_token(metadata_interceptor) 

543 # make sure we set an empty cookie 

544 assert reply_token == "" 

545 

546 

547def test_signup_without_password(db): 

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

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

550 auth_api.SignupFlow( 

551 auth_pb2.SignupFlowReq( 

552 basic=auth_pb2.SignupBasic(name="Räksmörgås", email="a1@b.com"), 

553 account=auth_pb2.SignupAccount( 

554 username="frodo", 

555 password="bad", 

556 city="Minas Tirith", 

557 birthdate="9999-12-31", # arbitrary future birthdate 

558 gender="Robot", 

559 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

560 lat=1, 

561 lng=1, 

562 radius=100, 

563 accept_tos=True, 

564 ), 

565 feedback=auth_pb2.ContributorForm(), 

566 ) 

567 ) 

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

569 assert e.value.details() == errors.PASSWORD_TOO_SHORT 

570 

571 

572def test_signup_invalid_birthdate(db): 

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

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

575 auth_api.SignupFlow( 

576 auth_pb2.SignupFlowReq( 

577 basic=auth_pb2.SignupBasic(name="Räksmörgås", email="a1@b.com"), 

578 account=auth_pb2.SignupAccount( 

579 username="frodo", 

580 password="a very insecure password", 

581 city="Minas Tirith", 

582 birthdate="9999-12-31", # arbitrary future birthdate 

583 gender="Robot", 

584 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

585 lat=1, 

586 lng=1, 

587 radius=100, 

588 accept_tos=True, 

589 ), 

590 feedback=auth_pb2.ContributorForm(), 

591 ) 

592 ) 

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

594 assert e.value.details() == errors.INVALID_BIRTHDATE 

595 

596 res = auth_api.SignupFlow( 

597 auth_pb2.SignupFlowReq( 

598 basic=auth_pb2.SignupBasic(name="Christopher", email="a2@b.com"), 

599 account=auth_pb2.SignupAccount( 

600 username="ceelo", 

601 password="a very insecure password", 

602 city="New York City", 

603 birthdate="2000-12-31", # arbitrary birthdate older than 18 years 

604 gender="Helicopter", 

605 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

606 lat=1, 

607 lng=1, 

608 radius=100, 

609 accept_tos=True, 

610 ), 

611 feedback=auth_pb2.ContributorForm(), 

612 ) 

613 ) 

614 

615 assert res.flow_token 

616 

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

618 auth_api.SignupFlow( 

619 auth_pb2.SignupFlowReq( 

620 basic=auth_pb2.SignupBasic(name="Franklin", email="a3@b.com"), 

621 account=auth_pb2.SignupAccount( 

622 username="franklin", 

623 password="a very insecure password", 

624 city="Los Santos", 

625 birthdate="2010-04-09", # arbitrary birthdate < 18 yrs 

626 gender="Male", 

627 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

628 lat=1, 

629 lng=1, 

630 radius=100, 

631 accept_tos=True, 

632 ), 

633 feedback=auth_pb2.ContributorForm(), 

634 ) 

635 ) 

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

637 assert e.value.details() == errors.INVALID_BIRTHDATE 

638 

639 with session_scope() as session: 

640 assert session.execute(select(func.count()).select_from(SignupFlow)).scalar_one() == 1 

641 

642 

643def test_signup_invalid_email(db): 

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

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

646 reply = auth_api.SignupFlow(auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="frodo", email="a"))) 

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

648 assert e.value.details() == errors.INVALID_EMAIL 

649 

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

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

652 reply = auth_api.SignupFlow(auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="frodo", email="a@b"))) 

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

654 assert e.value.details() == errors.INVALID_EMAIL 

655 

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

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

658 reply = auth_api.SignupFlow(auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="frodo", email="a@b."))) 

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

660 assert e.value.details() == errors.INVALID_EMAIL 

661 

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

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

664 reply = auth_api.SignupFlow(auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="frodo", email="a@b.c"))) 

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

666 assert e.value.details() == errors.INVALID_EMAIL 

667 

668 

669def test_signup_existing_email(db): 

670 # Signed up user 

671 user, _ = generate_user() 

672 

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

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

675 reply = auth_api.SignupFlow( 

676 auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="frodo", email=user.email)) 

677 ) 

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

679 assert e.value.details() == errors.SIGNUP_FLOW_EMAIL_TAKEN 

680 

681 

682def test_signup_continue_with_email(db): 

683 testing_email = f"{random_hex(12)}@couchers.org.invalid" 

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

685 res = auth_api.SignupFlow(auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="frodo", email=testing_email))) 

686 flow_token = res.flow_token 

687 assert flow_token 

688 

689 # continue with same email, should just send another email to the user 

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

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

692 res = auth_api.SignupFlow( 

693 auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="frodo", email=testing_email)) 

694 ) 

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

696 assert e.value.details() == errors.SIGNUP_FLOW_EMAIL_STARTED_SIGNUP 

697 

698 

699def test_successful_authenticate(db): 

700 user, _ = generate_user(hashed_password=hash_password("password")) 

701 

702 # Authenticate with username 

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

704 reply = auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password="password")) 

705 assert not reply.jailed 

706 

707 # Authenticate with email 

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

709 reply = auth_api.Authenticate(auth_pb2.AuthReq(user=user.email, password="password")) 

710 assert not reply.jailed 

711 

712 

713def test_unsuccessful_authenticate(db): 

714 user, _ = generate_user(hashed_password=hash_password("password")) 

715 

716 # Invalid password 

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

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

719 reply = auth_api.Authenticate(auth_pb2.AuthReq(user=user.username, password="incorrectpassword")) 

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

721 assert e.value.details() == errors.INVALID_PASSWORD 

722 

723 # Invalid username 

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

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

726 reply = auth_api.Authenticate(auth_pb2.AuthReq(user="notarealusername", password="password")) 

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

728 assert e.value.details() == errors.ACCOUNT_NOT_FOUND 

729 

730 # Invalid email 

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

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

733 reply = auth_api.Authenticate( 

734 auth_pb2.AuthReq(user=f"{random_hex(12)}@couchers.org.invalid", password="password") 

735 ) 

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

737 assert e.value.details() == errors.ACCOUNT_NOT_FOUND 

738 

739 # Invalid id 

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

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

742 reply = auth_api.Authenticate(auth_pb2.AuthReq(user="-1", password="password")) 

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

744 assert e.value.details() == errors.ACCOUNT_NOT_FOUND 

745 

746 

747def test_complete_signup(db): 

748 testing_email = f"{random_hex(12)}@couchers.org.invalid" 

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

750 reply = auth_api.SignupFlow( 

751 auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="Tester", email=testing_email)) 

752 ) 

753 

754 flow_token = reply.flow_token 

755 

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

757 # Invalid username 

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

759 reply = auth_api.SignupFlow( 

760 auth_pb2.SignupFlowReq( 

761 flow_token=flow_token, 

762 account=auth_pb2.SignupAccount( 

763 username=" ", 

764 password="a very insecure password", 

765 city="Minas Tirith", 

766 birthdate="1980-12-31", 

767 gender="Robot", 

768 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

769 lat=1, 

770 lng=1, 

771 radius=100, 

772 accept_tos=True, 

773 ), 

774 ) 

775 ) 

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

777 assert e.value.details() == errors.INVALID_USERNAME 

778 

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

780 # Invalid name 

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

782 reply = auth_api.SignupFlow( 

783 auth_pb2.SignupFlowReq( 

784 basic=auth_pb2.SignupBasic(name=" ", email=f"{random_hex(12)}@couchers.org.invalid") 

785 ) 

786 ) 

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

788 assert e.value.details() == errors.INVALID_NAME 

789 

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

791 # Hosting status required 

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

793 reply = auth_api.SignupFlow( 

794 auth_pb2.SignupFlowReq( 

795 flow_token=flow_token, 

796 account=auth_pb2.SignupAccount( 

797 username="frodo", 

798 password="a very insecure password", 

799 city="Minas Tirith", 

800 birthdate="1980-12-31", 

801 gender="Robot", 

802 hosting_status=None, 

803 lat=1, 

804 lng=1, 

805 radius=100, 

806 accept_tos=True, 

807 ), 

808 ) 

809 ) 

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

811 assert e.value.details() == errors.HOSTING_STATUS_REQUIRED 

812 

813 user, _ = generate_user() 

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

815 # Username unavailable 

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

817 reply = auth_api.SignupFlow( 

818 auth_pb2.SignupFlowReq( 

819 flow_token=flow_token, 

820 account=auth_pb2.SignupAccount( 

821 username=user.username, 

822 password="a very insecure password", 

823 city="Minas Tirith", 

824 birthdate="1980-12-31", 

825 gender="Robot", 

826 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

827 lat=1, 

828 lng=1, 

829 radius=100, 

830 accept_tos=True, 

831 ), 

832 ) 

833 ) 

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

835 assert e.value.details() == errors.USERNAME_NOT_AVAILABLE 

836 

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

838 # Invalid coordinate 

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

840 reply = auth_api.SignupFlow( 

841 auth_pb2.SignupFlowReq( 

842 flow_token=flow_token, 

843 account=auth_pb2.SignupAccount( 

844 username="frodo", 

845 password="a very insecure password", 

846 city="Minas Tirith", 

847 birthdate="1980-12-31", 

848 gender="Robot", 

849 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

850 lat=0, 

851 lng=0, 

852 radius=100, 

853 accept_tos=True, 

854 ), 

855 ) 

856 ) 

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

858 assert e.value.details() == errors.INVALID_COORDINATE 

859 

860 

861def test_signup_token_regression(db): 

862 # Repro steps: 

863 # 1. Start a signup 

864 # 2. Confirm the email 

865 # 3. Start a new signup with the same email 

866 # Expected: send a link to the email to continue signing up. 

867 # Actual: `AttributeError: 'SignupFlow' object has no attribute 'token'` 

868 

869 testing_email = f"{random_hex(12)}@couchers.org.invalid" 

870 

871 # 1. Start a signup 

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

873 res = auth_api.SignupFlow(auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="frodo", email=testing_email))) 

874 flow_token = res.flow_token 

875 assert flow_token 

876 

877 # 2. Confirm the email 

878 with session_scope() as session: 

879 email_token = ( 

880 session.execute(select(SignupFlow).where(SignupFlow.flow_token == flow_token)).scalar_one().email_token 

881 ) 

882 

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

884 res = auth_api.SignupFlow( 

885 auth_pb2.SignupFlowReq( 

886 flow_token=flow_token, 

887 email_token=email_token, 

888 ) 

889 ) 

890 

891 # 3. Start a new signup with the same email 

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

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

894 res = auth_api.SignupFlow( 

895 auth_pb2.SignupFlowReq(basic=auth_pb2.SignupBasic(name="frodo", email=testing_email)) 

896 ) 

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

898 assert e.value.details() == errors.SIGNUP_FLOW_EMAIL_STARTED_SIGNUP 

899 

900 

901@pytest.mark.parametrize("opt_out", [True, False]) 

902def test_opt_out_of_newsletter(db, opt_out): 

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

904 res = auth_api.SignupFlow( 

905 auth_pb2.SignupFlowReq( 

906 basic=auth_pb2.SignupBasic(name="testing", email="email@couchers.org.invalid"), 

907 account=auth_pb2.SignupAccount( 

908 username="frodo", 

909 password="a very insecure password", 

910 birthdate="1970-01-01", 

911 gender="Bot", 

912 hosting_status=api_pb2.HOSTING_STATUS_CAN_HOST, 

913 city="New York City", 

914 lat=40.7331, 

915 lng=-73.9778, 

916 radius=500, 

917 accept_tos=True, 

918 opt_out_of_newsletter=opt_out, 

919 ), 

920 feedback=auth_pb2.ContributorForm(), 

921 accept_community_guidelines=wrappers_pb2.BoolValue(value=True), 

922 ) 

923 ) 

924 

925 with session_scope() as session: 

926 email_token = ( 

927 session.execute(select(SignupFlow).where(SignupFlow.flow_token == res.flow_token)).scalar_one().email_token 

928 ) 

929 

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

931 res = auth_api.SignupFlow(auth_pb2.SignupFlowReq(email_token=email_token)) 

932 

933 user_id = res.auth_res.user_id 

934 

935 with session_scope() as session: 

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

937 assert not user.in_sync_with_newsletter 

938 assert user.opt_out_of_newsletter == opt_out 

939 

940 

941def test_GetAuthState(db): 

942 user, token = generate_user() 

943 jailed_user, jailed_token = generate_user(accepted_tos=0) 

944 

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

946 res = auth_api.GetAuthState(empty_pb2.Empty()) 

947 assert not res.logged_in 

948 assert not res.HasField("auth_res") 

949 

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

951 res = auth_api.GetAuthState(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={token}"),)) 

952 assert res.logged_in 

953 assert res.HasField("auth_res") 

954 assert res.auth_res.user_id == user.id 

955 assert not res.auth_res.jailed 

956 

957 auth_api.Deauthenticate(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={token}"),)) 

958 

959 res = auth_api.GetAuthState(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={token}"),)) 

960 assert not res.logged_in 

961 assert not res.HasField("auth_res") 

962 

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

964 res = auth_api.GetAuthState(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={jailed_token}"),)) 

965 assert res.logged_in 

966 assert res.HasField("auth_res") 

967 assert res.auth_res.user_id == jailed_user.id 

968 assert res.auth_res.jailed 

969 

970 

971# tests for ConfirmChangeEmail within test_account.py tests for test_ChangeEmail_*