Coverage for src/tests/test_postal_verification.py: 100%
295 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-20 11:53 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-20 11:53 +0000
1from datetime import timedelta
2from unittest.mock import patch
4import grpc
5import pytest
7import couchers.servicers.postal_verification
8from couchers.config import config
9from couchers.constants import (
10 POSTAL_VERIFICATION_CODE_LIFETIME,
11 POSTAL_VERIFICATION_MAX_ATTEMPTS,
12 POSTAL_VERIFICATION_RATE_LIMIT,
13)
14from couchers.db import session_scope
15from couchers.helpers.postal_verification import generate_postal_verification_code, has_postal_verification
16from couchers.jobs.worker import process_job
17from couchers.models import User
18from couchers.models.postal_verification import PostalVerificationAttempt
19from couchers.proto import postal_verification_pb2
20from couchers.sql import couchers_select as select
21from couchers.utils import now
22from tests.test_fixtures import generate_user, postal_verification_session
25@pytest.fixture(autouse=True)
26def _(testconfig):
27 pass
30def _monkeypatch_postal_verification_config(monkeypatch):
31 new_config = config.copy()
32 new_config["ENABLE_POSTAL_VERIFICATION"] = True
33 monkeypatch.setattr(couchers.servicers.postal_verification, "config", new_config)
36def test_generate_postal_verification_code():
37 """Test that generated codes meet requirements."""
38 allowed = set("ABCDEFGHJKLMNPQRSTUVWXYZ23456789")
39 for _ in range(100):
40 code = generate_postal_verification_code()
41 assert len(code) == 6
42 assert all(c in allowed for c in code)
43 # Should not contain confusing characters
44 for char in "IO01":
45 assert char not in code
48def test_postal_verification_disabled(db):
49 """Test that postal verification is disabled by default."""
50 user, token = generate_user()
52 with postal_verification_session(token) as pv:
53 with pytest.raises(grpc.RpcError) as e:
54 pv.InitiatePostalVerification(
55 postal_verification_pb2.InitiatePostalVerificationReq(
56 address=postal_verification_pb2.PostalAddress(
57 address_line_1="123 Main St",
58 city="Test City",
59 country="US",
60 )
61 )
62 )
63 assert e.value.code() == grpc.StatusCode.UNAVAILABLE
66def test_postal_verification_happy_path(db, monkeypatch):
67 """Test the complete happy path for postal verification."""
68 _monkeypatch_postal_verification_config(monkeypatch)
70 user, token = generate_user()
72 # Check initial status
73 with postal_verification_session(token) as pv:
74 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
75 assert not status.has_postal_verification
76 assert status.can_initiate_new_attempt
77 assert not status.has_active_attempt
79 # Step 1: Initiate postal verification
80 with postal_verification_session(token) as pv:
81 res = pv.InitiatePostalVerification(
82 postal_verification_pb2.InitiatePostalVerificationReq(
83 address=postal_verification_pb2.PostalAddress(
84 address_line_1="123 Main St",
85 address_line_2="Apt 4",
86 city="Test City",
87 state="CA",
88 postal_code="12345",
89 country="US",
90 )
91 )
92 )
93 attempt_id = res.postal_verification_attempt_id
94 assert attempt_id > 0
96 # Check status after initiation
97 with postal_verification_session(token) as pv:
98 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
99 assert not status.has_postal_verification
100 assert not status.can_initiate_new_attempt
101 assert status.has_active_attempt
102 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_PENDING_ADDRESS_CONFIRMATION
104 # Step 2: Confirm address
105 with postal_verification_session(token) as pv:
106 pv.ConfirmPostalAddress(
107 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
108 )
110 # Check status after confirmation
111 with postal_verification_session(token) as pv:
112 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
113 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_IN_PROGRESS
115 # Process background job to send postcard
116 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
117 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
118 while process_job():
119 pass
121 # Check status after postcard sent
122 with postal_verification_session(token) as pv:
123 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
124 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_AWAITING_VERIFICATION
125 assert status.postcard_sent_at.seconds > 0
127 # Get the verification code from the database
128 with session_scope() as session:
129 attempt = session.execute(
130 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id)
131 ).scalar_one()
132 verification_code = attempt.verification_code
134 # Step 3: Verify the code
135 with postal_verification_session(token) as pv:
136 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code))
137 assert res.success
139 # Check final status
140 with postal_verification_session(token) as pv:
141 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
142 assert status.has_postal_verification
143 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_SUCCEEDED
145 # Verify with helper function
146 with session_scope() as session:
147 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one()
148 assert has_postal_verification(session, db_user)
151def test_postal_verification_wrong_code(db, monkeypatch):
152 """Test entering wrong verification codes."""
153 _monkeypatch_postal_verification_config(monkeypatch)
155 user, token = generate_user()
157 # Initiate and confirm
158 with postal_verification_session(token) as pv:
159 res = pv.InitiatePostalVerification(
160 postal_verification_pb2.InitiatePostalVerificationReq(
161 address=postal_verification_pb2.PostalAddress(
162 address_line_1="123 Main St",
163 city="Test City",
164 country="US",
165 )
166 )
167 )
168 attempt_id = res.postal_verification_attempt_id
170 with postal_verification_session(token) as pv:
171 pv.ConfirmPostalAddress(
172 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
173 )
175 # Process background job
176 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
177 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
178 while process_job():
179 pass
181 # Try wrong codes
182 with postal_verification_session(token) as pv:
183 for i in range(POSTAL_VERIFICATION_MAX_ATTEMPTS - 1):
184 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code="WRONGX"))
185 assert not res.success
186 assert res.remaining_attempts == POSTAL_VERIFICATION_MAX_ATTEMPTS - 1 - i
188 # Last attempt should fail and lock the attempt
189 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code="WRONGX"))
190 assert not res.success
191 assert res.remaining_attempts == 0
193 # Check status is failed
194 with postal_verification_session(token) as pv:
195 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
196 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_FAILED
199def test_postal_verification_code_expiry(db, monkeypatch):
200 """Test that codes expire after the configured lifetime."""
201 _monkeypatch_postal_verification_config(monkeypatch)
203 user, token = generate_user()
205 # Initiate and confirm
206 with postal_verification_session(token) as pv:
207 res = pv.InitiatePostalVerification(
208 postal_verification_pb2.InitiatePostalVerificationReq(
209 address=postal_verification_pb2.PostalAddress(
210 address_line_1="123 Main St",
211 city="Test City",
212 country="US",
213 )
214 )
215 )
216 attempt_id = res.postal_verification_attempt_id
218 with postal_verification_session(token) as pv:
219 pv.ConfirmPostalAddress(
220 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
221 )
223 # Process background job
224 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
225 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
226 while process_job():
227 pass
229 # Get the code
230 with session_scope() as session:
231 attempt = session.execute(
232 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id)
233 ).scalar_one()
234 verification_code = attempt.verification_code
235 # Set postcard_sent_at to be past expiry
236 attempt.postcard_sent_at = now() - POSTAL_VERIFICATION_CODE_LIFETIME - timedelta(days=1)
238 # Try to verify - should fail due to expiry
239 with postal_verification_session(token) as pv:
240 with pytest.raises(grpc.RpcError) as e:
241 pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code))
242 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
245def test_postal_verification_rate_limit(db, monkeypatch):
246 """Test rate limiting on postal verification attempts."""
247 _monkeypatch_postal_verification_config(monkeypatch)
249 user, token = generate_user()
251 # First attempt
252 with postal_verification_session(token) as pv:
253 res = pv.InitiatePostalVerification(
254 postal_verification_pb2.InitiatePostalVerificationReq(
255 address=postal_verification_pb2.PostalAddress(
256 address_line_1="123 Main St",
257 city="Test City",
258 country="US",
259 )
260 )
261 )
262 attempt_id = res.postal_verification_attempt_id
264 # Cancel the first attempt
265 with postal_verification_session(token) as pv:
266 pv.CancelPostalVerification(
267 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id)
268 )
270 # Try to initiate again immediately - should be rate limited
271 with postal_verification_session(token) as pv:
272 with pytest.raises(grpc.RpcError) as e:
273 pv.InitiatePostalVerification(
274 postal_verification_pb2.InitiatePostalVerificationReq(
275 address=postal_verification_pb2.PostalAddress(
276 address_line_1="456 Other St",
277 city="Test City",
278 country="US",
279 )
280 )
281 )
282 assert e.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
284 # Check status shows rate limit info
285 with postal_verification_session(token) as pv:
286 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
287 assert not status.can_initiate_new_attempt
288 assert status.next_attempt_allowed_at.seconds > 0
291def test_postal_verification_already_in_progress(db, monkeypatch):
292 """Test that you can't start a new attempt while one is in progress."""
293 _monkeypatch_postal_verification_config(monkeypatch)
295 user, token = generate_user()
297 # First attempt
298 with postal_verification_session(token) as pv:
299 pv.InitiatePostalVerification(
300 postal_verification_pb2.InitiatePostalVerificationReq(
301 address=postal_verification_pb2.PostalAddress(
302 address_line_1="123 Main St",
303 city="Test City",
304 country="US",
305 )
306 )
307 )
309 # Try to initiate another - should fail
310 with postal_verification_session(token) as pv:
311 with pytest.raises(grpc.RpcError) as e:
312 pv.InitiatePostalVerification(
313 postal_verification_pb2.InitiatePostalVerificationReq(
314 address=postal_verification_pb2.PostalAddress(
315 address_line_1="456 Other St",
316 city="Test City",
317 country="US",
318 )
319 )
320 )
321 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
324def test_postal_verification_cancel(db, monkeypatch):
325 """Test cancelling a postal verification attempt."""
326 _monkeypatch_postal_verification_config(monkeypatch)
328 user, token = generate_user()
330 # Initiate
331 with postal_verification_session(token) as pv:
332 res = pv.InitiatePostalVerification(
333 postal_verification_pb2.InitiatePostalVerificationReq(
334 address=postal_verification_pb2.PostalAddress(
335 address_line_1="123 Main St",
336 city="Test City",
337 country="US",
338 )
339 )
340 )
341 attempt_id = res.postal_verification_attempt_id
343 # Cancel
344 with postal_verification_session(token) as pv:
345 pv.CancelPostalVerification(
346 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id)
347 )
349 # Check status
350 with postal_verification_session(token) as pv:
351 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
352 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_CANCELLED
353 assert not status.has_active_attempt
356def test_postal_verification_can_cancel_after_postcard_sent(db, monkeypatch):
357 """Test that you CAN cancel after the postcard is sent (e.g., if postcard is lost)."""
358 _monkeypatch_postal_verification_config(monkeypatch)
360 user, token = generate_user()
362 # Initiate and confirm
363 with postal_verification_session(token) as pv:
364 res = pv.InitiatePostalVerification(
365 postal_verification_pb2.InitiatePostalVerificationReq(
366 address=postal_verification_pb2.PostalAddress(
367 address_line_1="123 Main St",
368 city="Test City",
369 country="US",
370 )
371 )
372 )
373 attempt_id = res.postal_verification_attempt_id
375 with postal_verification_session(token) as pv:
376 pv.ConfirmPostalAddress(
377 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
378 )
380 # Process background job
381 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
382 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
383 while process_job():
384 pass
386 # Verify status is awaiting_verification
387 with postal_verification_session(token) as pv:
388 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
389 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_AWAITING_VERIFICATION
391 # Cancel - should succeed (user can cancel if postcard is lost)
392 with postal_verification_session(token) as pv:
393 pv.CancelPostalVerification(
394 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id)
395 )
397 # Verify status is cancelled
398 with postal_verification_session(token) as pv:
399 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
400 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_CANCELLED
401 assert not status.has_active_attempt
404def test_postal_verification_list_attempts(db, monkeypatch):
405 """Test listing postal verification attempts."""
406 _monkeypatch_postal_verification_config(monkeypatch)
408 user, token = generate_user()
410 # Create first attempt and cancel it
411 with postal_verification_session(token) as pv:
412 res = pv.InitiatePostalVerification(
413 postal_verification_pb2.InitiatePostalVerificationReq(
414 address=postal_verification_pb2.PostalAddress(
415 address_line_1="123 Main St",
416 city="Test City",
417 country="US",
418 )
419 )
420 )
421 attempt_id_1 = res.postal_verification_attempt_id
423 with postal_verification_session(token) as pv:
424 pv.CancelPostalVerification(
425 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id_1)
426 )
428 # Move created time back to bypass rate limit
429 with session_scope() as session:
430 attempt = session.execute(
431 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id_1)
432 ).scalar_one()
433 attempt.created = now() - POSTAL_VERIFICATION_RATE_LIMIT - timedelta(days=1)
435 # Create second attempt
436 with postal_verification_session(token) as pv:
437 res = pv.InitiatePostalVerification(
438 postal_verification_pb2.InitiatePostalVerificationReq(
439 address=postal_verification_pb2.PostalAddress(
440 address_line_1="456 Other St",
441 city="Other City",
442 country="CA",
443 )
444 )
445 )
446 attempt_id_2 = res.postal_verification_attempt_id
448 # List attempts
449 with postal_verification_session(token) as pv:
450 res = pv.ListPostalVerificationAttempts(postal_verification_pb2.ListPostalVerificationAttemptsReq())
451 assert len(res.attempts) == 2
452 # Most recent first
453 assert res.attempts[0].postal_verification_attempt_id == attempt_id_2
454 assert res.attempts[1].postal_verification_attempt_id == attempt_id_1
457def test_postal_verification_address_validation(db, monkeypatch):
458 """Test address validation errors."""
459 _monkeypatch_postal_verification_config(monkeypatch)
461 user, token = generate_user()
463 # Missing required fields
464 with postal_verification_session(token) as pv:
465 # Missing address_line_1
466 with pytest.raises(grpc.RpcError) as e:
467 pv.InitiatePostalVerification(
468 postal_verification_pb2.InitiatePostalVerificationReq(
469 address=postal_verification_pb2.PostalAddress(
470 city="Test City",
471 country="US",
472 )
473 )
474 )
475 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
477 # Missing city
478 with pytest.raises(grpc.RpcError) as e:
479 pv.InitiatePostalVerification(
480 postal_verification_pb2.InitiatePostalVerificationReq(
481 address=postal_verification_pb2.PostalAddress(
482 address_line_1="123 Main St",
483 country="US",
484 )
485 )
486 )
487 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
489 # Missing country
490 with pytest.raises(grpc.RpcError) as e:
491 pv.InitiatePostalVerification(
492 postal_verification_pb2.InitiatePostalVerificationReq(
493 address=postal_verification_pb2.PostalAddress(
494 address_line_1="123 Main St",
495 city="Test City",
496 )
497 )
498 )
499 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
502def test_postal_verification_postcard_send_failure(db, monkeypatch):
503 """Test handling of postcard send failure."""
504 _monkeypatch_postal_verification_config(monkeypatch)
506 user, token = generate_user()
508 # Initiate and confirm
509 with postal_verification_session(token) as pv:
510 res = pv.InitiatePostalVerification(
511 postal_verification_pb2.InitiatePostalVerificationReq(
512 address=postal_verification_pb2.PostalAddress(
513 address_line_1="123 Main St",
514 city="Test City",
515 country="US",
516 )
517 )
518 )
519 attempt_id = res.postal_verification_attempt_id
521 with postal_verification_session(token) as pv:
522 pv.ConfirmPostalAddress(
523 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
524 )
526 # Simulate postcard send failure
527 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
528 mock_send.return_value = type("PostcardResult", (), {"success": False, "error_message": "API error"})()
529 while process_job():
530 pass
532 # Check status is failed
533 with postal_verification_session(token) as pv:
534 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
535 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_FAILED
538def test_postal_verification_code_case_insensitive(db, monkeypatch):
539 """Test that verification codes are case insensitive."""
540 _monkeypatch_postal_verification_config(monkeypatch)
542 user, token = generate_user()
544 # Initiate and confirm
545 with postal_verification_session(token) as pv:
546 res = pv.InitiatePostalVerification(
547 postal_verification_pb2.InitiatePostalVerificationReq(
548 address=postal_verification_pb2.PostalAddress(
549 address_line_1="123 Main St",
550 city="Test City",
551 country="US",
552 )
553 )
554 )
555 attempt_id = res.postal_verification_attempt_id
557 with postal_verification_session(token) as pv:
558 pv.ConfirmPostalAddress(
559 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
560 )
562 # Process background job
563 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
564 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
565 while process_job():
566 pass
568 # Get the code
569 with session_scope() as session:
570 attempt = session.execute(
571 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id)
572 ).scalar_one()
573 verification_code = attempt.verification_code
575 # Verify with lowercase code
576 with postal_verification_session(token) as pv:
577 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code.lower()))
578 assert res.success
581def test_postal_verification_attempt_not_found(db, monkeypatch):
582 """Test accessing non-existent attempts."""
583 _monkeypatch_postal_verification_config(monkeypatch)
585 user, token = generate_user()
587 with postal_verification_session(token) as pv:
588 # Try to confirm non-existent attempt
589 with pytest.raises(grpc.RpcError) as e:
590 pv.ConfirmPostalAddress(
591 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=999999)
592 )
593 assert e.value.code() == grpc.StatusCode.NOT_FOUND
595 # Try to cancel non-existent attempt
596 with pytest.raises(grpc.RpcError) as e:
597 pv.CancelPostalVerification(
598 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=999999)
599 )
600 assert e.value.code() == grpc.StatusCode.NOT_FOUND
603def test_postal_verification_other_user_attempt(db, monkeypatch):
604 """Test that users cannot access other users' attempts."""
605 _monkeypatch_postal_verification_config(monkeypatch)
607 user1, token1 = generate_user()
608 user2, token2 = generate_user()
610 # User 1 creates an attempt
611 with postal_verification_session(token1) as pv:
612 res = pv.InitiatePostalVerification(
613 postal_verification_pb2.InitiatePostalVerificationReq(
614 address=postal_verification_pb2.PostalAddress(
615 address_line_1="123 Main St",
616 city="Test City",
617 country="US",
618 )
619 )
620 )
621 attempt_id = res.postal_verification_attempt_id
623 # User 2 tries to confirm user 1's attempt
624 with postal_verification_session(token2) as pv:
625 with pytest.raises(grpc.RpcError) as e:
626 pv.ConfirmPostalAddress(
627 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
628 )
629 assert e.value.code() == grpc.StatusCode.NOT_FOUND
631 # User 2 tries to cancel user 1's attempt
632 with postal_verification_session(token2) as pv:
633 with pytest.raises(grpc.RpcError) as e:
634 pv.CancelPostalVerification(
635 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id)
636 )
637 assert e.value.code() == grpc.StatusCode.NOT_FOUND
640def test_has_postal_verification_helper(db, monkeypatch):
641 """Test the has_postal_verification helper function."""
642 _monkeypatch_postal_verification_config(monkeypatch)
644 user, token = generate_user()
646 # Initially no verification
647 with session_scope() as session:
648 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one()
649 assert not has_postal_verification(session, db_user)
651 # Complete verification
652 with postal_verification_session(token) as pv:
653 res = pv.InitiatePostalVerification(
654 postal_verification_pb2.InitiatePostalVerificationReq(
655 address=postal_verification_pb2.PostalAddress(
656 address_line_1="123 Main St",
657 city="Test City",
658 country="US",
659 )
660 )
661 )
662 attempt_id = res.postal_verification_attempt_id
664 with postal_verification_session(token) as pv:
665 pv.ConfirmPostalAddress(
666 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
667 )
669 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
670 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
671 while process_job():
672 pass
674 with session_scope() as session:
675 attempt = session.execute(
676 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id)
677 ).scalar_one()
678 verification_code = attempt.verification_code
680 with postal_verification_session(token) as pv:
681 pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code))
683 # Now should have verification
684 with session_scope() as session:
685 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one()
686 assert has_postal_verification(session, db_user)