Coverage for app / backend / src / tests / test_postal_verification.py: 100%
297 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
1from datetime import timedelta
2from unittest.mock import patch
4import grpc
5import pytest
6from sqlalchemy import select
8import couchers.servicers.postal_verification
9from couchers.config import config
10from couchers.constants import (
11 POSTAL_VERIFICATION_CODE_LIFETIME,
12 POSTAL_VERIFICATION_MAX_ATTEMPTS,
13 POSTAL_VERIFICATION_RATE_LIMIT,
14)
15from couchers.db import session_scope
16from couchers.helpers.postal_verification import generate_postal_verification_code, has_postal_verification
17from couchers.jobs.worker import process_job
18from couchers.models import User
19from couchers.models.postal_verification import PostalVerificationAttempt
20from couchers.proto import postal_verification_pb2
21from couchers.utils import now
22from tests.fixtures.db import generate_user
23from tests.fixtures.sessions import postal_verification_session
26@pytest.fixture(autouse=True)
27def _(testconfig):
28 pass
31def _monkeypatch_postal_verification_config(monkeypatch):
32 new_config = config.copy()
33 new_config["ENABLE_POSTAL_VERIFICATION"] = True
34 monkeypatch.setattr(couchers.servicers.postal_verification, "config", new_config)
37def test_generate_postal_verification_code():
38 """Test that generated codes meet requirements."""
39 allowed = set("ABCDEFGHJKLMNPQRSTUVWXYZ23456789")
40 for _ in range(100):
41 code = generate_postal_verification_code()
42 assert len(code) == 6
43 assert all(c in allowed for c in code)
44 # Should not contain confusing characters
45 for char in "IO01":
46 assert char not in code
49def test_postal_verification_disabled(db):
50 """Test that postal verification is disabled by default."""
51 user, token = generate_user()
53 with postal_verification_session(token) as pv:
54 with pytest.raises(grpc.RpcError) as e:
55 pv.InitiatePostalVerification(
56 postal_verification_pb2.InitiatePostalVerificationReq(
57 address=postal_verification_pb2.PostalAddress(
58 address_line_1="123 Main St",
59 city="Test City",
60 country="US",
61 )
62 )
63 )
64 assert e.value.code() == grpc.StatusCode.UNAVAILABLE
67def test_postal_verification_happy_path(db, monkeypatch):
68 """Test the complete happy path for postal verification."""
69 _monkeypatch_postal_verification_config(monkeypatch)
71 user, token = generate_user()
73 # Check initial status
74 with postal_verification_session(token) as pv:
75 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
76 assert not status.has_postal_verification
77 assert status.can_initiate_new_attempt
78 assert not status.has_active_attempt
80 # Step 1: Initiate postal verification
81 with postal_verification_session(token) as pv:
82 res = pv.InitiatePostalVerification(
83 postal_verification_pb2.InitiatePostalVerificationReq(
84 address=postal_verification_pb2.PostalAddress(
85 address_line_1="123 Main St",
86 address_line_2="Apt 4",
87 city="Test City",
88 state="CA",
89 postal_code="12345",
90 country="US",
91 )
92 )
93 )
94 attempt_id = res.postal_verification_attempt_id
95 assert attempt_id > 0
97 # Check status after initiation
98 with postal_verification_session(token) as pv:
99 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
100 assert not status.has_postal_verification
101 assert not status.can_initiate_new_attempt
102 assert status.has_active_attempt
103 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_PENDING_ADDRESS_CONFIRMATION
105 # Step 2: Confirm address
106 with postal_verification_session(token) as pv:
107 pv.ConfirmPostalAddress(
108 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
109 )
111 # Check status after confirmation
112 with postal_verification_session(token) as pv:
113 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
114 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_IN_PROGRESS
116 # Process background job to send postcard
117 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
118 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
119 while process_job():
120 pass
122 # Check status after postcard sent
123 with postal_verification_session(token) as pv:
124 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
125 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_AWAITING_VERIFICATION
126 assert status.postcard_sent_at.seconds > 0
128 # Get the verification code from the database
129 with session_scope() as session:
130 attempt = session.execute(
131 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id)
132 ).scalar_one()
133 verification_code = attempt.verification_code
135 # Step 3: Verify the code
136 with postal_verification_session(token) as pv:
137 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code))
138 assert res.success
140 # Check final status
141 with postal_verification_session(token) as pv:
142 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
143 assert status.has_postal_verification
144 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_SUCCEEDED
146 # Verify with helper function
147 with session_scope() as session:
148 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one()
149 assert has_postal_verification(session, db_user)
152def test_postal_verification_wrong_code(db, monkeypatch):
153 """Test entering wrong verification codes."""
154 _monkeypatch_postal_verification_config(monkeypatch)
156 user, token = generate_user()
158 # Initiate and confirm
159 with postal_verification_session(token) as pv:
160 res = pv.InitiatePostalVerification(
161 postal_verification_pb2.InitiatePostalVerificationReq(
162 address=postal_verification_pb2.PostalAddress(
163 address_line_1="123 Main St",
164 city="Test City",
165 country="US",
166 )
167 )
168 )
169 attempt_id = res.postal_verification_attempt_id
171 with postal_verification_session(token) as pv:
172 pv.ConfirmPostalAddress(
173 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
174 )
176 # Process background job
177 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
178 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
179 while process_job():
180 pass
182 # Try wrong codes
183 with postal_verification_session(token) as pv:
184 for i in range(POSTAL_VERIFICATION_MAX_ATTEMPTS - 1):
185 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code="WRONGX"))
186 assert not res.success
187 assert res.remaining_attempts == POSTAL_VERIFICATION_MAX_ATTEMPTS - 1 - i
189 # Last attempt should fail and lock the attempt
190 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code="WRONGX"))
191 assert not res.success
192 assert res.remaining_attempts == 0
194 # Check status is failed
195 with postal_verification_session(token) as pv:
196 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
197 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_FAILED
200def test_postal_verification_code_expiry(db, monkeypatch):
201 """Test that codes expire after the configured lifetime."""
202 _monkeypatch_postal_verification_config(monkeypatch)
204 user, token = generate_user()
206 # Initiate and confirm
207 with postal_verification_session(token) as pv:
208 res = pv.InitiatePostalVerification(
209 postal_verification_pb2.InitiatePostalVerificationReq(
210 address=postal_verification_pb2.PostalAddress(
211 address_line_1="123 Main St",
212 city="Test City",
213 country="US",
214 )
215 )
216 )
217 attempt_id = res.postal_verification_attempt_id
219 with postal_verification_session(token) as pv:
220 pv.ConfirmPostalAddress(
221 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
222 )
224 # Process background job
225 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
226 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
227 while process_job():
228 pass
230 # Get the code
231 with session_scope() as session:
232 attempt = session.execute(
233 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id)
234 ).scalar_one()
235 verification_code = attempt.verification_code
236 # Set postcard_sent_at to be past expiry
237 attempt.postcard_sent_at = now() - POSTAL_VERIFICATION_CODE_LIFETIME - timedelta(days=1)
239 # Try to verify - should fail due to expiry
240 with postal_verification_session(token) as pv:
241 with pytest.raises(grpc.RpcError) as e:
242 pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code))
243 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
246def test_postal_verification_rate_limit(db, monkeypatch):
247 """Test rate limiting on postal verification attempts."""
248 _monkeypatch_postal_verification_config(monkeypatch)
250 user, token = generate_user()
252 # First attempt
253 with postal_verification_session(token) as pv:
254 res = pv.InitiatePostalVerification(
255 postal_verification_pb2.InitiatePostalVerificationReq(
256 address=postal_verification_pb2.PostalAddress(
257 address_line_1="123 Main St",
258 city="Test City",
259 country="US",
260 )
261 )
262 )
263 attempt_id = res.postal_verification_attempt_id
265 # Cancel the first attempt
266 with postal_verification_session(token) as pv:
267 pv.CancelPostalVerification(
268 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id)
269 )
271 # Try to initiate again immediately - should be rate limited
272 with postal_verification_session(token) as pv:
273 with pytest.raises(grpc.RpcError) as e:
274 pv.InitiatePostalVerification(
275 postal_verification_pb2.InitiatePostalVerificationReq(
276 address=postal_verification_pb2.PostalAddress(
277 address_line_1="456 Other St",
278 city="Test City",
279 country="US",
280 )
281 )
282 )
283 assert e.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
285 # Check status shows rate limit info
286 with postal_verification_session(token) as pv:
287 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
288 assert not status.can_initiate_new_attempt
289 assert status.next_attempt_allowed_at.seconds > 0
292def test_postal_verification_already_in_progress(db, monkeypatch):
293 """Test that you can't start a new attempt while one is in progress."""
294 _monkeypatch_postal_verification_config(monkeypatch)
296 user, token = generate_user()
298 # First attempt
299 with postal_verification_session(token) as pv:
300 pv.InitiatePostalVerification(
301 postal_verification_pb2.InitiatePostalVerificationReq(
302 address=postal_verification_pb2.PostalAddress(
303 address_line_1="123 Main St",
304 city="Test City",
305 country="US",
306 )
307 )
308 )
310 # Try to initiate another - should fail
311 with postal_verification_session(token) as pv:
312 with pytest.raises(grpc.RpcError) as e:
313 pv.InitiatePostalVerification(
314 postal_verification_pb2.InitiatePostalVerificationReq(
315 address=postal_verification_pb2.PostalAddress(
316 address_line_1="456 Other St",
317 city="Test City",
318 country="US",
319 )
320 )
321 )
322 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
325def test_postal_verification_cancel(db, monkeypatch):
326 """Test cancelling a postal verification attempt."""
327 _monkeypatch_postal_verification_config(monkeypatch)
329 user, token = generate_user()
331 # Initiate
332 with postal_verification_session(token) as pv:
333 res = pv.InitiatePostalVerification(
334 postal_verification_pb2.InitiatePostalVerificationReq(
335 address=postal_verification_pb2.PostalAddress(
336 address_line_1="123 Main St",
337 city="Test City",
338 country="US",
339 )
340 )
341 )
342 attempt_id = res.postal_verification_attempt_id
344 # Cancel
345 with postal_verification_session(token) as pv:
346 pv.CancelPostalVerification(
347 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id)
348 )
350 # Check status
351 with postal_verification_session(token) as pv:
352 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
353 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_CANCELLED
354 assert not status.has_active_attempt
357def test_postal_verification_can_cancel_after_postcard_sent(db, monkeypatch):
358 """Test that you CAN cancel after the postcard is sent (e.g., if postcard is lost)."""
359 _monkeypatch_postal_verification_config(monkeypatch)
361 user, token = generate_user()
363 # Initiate and confirm
364 with postal_verification_session(token) as pv:
365 res = pv.InitiatePostalVerification(
366 postal_verification_pb2.InitiatePostalVerificationReq(
367 address=postal_verification_pb2.PostalAddress(
368 address_line_1="123 Main St",
369 city="Test City",
370 country="US",
371 )
372 )
373 )
374 attempt_id = res.postal_verification_attempt_id
376 with postal_verification_session(token) as pv:
377 pv.ConfirmPostalAddress(
378 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
379 )
381 # Process background job
382 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
383 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
384 while process_job():
385 pass
387 # Verify status is awaiting_verification
388 with postal_verification_session(token) as pv:
389 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
390 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_AWAITING_VERIFICATION
392 # Cancel - should succeed (user can cancel if postcard is lost)
393 with postal_verification_session(token) as pv:
394 pv.CancelPostalVerification(
395 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id)
396 )
398 # Verify status is cancelled
399 with postal_verification_session(token) as pv:
400 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
401 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_CANCELLED
402 assert not status.has_active_attempt
405def test_postal_verification_list_attempts(db, monkeypatch):
406 """Test listing postal verification attempts."""
407 _monkeypatch_postal_verification_config(monkeypatch)
409 user, token = generate_user()
411 # Create first attempt and cancel it
412 with postal_verification_session(token) as pv:
413 res = pv.InitiatePostalVerification(
414 postal_verification_pb2.InitiatePostalVerificationReq(
415 address=postal_verification_pb2.PostalAddress(
416 address_line_1="123 Main St",
417 city="Test City",
418 country="US",
419 )
420 )
421 )
422 attempt_id_1 = res.postal_verification_attempt_id
424 with postal_verification_session(token) as pv:
425 pv.CancelPostalVerification(
426 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id_1)
427 )
429 # Move created time back to bypass rate limit
430 with session_scope() as session:
431 attempt = session.execute(
432 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id_1)
433 ).scalar_one()
434 attempt.created = now() - POSTAL_VERIFICATION_RATE_LIMIT - timedelta(days=1)
436 # Create second attempt
437 with postal_verification_session(token) as pv:
438 res = pv.InitiatePostalVerification(
439 postal_verification_pb2.InitiatePostalVerificationReq(
440 address=postal_verification_pb2.PostalAddress(
441 address_line_1="456 Other St",
442 city="Other City",
443 country="CA",
444 )
445 )
446 )
447 attempt_id_2 = res.postal_verification_attempt_id
449 # List attempts
450 with postal_verification_session(token) as pv:
451 res = pv.ListPostalVerificationAttempts(postal_verification_pb2.ListPostalVerificationAttemptsReq())
452 assert len(res.attempts) == 2
453 # Most recent first
454 assert res.attempts[0].postal_verification_attempt_id == attempt_id_2
455 assert res.attempts[1].postal_verification_attempt_id == attempt_id_1
458def test_postal_verification_address_validation(db, monkeypatch):
459 """Test address validation errors."""
460 _monkeypatch_postal_verification_config(monkeypatch)
462 user, token = generate_user()
464 # Missing required fields
465 with postal_verification_session(token) as pv:
466 # Missing address_line_1
467 with pytest.raises(grpc.RpcError) as e:
468 pv.InitiatePostalVerification(
469 postal_verification_pb2.InitiatePostalVerificationReq(
470 address=postal_verification_pb2.PostalAddress(
471 city="Test City",
472 country="US",
473 )
474 )
475 )
476 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
478 # Missing city
479 with pytest.raises(grpc.RpcError) as e:
480 pv.InitiatePostalVerification(
481 postal_verification_pb2.InitiatePostalVerificationReq(
482 address=postal_verification_pb2.PostalAddress(
483 address_line_1="123 Main St",
484 country="US",
485 )
486 )
487 )
488 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
490 # Missing country
491 with pytest.raises(grpc.RpcError) as e:
492 pv.InitiatePostalVerification(
493 postal_verification_pb2.InitiatePostalVerificationReq(
494 address=postal_verification_pb2.PostalAddress(
495 address_line_1="123 Main St",
496 city="Test City",
497 )
498 )
499 )
500 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
503def test_postal_verification_postcard_send_failure(db, monkeypatch):
504 """Test handling of postcard send failure."""
505 _monkeypatch_postal_verification_config(monkeypatch)
507 user, token = generate_user()
509 # Initiate and confirm
510 with postal_verification_session(token) as pv:
511 res = pv.InitiatePostalVerification(
512 postal_verification_pb2.InitiatePostalVerificationReq(
513 address=postal_verification_pb2.PostalAddress(
514 address_line_1="123 Main St",
515 city="Test City",
516 country="US",
517 )
518 )
519 )
520 attempt_id = res.postal_verification_attempt_id
522 with postal_verification_session(token) as pv:
523 pv.ConfirmPostalAddress(
524 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
525 )
527 # Simulate postcard send failure
528 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
529 mock_send.return_value = type("PostcardResult", (), {"success": False, "error_message": "API error"})()
530 while process_job():
531 pass
533 # Check status is failed
534 with postal_verification_session(token) as pv:
535 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq())
536 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_FAILED
539def test_postal_verification_code_case_insensitive(db, monkeypatch):
540 """Test that verification codes are case insensitive."""
541 _monkeypatch_postal_verification_config(monkeypatch)
543 user, token = generate_user()
545 # Initiate and confirm
546 with postal_verification_session(token) as pv:
547 res = pv.InitiatePostalVerification(
548 postal_verification_pb2.InitiatePostalVerificationReq(
549 address=postal_verification_pb2.PostalAddress(
550 address_line_1="123 Main St",
551 city="Test City",
552 country="US",
553 )
554 )
555 )
556 attempt_id = res.postal_verification_attempt_id
558 with postal_verification_session(token) as pv:
559 pv.ConfirmPostalAddress(
560 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
561 )
563 # Process background job
564 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
565 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
566 while process_job():
567 pass
569 # Get the code
570 with session_scope() as session:
571 attempt = session.execute(
572 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id)
573 ).scalar_one()
574 verification_code = attempt.verification_code
575 assert verification_code
577 # Verify with lowercase code
578 with postal_verification_session(token) as pv:
579 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code.lower()))
580 assert res.success
583def test_postal_verification_attempt_not_found(db, monkeypatch):
584 """Test accessing non-existent attempts."""
585 _monkeypatch_postal_verification_config(monkeypatch)
587 user, token = generate_user()
589 with postal_verification_session(token) as pv:
590 # Try to confirm non-existent attempt
591 with pytest.raises(grpc.RpcError) as e:
592 pv.ConfirmPostalAddress(
593 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=999999)
594 )
595 assert e.value.code() == grpc.StatusCode.NOT_FOUND
597 # Try to cancel non-existent attempt
598 with pytest.raises(grpc.RpcError) as e:
599 pv.CancelPostalVerification(
600 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=999999)
601 )
602 assert e.value.code() == grpc.StatusCode.NOT_FOUND
605def test_postal_verification_other_user_attempt(db, monkeypatch):
606 """Test that users cannot access other users' attempts."""
607 _monkeypatch_postal_verification_config(monkeypatch)
609 user1, token1 = generate_user()
610 user2, token2 = generate_user()
612 # User 1 creates an attempt
613 with postal_verification_session(token1) as pv:
614 res = pv.InitiatePostalVerification(
615 postal_verification_pb2.InitiatePostalVerificationReq(
616 address=postal_verification_pb2.PostalAddress(
617 address_line_1="123 Main St",
618 city="Test City",
619 country="US",
620 )
621 )
622 )
623 attempt_id = res.postal_verification_attempt_id
625 # User 2 tries to confirm user 1's attempt
626 with postal_verification_session(token2) as pv:
627 with pytest.raises(grpc.RpcError) as e:
628 pv.ConfirmPostalAddress(
629 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
630 )
631 assert e.value.code() == grpc.StatusCode.NOT_FOUND
633 # User 2 tries to cancel user 1's attempt
634 with postal_verification_session(token2) as pv:
635 with pytest.raises(grpc.RpcError) as e:
636 pv.CancelPostalVerification(
637 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id)
638 )
639 assert e.value.code() == grpc.StatusCode.NOT_FOUND
642def test_has_postal_verification_helper(db, monkeypatch):
643 """Test the has_postal_verification helper function."""
644 _monkeypatch_postal_verification_config(monkeypatch)
646 user, token = generate_user()
648 # Initially no verification
649 with session_scope() as session:
650 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one()
651 assert not has_postal_verification(session, db_user)
653 # Complete verification
654 with postal_verification_session(token) as pv:
655 res = pv.InitiatePostalVerification(
656 postal_verification_pb2.InitiatePostalVerificationReq(
657 address=postal_verification_pb2.PostalAddress(
658 address_line_1="123 Main St",
659 city="Test City",
660 country="US",
661 )
662 )
663 )
664 attempt_id = res.postal_verification_attempt_id
666 with postal_verification_session(token) as pv:
667 pv.ConfirmPostalAddress(
668 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id)
669 )
671 with patch("couchers.jobs.handlers.send_postcard") as mock_send:
672 mock_send.return_value = type("PostcardResult", (), {"success": True, "error_message": None})()
673 while process_job():
674 pass
676 with session_scope() as session:
677 attempt = session.execute(
678 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id)
679 ).scalar_one()
680 verification_code = attempt.verification_code
682 with postal_verification_session(token) as pv:
683 pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code))
685 # Now should have verification
686 with session_scope() as session:
687 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one()
688 assert has_postal_verification(session, db_user)