# Test Report – T05: Email Service + Contact & Rental Request Backend ## Test Commands ### Targeted T05 Tests ```bash python -m pytest tests/test_contact_router.py tests/test_rental_router.py tests/test_email_service.py tests/test_rate_limiting.py -v ``` **Result: 23 passed in 0.81s** ### Full Suite with Coverage ```bash python -m pytest tests/ -v --cov=app --cov-report=term-missing ``` **Result: 57 passed, 4 warnings in 7.31s** **Coverage: 78% overall** ## Test Files Created ### tests/test_contact_router.py (6 tests) - test_contact_valid_returns_200 – POST /api/contact valid → 200, success=True - test_contact_invalid_email_returns_422 – invalid email → 422 - test_contact_privacy_consent_false_returns_422 – privacy_consent=false → 422 - test_contact_triggers_email – mock EmailService.send_contact_email called with correct data - test_contact_saved_to_db – Contact record saved with email_sent=True - test_contact_email_failure_does_not_break_response – email failure still returns 200 ### tests/test_rental_router.py (7 tests) - test_rental_valid_returns_201 – valid POST → 201 with HMS-YYYY-NNNNN reference - test_rental_date_end_before_start_returns_422 – date_end < date_start → 422 - test_rental_empty_items_returns_422 – empty items array → 422 - test_rental_saved_to_db – RentalRequest + RentalRequestItem saved, rentman_sync_status=success - test_rental_triggers_rentman – RentmanService.create_project_request + add_equipment_to_request called - test_rental_triggers_email – EmailService.send_rental_confirmation called with ref + items - test_rental_rentman_failure_still_returns_201 – Rentman failure still returns 201 ### tests/test_email_service.py (7 tests) - test_send_contact_email_success – aiosmtplib.send called, MIMEMultipart contains name + message - test_send_rental_confirmation_success – email contains reference_number + items - test_send_email_smtp_failure_returns_false – SMTP exception → returns False, no crash - test_send_contact_email_smtp_failure_returns_false – contact email SMTP failure → False - test_send_rental_confirmation_smtp_failure_returns_false – rental email SMTP failure → False - test_send_email_failure_saves_to_queue – failed email saved to email_queue table - test_retry_failed_emails_resends_pending – retry sends queued email, marks as sent ### tests/test_rate_limiting.py (3 tests) - test_contact_rate_limit_allows_5_blocks_6th – 5 requests OK, 6th → 429 - test_rental_rate_limit_allows_5_blocks_6th – 5 requests OK, 6th → 429 - test_contact_rate_window_reset – rate window reset allows new requests after TTL ## New Feature: Email Retry Queue - `app/models/email_queue.py` – EmailQueue model (id, to_addr, subject, html_body, text_body, reply_to, status, attempts, created_at, updated_at) - `app/services/email_service.py` – send_email now accepts optional `db` param; on SMTP failure saves to queue - `EmailService.retry_failed_emails(db)` – static method to retry pending emails (max 5 attempts) - `app/main.py` – APScheduler job `run_email_retry` every 15 minutes - Routers updated to pass `db` to email service methods ## Smoke Test - All 57 tests pass (23 new T05 + 34 existing) - No existing tests broken - Coverage: 78% overall, email_service 90%, contact.py 67%, rental_requests.py 46% - Rate limiting works via mock cache.incr_rate with sequential return values - Email queue persistence verified with in-memory SQLite - Retry mechanism verified: pending → sent on success