Coverage for app/backend/src/tests/test_strong_verification.py: 99%
374 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
1import json
2from datetime import date, timedelta
3from unittest.mock import ANY, patch
4from urllib.parse import urlencode
6import grpc
7import pytest
8from google.protobuf import empty_pb2
9from sqlalchemy import select, update
10from sqlalchemy.sql import or_
12from couchers.config import config
13from couchers.crypto import asym_decrypt, b64encode_unpadded
14from couchers.db import session_scope
15from couchers.jobs.handlers import update_badges
16from couchers.jobs.worker import process_job
17from couchers.materialized_views import refresh_materialized_views_rapid
18from couchers.models import (
19 PassportSex,
20 StrongVerificationAttempt,
21 StrongVerificationAttemptStatus,
22 StrongVerificationCallbackEvent,
23 User,
24)
25from couchers.proto import account_pb2, admin_pb2, api_pb2
26from couchers.proto.google.api import httpbody_pb2
27from tests.fixtures.db import generate_user
28from tests.fixtures.misc import PushCollector
29from tests.fixtures.sessions import account_session, api_session, real_admin_session, real_iris_session
32def _emulate_iris_callback(session_id, session_state, reference):
33 assert session_state in ["CREATED", "INITIATED", "FAILED", "ABORTED", "COMPLETED", "REJECTED", "APPROVED"]
34 with real_iris_session() as iris:
35 data = json.dumps(
36 {"session_id": session_id, "session_state": session_state, "session_reference": reference}
37 ).encode("ascii")
38 iris.Webhook(httpbody_pb2.HttpBody(content_type="application/json", data=data))
41default_expiry = date.today() + timedelta(days=5 * 365)
44def do_and_check_sv(
45 user,
46 token,
47 verification_id,
48 sex,
49 dob,
50 document_type,
51 document_number,
52 document_expiry,
53 nationality,
54 return_after=None,
55):
56 iris_token_data = {
57 "merchant_id": 5731012934821982,
58 "session_id": verification_id,
59 "seed": 1674246339,
60 "face_verification": False,
61 "host": "https://passportreader.app",
62 }
63 iris_token = b64encode_unpadded(json.dumps(iris_token_data).encode("utf8"))
65 with account_session(token) as account:
66 # start by initiation
67 with patch("couchers.servicers.account.requests.post") as mock:
68 json_resp1 = {
69 "id": verification_id,
70 "token": iris_token,
71 }
72 mock.return_value = type(
73 "__MockResponse",
74 (),
75 {
76 "status_code": 200,
77 "text": json.dumps(json_resp1),
78 "json": lambda: json_resp1,
79 },
80 )
81 res = account.InitiateStrongVerification(empty_pb2.Empty())
82 mock.assert_called_once_with(
83 "https://passportreader.app/api/v1/session.create",
84 auth=("dummy_pubkey", "dummy_secret"),
85 json={
86 "callback_url": "http://localhost:8888/iris/webhook",
87 "face_verification": False,
88 "passport_only": True,
89 "reference": ANY,
90 },
91 timeout=10,
92 verify="/etc/ssl/certs/ca-certificates.crt",
93 )
94 reference_data = mock.call_args.kwargs["json"]["reference"]
95 verification_attempt_token = res.verification_attempt_token
96 return_url = f"http://localhost:3000/complete-strong-verification?verification_attempt_token={verification_attempt_token}"
97 assert res.redirect_url == "https://passportreader.app/open?" + urlencode(
98 {"token": iris_token, "redirect_url": return_url}
99 )
101 assert (
102 account.GetStrongVerificationAttemptStatus(
103 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token)
104 ).status
105 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_IN_PROGRESS_WAITING_ON_USER_TO_OPEN_APP
106 )
108 # ok, now the user downloads the app, scans their id, and Iris ID sends callbacks to the server
109 _emulate_iris_callback(verification_id, "INITIATED", reference_data)
111 with account_session(token) as account:
112 assert (
113 account.GetStrongVerificationAttemptStatus(
114 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token)
115 ).status
116 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_IN_PROGRESS_WAITING_ON_USER_IN_APP
117 )
119 if return_after == "INITIATED":
120 return reference_data
122 _emulate_iris_callback(verification_id, "COMPLETED", reference_data)
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_BACKEND
130 )
132 if return_after == "COMPLETED": 132 ↛ 133line 132 didn't jump to line 133 because the condition on line 132 was never true
133 return reference_data
135 _emulate_iris_callback(verification_id, "APPROVED", reference_data)
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 )
145 if return_after == "APPROVED":
146 return reference_data
148 with patch("couchers.jobs.handlers.requests.post") as mock:
149 json_resp2 = {
150 "id": verification_id,
151 "created": "2024-05-11T15:46:46Z",
152 "expires": "2024-05-11T16:17:26Z",
153 "state": "APPROVED",
154 "reference": reference_data,
155 "user_ip": "10.123.123.123",
156 "user_agent": "Iris%20ID/168357896 CFNetwork/1494.0.7 Darwin/23.4.0",
157 "given_names": "John Wayne",
158 "surname": "Doe",
159 "nationality": nationality,
160 "sex": sex,
161 "date_of_birth": dob,
162 "document_type": document_type,
163 "document_number": document_number,
164 "expiry_date": document_expiry.isoformat(),
165 "issuing_country": nationality,
166 "issuer": "Department of State, U.S. Government",
167 "portrait": "dGVzdHRlc3R0ZXN0...",
168 }
169 mock.return_value = type(
170 "__MockResponse",
171 (),
172 {
173 "status_code": 200,
174 "text": json.dumps(json_resp2),
175 "json": lambda: json_resp2,
176 },
177 )
178 while process_job():
179 pass
181 mock.assert_called_once_with(
182 "https://passportreader.app/api/v1/session.get",
183 auth=("dummy_pubkey", "dummy_secret"),
184 json={"id": verification_id},
185 timeout=10,
186 verify="/etc/ssl/certs/ca-certificates.crt",
187 )
189 with account_session(token) as account:
190 assert (
191 account.GetStrongVerificationAttemptStatus(
192 account_pb2.GetStrongVerificationAttemptStatusReq(verification_attempt_token=verification_attempt_token)
193 ).status
194 == account_pb2.STRONG_VERIFICATION_ATTEMPT_STATUS_SUCCEEDED
195 )
197 with session_scope() as session:
198 verification_attempt = session.execute(
199 select(StrongVerificationAttempt).where(
200 StrongVerificationAttempt.verification_attempt_token == verification_attempt_token
201 )
202 ).scalar_one()
203 assert verification_attempt.user_id == user.id
204 assert verification_attempt.status == StrongVerificationAttemptStatus.succeeded
205 assert verification_attempt.has_full_data
206 assert verification_attempt.passport_encrypted_data
207 # assert verification_attempt.passport_date_of_birth == date(1988, 1, 1)
208 # assert verification_attempt.passport_sex == PassportSex.male
209 assert verification_attempt.has_minimal_data
210 assert verification_attempt.passport_expiry_date == document_expiry
211 assert verification_attempt.passport_nationality == nationality
212 assert verification_attempt.passport_last_three_document_chars == document_number[-3:]
213 assert verification_attempt.iris_token == iris_token
214 assert verification_attempt.iris_session_id == verification_id
216 private_key = bytes.fromhex("e6c2fbf3756b387bc09a458a7b85935718ef3eb1c2777ef41d335c9f6c0ab272")
217 decrypted_data = json.loads(asym_decrypt(private_key, verification_attempt.passport_encrypted_data))
218 assert decrypted_data == json_resp2
220 callbacks = (
221 session.execute(
222 select(StrongVerificationCallbackEvent.iris_status)
223 .where(StrongVerificationCallbackEvent.verification_attempt_id == verification_attempt.id)
224 .order_by(StrongVerificationCallbackEvent.created.asc())
225 )
226 .scalars()
227 .all()
228 )
229 assert callbacks == ["INITIATED", "COMPLETED", "APPROVED"]
232@pytest.fixture
233def sv_config() -> None:
234 # An autouse fixture runs before non-autouse fixtures of the same scope.
235 config.IRIS_ID_PUBKEY = "dummy_pubkey"
236 config.IRIS_ID_SECRET = "dummy_secret"
237 config.VERIFICATION_DATA_PUBLIC_KEY = bytes.fromhex(
238 "dd740a2b2a35bf05041a28257ea439b30f76f056f3698000b71e6470cd82275f"
239 )
242def test_strong_verification_happy_path(db, sv_config):
243 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man")
244 _, superuser_token = generate_user(is_superuser=True)
246 update_badges(empty_pb2.Empty())
247 refresh_materialized_views_rapid(empty_pb2.Empty())
249 with api_session(token) as api:
250 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
251 assert "strong_verification" not in res.badges
252 assert not res.has_strong_verification
253 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_UNVERIFIED
254 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_UNVERIFIED
255 assert (
256 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
257 == res.has_strong_verification
258 )
260 do_and_check_sv(
261 user,
262 token,
263 verification_id=5731012934821983,
264 sex="MALE",
265 dob="1988-01-01",
266 document_type="PASSPORT",
267 document_number="31195855",
268 document_expiry=default_expiry,
269 nationality="US",
270 )
272 with session_scope() as session:
273 verification_attempt = session.execute(
274 select(StrongVerificationAttempt).where(StrongVerificationAttempt.user_id == user.id)
275 ).scalar_one()
276 assert verification_attempt.status == StrongVerificationAttemptStatus.succeeded
277 assert verification_attempt.passport_date_of_birth == date(1988, 1, 1)
278 assert verification_attempt.passport_sex == PassportSex.male
279 assert verification_attempt.passport_expiry_date == default_expiry
280 assert verification_attempt.passport_nationality == "US"
281 assert verification_attempt.passport_last_three_document_chars == "855"
283 update_badges(empty_pb2.Empty())
284 refresh_materialized_views_rapid(empty_pb2.Empty())
286 # the user should now have strong verification
287 with api_session(token) as api:
288 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
289 assert "strong_verification" in res.badges
290 assert res.has_strong_verification
291 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED
292 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED
293 assert (
294 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
295 == res.has_strong_verification
296 )
298 # wrong dob = no badge
299 with session_scope() as session:
300 session.execute(update(User).where(User.id == user.id).values(birthdate=date(1988, 1, 2)))
302 update_badges(empty_pb2.Empty())
303 refresh_materialized_views_rapid(empty_pb2.Empty())
305 with api_session(token) as api:
306 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
307 assert "strong_verification" not in res.badges
308 assert not res.has_strong_verification
309 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_MISMATCH
310 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED
311 assert (
312 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
313 == res.has_strong_verification
314 )
316 # bad gender-sex correspondence = no badge
317 with session_scope() as session:
318 session.execute(update(User).where(User.id == user.id).values(birthdate=date(1988, 1, 1), gender="Woman"))
320 update_badges(empty_pb2.Empty())
321 refresh_materialized_views_rapid(empty_pb2.Empty())
323 with api_session(token) as api:
324 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
325 assert "strong_verification" not in res.badges
326 assert not res.has_strong_verification
327 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED
328 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH
329 assert (
330 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
331 == res.has_strong_verification
332 )
334 with account_session(token) as account:
335 res = account.GetAccountInfo(empty_pb2.Empty())
336 assert not res.has_strong_verification
337 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED
338 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH
340 # back to should have a badge
341 with session_scope() as session:
342 session.execute(update(User).where(User.id == user.id).values(gender="Man"))
344 update_badges(empty_pb2.Empty())
345 refresh_materialized_views_rapid(empty_pb2.Empty())
347 with api_session(token) as api:
348 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
349 assert "strong_verification" in res.badges
350 assert res.has_strong_verification
351 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED
352 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED
353 assert (
354 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
355 == res.has_strong_verification
356 )
358 # check has_passport_sex_gender_exception
359 with real_admin_session(superuser_token) as admin:
360 res = admin.GetUserDetails(admin_pb2.GetUserDetailsReq(user=user.username))
361 assert "strong_verification" in res.badges
362 assert res.has_strong_verification
363 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED
364 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED
366 admin.SetPassportSexGenderException(
367 admin_pb2.SetPassportSexGenderExceptionReq(user=user.username, passport_sex_gender_exception=True)
368 )
369 admin.ChangeUserGender(admin_pb2.ChangeUserGenderReq(user=user.username, gender="Woman"))
371 update_badges(empty_pb2.Empty())
372 refresh_materialized_views_rapid(empty_pb2.Empty())
374 with api_session(token) as api:
375 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
376 assert "strong_verification" in res.badges
377 assert res.has_strong_verification
378 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED
379 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED
380 assert (
381 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
382 == res.has_strong_verification
383 )
385 with real_admin_session(superuser_token) as admin:
386 res = admin.GetUserDetails(admin_pb2.GetUserDetailsReq(user=user.username))
387 assert "strong_verification" in res.badges
388 assert res.has_strong_verification
389 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED
390 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED
392 # now turn exception off
393 admin.SetPassportSexGenderException(
394 admin_pb2.SetPassportSexGenderExceptionReq(user=user.username, passport_sex_gender_exception=False)
395 )
397 update_badges(empty_pb2.Empty())
398 refresh_materialized_views_rapid(empty_pb2.Empty())
400 with api_session(token) as api:
401 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
402 assert "strong_verification" not in res.badges
403 assert not res.has_strong_verification
404 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED
405 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH
406 assert (
407 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
408 == res.has_strong_verification
409 )
411 with real_admin_session(superuser_token) as admin:
412 res = admin.GetUserDetails(admin_pb2.GetUserDetailsReq(user=user.username))
413 assert "strong_verification" not in res.badges
414 assert not res.has_strong_verification
415 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED
416 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_MISMATCH
419def test_strong_verification_delete_data(db, sv_config):
420 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man")
421 _, superuser_token = generate_user(is_superuser=True)
423 refresh_materialized_views_rapid(empty_pb2.Empty())
425 with api_session(token) as api:
426 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
427 assert (
428 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
429 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
430 )
432 # can remove SV data even if there is none, should do nothing
433 with account_session(token) as account:
434 account.DeleteStrongVerificationData(empty_pb2.Empty())
436 do_and_check_sv(
437 user,
438 token,
439 verification_id=5731012934821983,
440 sex="MALE",
441 dob="1988-01-01",
442 document_type="PASSPORT",
443 document_number="31195855",
444 document_expiry=default_expiry,
445 nationality="US",
446 )
448 refresh_materialized_views_rapid(empty_pb2.Empty())
450 # the user should now have strong verification
451 with api_session(token) as api:
452 assert api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
453 assert (
454 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
455 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
456 )
458 # check removing SV data
459 with account_session(token) as account:
460 account.DeleteStrongVerificationData(empty_pb2.Empty())
462 refresh_materialized_views_rapid(empty_pb2.Empty())
464 with api_session(token) as api:
465 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
466 assert (
467 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
468 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
469 )
471 with session_scope() as session:
472 assert (
473 len(
474 session.execute(
475 select(StrongVerificationAttempt).where(
476 or_(
477 StrongVerificationAttempt.passport_encrypted_data != None,
478 StrongVerificationAttempt.passport_date_of_birth != None,
479 StrongVerificationAttempt.passport_sex != None,
480 )
481 )
482 )
483 .scalars()
484 .all()
485 )
486 == 0
487 )
490def test_strong_verification_expiry(db, sv_config):
491 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man")
492 _, superuser_token = generate_user(is_superuser=True)
494 refresh_materialized_views_rapid(empty_pb2.Empty())
496 with api_session(token) as api:
497 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
498 assert (
499 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
500 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
501 )
503 expiry = date.today() + timedelta(days=10)
505 do_and_check_sv(
506 user,
507 token,
508 verification_id=5731012934821983,
509 sex="MALE",
510 dob="1988-01-01",
511 document_type="PASSPORT",
512 document_number="31195855",
513 document_expiry=expiry,
514 nationality="US",
515 )
517 # the user should now have strong verification
518 with api_session(token) as api:
519 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
520 assert res.has_strong_verification
521 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_VERIFIED
522 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_VERIFIED
524 with session_scope() as session:
525 attempt = session.execute(select(StrongVerificationAttempt)).scalars().one()
526 attempt.passport_expiry_date = date.today() - timedelta(days=2)
528 with api_session(token) as api:
529 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
530 assert not res.has_strong_verification
531 assert res.birthdate_verification_status == api_pb2.BIRTHDATE_VERIFICATION_STATUS_UNVERIFIED
532 assert res.gender_verification_status == api_pb2.GENDER_VERIFICATION_STATUS_UNVERIFIED
534 res = api.GetUser(api_pb2.GetUserReq(user=user.username))
535 assert not res.has_strong_verification
536 assert not res.has_strong_verification
538 do_and_check_sv(
539 user,
540 token,
541 verification_id=5731012934821985,
542 sex="MALE",
543 dob="1988-01-01",
544 document_type="PASSPORT",
545 document_number="PA41323412",
546 document_expiry=date.today() + timedelta(days=365),
547 nationality="AU",
548 )
550 refresh_materialized_views_rapid(empty_pb2.Empty())
552 with api_session(token) as api:
553 assert api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
554 assert (
555 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
556 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
557 )
560def test_strong_verification_regression(db, sv_config):
561 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man")
563 do_and_check_sv(
564 user,
565 token,
566 verification_id=5731012934821983,
567 sex="MALE",
568 dob="1988-01-01",
569 document_type="PASSPORT",
570 document_number="31195855",
571 document_expiry=default_expiry,
572 nationality="US",
573 return_after="INITIATED",
574 )
576 with api_session(token) as api:
577 api.Ping(api_pb2.PingReq())
580def test_strong_verification_regression2(db, sv_config):
581 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man")
583 do_and_check_sv(
584 user,
585 token,
586 verification_id=5731012934821983,
587 sex="MALE",
588 dob="1988-01-01",
589 document_type="PASSPORT",
590 document_number="31195855",
591 document_expiry=default_expiry,
592 nationality="US",
593 return_after="INITIATED",
594 )
596 do_and_check_sv(
597 user,
598 token,
599 verification_id=5731012934821985,
600 sex="MALE",
601 dob="1988-01-01",
602 document_type="PASSPORT",
603 document_number="PA41323412",
604 document_expiry=default_expiry,
605 nationality="AU",
606 )
608 refresh_materialized_views_rapid(empty_pb2.Empty())
610 with api_session(token) as api:
611 assert api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
612 assert (
613 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
614 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
615 )
618def test_strong_verification_disabled(db, feature_flags):
619 feature_flags.set("strong_verification_enabled", False)
620 user, token = generate_user()
622 with account_session(token) as account:
623 with pytest.raises(grpc.RpcError) as e:
624 account.InitiateStrongVerification(empty_pb2.Empty())
625 assert e.value.code() == grpc.StatusCode.UNAVAILABLE
626 assert e.value.details() == "Strong verification is currently disabled."
629def test_strong_verification_delete_data_cant_reverify(db, sv_config, push_collector: PushCollector):
630 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man")
631 _, superuser_token = generate_user(is_superuser=True)
633 refresh_materialized_views_rapid(empty_pb2.Empty())
635 with api_session(token) as api:
636 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
637 assert (
638 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
639 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
640 )
642 do_and_check_sv(
643 user,
644 token,
645 verification_id=5731012934821983,
646 sex="MALE",
647 dob="1988-01-01",
648 document_type="PASSPORT",
649 document_number="31195855",
650 document_expiry=default_expiry,
651 nationality="US",
652 )
654 refresh_materialized_views_rapid(empty_pb2.Empty())
656 # the user should now have strong verification
657 with api_session(token) as api:
658 assert api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
659 assert (
660 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
661 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
662 )
664 # There should be a notification confirming it
665 push_collector.pop_for_user(user.id, last=True)
667 # check removing SV data
668 with account_session(token) as account:
669 account.DeleteStrongVerificationData(empty_pb2.Empty())
671 refresh_materialized_views_rapid(empty_pb2.Empty())
673 with api_session(token) as api:
674 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
675 assert (
676 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
677 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
678 )
680 with session_scope() as session:
681 assert (
682 len(
683 session.execute(
684 select(StrongVerificationAttempt).where(
685 or_(
686 StrongVerificationAttempt.passport_encrypted_data != None,
687 StrongVerificationAttempt.passport_date_of_birth != None,
688 StrongVerificationAttempt.passport_sex != None,
689 )
690 )
691 )
692 .scalars()
693 .all()
694 )
695 == 0
696 )
698 reference_data = do_and_check_sv(
699 user,
700 token,
701 verification_id=5731012934821984,
702 sex="MALE",
703 dob="1988-01-01",
704 document_type="PASSPORT",
705 document_number="31195855",
706 document_expiry=default_expiry,
707 nationality="US",
708 return_after="APPROVED",
709 )
711 with patch("couchers.jobs.handlers.requests.post") as mock:
712 json_resp2 = {
713 "id": 5731012934821984,
714 "created": "2024-05-11T15:46:46Z",
715 "expires": "2024-05-11T16:17:26Z",
716 "state": "APPROVED",
717 "reference": reference_data,
718 "user_ip": "10.123.123.123",
719 "user_agent": "Iris%20ID/168357896 CFNetwork/1494.0.7 Darwin/23.4.0",
720 "given_names": "John Wayne",
721 "surname": "Doe",
722 "nationality": "US",
723 "sex": "MALE",
724 "date_of_birth": "1988-01-01",
725 "document_type": "PASSPORT",
726 "document_number": "31195855",
727 "expiry_date": default_expiry.isoformat(),
728 "issuing_country": "US",
729 "issuer": "Department of State, U.S. Government",
730 "portrait": "dGVzdHRlc3R0ZXN0...",
731 }
732 mock.return_value = type(
733 "__MockResponse",
734 (),
735 {
736 "status_code": 200,
737 "text": json.dumps(json_resp2),
738 "json": lambda: json_resp2,
739 },
740 )
741 while process_job():
742 pass
744 mock.assert_called_once_with(
745 "https://passportreader.app/api/v1/session.get",
746 auth=("dummy_pubkey", "dummy_secret"),
747 json={"id": 5731012934821984},
748 timeout=10,
749 verify="/etc/ssl/certs/ca-certificates.crt",
750 )
752 with session_scope() as session:
753 verification_attempt = session.execute(
754 select(StrongVerificationAttempt).where(StrongVerificationAttempt.iris_session_id == 5731012934821984)
755 ).scalar_one()
756 assert verification_attempt.user_id == user.id
757 assert verification_attempt.status == StrongVerificationAttemptStatus.duplicate
759 push = push_collector.pop_for_user(user.id, last=True)
760 assert push.content.title == "Strong Verification failed"
761 assert (
762 push.content.body
763 == "You used a passport that has already been used for verification. Please use another passport."
764 )
766 refresh_materialized_views_rapid(empty_pb2.Empty())
768 with api_session(token) as api:
769 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
770 assert (
771 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
772 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
773 )
776def test_strong_verification_duplicate_other_user(db, sv_config, push_collector: PushCollector):
777 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man")
778 user2, token2 = generate_user(birthdate=date(1988, 1, 1), gender="Man")
779 _, superuser_token = generate_user(is_superuser=True)
781 refresh_materialized_views_rapid(empty_pb2.Empty())
783 with api_session(token) as api:
784 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
785 assert (
786 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
787 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
788 )
790 # can remove SV data even if there is none, should do nothing
791 with account_session(token) as account:
792 account.DeleteStrongVerificationData(empty_pb2.Empty())
794 do_and_check_sv(
795 user,
796 token,
797 verification_id=5731012934821983,
798 sex="MALE",
799 dob="1988-01-01",
800 document_type="PASSPORT",
801 document_number="31195855",
802 document_expiry=default_expiry,
803 nationality="US",
804 )
806 refresh_materialized_views_rapid(empty_pb2.Empty())
808 # the user should now have strong verification
809 with api_session(token) as api:
810 assert api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
811 assert (
812 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
813 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
814 )
816 # check removing SV data
817 with account_session(token) as account:
818 account.DeleteStrongVerificationData(empty_pb2.Empty())
820 refresh_materialized_views_rapid(empty_pb2.Empty())
822 with api_session(token) as api:
823 assert not api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
824 assert (
825 api.GetLiteUser(api_pb2.GetLiteUserReq(user=user.username)).has_strong_verification
826 == api.GetUser(api_pb2.GetUserReq(user=user.username)).has_strong_verification
827 )
829 with session_scope() as session:
830 assert (
831 len(
832 session.execute(
833 select(StrongVerificationAttempt).where(
834 or_(
835 StrongVerificationAttempt.passport_encrypted_data != None,
836 StrongVerificationAttempt.passport_date_of_birth != None,
837 StrongVerificationAttempt.passport_sex != None,
838 )
839 )
840 )
841 .scalars()
842 .all()
843 )
844 == 0
845 )
847 reference_data = do_and_check_sv(
848 user2,
849 token2,
850 verification_id=5731012934821984,
851 sex="MALE",
852 dob="1988-01-01",
853 document_type="PASSPORT",
854 document_number="31195855",
855 document_expiry=default_expiry,
856 nationality="US",
857 return_after="APPROVED",
858 )
860 with patch("couchers.jobs.handlers.requests.post") as mock:
861 json_resp2 = {
862 "id": 5731012934821984,
863 "created": "2024-05-11T15:46:46Z",
864 "expires": "2024-05-11T16:17:26Z",
865 "state": "APPROVED",
866 "reference": reference_data,
867 "user_ip": "10.123.123.123",
868 "user_agent": "Iris%20ID/168357896 CFNetwork/1494.0.7 Darwin/23.4.0",
869 "given_names": "John Wayne",
870 "surname": "Doe",
871 "nationality": "US",
872 "sex": "MALE",
873 "date_of_birth": "1988-01-01",
874 "document_type": "PASSPORT",
875 "document_number": "31195855",
876 "expiry_date": default_expiry.isoformat(),
877 "issuing_country": "US",
878 "issuer": "Department of State, U.S. Government",
879 "portrait": "dGVzdHRlc3R0ZXN0...",
880 }
881 mock.return_value = type(
882 "__MockResponse",
883 (),
884 {
885 "status_code": 200,
886 "text": json.dumps(json_resp2),
887 "json": lambda: json_resp2,
888 },
889 )
890 while process_job():
891 pass
893 mock.assert_called_once_with(
894 "https://passportreader.app/api/v1/session.get",
895 auth=("dummy_pubkey", "dummy_secret"),
896 json={"id": 5731012934821984},
897 timeout=10,
898 verify="/etc/ssl/certs/ca-certificates.crt",
899 )
901 with session_scope() as session:
902 verification_attempt = session.execute(
903 select(StrongVerificationAttempt).where(StrongVerificationAttempt.iris_session_id == 5731012934821984)
904 ).scalar_one()
905 assert verification_attempt.user_id == user2.id
906 assert verification_attempt.status == StrongVerificationAttemptStatus.duplicate
908 push = push_collector.pop_for_user(user2.id, last=True)
909 assert push.content.title == "Strong Verification failed"
910 assert (
911 push.content.body
912 == "You used a passport that has already been used for verification. Please use another passport."
913 )
916def test_strong_verification_non_passport(db, sv_config, push_collector: PushCollector):
917 user, token = generate_user(birthdate=date(1988, 1, 1), gender="Man")
918 _, superuser_token = generate_user(is_superuser=True)
920 reference_data = do_and_check_sv(
921 user,
922 token,
923 verification_id=5731012934821984,
924 sex="MALE",
925 dob="1988-01-01",
926 document_type="IDENTITY_CARD",
927 document_number="31195855",
928 document_expiry=default_expiry,
929 nationality="US",
930 return_after="APPROVED",
931 )
933 with patch("couchers.jobs.handlers.requests.post") as mock:
934 json_resp2 = {
935 "id": 5731012934821984,
936 "created": "2024-05-11T15:46:46Z",
937 "expires": "2024-05-11T16:17:26Z",
938 "state": "APPROVED",
939 "reference": reference_data,
940 "user_ip": "10.123.123.123",
941 "user_agent": "Iris%20ID/168357896 CFNetwork/1494.0.7 Darwin/23.4.0",
942 "given_names": "John Wayne",
943 "surname": "Doe",
944 "nationality": "US",
945 "sex": "MALE",
946 "date_of_birth": "1988-01-01",
947 "document_type": "IDENTITY_CARD",
948 "document_number": "31195855",
949 "expiry_date": default_expiry.isoformat(),
950 "issuing_country": "US",
951 "issuer": "Department of State, U.S. Government",
952 "portrait": "dGVzdHRlc3R0ZXN0...",
953 }
954 mock.return_value = type(
955 "__MockResponse",
956 (),
957 {
958 "status_code": 200,
959 "text": json.dumps(json_resp2),
960 "json": lambda: json_resp2,
961 },
962 )
963 while process_job():
964 pass
966 mock.assert_called_once_with(
967 "https://passportreader.app/api/v1/session.get",
968 auth=("dummy_pubkey", "dummy_secret"),
969 json={"id": 5731012934821984},
970 timeout=10,
971 verify="/etc/ssl/certs/ca-certificates.crt",
972 )
974 with session_scope() as session:
975 verification_attempt = session.execute(
976 select(StrongVerificationAttempt).where(StrongVerificationAttempt.iris_session_id == 5731012934821984)
977 ).scalar_one()
978 assert verification_attempt.user_id == user.id
979 assert verification_attempt.status == StrongVerificationAttemptStatus.failed
981 push = push_collector.pop_for_user(user.id, last=True)
982 assert push.content.title == "Strong Verification failed"
983 assert (
984 push.content.body
985 == "You used a document other than a passport. You can only use a passport for Strong Verification."
986 )
989def test_lite_user_strong_verification_not_shared_between_users(db):
990 # regression: the lite_users view joined attempts to users only on birthdate/gender, so anyone born on the same
991 # day as a strongly verified user (with a compatible gender) got the badge too
992 verified_user, _ = generate_user(birthdate=date(1988, 1, 1), gender="Man", strong_verification=True)
993 other_user, other_token = generate_user(birthdate=date(1988, 1, 1), gender="Man")
995 refresh_materialized_views_rapid(empty_pb2.Empty())
997 with api_session(other_token) as api:
998 assert api.GetLiteUser(api_pb2.GetLiteUserReq(user=verified_user.username)).has_strong_verification
999 assert not api.GetLiteUser(api_pb2.GetLiteUserReq(user=other_user.username)).has_strong_verification
1002def test_attempt_only_verifies_its_own_user(db):
1003 verified_user, _ = generate_user(birthdate=date(1988, 1, 1), gender="Man", strong_verification=True)
1004 other_user, _ = generate_user(birthdate=date(1988, 1, 1), gender="Man")
1006 with session_scope() as session:
1007 attempt = session.execute(
1008 select(StrongVerificationAttempt).where(StrongVerificationAttempt.user_id == verified_user.id)
1009 ).scalar_one()
1010 assert attempt.has_strong_verification(verified_user)
1011 assert not attempt.has_strong_verification(other_user)
1013 # as a SQL expression, without the join the two tables are still linked by the birthdate and gender predicates
1014 assert session.execute(
1015 select(User.id)
1016 .select_from(StrongVerificationAttempt)
1017 .where(StrongVerificationAttempt.has_strong_verification(User))
1018 ).scalars().all() == [verified_user.id]