Coverage for app/backend/src/tests/test_postal_verification.py: 100%

303 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-21 09:29 +0000

1from datetime import timedelta 

2from pathlib import Path 

3from unittest.mock import patch 

4 

5import grpc 

6import pytest 

7from sqlalchemy import select 

8 

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, PostalVerificationStatus 

19from couchers.postal.my_postcard import _generate_back_left_side_png 

20from couchers.proto import postal_verification_pb2 

21from couchers.resources import get_postcard_front_image 

22from couchers.utils import now 

23from tests.fixtures.db import generate_user 

24from tests.fixtures.sessions import postal_verification_session 

25 

26 

27@pytest.fixture(autouse=True) 

28def _(testconfig): 

29 pass 

30 

31 

32def test_generate_postal_verification_code(): 

33 """Test that generated codes meet requirements.""" 

34 allowed = set("ABCDEFGHJKLMNPQRSTUVWXYZ23456789") 

35 for _ in range(100): 

36 code = generate_postal_verification_code() 

37 assert len(code) == 6 

38 assert all(c in allowed for c in code) 

39 # Should not contain confusing characters 

40 for char in "IO01": 

41 assert char not in code 

42 

43 

44def test_postal_verification_disabled(db, feature_flags): 

45 """Test that postal verification is disabled.""" 

46 feature_flags.set("postal_verification_enabled", False) 

47 user, token = generate_user() 

48 

49 with postal_verification_session(token) as pv: 

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

51 pv.InitiatePostalVerification( 

52 postal_verification_pb2.InitiatePostalVerificationReq( 

53 address=postal_verification_pb2.PostalAddress( 

54 address_line_1="123 Main St", 

55 city="Test City", 

56 country_code="US", 

57 ) 

58 ) 

59 ) 

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

61 

62 

63def test_postal_verification_confirm_disabled(db, feature_flags): 

64 """Confirming (which queues the paid postcard) must respect the flag, not just initiation.""" 

65 feature_flags.set("postal_verification_enabled", False) 

66 user, token = generate_user() 

67 

68 # Seed a pending attempt directly, since initiation is gated by the same flag. 

69 with session_scope() as session: 

70 attempt = PostalVerificationAttempt( 

71 user_id=user.id, 

72 status=PostalVerificationStatus.pending_address_confirmation, 

73 address_line_1="123 Main St", 

74 city="Test City", 

75 country_code="US", 

76 ) 

77 session.add(attempt) 

78 session.flush() 

79 attempt_id = attempt.id 

80 

81 with postal_verification_session(token) as pv: 

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

83 pv.ConfirmPostalAddress( 

84 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

85 ) 

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

87 

88 

89def test_postal_verification_happy_path(db): 

90 """Test the complete happy path for postal verification.""" 

91 user, token = generate_user() 

92 

93 # Check initial status 

94 with postal_verification_session(token) as pv: 

95 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

96 assert not status.has_postal_verification 

97 assert status.can_initiate_new_attempt 

98 assert not status.has_active_attempt 

99 

100 # Step 1: Initiate postal verification 

101 with postal_verification_session(token) as pv: 

102 res = pv.InitiatePostalVerification( 

103 postal_verification_pb2.InitiatePostalVerificationReq( 

104 address=postal_verification_pb2.PostalAddress( 

105 address_line_1="123 Main St", 

106 address_line_2="Apt 4", 

107 city="Test City", 

108 state="CA", 

109 postal_code="12345", 

110 country_code="US", 

111 ) 

112 ) 

113 ) 

114 attempt_id = res.postal_verification_attempt_id 

115 assert attempt_id > 0 

116 

117 # Check status after initiation 

118 with postal_verification_session(token) as pv: 

119 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

120 assert not status.has_postal_verification 

121 assert not status.can_initiate_new_attempt 

122 assert status.has_active_attempt 

123 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_PENDING_ADDRESS_CONFIRMATION 

124 

125 # Step 2: Confirm address 

126 with postal_verification_session(token) as pv: 

127 pv.ConfirmPostalAddress( 

128 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

129 ) 

130 

131 # Check status after confirmation 

132 with postal_verification_session(token) as pv: 

133 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

134 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_IN_PROGRESS 

135 

136 # Process background job to send postcard 

137 with patch("couchers.jobs.handlers.send_postcard") as mock_send: 

138 mock_send.return_value = 12345 

139 while process_job(): 

140 pass 

141 

142 # Check status after postcard sent 

143 with postal_verification_session(token) as pv: 

144 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

145 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_AWAITING_VERIFICATION 

146 assert status.postcard_sent_at.seconds > 0 

147 

148 # Get the verification code from the database 

149 with session_scope() as session: 

150 attempt = session.execute( 

151 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id) 

152 ).scalar_one() 

153 verification_code = attempt.verification_code 

154 

155 # Step 3: Verify the code 

156 with postal_verification_session(token) as pv: 

157 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code)) 

158 assert res.success 

159 

160 # Check final status 

161 with postal_verification_session(token) as pv: 

162 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

163 assert status.has_postal_verification 

164 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_SUCCEEDED 

165 

166 # Verify with helper function 

167 with session_scope() as session: 

168 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one() 

169 assert has_postal_verification(session, db_user) 

170 

171 

172def test_postal_verification_wrong_code(db): 

173 """Test entering wrong verification codes.""" 

174 user, token = generate_user() 

175 

176 # Initiate and confirm 

177 with postal_verification_session(token) as pv: 

178 res = pv.InitiatePostalVerification( 

179 postal_verification_pb2.InitiatePostalVerificationReq( 

180 address=postal_verification_pb2.PostalAddress( 

181 address_line_1="123 Main St", 

182 city="Test City", 

183 country_code="US", 

184 ) 

185 ) 

186 ) 

187 attempt_id = res.postal_verification_attempt_id 

188 

189 with postal_verification_session(token) as pv: 

190 pv.ConfirmPostalAddress( 

191 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

192 ) 

193 

194 # Process background job 

195 with patch("couchers.jobs.handlers.send_postcard") as mock_send: 

196 mock_send.return_value = 12345 

197 while process_job(): 

198 pass 

199 

200 # Try wrong codes 

201 with postal_verification_session(token) as pv: 

202 for i in range(POSTAL_VERIFICATION_MAX_ATTEMPTS - 1): 

203 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code="WRONGX")) 

204 assert not res.success 

205 assert res.remaining_attempts == POSTAL_VERIFICATION_MAX_ATTEMPTS - 1 - i 

206 

207 # Last attempt should fail and lock the attempt 

208 res = pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code="WRONGX")) 

209 assert not res.success 

210 assert res.remaining_attempts == 0 

211 

212 # Check status is failed 

213 with postal_verification_session(token) as pv: 

214 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

215 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_FAILED 

216 

217 

218def test_postal_verification_code_expiry(db): 

219 """Test that codes expire after the configured lifetime.""" 

220 user, token = generate_user() 

221 

222 # Initiate and confirm 

223 with postal_verification_session(token) as pv: 

224 res = pv.InitiatePostalVerification( 

225 postal_verification_pb2.InitiatePostalVerificationReq( 

226 address=postal_verification_pb2.PostalAddress( 

227 address_line_1="123 Main St", 

228 city="Test City", 

229 country_code="US", 

230 ) 

231 ) 

232 ) 

233 attempt_id = res.postal_verification_attempt_id 

234 

235 with postal_verification_session(token) as pv: 

236 pv.ConfirmPostalAddress( 

237 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

238 ) 

239 

240 # Process background job 

241 with patch("couchers.jobs.handlers.send_postcard") as mock_send: 

242 mock_send.return_value = 12345 

243 while process_job(): 

244 pass 

245 

246 # Get the code 

247 with session_scope() as session: 

248 attempt = session.execute( 

249 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id) 

250 ).scalar_one() 

251 verification_code = attempt.verification_code 

252 # Set postcard_sent_at to be past expiry 

253 attempt.postcard_sent_at = now() - POSTAL_VERIFICATION_CODE_LIFETIME - timedelta(days=1) 

254 

255 # Try to verify - should fail due to expiry 

256 with postal_verification_session(token) as pv: 

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

258 pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code)) 

259 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

260 

261 

262def test_postal_verification_rate_limit(db): 

263 """Test rate limiting on postal verification attempts.""" 

264 user, token = generate_user() 

265 

266 # First attempt 

267 with postal_verification_session(token) as pv: 

268 res = pv.InitiatePostalVerification( 

269 postal_verification_pb2.InitiatePostalVerificationReq( 

270 address=postal_verification_pb2.PostalAddress( 

271 address_line_1="123 Main St", 

272 city="Test City", 

273 country_code="US", 

274 ) 

275 ) 

276 ) 

277 attempt_id = res.postal_verification_attempt_id 

278 

279 # Cancel the first attempt 

280 with postal_verification_session(token) as pv: 

281 pv.CancelPostalVerification( 

282 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id) 

283 ) 

284 

285 # Try to initiate again immediately - should be rate limited 

286 with postal_verification_session(token) as pv: 

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

288 pv.InitiatePostalVerification( 

289 postal_verification_pb2.InitiatePostalVerificationReq( 

290 address=postal_verification_pb2.PostalAddress( 

291 address_line_1="456 Other St", 

292 city="Test City", 

293 country_code="US", 

294 ) 

295 ) 

296 ) 

297 assert e.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED 

298 

299 # Check status shows rate limit info 

300 with postal_verification_session(token) as pv: 

301 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

302 assert not status.can_initiate_new_attempt 

303 assert status.next_attempt_allowed_at.seconds > 0 

304 

305 

306def test_postal_verification_already_in_progress(db): 

307 """Test that you can't start a new attempt while one is in progress.""" 

308 user, token = generate_user() 

309 

310 # First attempt 

311 with postal_verification_session(token) as pv: 

312 pv.InitiatePostalVerification( 

313 postal_verification_pb2.InitiatePostalVerificationReq( 

314 address=postal_verification_pb2.PostalAddress( 

315 address_line_1="123 Main St", 

316 city="Test City", 

317 country_code="US", 

318 ) 

319 ) 

320 ) 

321 

322 # Try to initiate another - should fail 

323 with postal_verification_session(token) as pv: 

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

325 pv.InitiatePostalVerification( 

326 postal_verification_pb2.InitiatePostalVerificationReq( 

327 address=postal_verification_pb2.PostalAddress( 

328 address_line_1="456 Other St", 

329 city="Test City", 

330 country_code="US", 

331 ) 

332 ) 

333 ) 

334 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

335 

336 

337def test_postal_verification_cancel(db): 

338 """Test cancelling a postal verification attempt.""" 

339 user, token = generate_user() 

340 

341 # Initiate 

342 with postal_verification_session(token) as pv: 

343 res = pv.InitiatePostalVerification( 

344 postal_verification_pb2.InitiatePostalVerificationReq( 

345 address=postal_verification_pb2.PostalAddress( 

346 address_line_1="123 Main St", 

347 city="Test City", 

348 country_code="US", 

349 ) 

350 ) 

351 ) 

352 attempt_id = res.postal_verification_attempt_id 

353 

354 # Cancel 

355 with postal_verification_session(token) as pv: 

356 pv.CancelPostalVerification( 

357 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id) 

358 ) 

359 

360 # Check status 

361 with postal_verification_session(token) as pv: 

362 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

363 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_CANCELLED 

364 assert not status.has_active_attempt 

365 

366 

367def test_postal_verification_can_cancel_after_postcard_sent(db): 

368 """Test that you CAN cancel after the postcard is sent (e.g., if postcard is lost).""" 

369 user, token = generate_user() 

370 

371 # Initiate and confirm 

372 with postal_verification_session(token) as pv: 

373 res = pv.InitiatePostalVerification( 

374 postal_verification_pb2.InitiatePostalVerificationReq( 

375 address=postal_verification_pb2.PostalAddress( 

376 address_line_1="123 Main St", 

377 city="Test City", 

378 country_code="US", 

379 ) 

380 ) 

381 ) 

382 attempt_id = res.postal_verification_attempt_id 

383 

384 with postal_verification_session(token) as pv: 

385 pv.ConfirmPostalAddress( 

386 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

387 ) 

388 

389 # Process background job 

390 with patch("couchers.jobs.handlers.send_postcard") as mock_send: 

391 mock_send.return_value = 12345 

392 while process_job(): 

393 pass 

394 

395 # Verify status is awaiting_verification 

396 with postal_verification_session(token) as pv: 

397 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

398 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_AWAITING_VERIFICATION 

399 

400 # Cancel - should succeed (user can cancel if postcard is lost) 

401 with postal_verification_session(token) as pv: 

402 pv.CancelPostalVerification( 

403 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id) 

404 ) 

405 

406 # Verify status is cancelled 

407 with postal_verification_session(token) as pv: 

408 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

409 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_CANCELLED 

410 assert not status.has_active_attempt 

411 

412 

413def test_postal_verification_list_attempts(db): 

414 """Test listing postal verification attempts.""" 

415 user, token = generate_user() 

416 

417 # Create first attempt and cancel it 

418 with postal_verification_session(token) as pv: 

419 res = pv.InitiatePostalVerification( 

420 postal_verification_pb2.InitiatePostalVerificationReq( 

421 address=postal_verification_pb2.PostalAddress( 

422 address_line_1="123 Main St", 

423 city="Test City", 

424 country_code="US", 

425 ) 

426 ) 

427 ) 

428 attempt_id_1 = res.postal_verification_attempt_id 

429 

430 with postal_verification_session(token) as pv: 

431 pv.CancelPostalVerification( 

432 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id_1) 

433 ) 

434 

435 # Move created time back to bypass rate limit 

436 with session_scope() as session: 

437 attempt = session.execute( 

438 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id_1) 

439 ).scalar_one() 

440 attempt.created = now() - POSTAL_VERIFICATION_RATE_LIMIT - timedelta(days=1) 

441 

442 # Create second attempt 

443 with postal_verification_session(token) as pv: 

444 res = pv.InitiatePostalVerification( 

445 postal_verification_pb2.InitiatePostalVerificationReq( 

446 address=postal_verification_pb2.PostalAddress( 

447 address_line_1="456 Other St", 

448 city="Other City", 

449 country_code="CA", 

450 ) 

451 ) 

452 ) 

453 attempt_id_2 = res.postal_verification_attempt_id 

454 

455 # List attempts 

456 with postal_verification_session(token) as pv: 

457 res = pv.ListPostalVerificationAttempts(postal_verification_pb2.ListPostalVerificationAttemptsReq()) 

458 assert len(res.attempts) == 2 

459 # Most recent first 

460 assert res.attempts[0].postal_verification_attempt_id == attempt_id_2 

461 assert res.attempts[1].postal_verification_attempt_id == attempt_id_1 

462 

463 

464def test_postal_verification_address_validation(db): 

465 """Test address validation errors.""" 

466 user, token = generate_user() 

467 

468 # Missing required fields 

469 with postal_verification_session(token) as pv: 

470 # Missing address_line_1 

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

472 pv.InitiatePostalVerification( 

473 postal_verification_pb2.InitiatePostalVerificationReq( 

474 address=postal_verification_pb2.PostalAddress( 

475 city="Test City", 

476 country_code="US", 

477 ) 

478 ) 

479 ) 

480 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

481 

482 # Missing city 

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

484 pv.InitiatePostalVerification( 

485 postal_verification_pb2.InitiatePostalVerificationReq( 

486 address=postal_verification_pb2.PostalAddress( 

487 address_line_1="123 Main St", 

488 country_code="US", 

489 ) 

490 ) 

491 ) 

492 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

493 

494 # Missing country 

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

496 pv.InitiatePostalVerification( 

497 postal_verification_pb2.InitiatePostalVerificationReq( 

498 address=postal_verification_pb2.PostalAddress( 

499 address_line_1="123 Main St", 

500 city="Test City", 

501 ) 

502 ) 

503 ) 

504 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

505 

506 

507def test_postal_verification_postcard_send_failure(db): 

508 """Test handling of postcard send failure.""" 

509 user, token = generate_user() 

510 

511 # Initiate and confirm 

512 with postal_verification_session(token) as pv: 

513 res = pv.InitiatePostalVerification( 

514 postal_verification_pb2.InitiatePostalVerificationReq( 

515 address=postal_verification_pb2.PostalAddress( 

516 address_line_1="123 Main St", 

517 city="Test City", 

518 country_code="US", 

519 ) 

520 ) 

521 ) 

522 attempt_id = res.postal_verification_attempt_id 

523 

524 with postal_verification_session(token) as pv: 

525 pv.ConfirmPostalAddress( 

526 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

527 ) 

528 

529 # Simulate postcard send failure 

530 with patch("couchers.jobs.handlers.send_postcard") as mock_send: 

531 mock_send.side_effect = Exception("API error") 

532 with pytest.raises(Exception, match="API error"): 

533 process_job() 

534 

535 # Attempt should still be in_progress (job failed, not the attempt) 

536 with postal_verification_session(token) as pv: 

537 status = pv.GetPostalVerificationStatus(postal_verification_pb2.GetPostalVerificationStatusReq()) 

538 assert status.status == postal_verification_pb2.POSTAL_VERIFICATION_STATUS_IN_PROGRESS 

539 

540 

541def test_postal_verification_code_case_insensitive(db): 

542 """Test that verification codes are case insensitive.""" 

543 user, token = generate_user() 

544 

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_code="US", 

553 ) 

554 ) 

555 ) 

556 attempt_id = res.postal_verification_attempt_id 

557 

558 with postal_verification_session(token) as pv: 

559 pv.ConfirmPostalAddress( 

560 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

561 ) 

562 

563 # Process background job 

564 with patch("couchers.jobs.handlers.send_postcard") as mock_send: 

565 mock_send.return_value = 12345 

566 while process_job(): 

567 pass 

568 

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 

576 

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 

581 

582 

583def test_postal_verification_attempt_not_found(db): 

584 """Test accessing non-existent attempts.""" 

585 user, token = generate_user() 

586 

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 

594 

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 

601 

602 

603def test_postal_verification_other_user_attempt(db): 

604 """Test that users cannot access other users' attempts.""" 

605 user1, token1 = generate_user() 

606 user2, token2 = generate_user() 

607 

608 # User 1 creates an attempt 

609 with postal_verification_session(token1) as pv: 

610 res = pv.InitiatePostalVerification( 

611 postal_verification_pb2.InitiatePostalVerificationReq( 

612 address=postal_verification_pb2.PostalAddress( 

613 address_line_1="123 Main St", 

614 city="Test City", 

615 country_code="US", 

616 ) 

617 ) 

618 ) 

619 attempt_id = res.postal_verification_attempt_id 

620 

621 # User 2 tries to confirm user 1's attempt 

622 with postal_verification_session(token2) as pv: 

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

624 pv.ConfirmPostalAddress( 

625 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

626 ) 

627 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

628 

629 # User 2 tries to cancel user 1's attempt 

630 with postal_verification_session(token2) as pv: 

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

632 pv.CancelPostalVerification( 

633 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id) 

634 ) 

635 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

636 

637 

638def test_has_postal_verification_helper(db): 

639 """Test the has_postal_verification helper function.""" 

640 user, token = generate_user() 

641 

642 # Initially no verification 

643 with session_scope() as session: 

644 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one() 

645 assert not has_postal_verification(session, db_user) 

646 

647 # Complete verification 

648 with postal_verification_session(token) as pv: 

649 res = pv.InitiatePostalVerification( 

650 postal_verification_pb2.InitiatePostalVerificationReq( 

651 address=postal_verification_pb2.PostalAddress( 

652 address_line_1="123 Main St", 

653 city="Test City", 

654 country_code="US", 

655 ) 

656 ) 

657 ) 

658 attempt_id = res.postal_verification_attempt_id 

659 

660 with postal_verification_session(token) as pv: 

661 pv.ConfirmPostalAddress( 

662 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

663 ) 

664 

665 with patch("couchers.jobs.handlers.send_postcard") as mock_send: 

666 mock_send.return_value = 12345 

667 while process_job(): 

668 pass 

669 

670 with session_scope() as session: 

671 attempt = session.execute( 

672 select(PostalVerificationAttempt).where(PostalVerificationAttempt.id == attempt_id) 

673 ).scalar_one() 

674 verification_code = attempt.verification_code 

675 

676 with postal_verification_session(token) as pv: 

677 pv.VerifyPostalCode(postal_verification_pb2.VerifyPostalCodeReq(code=verification_code)) 

678 

679 # Now should have verification 

680 with session_scope() as session: 

681 db_user = session.execute(select(User).where(User.id == user.id)).scalar_one() 

682 assert has_postal_verification(session, db_user) 

683 

684 

685def test_generate_postcard_images(): 

686 """ 

687 Generates sample postcard front and back images for visual inspection. 

688 

689 Output is written to test_artifacts/ (gitignored) and picked up by CI. 

690 """ 

691 code = "ABC123" 

692 front = get_postcard_front_image() 

693 back = _generate_back_left_side_png(code) 

694 

695 assert len(front) > 0 

696 assert len(back) > 0 

697 

698 output_path = Path(__file__).resolve().parents[2] / "test_artifacts" 

699 output_path.mkdir(parents=True, exist_ok=True) 

700 (output_path / "postcard_front.png").write_bytes(front) 

701 (output_path / "postcard_back.png").write_bytes(back)