Coverage for src/tests/test_strong_verification.py: 99%

377 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-08-28 14:55 +0000

1import json 

2from datetime import date, timedelta 

3from unittest.mock import ANY, patch 

4from urllib.parse import urlencode 

5 

6import grpc 

7import pytest 

8from google.protobuf import empty_pb2 

9from sqlalchemy.sql import or_ 

10 

11import couchers.jobs.handlers 

12import couchers.servicers.account 

13from couchers import errors 

14from couchers.config import config 

15from couchers.crypto import asym_decrypt, b64encode_unpadded 

16from couchers.db import session_scope 

17from couchers.jobs.handlers import update_badges 

18from couchers.jobs.worker import process_job 

19from couchers.materialized_views import refresh_materialized_views_rapid 

20from couchers.models import ( 

21 PassportSex, 

22 StrongVerificationAttempt, 

23 StrongVerificationAttemptStatus, 

24 StrongVerificationCallbackEvent, 

25 User, 

26) 

27from couchers.sql import couchers_select as select 

28from proto import account_pb2, admin_pb2, api_pb2 

29from proto.google.api import httpbody_pb2 

30from tests.test_fixtures import ( # noqa 

31 account_session, 

32 api_session, 

33 db, 

34 generate_user, 

35 push_collector, 

36 real_admin_session, 

37 real_iris_session, 

38 testconfig, 

39) 

40 

41 

42@pytest.fixture(autouse=True) 

43def _(testconfig): 

44 pass 

45 

46 

47def _emulate_iris_callback(session_id, session_state, reference): 

48 assert session_state in ["CREATED", "INITIATED", "FAILED", "ABORTED", "COMPLETED", "REJECTED", "APPROVED"] 

49 with real_iris_session() as iris: 

50 data = json.dumps( 

51 {"session_id": session_id, "session_state": session_state, "session_reference": reference} 

52 ).encode("ascii") 

53 iris.Webhook(httpbody_pb2.HttpBody(content_type="application/json", data=data)) 

54 

55 

56default_expiry = date.today() + timedelta(days=5 * 365) 

57 

58 

59def do_and_check_sv( 

60 user, 

61 token, 

62 verification_id, 

63 sex, 

64 dob, 

65 document_type, 

66 document_number, 

67 document_expiry, 

68 nationality, 

69 return_after=None, 

70): 

71 iris_token_data = { 

72 "merchant_id": 5731012934821982, 

73 "session_id": verification_id, 

74 "seed": 1674246339, 

75 "face_verification": False, 

76 "host": "https://passportreader.app", 

77 } 

78 iris_token = b64encode_unpadded(json.dumps(iris_token_data).encode("utf8")) 

79 

80 with account_session(token) as account: 

81 # start by initiation 

82 with patch("couchers.servicers.account.requests.post") as mock: 

83 json_resp1 = { 

84 "id": verification_id, 

85 "token": iris_token, 

86 } 

87 mock.return_value = type( 

88 "__MockResponse", 

89 (), 

90 { 

91 "status_code": 200, 

92 "text": json.dumps(json_resp1), 

93 "json": lambda: json_resp1, 

94 }, 

95 ) 

96 res = account.InitiateStrongVerification(empty_pb2.Empty()) 

97 mock.assert_called_once_with( 

98 "https://passportreader.app/api/v1/session.create", 

99 auth=("dummy_pubkey", "dummy_secret"), 

100 json={ 

101 "callback_url": "http://localhost:8888/iris/webhook", 

102 "face_verification": False, 

103 "reference": ANY, 

104 }, 

105 timeout=10, 

106 ) 

107 reference_data = mock.call_args.kwargs["json"]["reference"] 

108 verification_attempt_token = res.verification_attempt_token 

109 return_url = f"http://localhost:3000/complete-strong-verification?verification_attempt_token={verification_attempt_token}" 

110 assert res.redirect_url == "https://passportreader.app/open?" + urlencode( 

111 {"token": iris_token, "redirect_url": return_url} 

112 ) 

113 

114 assert ( 

115 account.GetStrongVerificationAttemptStatus( 

116 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token) 

117 ).status 

118 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_IN_PROGRESS_WAITING_ON_USER_TO_OPEN_APP 

119 ) 

120 

121 # ok, now the user downloads the app, scans their id, and Iris ID sends callbacks to the server 

122 _emulate_iris_callback(verification_id, "INITIATED", reference_data) 

123 

124 with account_session(token) as account: 

125 assert ( 

126 account.GetStrongVerificationAttemptStatus( 

127 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token) 

128 ).status 

129 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_IN_PROGRESS_WAITING_ON_USER_IN_APP 

130 ) 

131 

132 if return_after == "INITIATED": 

133 return reference_data 

134 

135 _emulate_iris_callback(verification_id, "COMPLETED", reference_data) 

136 

137 with account_session(token) as account: 

138 assert ( 

139 account.GetStrongVerificationAttemptStatus( 

140 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token) 

141 ).status 

142 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_IN_PROGRESS_WAITING_ON_BACKEND 

143 ) 

144 

145 if return_after == "COMPLETED": 

146 return reference_data 

147 

148 _emulate_iris_callback(verification_id, "APPROVED", reference_data) 

149 

150 with account_session(token) as account: 

151 assert ( 

152 account.GetStrongVerificationAttemptStatus( 

153 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token) 

154 ).status 

155 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_IN_PROGRESS_WAITING_ON_BACKEND 

156 ) 

157 

158 if return_after == "APPROVED": 

159 return reference_data 

160 

161 with patch("couchers.jobs.handlers.requests.post") as mock: 

162 json_resp2 = { 

163 "id": verification_id, 

164 "created": "2024-05-11T15:46:46Z", 

165 "expires": "2024-05-11T16:17:26Z", 

166 "state": "APPROVED", 

167 "reference": reference_data, 

168 "user_ip": "10.123.123.123", 

169 "user_agent": "Iris%20ID/168357896 CFNetwork/1494.0.7 Darwin/23.4.0", 

170 "given_names": "John Wayne", 

171 "surname": "Doe", 

172 "nationality": nationality, 

173 "sex": sex, 

174 "date_of_birth": dob, 

175 "document_type": document_type, 

176 "document_number": document_number, 

177 "expiry_date": document_expiry.isoformat(), 

178 "issuing_country": nationality, 

179 "issuer": "Department of State, U.S. Government", 

180 "portrait": "dGVzdHRlc3R0ZXN0...", 

181 } 

182 mock.return_value = type( 

183 "__MockResponse", 

184 (), 

185 { 

186 "status_code": 200, 

187 "text": json.dumps(json_resp2), 

188 "json": lambda: json_resp2, 

189 }, 

190 ) 

191 while process_job(): 

192 pass 

193 

194 mock.assert_called_once_with( 

195 "https://passportreader.app/api/v1/session.get", 

196 auth=("dummy_pubkey", "dummy_secret"), 

197 json={"id": verification_id}, 

198 timeout=10, 

199 ) 

200 

201 with account_session(token) as account: 

202 assert ( 

203 account.GetStrongVerificationAttemptStatus( 

204 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token) 

205 ).status 

206 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_SUCCEEDED 

207 ) 

208 

209 with session_scope() as session: 

210 verification_attempt = session.execute( 

211 select(StrongVerificationAttempt).where( 

212 StrongVerificationAttempt.verification_attempt_token == verification_attempt_token 

213 ) 

214 ).scalar_one() 

215 assert verification_attempt.user_id == user.id 

216 assert verification_attempt.status == StrongVerificationAttemptStatus.succeeded 

217 assert verification_attempt.has_full_data 

218 assert verification_attempt.passport_encrypted_data 

219 # assert verification_attempt.passport_date_of_birth == date(1988, 1, 1) 

220 # assert verification_attempt.passport_sex == PassportSex.male 

221 assert verification_attempt.has_minimal_data 

222 assert verification_attempt.passport_expiry_date == document_expiry 

223 assert verification_attempt.passport_nationality == nationality 

224 assert verification_attempt.passport_last_three_document_chars == document_number[-3:] 

225 assert verification_attempt.iris_token == iris_token 

226 assert verification_attempt.iris_session_id == verification_id 

227 

228 private_key = bytes.fromhex("e6c2fbf3756b387bc09a458a7b85935718ef3eb1c2777ef41d335c9f6c0ab272") 

229 decrypted_data = json.loads(asym_decrypt(private_key, verification_attempt.passport_encrypted_data)) 

230 assert decrypted_data == json_resp2 

231 

232 callbacks = ( 

233 session.execute( 

234 select(StrongVerificationCallbackEvent.iris_status) 

235 .where(StrongVerificationCallbackEvent.verification_attempt_id == verification_attempt.id) 

236 .order_by(StrongVerificationCallbackEvent.created.asc()) 

237 ) 

238 .scalars() 

239 .all() 

240 ) 

241 assert callbacks == ["INITIATED", "COMPLETED", "APPROVED"] 

242 

243 

244def monkeypatch_sv_config(monkeypatch): 

245 new_config = config.copy() 

246 new_config["ENABLE_STRONG_VERIFICATION"] = True 

247 new_config["IRIS_ID_PUBKEY"] = "dummy_pubkey" 

248 new_config["IRIS_ID_SECRET"] = "dummy_secret" 

249 new_config["VERIFICATION_DATA_PUBLIC_KEY"] = bytes.fromhex( 

250 "dd740a2b2a35bf05041a28257ea439b30f76f056f3698000b71e6470cd82275f" 

251 ) 

252 

253 private_key = bytes.fromhex("e6c2fbf3756b387bc09a458a7b85935718ef3eb1c2777ef41d335c9f6c0ab272") 

254 

255 monkeypatch.setattr(couchers.servicers.account, "config", new_config) 

256 monkeypatch.setattr(couchers.jobs.handlers, "config", new_config) 

257 

258 

259def test_strong_verification_happy_path(db, monkeypatch): 

260 monkeypatch_sv_config(monkeypatch) 

261 

262 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man") 

263 _, superuser_token = generate_user(is_superuser=True) 

264 

265 update_badges(empty_pb2.Empty()) 

266 refresh_materialized_views_rapid(None) 

267 

268 with api_session(token) as api: 

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

270 assert "strong_verification" not in res.badges 

271 assert not res.has_strong_verification 

272 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_UNVERIFIED 

273 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_UNVERIFIED 

274 assert ( 

275 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

276 == res.has_strong_verification 

277 ) 

278 

279 do_and_check_sv( 

280 user, 

281 token, 

282 verification_id=5731012934821983, 

283 sex="MALE", 

284 dob="1988-01-01", 

285 document_type="PASSPORT", 

286 document_number="31195855", 

287 document_expiry=default_expiry, 

288 nationality="US", 

289 ) 

290 

291 with session_scope() as session: 

292 verification_attempt = session.execute( 

293 select(StrongVerificationAttempt).where(StrongVerificationAttempt.user_id == user.id) 

294 ).scalar_one() 

295 assert verification_attempt.status == StrongVerificationAttemptStatus.succeeded 

296 assert verification_attempt.passport_date_of_birth == date(1988, 1, 1) 

297 assert verification_attempt.passport_sex == PassportSex.male 

298 assert verification_attempt.passport_expiry_date == default_expiry 

299 assert verification_attempt.passport_nationality == "US" 

300 assert verification_attempt.passport_last_three_document_chars == "855" 

301 

302 update_badges(empty_pb2.Empty()) 

303 refresh_materialized_views_rapid(None) 

304 

305 # the user should now have strong verification 

306 with api_session(token) as api: 

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

308 assert "strong_verification" in res.badges 

309 assert res.has_strong_verification 

310 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

311 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

312 assert ( 

313 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

314 == res.has_strong_verification 

315 ) 

316 

317 # wrong dob = no badge 

318 with session_scope() as session: 

319 user_ = session.execute(select(User).where(User.id == user.id)).scalar_one() 

320 user_.birthdate = date(1988, 1, 2) 

321 

322 update_badges(empty_pb2.Empty()) 

323 refresh_materialized_views_rapid(None) 

324 

325 with api_session(token) as api: 

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

327 assert "strong_verification" not in res.badges 

328 assert not res.has_strong_verification 

329 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_MISMATCH 

330 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

331 assert ( 

332 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

333 == res.has_strong_verification 

334 ) 

335 

336 # bad gender-sex correspondence = no badge 

337 with session_scope() as session: 

338 user_ = session.execute(select(User).where(User.id == user.id)).scalar_one() 

339 user_.birthdate = date(1988, 1, 1) 

340 user_.gender = "Woman" 

341 

342 update_badges(empty_pb2.Empty()) 

343 refresh_materialized_views_rapid(None) 

344 

345 with api_session(token) as api: 

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

347 assert "strong_verification" not in res.badges 

348 assert not res.has_strong_verification 

349 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

350 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH 

351 assert ( 

352 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

353 == res.has_strong_verification 

354 ) 

355 

356 with account_session(token) as account: 

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

358 assert not res.has_strong_verification 

359 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

360 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH 

361 

362 # back to should have a badge 

363 with session_scope() as session: 

364 user_ = session.execute(select(User).where(User.id == user.id)).scalar_one() 

365 user_.gender = "Man" 

366 

367 update_badges(empty_pb2.Empty()) 

368 refresh_materialized_views_rapid(None) 

369 

370 with api_session(token) as api: 

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

372 assert "strong_verification" in res.badges 

373 assert res.has_strong_verification 

374 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

375 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

376 assert ( 

377 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

378 == res.has_strong_verification 

379 ) 

380 with account_session(token) as account: 

381 assert not any( 

382 reminder.HasField("complete_verification_reminder") 

383 for reminder in account.GetReminders(empty_pb2.Empty()).reminders 

384 ) 

385 

386 # check has_passport_sex_gender_exception 

387 with real_admin_session(superuser_token) as admin: 

388 res = admin.GetUserDetails(admin_pb2.GetUserDetailsReq(user=user.username)) 

389 assert "strong_verification" in res.badges 

390 assert res.has_strong_verification 

391 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

392 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

393 

394 admin.SetPassportSexGenderException( 

395 admin_pb2.SetPassportSexGenderExceptionReq(user=user.username, passport_sex_gender_exception=True) 

396 ) 

397 admin.ChangeUserGender(admin_pb2.ChangeUserGenderReq(user=user.username, gender="Woman")) 

398 

399 update_badges(empty_pb2.Empty()) 

400 refresh_materialized_views_rapid(None) 

401 

402 with api_session(token) as api: 

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

404 assert "strong_verification" in res.badges 

405 assert res.has_strong_verification 

406 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

407 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

408 assert ( 

409 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

410 == res.has_strong_verification 

411 ) 

412 with account_session(token) as account: 

413 assert not any( 

414 reminder.HasField("complete_verification_reminder") 

415 for reminder in account.GetReminders(empty_pb2.Empty()).reminders 

416 ) 

417 

418 with real_admin_session(superuser_token) as admin: 

419 res = admin.GetUserDetails(admin_pb2.GetUserDetailsReq(user=user.username)) 

420 assert "strong_verification" in res.badges 

421 assert res.has_strong_verification 

422 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

423 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

424 

425 # now turn exception off 

426 admin.SetPassportSexGenderException( 

427 admin_pb2.SetPassportSexGenderExceptionReq(user=user.username, passport_sex_gender_exception=False) 

428 ) 

429 

430 update_badges(empty_pb2.Empty()) 

431 refresh_materialized_views_rapid(None) 

432 

433 with api_session(token) as api: 

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

435 assert "strong_verification" not in res.badges 

436 assert not res.has_strong_verification 

437 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

438 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH 

439 assert ( 

440 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

441 == res.has_strong_verification 

442 ) 

443 with account_session(token) as account: 

444 assert any( 

445 reminder.HasField("complete_verification_reminder") 

446 for reminder in account.GetReminders(empty_pb2.Empty()).reminders 

447 ) 

448 

449 with real_admin_session(superuser_token) as admin: 

450 res = admin.GetUserDetails(admin_pb2.GetUserDetailsReq(user=user.username)) 

451 assert "strong_verification" not in res.badges 

452 assert not res.has_strong_verification 

453 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

454 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH 

455 

456 

457def test_strong_verification_delete_data(db, monkeypatch): 

458 monkeypatch_sv_config(monkeypatch) 

459 

460 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man") 

461 _, superuser_token = generate_user(is_superuser=True) 

462 

463 refresh_materialized_views_rapid(None) 

464 

465 with api_session(token) as api: 

466 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

467 assert ( 

468 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

469 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

470 ) 

471 

472 # can remove SV data even if there is none, should do nothing 

473 with account_session(token) as account: 

474 account.DeleteStrongVerificationData(empty_pb2.Empty()) 

475 

476 do_and_check_sv( 

477 user, 

478 token, 

479 verification_id=5731012934821983, 

480 sex="MALE", 

481 dob="1988-01-01", 

482 document_type="PASSPORT", 

483 document_number="31195855", 

484 document_expiry=default_expiry, 

485 nationality="US", 

486 ) 

487 

488 refresh_materialized_views_rapid(None) 

489 

490 # the user should now have strong verification 

491 with api_session(token) as api: 

492 assert api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

493 assert ( 

494 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

495 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

496 ) 

497 

498 # check removing SV data 

499 with account_session(token) as account: 

500 account.DeleteStrongVerificationData(empty_pb2.Empty()) 

501 

502 refresh_materialized_views_rapid(None) 

503 

504 with api_session(token) as api: 

505 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

506 assert ( 

507 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

508 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

509 ) 

510 

511 with session_scope() as session: 

512 assert ( 

513 len( 

514 session.execute( 

515 select(StrongVerificationAttempt).where( 

516 or_( 

517 StrongVerificationAttempt.passport_encrypted_data != None, 

518 StrongVerificationAttempt.passport_date_of_birth != None, 

519 StrongVerificationAttempt.passport_sex != None, 

520 ) 

521 ) 

522 ) 

523 .scalars() 

524 .all() 

525 ) 

526 == 0 

527 ) 

528 

529 

530def test_strong_verification_expiry(db, monkeypatch): 

531 monkeypatch_sv_config(monkeypatch) 

532 

533 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man") 

534 _, superuser_token = generate_user(is_superuser=True) 

535 

536 refresh_materialized_views_rapid(None) 

537 

538 with api_session(token) as api: 

539 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

540 assert ( 

541 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

542 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

543 ) 

544 

545 expiry = date.today() + timedelta(days=10) 

546 

547 do_and_check_sv( 

548 user, 

549 token, 

550 verification_id=5731012934821983, 

551 sex="MALE", 

552 dob="1988-01-01", 

553 document_type="PASSPORT", 

554 document_number="31195855", 

555 document_expiry=expiry, 

556 nationality="US", 

557 ) 

558 

559 # the user should now have strong verification 

560 with api_session(token) as api: 

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

562 assert res.has_strong_verification 

563 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

564 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

565 

566 with session_scope() as session: 

567 attempt = session.execute(select(StrongVerificationAttempt)).scalars().one() 

568 attempt.passport_expiry_date = date.today() - timedelta(days=2) 

569 

570 with api_session(token) as api: 

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

572 assert not res.has_strong_verification 

573 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_UNVERIFIED 

574 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_UNVERIFIED 

575 

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

577 assert not res.has_strong_verification 

578 assert not res.has_strong_verification 

579 

580 do_and_check_sv( 

581 user, 

582 token, 

583 verification_id=5731012934821985, 

584 sex="MALE", 

585 dob="1988-01-01", 

586 document_type="PASSPORT", 

587 document_number="PA41323412", 

588 document_expiry=date.today() + timedelta(days=365), 

589 nationality="AU", 

590 ) 

591 

592 refresh_materialized_views_rapid(None) 

593 

594 with api_session(token) as api: 

595 assert api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

596 assert ( 

597 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

598 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

599 ) 

600 

601 

602def test_strong_verification_regression(db, monkeypatch): 

603 monkeypatch_sv_config(monkeypatch) 

604 

605 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man") 

606 

607 do_and_check_sv( 

608 user, 

609 token, 

610 verification_id=5731012934821983, 

611 sex="MALE", 

612 dob="1988-01-01", 

613 document_type="PASSPORT", 

614 document_number="31195855", 

615 document_expiry=default_expiry, 

616 nationality="US", 

617 return_after="INITIATED", 

618 ) 

619 

620 with api_session(token) as api: 

621 api.Ping(api_pb2.PingReq()) 

622 

623 

624def test_strong_verification_regression2(db, monkeypatch): 

625 monkeypatch_sv_config(monkeypatch) 

626 

627 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man") 

628 

629 do_and_check_sv( 

630 user, 

631 token, 

632 verification_id=5731012934821983, 

633 sex="MALE", 

634 dob="1988-01-01", 

635 document_type="PASSPORT", 

636 document_number="31195855", 

637 document_expiry=default_expiry, 

638 nationality="US", 

639 return_after="INITIATED", 

640 ) 

641 

642 do_and_check_sv( 

643 user, 

644 token, 

645 verification_id=5731012934821985, 

646 sex="MALE", 

647 dob="1988-01-01", 

648 document_type="PASSPORT", 

649 document_number="PA41323412", 

650 document_expiry=default_expiry, 

651 nationality="AU", 

652 ) 

653 

654 refresh_materialized_views_rapid(None) 

655 

656 with api_session(token) as api: 

657 assert api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

658 assert ( 

659 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

660 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

661 ) 

662 

663 

664def test_strong_verification_disabled(db): 

665 user, token = generate_user() 

666 

667 with account_session(token) as account: 

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

669 account.InitiateStrongVerification(empty_pb2.Empty()) 

670 assert e.value.code() == grpc.StatusCode.UNAVAILABLE 

671 assert e.value.details() == errors.STRONG_VERIFICATION_DISABLED 

672 

673 

674def test_strong_verification_delete_data_cant_reverify(db, monkeypatch, push_collector): 

675 monkeypatch_sv_config(monkeypatch) 

676 

677 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man") 

678 _, superuser_token = generate_user(is_superuser=True) 

679 

680 refresh_materialized_views_rapid(None) 

681 

682 with api_session(token) as api: 

683 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

684 assert ( 

685 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

686 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

687 ) 

688 

689 do_and_check_sv( 

690 user, 

691 token, 

692 verification_id=5731012934821983, 

693 sex="MALE", 

694 dob="1988-01-01", 

695 document_type="PASSPORT", 

696 document_number="31195855", 

697 document_expiry=default_expiry, 

698 nationality="US", 

699 ) 

700 

701 refresh_materialized_views_rapid(None) 

702 

703 # the user should now have strong verification 

704 with api_session(token) as api: 

705 assert api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

706 assert ( 

707 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

708 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

709 ) 

710 

711 # check removing SV data 

712 with account_session(token) as account: 

713 account.DeleteStrongVerificationData(empty_pb2.Empty()) 

714 

715 refresh_materialized_views_rapid(None) 

716 

717 with api_session(token) as api: 

718 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

719 assert ( 

720 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

721 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

722 ) 

723 

724 with session_scope() as session: 

725 assert ( 

726 len( 

727 session.execute( 

728 select(StrongVerificationAttempt).where( 

729 or_( 

730 StrongVerificationAttempt.passport_encrypted_data != None, 

731 StrongVerificationAttempt.passport_date_of_birth != None, 

732 StrongVerificationAttempt.passport_sex != None, 

733 ) 

734 ) 

735 ) 

736 .scalars() 

737 .all() 

738 ) 

739 == 0 

740 ) 

741 

742 reference_data = do_and_check_sv( 

743 user, 

744 token, 

745 verification_id=5731012934821984, 

746 sex="MALE", 

747 dob="1988-01-01", 

748 document_type="PASSPORT", 

749 document_number="31195855", 

750 document_expiry=default_expiry, 

751 nationality="US", 

752 return_after="APPROVED", 

753 ) 

754 

755 with patch("couchers.jobs.handlers.requests.post") as mock: 

756 json_resp2 = { 

757 "id": 5731012934821984, 

758 "created": "2024-05-11T15:46:46Z", 

759 "expires": "2024-05-11T16:17:26Z", 

760 "state": "APPROVED", 

761 "reference": reference_data, 

762 "user_ip": "10.123.123.123", 

763 "user_agent": "Iris%20ID/168357896 CFNetwork/1494.0.7 Darwin/23.4.0", 

764 "given_names": "John Wayne", 

765 "surname": "Doe", 

766 "nationality": "US", 

767 "sex": "MALE", 

768 "date_of_birth": "1988-01-01", 

769 "document_type": "PASSPORT", 

770 "document_number": "31195855", 

771 "expiry_date": default_expiry.isoformat(), 

772 "issuing_country": "US", 

773 "issuer": "Department of State, U.S. Government", 

774 "portrait": "dGVzdHRlc3R0ZXN0...", 

775 } 

776 mock.return_value = type( 

777 "__MockResponse", 

778 (), 

779 { 

780 "status_code": 200, 

781 "text": json.dumps(json_resp2), 

782 "json": lambda: json_resp2, 

783 }, 

784 ) 

785 while process_job(): 

786 pass 

787 

788 mock.assert_called_once_with( 

789 "https://passportreader.app/api/v1/session.get", 

790 auth=("dummy_pubkey", "dummy_secret"), 

791 json={"id": 5731012934821984}, 

792 timeout=10, 

793 ) 

794 

795 with session_scope() as session: 

796 verification_attempt = session.execute( 

797 select(StrongVerificationAttempt).where(StrongVerificationAttempt.iris_session_id == 5731012934821984) 

798 ).scalar_one() 

799 assert verification_attempt.user_id == user.id 

800 assert verification_attempt.status == StrongVerificationAttemptStatus.duplicate 

801 

802 push_collector.assert_user_push_matches_fields( 

803 user.id, 

804 ix=1, 

805 title="Strong Verification failed", 

806 body="You tried to verify with a passport that has already been used for verification. Please use another passport.", 

807 ) 

808 

809 refresh_materialized_views_rapid(None) 

810 

811 with api_session(token) as api: 

812 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

813 assert ( 

814 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

815 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

816 ) 

817 

818 

819def test_strong_verification_duplicate_other_user(db, monkeypatch, push_collector): 

820 monkeypatch_sv_config(monkeypatch) 

821 

822 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man") 

823 user2, token2 = generate_user(birthdate=date(1988, 1, 1), gender="Man") 

824 _, superuser_token = generate_user(is_superuser=True) 

825 

826 refresh_materialized_views_rapid(None) 

827 

828 with api_session(token) as api: 

829 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

830 assert ( 

831 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

832 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

833 ) 

834 

835 # can remove SV data even if there is none, should do nothing 

836 with account_session(token) as account: 

837 account.DeleteStrongVerificationData(empty_pb2.Empty()) 

838 

839 do_and_check_sv( 

840 user, 

841 token, 

842 verification_id=5731012934821983, 

843 sex="MALE", 

844 dob="1988-01-01", 

845 document_type="PASSPORT", 

846 document_number="31195855", 

847 document_expiry=default_expiry, 

848 nationality="US", 

849 ) 

850 

851 refresh_materialized_views_rapid(None) 

852 

853 # the user should now have strong verification 

854 with api_session(token) as api: 

855 assert api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

856 assert ( 

857 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

858 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

859 ) 

860 

861 # check removing SV data 

862 with account_session(token) as account: 

863 account.DeleteStrongVerificationData(empty_pb2.Empty()) 

864 

865 refresh_materialized_views_rapid(None) 

866 

867 with api_session(token) as api: 

868 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

869 assert ( 

870 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification 

871 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification 

872 ) 

873 

874 with session_scope() as session: 

875 assert ( 

876 len( 

877 session.execute( 

878 select(StrongVerificationAttempt).where( 

879 or_( 

880 StrongVerificationAttempt.passport_encrypted_data != None, 

881 StrongVerificationAttempt.passport_date_of_birth != None, 

882 StrongVerificationAttempt.passport_sex != None, 

883 ) 

884 ) 

885 ) 

886 .scalars() 

887 .all() 

888 ) 

889 == 0 

890 ) 

891 

892 reference_data = do_and_check_sv( 

893 user2, 

894 token2, 

895 verification_id=5731012934821984, 

896 sex="MALE", 

897 dob="1988-01-01", 

898 document_type="PASSPORT", 

899 document_number="31195855", 

900 document_expiry=default_expiry, 

901 nationality="US", 

902 return_after="APPROVED", 

903 ) 

904 

905 with patch("couchers.jobs.handlers.requests.post") as mock: 

906 json_resp2 = { 

907 "id": 5731012934821984, 

908 "created": "2024-05-11T15:46:46Z", 

909 "expires": "2024-05-11T16:17:26Z", 

910 "state": "APPROVED", 

911 "reference": reference_data, 

912 "user_ip": "10.123.123.123", 

913 "user_agent": "Iris%20ID/168357896 CFNetwork/1494.0.7 Darwin/23.4.0", 

914 "given_names": "John Wayne", 

915 "surname": "Doe", 

916 "nationality": "US", 

917 "sex": "MALE", 

918 "date_of_birth": "1988-01-01", 

919 "document_type": "PASSPORT", 

920 "document_number": "31195855", 

921 "expiry_date": default_expiry.isoformat(), 

922 "issuing_country": "US", 

923 "issuer": "Department of State, U.S. Government", 

924 "portrait": "dGVzdHRlc3R0ZXN0...", 

925 } 

926 mock.return_value = type( 

927 "__MockResponse", 

928 (), 

929 { 

930 "status_code": 200, 

931 "text": json.dumps(json_resp2), 

932 "json": lambda: json_resp2, 

933 }, 

934 ) 

935 while process_job(): 

936 pass 

937 

938 mock.assert_called_once_with( 

939 "https://passportreader.app/api/v1/session.get", 

940 auth=("dummy_pubkey", "dummy_secret"), 

941 json={"id": 5731012934821984}, 

942 timeout=10, 

943 ) 

944 

945 with session_scope() as session: 

946 verification_attempt = session.execute( 

947 select(StrongVerificationAttempt).where(StrongVerificationAttempt.iris_session_id == 5731012934821984) 

948 ).scalar_one() 

949 assert verification_attempt.user_id == user2.id 

950 assert verification_attempt.status == StrongVerificationAttemptStatus.duplicate 

951 

952 push_collector.assert_user_push_matches_fields( 

953 user2.id, 

954 title="Strong Verification failed", 

955 body="You tried to verify with a passport that has already been used for verification. Please use another passport.", 

956 ) 

957 

958 

959def test_strong_verification_non_passport(db, monkeypatch, push_collector): 

960 monkeypatch_sv_config(monkeypatch) 

961 

962 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man") 

963 _, superuser_token = generate_user(is_superuser=True) 

964 

965 reference_data = do_and_check_sv( 

966 user, 

967 token, 

968 verification_id=5731012934821984, 

969 sex="MALE", 

970 dob="1988-01-01", 

971 document_type="IDENTITY_CARD", 

972 document_number="31195855", 

973 document_expiry=default_expiry, 

974 nationality="US", 

975 return_after="APPROVED", 

976 ) 

977 

978 with patch("couchers.jobs.handlers.requests.post") as mock: 

979 json_resp2 = { 

980 "id": 5731012934821984, 

981 "created": "2024-05-11T15:46:46Z", 

982 "expires": "2024-05-11T16:17:26Z", 

983 "state": "APPROVED", 

984 "reference": reference_data, 

985 "user_ip": "10.123.123.123", 

986 "user_agent": "Iris%20ID/168357896 CFNetwork/1494.0.7 Darwin/23.4.0", 

987 "given_names": "John Wayne", 

988 "surname": "Doe", 

989 "nationality": "US", 

990 "sex": "MALE", 

991 "date_of_birth": "1988-01-01", 

992 "document_type": "IDENTITY_CARD", 

993 "document_number": "31195855", 

994 "expiry_date": default_expiry.isoformat(), 

995 "issuing_country": "US", 

996 "issuer": "Department of State, U.S. Government", 

997 "portrait": "dGVzdHRlc3R0ZXN0...", 

998 } 

999 mock.return_value = type( 

1000 "__MockResponse", 

1001 (), 

1002 { 

1003 "status_code": 200, 

1004 "text": json.dumps(json_resp2), 

1005 "json": lambda: json_resp2, 

1006 }, 

1007 ) 

1008 while process_job(): 

1009 pass 

1010 

1011 mock.assert_called_once_with( 

1012 "https://passportreader.app/api/v1/session.get", 

1013 auth=("dummy_pubkey", "dummy_secret"), 

1014 json={"id": 5731012934821984}, 

1015 timeout=10, 

1016 ) 

1017 

1018 with session_scope() as session: 

1019 verification_attempt = session.execute( 

1020 select(StrongVerificationAttempt).where(StrongVerificationAttempt.iris_session_id == 5731012934821984) 

1021 ).scalar_one() 

1022 assert verification_attempt.user_id == user.id 

1023 assert verification_attempt.status == StrongVerificationAttemptStatus.failed 

1024 

1025 push_collector.assert_user_push_matches_fields( 

1026 user.id, 

1027 title="Strong Verification failed", 

1028 body="You tried to verify with a document that is not a passport. You can only use a passport for Strong Verification.", 

1029 )