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

1from datetime import timedelta 

2from unittest.mock import patch 

3 

4import grpc 

5import pytest 

6 

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 

23 

24 

25@pytest.fixture(autouse=True) 

26def _(testconfig): 

27 pass 

28 

29 

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) 

34 

35 

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 

46 

47 

48def test_postal_verification_disabled(db): 

49 """Test that postal verification is disabled by default.""" 

50 user, token = generate_user() 

51 

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 

64 

65 

66def test_postal_verification_happy_path(db, monkeypatch): 

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

68 _monkeypatch_postal_verification_config(monkeypatch) 

69 

70 user, token = generate_user() 

71 

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 

78 

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 

95 

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 

103 

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 ) 

109 

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 

114 

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 

120 

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 

126 

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 

133 

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 

138 

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 

144 

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) 

149 

150 

151def test_postal_verification_wrong_code(db, monkeypatch): 

152 """Test entering wrong verification codes.""" 

153 _monkeypatch_postal_verification_config(monkeypatch) 

154 

155 user, token = generate_user() 

156 

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 

169 

170 with postal_verification_session(token) as pv: 

171 pv.ConfirmPostalAddress( 

172 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

173 ) 

174 

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 

180 

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 

187 

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 

192 

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 

197 

198 

199def test_postal_verification_code_expiry(db, monkeypatch): 

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

201 _monkeypatch_postal_verification_config(monkeypatch) 

202 

203 user, token = generate_user() 

204 

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 

217 

218 with postal_verification_session(token) as pv: 

219 pv.ConfirmPostalAddress( 

220 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

221 ) 

222 

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 

228 

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) 

237 

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 

243 

244 

245def test_postal_verification_rate_limit(db, monkeypatch): 

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

247 _monkeypatch_postal_verification_config(monkeypatch) 

248 

249 user, token = generate_user() 

250 

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 

263 

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 ) 

269 

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 

283 

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 

289 

290 

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) 

294 

295 user, token = generate_user() 

296 

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 ) 

308 

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 

322 

323 

324def test_postal_verification_cancel(db, monkeypatch): 

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

326 _monkeypatch_postal_verification_config(monkeypatch) 

327 

328 user, token = generate_user() 

329 

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 

342 

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 ) 

348 

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 

354 

355 

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) 

359 

360 user, token = generate_user() 

361 

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 

374 

375 with postal_verification_session(token) as pv: 

376 pv.ConfirmPostalAddress( 

377 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

378 ) 

379 

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 

385 

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 

390 

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 ) 

396 

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 

402 

403 

404def test_postal_verification_list_attempts(db, monkeypatch): 

405 """Test listing postal verification attempts.""" 

406 _monkeypatch_postal_verification_config(monkeypatch) 

407 

408 user, token = generate_user() 

409 

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 

422 

423 with postal_verification_session(token) as pv: 

424 pv.CancelPostalVerification( 

425 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id_1) 

426 ) 

427 

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) 

434 

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 

447 

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 

455 

456 

457def test_postal_verification_address_validation(db, monkeypatch): 

458 """Test address validation errors.""" 

459 _monkeypatch_postal_verification_config(monkeypatch) 

460 

461 user, token = generate_user() 

462 

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 

476 

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 

488 

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 

500 

501 

502def test_postal_verification_postcard_send_failure(db, monkeypatch): 

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

504 _monkeypatch_postal_verification_config(monkeypatch) 

505 

506 user, token = generate_user() 

507 

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 

520 

521 with postal_verification_session(token) as pv: 

522 pv.ConfirmPostalAddress( 

523 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

524 ) 

525 

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 

531 

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 

536 

537 

538def test_postal_verification_code_case_insensitive(db, monkeypatch): 

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

540 _monkeypatch_postal_verification_config(monkeypatch) 

541 

542 user, token = generate_user() 

543 

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 

556 

557 with postal_verification_session(token) as pv: 

558 pv.ConfirmPostalAddress( 

559 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

560 ) 

561 

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 

567 

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 

574 

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 

579 

580 

581def test_postal_verification_attempt_not_found(db, monkeypatch): 

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

583 _monkeypatch_postal_verification_config(monkeypatch) 

584 

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, monkeypatch): 

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

605 _monkeypatch_postal_verification_config(monkeypatch) 

606 

607 user1, token1 = generate_user() 

608 user2, token2 = generate_user() 

609 

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 

622 

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 

630 

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 

638 

639 

640def test_has_postal_verification_helper(db, monkeypatch): 

641 """Test the has_postal_verification helper function.""" 

642 _monkeypatch_postal_verification_config(monkeypatch) 

643 

644 user, token = generate_user() 

645 

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) 

650 

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 

663 

664 with postal_verification_session(token) as pv: 

665 pv.ConfirmPostalAddress( 

666 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

667 ) 

668 

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 

673 

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 

679 

680 with postal_verification_session(token) as pv: 

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

682 

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)