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

1from datetime import timedelta 

2from unittest.mock import patch 

3 

4import grpc 

5import pytest 

6from sqlalchemy import select 

7 

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 

24 

25 

26@pytest.fixture(autouse=True) 

27def _(testconfig): 

28 pass 

29 

30 

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) 

35 

36 

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 

47 

48 

49def test_postal_verification_disabled(db): 

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

51 user, token = generate_user() 

52 

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 

65 

66 

67def test_postal_verification_happy_path(db, monkeypatch): 

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

69 _monkeypatch_postal_verification_config(monkeypatch) 

70 

71 user, token = generate_user() 

72 

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 

79 

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 

96 

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 

104 

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 ) 

110 

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 

115 

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 

121 

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 

127 

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 

134 

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 

139 

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 

145 

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) 

150 

151 

152def test_postal_verification_wrong_code(db, monkeypatch): 

153 """Test entering wrong verification codes.""" 

154 _monkeypatch_postal_verification_config(monkeypatch) 

155 

156 user, token = generate_user() 

157 

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 

170 

171 with postal_verification_session(token) as pv: 

172 pv.ConfirmPostalAddress( 

173 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

174 ) 

175 

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 

181 

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 

188 

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 

193 

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 

198 

199 

200def test_postal_verification_code_expiry(db, monkeypatch): 

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

202 _monkeypatch_postal_verification_config(monkeypatch) 

203 

204 user, token = generate_user() 

205 

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 

218 

219 with postal_verification_session(token) as pv: 

220 pv.ConfirmPostalAddress( 

221 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

222 ) 

223 

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 

229 

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) 

238 

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 

244 

245 

246def test_postal_verification_rate_limit(db, monkeypatch): 

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

248 _monkeypatch_postal_verification_config(monkeypatch) 

249 

250 user, token = generate_user() 

251 

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 

264 

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 ) 

270 

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 

284 

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 

290 

291 

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) 

295 

296 user, token = generate_user() 

297 

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 ) 

309 

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 

323 

324 

325def test_postal_verification_cancel(db, monkeypatch): 

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

327 _monkeypatch_postal_verification_config(monkeypatch) 

328 

329 user, token = generate_user() 

330 

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 

343 

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 ) 

349 

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 

355 

356 

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) 

360 

361 user, token = generate_user() 

362 

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 

375 

376 with postal_verification_session(token) as pv: 

377 pv.ConfirmPostalAddress( 

378 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

379 ) 

380 

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 

386 

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 

391 

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 ) 

397 

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 

403 

404 

405def test_postal_verification_list_attempts(db, monkeypatch): 

406 """Test listing postal verification attempts.""" 

407 _monkeypatch_postal_verification_config(monkeypatch) 

408 

409 user, token = generate_user() 

410 

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 

423 

424 with postal_verification_session(token) as pv: 

425 pv.CancelPostalVerification( 

426 postal_verification_pb2.CancelPostalVerificationReq(postal_verification_attempt_id=attempt_id_1) 

427 ) 

428 

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) 

435 

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 

448 

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 

456 

457 

458def test_postal_verification_address_validation(db, monkeypatch): 

459 """Test address validation errors.""" 

460 _monkeypatch_postal_verification_config(monkeypatch) 

461 

462 user, token = generate_user() 

463 

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 

477 

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 

489 

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 

501 

502 

503def test_postal_verification_postcard_send_failure(db, monkeypatch): 

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

505 _monkeypatch_postal_verification_config(monkeypatch) 

506 

507 user, token = generate_user() 

508 

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 

521 

522 with postal_verification_session(token) as pv: 

523 pv.ConfirmPostalAddress( 

524 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

525 ) 

526 

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 

532 

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 

537 

538 

539def test_postal_verification_code_case_insensitive(db, monkeypatch): 

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

541 _monkeypatch_postal_verification_config(monkeypatch) 

542 

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="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 = type("PostcardResult", (), {"success": True, "error_message": None})() 

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

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

585 _monkeypatch_postal_verification_config(monkeypatch) 

586 

587 user, token = generate_user() 

588 

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 

596 

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 

603 

604 

605def test_postal_verification_other_user_attempt(db, monkeypatch): 

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

607 _monkeypatch_postal_verification_config(monkeypatch) 

608 

609 user1, token1 = generate_user() 

610 user2, token2 = generate_user() 

611 

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 

624 

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 

632 

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 

640 

641 

642def test_has_postal_verification_helper(db, monkeypatch): 

643 """Test the has_postal_verification helper function.""" 

644 _monkeypatch_postal_verification_config(monkeypatch) 

645 

646 user, token = generate_user() 

647 

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) 

652 

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 

665 

666 with postal_verification_session(token) as pv: 

667 pv.ConfirmPostalAddress( 

668 postal_verification_pb2.ConfirmPostalAddressReq(postal_verification_attempt_id=attempt_id) 

669 ) 

670 

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 

675 

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 

681 

682 with postal_verification_session(token) as pv: 

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

684 

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)