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

377 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-09 20:28 +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 "passport_only": True, 

104 "reference": ANY, 

105 }, 

106 timeout=10, 

107 verify="/etc/ssl/certs/ca-certificates.crt", 

108 ) 

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

110 verification_attempt_token = res.verification_attempt_token 

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

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

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

114 ) 

115 

116 assert ( 

117 account.GetStrongVerificationAttemptStatus( 

118 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token) 

119 ).status 

120 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_IN_PROGRESS_WAITING_ON_USER_TO_OPEN_APP 

121 ) 

122 

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

124 _emulate_iris_callback(verification_id, "INITIATED", reference_data) 

125 

126 with account_session(token) as account: 

127 assert ( 

128 account.GetStrongVerificationAttemptStatus( 

129 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token) 

130 ).status 

131 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_IN_PROGRESS_WAITING_ON_USER_IN_APP 

132 ) 

133 

134 if return_after == "INITIATED": 

135 return reference_data 

136 

137 _emulate_iris_callback(verification_id, "COMPLETED", reference_data) 

138 

139 with account_session(token) as account: 

140 assert ( 

141 account.GetStrongVerificationAttemptStatus( 

142 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token) 

143 ).status 

144 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_IN_PROGRESS_WAITING_ON_BACKEND 

145 ) 

146 

147 if return_after == "COMPLETED": 

148 return reference_data 

149 

150 _emulate_iris_callback(verification_id, "APPROVED", reference_data) 

151 

152 with account_session(token) as account: 

153 assert ( 

154 account.GetStrongVerificationAttemptStatus( 

155 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token) 

156 ).status 

157 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_IN_PROGRESS_WAITING_ON_BACKEND 

158 ) 

159 

160 if return_after == "APPROVED": 

161 return reference_data 

162 

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

164 json_resp2 = { 

165 "id": verification_id, 

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

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

168 "state": "APPROVED", 

169 "reference": reference_data, 

170 "user_ip": "10.123.123.123", 

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

172 "given_names": "John Wayne", 

173 "surname": "Doe", 

174 "nationality": nationality, 

175 "sex": sex, 

176 "date_of_birth": dob, 

177 "document_type": document_type, 

178 "document_number": document_number, 

179 "expiry_date": document_expiry.isoformat(), 

180 "issuing_country": nationality, 

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

182 "portrait": "dGVzdHRlc3R0ZXN0...", 

183 } 

184 mock.return_value = type( 

185 "__MockResponse", 

186 (), 

187 { 

188 "status_code": 200, 

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

190 "json": lambda: json_resp2, 

191 }, 

192 ) 

193 while process_job(): 

194 pass 

195 

196 mock.assert_called_once_with( 

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

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

199 json={"id": verification_id}, 

200 timeout=10, 

201 verify="/etc/ssl/certs/ca-certificates.crt", 

202 ) 

203 

204 with account_session(token) as account: 

205 assert ( 

206 account.GetStrongVerificationAttemptStatus( 

207 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token) 

208 ).status 

209 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_SUCCEEDED 

210 ) 

211 

212 with session_scope() as session: 

213 verification_attempt = session.execute( 

214 select(StrongVerificationAttempt).where( 

215 StrongVerificationAttempt.verification_attempt_token == verification_attempt_token 

216 ) 

217 ).scalar_one() 

218 assert verification_attempt.user_id == user.id 

219 assert verification_attempt.status == StrongVerificationAttemptStatus.succeeded 

220 assert verification_attempt.has_full_data 

221 assert verification_attempt.passport_encrypted_data 

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

223 # assert verification_attempt.passport_sex == PassportSex.male 

224 assert verification_attempt.has_minimal_data 

225 assert verification_attempt.passport_expiry_date == document_expiry 

226 assert verification_attempt.passport_nationality == nationality 

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

228 assert verification_attempt.iris_token == iris_token 

229 assert verification_attempt.iris_session_id == verification_id 

230 

231 private_key = bytes.fromhex("e6c2fbf3756b387bc09a458a7b85935718ef3eb1c2777ef41d335c9f6c0ab272") 

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

233 assert decrypted_data == json_resp2 

234 

235 callbacks = ( 

236 session.execute( 

237 select(StrongVerificationCallbackEvent.iris_status) 

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

239 .order_by(StrongVerificationCallbackEvent.created.asc()) 

240 ) 

241 .scalars() 

242 .all() 

243 ) 

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

245 

246 

247def monkeypatch_sv_config(monkeypatch): 

248 new_config = config.copy() 

249 new_config["ENABLE_STRONG_VERIFICATION"] = True 

250 new_config["IRIS_ID_PUBKEY"] = "dummy_pubkey" 

251 new_config["IRIS_ID_SECRET"] = "dummy_secret" 

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

253 "dd740a2b2a35bf05041a28257ea439b30f76f056f3698000b71e6470cd82275f" 

254 ) 

255 

256 private_key = bytes.fromhex("e6c2fbf3756b387bc09a458a7b85935718ef3eb1c2777ef41d335c9f6c0ab272") 

257 

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

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

260 

261 

262def test_strong_verification_happy_path(db, monkeypatch): 

263 monkeypatch_sv_config(monkeypatch) 

264 

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

266 _, superuser_token = generate_user(is_superuser=True) 

267 

268 update_badges(empty_pb2.Empty()) 

269 refresh_materialized_views_rapid(None) 

270 

271 with api_session(token) as api: 

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

273 assert "strong_verification" not in res.badges 

274 assert not res.has_strong_verification 

275 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_UNVERIFIED 

276 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_UNVERIFIED 

277 assert ( 

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

279 == res.has_strong_verification 

280 ) 

281 

282 do_and_check_sv( 

283 user, 

284 token, 

285 verification_id=5731012934821983, 

286 sex="MALE", 

287 dob="1988-01-01", 

288 document_type="PASSPORT", 

289 document_number="31195855", 

290 document_expiry=default_expiry, 

291 nationality="US", 

292 ) 

293 

294 with session_scope() as session: 

295 verification_attempt = session.execute( 

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

297 ).scalar_one() 

298 assert verification_attempt.status == StrongVerificationAttemptStatus.succeeded 

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

300 assert verification_attempt.passport_sex == PassportSex.male 

301 assert verification_attempt.passport_expiry_date == default_expiry 

302 assert verification_attempt.passport_nationality == "US" 

303 assert verification_attempt.passport_last_three_document_chars == "855" 

304 

305 update_badges(empty_pb2.Empty()) 

306 refresh_materialized_views_rapid(None) 

307 

308 # the user should now have strong verification 

309 with api_session(token) as api: 

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

311 assert "strong_verification" in res.badges 

312 assert res.has_strong_verification 

313 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

314 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

315 assert ( 

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

317 == res.has_strong_verification 

318 ) 

319 

320 # wrong dob = no badge 

321 with session_scope() as session: 

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

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

324 

325 update_badges(empty_pb2.Empty()) 

326 refresh_materialized_views_rapid(None) 

327 

328 with api_session(token) as api: 

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

330 assert "strong_verification" not in res.badges 

331 assert not res.has_strong_verification 

332 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_MISMATCH 

333 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

334 assert ( 

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

336 == res.has_strong_verification 

337 ) 

338 

339 # bad gender-sex correspondence = no badge 

340 with session_scope() as session: 

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

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

343 user_.gender = "Woman" 

344 

345 update_badges(empty_pb2.Empty()) 

346 refresh_materialized_views_rapid(None) 

347 

348 with api_session(token) as api: 

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

350 assert "strong_verification" not in res.badges 

351 assert not res.has_strong_verification 

352 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

353 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH 

354 assert ( 

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

356 == res.has_strong_verification 

357 ) 

358 

359 with account_session(token) as account: 

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

361 assert not res.has_strong_verification 

362 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

363 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH 

364 

365 # back to should have a badge 

366 with session_scope() as session: 

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

368 user_.gender = "Man" 

369 

370 update_badges(empty_pb2.Empty()) 

371 refresh_materialized_views_rapid(None) 

372 

373 with api_session(token) as api: 

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

375 assert "strong_verification" in res.badges 

376 assert res.has_strong_verification 

377 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

378 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

379 assert ( 

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

381 == res.has_strong_verification 

382 ) 

383 with account_session(token) as account: 

384 assert not any( 

385 reminder.HasField("complete_verification_reminder") 

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

387 ) 

388 

389 # check has_passport_sex_gender_exception 

390 with real_admin_session(superuser_token) as admin: 

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

392 assert "strong_verification" in res.badges 

393 assert res.has_strong_verification 

394 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

395 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

396 

397 admin.SetPassportSexGenderException( 

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

399 ) 

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

401 

402 update_badges(empty_pb2.Empty()) 

403 refresh_materialized_views_rapid(None) 

404 

405 with api_session(token) as api: 

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

407 assert "strong_verification" in res.badges 

408 assert res.has_strong_verification 

409 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

410 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

411 assert ( 

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

413 == res.has_strong_verification 

414 ) 

415 with account_session(token) as account: 

416 assert not any( 

417 reminder.HasField("complete_verification_reminder") 

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

419 ) 

420 

421 with real_admin_session(superuser_token) as admin: 

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

423 assert "strong_verification" in res.badges 

424 assert res.has_strong_verification 

425 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

426 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

427 

428 # now turn exception off 

429 admin.SetPassportSexGenderException( 

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

431 ) 

432 

433 update_badges(empty_pb2.Empty()) 

434 refresh_materialized_views_rapid(None) 

435 

436 with api_session(token) as api: 

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

438 assert "strong_verification" not in res.badges 

439 assert not res.has_strong_verification 

440 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

441 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH 

442 assert ( 

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

444 == res.has_strong_verification 

445 ) 

446 with account_session(token) as account: 

447 assert any( 

448 reminder.HasField("complete_verification_reminder") 

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

450 ) 

451 

452 with real_admin_session(superuser_token) as admin: 

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

454 assert "strong_verification" not in res.badges 

455 assert not res.has_strong_verification 

456 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

457 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH 

458 

459 

460def test_strong_verification_delete_data(db, monkeypatch): 

461 monkeypatch_sv_config(monkeypatch) 

462 

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

464 _, superuser_token = generate_user(is_superuser=True) 

465 

466 refresh_materialized_views_rapid(None) 

467 

468 with api_session(token) as api: 

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

470 assert ( 

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

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

473 ) 

474 

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

476 with account_session(token) as account: 

477 account.DeleteStrongVerificationData(empty_pb2.Empty()) 

478 

479 do_and_check_sv( 

480 user, 

481 token, 

482 verification_id=5731012934821983, 

483 sex="MALE", 

484 dob="1988-01-01", 

485 document_type="PASSPORT", 

486 document_number="31195855", 

487 document_expiry=default_expiry, 

488 nationality="US", 

489 ) 

490 

491 refresh_materialized_views_rapid(None) 

492 

493 # the user should now have strong verification 

494 with api_session(token) as api: 

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

496 assert ( 

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

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

499 ) 

500 

501 # check removing SV data 

502 with account_session(token) as account: 

503 account.DeleteStrongVerificationData(empty_pb2.Empty()) 

504 

505 refresh_materialized_views_rapid(None) 

506 

507 with api_session(token) as api: 

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

509 assert ( 

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

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

512 ) 

513 

514 with session_scope() as session: 

515 assert ( 

516 len( 

517 session.execute( 

518 select(StrongVerificationAttempt).where( 

519 or_( 

520 StrongVerificationAttempt.passport_encrypted_data != None, 

521 StrongVerificationAttempt.passport_date_of_birth != None, 

522 StrongVerificationAttempt.passport_sex != None, 

523 ) 

524 ) 

525 ) 

526 .scalars() 

527 .all() 

528 ) 

529 == 0 

530 ) 

531 

532 

533def test_strong_verification_expiry(db, monkeypatch): 

534 monkeypatch_sv_config(monkeypatch) 

535 

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

537 _, superuser_token = generate_user(is_superuser=True) 

538 

539 refresh_materialized_views_rapid(None) 

540 

541 with api_session(token) as api: 

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

543 assert ( 

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

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

546 ) 

547 

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

549 

550 do_and_check_sv( 

551 user, 

552 token, 

553 verification_id=5731012934821983, 

554 sex="MALE", 

555 dob="1988-01-01", 

556 document_type="PASSPORT", 

557 document_number="31195855", 

558 document_expiry=expiry, 

559 nationality="US", 

560 ) 

561 

562 # the user should now have strong verification 

563 with api_session(token) as api: 

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

565 assert res.has_strong_verification 

566 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED 

567 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED 

568 

569 with session_scope() as session: 

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

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

572 

573 with api_session(token) as api: 

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

575 assert not res.has_strong_verification 

576 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_UNVERIFIED 

577 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_UNVERIFIED 

578 

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

580 assert not res.has_strong_verification 

581 assert not res.has_strong_verification 

582 

583 do_and_check_sv( 

584 user, 

585 token, 

586 verification_id=5731012934821985, 

587 sex="MALE", 

588 dob="1988-01-01", 

589 document_type="PASSPORT", 

590 document_number="PA41323412", 

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

592 nationality="AU", 

593 ) 

594 

595 refresh_materialized_views_rapid(None) 

596 

597 with api_session(token) as api: 

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

599 assert ( 

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

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

602 ) 

603 

604 

605def test_strong_verification_regression(db, monkeypatch): 

606 monkeypatch_sv_config(monkeypatch) 

607 

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

609 

610 do_and_check_sv( 

611 user, 

612 token, 

613 verification_id=5731012934821983, 

614 sex="MALE", 

615 dob="1988-01-01", 

616 document_type="PASSPORT", 

617 document_number="31195855", 

618 document_expiry=default_expiry, 

619 nationality="US", 

620 return_after="INITIATED", 

621 ) 

622 

623 with api_session(token) as api: 

624 api.Ping(api_pb2.PingReq()) 

625 

626 

627def test_strong_verification_regression2(db, monkeypatch): 

628 monkeypatch_sv_config(monkeypatch) 

629 

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

631 

632 do_and_check_sv( 

633 user, 

634 token, 

635 verification_id=5731012934821983, 

636 sex="MALE", 

637 dob="1988-01-01", 

638 document_type="PASSPORT", 

639 document_number="31195855", 

640 document_expiry=default_expiry, 

641 nationality="US", 

642 return_after="INITIATED", 

643 ) 

644 

645 do_and_check_sv( 

646 user, 

647 token, 

648 verification_id=5731012934821985, 

649 sex="MALE", 

650 dob="1988-01-01", 

651 document_type="PASSPORT", 

652 document_number="PA41323412", 

653 document_expiry=default_expiry, 

654 nationality="AU", 

655 ) 

656 

657 refresh_materialized_views_rapid(None) 

658 

659 with api_session(token) as api: 

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

661 assert ( 

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

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

664 ) 

665 

666 

667def test_strong_verification_disabled(db): 

668 user, token = generate_user() 

669 

670 with account_session(token) as account: 

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

672 account.InitiateStrongVerification(empty_pb2.Empty()) 

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

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

675 

676 

677def test_strong_verification_delete_data_cant_reverify(db, monkeypatch, push_collector): 

678 monkeypatch_sv_config(monkeypatch) 

679 

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

681 _, superuser_token = generate_user(is_superuser=True) 

682 

683 refresh_materialized_views_rapid(None) 

684 

685 with api_session(token) as api: 

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

687 assert ( 

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

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

690 ) 

691 

692 do_and_check_sv( 

693 user, 

694 token, 

695 verification_id=5731012934821983, 

696 sex="MALE", 

697 dob="1988-01-01", 

698 document_type="PASSPORT", 

699 document_number="31195855", 

700 document_expiry=default_expiry, 

701 nationality="US", 

702 ) 

703 

704 refresh_materialized_views_rapid(None) 

705 

706 # the user should now have strong verification 

707 with api_session(token) as api: 

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

709 assert ( 

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

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

712 ) 

713 

714 # check removing SV data 

715 with account_session(token) as account: 

716 account.DeleteStrongVerificationData(empty_pb2.Empty()) 

717 

718 refresh_materialized_views_rapid(None) 

719 

720 with api_session(token) as api: 

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

722 assert ( 

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

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

725 ) 

726 

727 with session_scope() as session: 

728 assert ( 

729 len( 

730 session.execute( 

731 select(StrongVerificationAttempt).where( 

732 or_( 

733 StrongVerificationAttempt.passport_encrypted_data != None, 

734 StrongVerificationAttempt.passport_date_of_birth != None, 

735 StrongVerificationAttempt.passport_sex != None, 

736 ) 

737 ) 

738 ) 

739 .scalars() 

740 .all() 

741 ) 

742 == 0 

743 ) 

744 

745 reference_data = do_and_check_sv( 

746 user, 

747 token, 

748 verification_id=5731012934821984, 

749 sex="MALE", 

750 dob="1988-01-01", 

751 document_type="PASSPORT", 

752 document_number="31195855", 

753 document_expiry=default_expiry, 

754 nationality="US", 

755 return_after="APPROVED", 

756 ) 

757 

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

759 json_resp2 = { 

760 "id": 5731012934821984, 

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

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

763 "state": "APPROVED", 

764 "reference": reference_data, 

765 "user_ip": "10.123.123.123", 

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

767 "given_names": "John Wayne", 

768 "surname": "Doe", 

769 "nationality": "US", 

770 "sex": "MALE", 

771 "date_of_birth": "1988-01-01", 

772 "document_type": "PASSPORT", 

773 "document_number": "31195855", 

774 "expiry_date": default_expiry.isoformat(), 

775 "issuing_country": "US", 

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

777 "portrait": "dGVzdHRlc3R0ZXN0...", 

778 } 

779 mock.return_value = type( 

780 "__MockResponse", 

781 (), 

782 { 

783 "status_code": 200, 

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

785 "json": lambda: json_resp2, 

786 }, 

787 ) 

788 while process_job(): 

789 pass 

790 

791 mock.assert_called_once_with( 

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

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

794 json={"id": 5731012934821984}, 

795 timeout=10, 

796 verify="/etc/ssl/certs/ca-certificates.crt", 

797 ) 

798 

799 with session_scope() as session: 

800 verification_attempt = session.execute( 

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

802 ).scalar_one() 

803 assert verification_attempt.user_id == user.id 

804 assert verification_attempt.status == StrongVerificationAttemptStatus.duplicate 

805 

806 push_collector.assert_user_push_matches_fields( 

807 user.id, 

808 ix=1, 

809 title="Strong Verification failed", 

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

811 ) 

812 

813 refresh_materialized_views_rapid(None) 

814 

815 with api_session(token) as api: 

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

817 assert ( 

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

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

820 ) 

821 

822 

823def test_strong_verification_duplicate_other_user(db, monkeypatch, push_collector): 

824 monkeypatch_sv_config(monkeypatch) 

825 

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

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

828 _, superuser_token = generate_user(is_superuser=True) 

829 

830 refresh_materialized_views_rapid(None) 

831 

832 with api_session(token) as api: 

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

834 assert ( 

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

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

837 ) 

838 

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

840 with account_session(token) as account: 

841 account.DeleteStrongVerificationData(empty_pb2.Empty()) 

842 

843 do_and_check_sv( 

844 user, 

845 token, 

846 verification_id=5731012934821983, 

847 sex="MALE", 

848 dob="1988-01-01", 

849 document_type="PASSPORT", 

850 document_number="31195855", 

851 document_expiry=default_expiry, 

852 nationality="US", 

853 ) 

854 

855 refresh_materialized_views_rapid(None) 

856 

857 # the user should now have strong verification 

858 with api_session(token) as api: 

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

860 assert ( 

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

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

863 ) 

864 

865 # check removing SV data 

866 with account_session(token) as account: 

867 account.DeleteStrongVerificationData(empty_pb2.Empty()) 

868 

869 refresh_materialized_views_rapid(None) 

870 

871 with api_session(token) as api: 

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

873 assert ( 

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

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

876 ) 

877 

878 with session_scope() as session: 

879 assert ( 

880 len( 

881 session.execute( 

882 select(StrongVerificationAttempt).where( 

883 or_( 

884 StrongVerificationAttempt.passport_encrypted_data != None, 

885 StrongVerificationAttempt.passport_date_of_birth != None, 

886 StrongVerificationAttempt.passport_sex != None, 

887 ) 

888 ) 

889 ) 

890 .scalars() 

891 .all() 

892 ) 

893 == 0 

894 ) 

895 

896 reference_data = do_and_check_sv( 

897 user2, 

898 token2, 

899 verification_id=5731012934821984, 

900 sex="MALE", 

901 dob="1988-01-01", 

902 document_type="PASSPORT", 

903 document_number="31195855", 

904 document_expiry=default_expiry, 

905 nationality="US", 

906 return_after="APPROVED", 

907 ) 

908 

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

910 json_resp2 = { 

911 "id": 5731012934821984, 

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

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

914 "state": "APPROVED", 

915 "reference": reference_data, 

916 "user_ip": "10.123.123.123", 

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

918 "given_names": "John Wayne", 

919 "surname": "Doe", 

920 "nationality": "US", 

921 "sex": "MALE", 

922 "date_of_birth": "1988-01-01", 

923 "document_type": "PASSPORT", 

924 "document_number": "31195855", 

925 "expiry_date": default_expiry.isoformat(), 

926 "issuing_country": "US", 

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

928 "portrait": "dGVzdHRlc3R0ZXN0...", 

929 } 

930 mock.return_value = type( 

931 "__MockResponse", 

932 (), 

933 { 

934 "status_code": 200, 

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

936 "json": lambda: json_resp2, 

937 }, 

938 ) 

939 while process_job(): 

940 pass 

941 

942 mock.assert_called_once_with( 

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

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

945 json={"id": 5731012934821984}, 

946 timeout=10, 

947 verify="/etc/ssl/certs/ca-certificates.crt", 

948 ) 

949 

950 with session_scope() as session: 

951 verification_attempt = session.execute( 

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

953 ).scalar_one() 

954 assert verification_attempt.user_id == user2.id 

955 assert verification_attempt.status == StrongVerificationAttemptStatus.duplicate 

956 

957 push_collector.assert_user_push_matches_fields( 

958 user2.id, 

959 title="Strong Verification failed", 

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

961 ) 

962 

963 

964def test_strong_verification_non_passport(db, monkeypatch, push_collector): 

965 monkeypatch_sv_config(monkeypatch) 

966 

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

968 _, superuser_token = generate_user(is_superuser=True) 

969 

970 reference_data = do_and_check_sv( 

971 user, 

972 token, 

973 verification_id=5731012934821984, 

974 sex="MALE", 

975 dob="1988-01-01", 

976 document_type="IDENTITY_CARD", 

977 document_number="31195855", 

978 document_expiry=default_expiry, 

979 nationality="US", 

980 return_after="APPROVED", 

981 ) 

982 

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

984 json_resp2 = { 

985 "id": 5731012934821984, 

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

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

988 "state": "APPROVED", 

989 "reference": reference_data, 

990 "user_ip": "10.123.123.123", 

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

992 "given_names": "John Wayne", 

993 "surname": "Doe", 

994 "nationality": "US", 

995 "sex": "MALE", 

996 "date_of_birth": "1988-01-01", 

997 "document_type": "IDENTITY_CARD", 

998 "document_number": "31195855", 

999 "expiry_date": default_expiry.isoformat(), 

1000 "issuing_country": "US", 

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

1002 "portrait": "dGVzdHRlc3R0ZXN0...", 

1003 } 

1004 mock.return_value = type( 

1005 "__MockResponse", 

1006 (), 

1007 { 

1008 "status_code": 200, 

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

1010 "json": lambda: json_resp2, 

1011 }, 

1012 ) 

1013 while process_job(): 

1014 pass 

1015 

1016 mock.assert_called_once_with( 

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

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

1019 json={"id": 5731012934821984}, 

1020 timeout=10, 

1021 verify="/etc/ssl/certs/ca-certificates.crt", 

1022 ) 

1023 

1024 with session_scope() as session: 

1025 verification_attempt = session.execute( 

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

1027 ).scalar_one() 

1028 assert verification_attempt.user_id == user.id 

1029 assert verification_attempt.status == StrongVerificationAttemptStatus.failed 

1030 

1031 push_collector.assert_user_push_matches_fields( 

1032 user.id, 

1033 title="Strong Verification failed", 

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

1035 )