""" Tests for all transactional email worker handlers. Each handler is tested by mocking send_email and calling the handler directly with a payload. Assertions cover: recipient, subject keywords, HTML content (design elements, i18n keys resolved), and from_addr. The TestResendLive class at the bottom sends real emails via Resend's @resend.dev test addresses. Skipped unless RESEND_API_KEY is set. """ import asyncio import os from datetime import UTC, datetime from unittest.mock import AsyncMock, patch import pytest from padelnomics.worker import ( handle_send_lead_forward_email, handle_send_lead_matched_notification, handle_send_magic_link, handle_send_quote_verification, handle_send_supplier_enquiry_email, handle_send_waitlist_confirmation, handle_send_welcome, ) from padelnomics import core # ── Helpers ────────────────────────────────────────────────────── def _call_kwargs(mock_send: AsyncMock) -> dict: """Extract kwargs from first call to mock send_email.""" mock_send.assert_called_once() return mock_send.call_args.kwargs def _assert_common_design(html: str, lang: str = "en"): """Verify shared email wrapper design elements are present.""" assert "padelnomics" in html # wordmark assert "Bricolage Grotesque" in html # brand font assert "#1D4ED8" in html # blue accent / button assert "padelnomics.io" in html # footer link assert f'lang="{lang}"' in html # html lang attribute # ── Magic Link ─────────────────────────────────────────────────── class TestMagicLink: @pytest.mark.asyncio async def test_sends_to_correct_recipient(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_magic_link({"email": "user@example.com", "token": "abc123"}) kw = _call_kwargs(mock_send) assert kw["to"] == "user@example.com" @pytest.mark.asyncio async def test_subject_contains_app_name(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_magic_link({"email": "user@example.com", "token": "abc123"}) kw = _call_kwargs(mock_send) assert core.config.APP_NAME.lower() in kw["subject"].lower() @pytest.mark.asyncio async def test_html_contains_verify_link(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_magic_link({"email": "user@example.com", "token": "abc123"}) kw = _call_kwargs(mock_send) assert "/auth/verify?token=abc123" in kw["html"] @pytest.mark.asyncio async def test_html_contains_fallback_link_text(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_magic_link({"email": "user@example.com", "token": "tok"}) html = _call_kwargs(mock_send)["html"] assert "word-break:break-all" in html # fallback URL block @pytest.mark.asyncio async def test_uses_transactional_from_addr(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_magic_link({"email": "user@example.com", "token": "tok"}) assert _call_kwargs(mock_send)["from_addr"] == core.EMAIL_ADDRESSES["transactional"] @pytest.mark.asyncio async def test_preheader_mentions_expiry(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_magic_link({"email": "user@example.com", "token": "tok"}) html = _call_kwargs(mock_send)["html"] # preheader is hidden span; should mention minutes assert "display:none" in html # preheader present @pytest.mark.asyncio async def test_design_elements_present(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_magic_link({"email": "user@example.com", "token": "tok"}) _assert_common_design(_call_kwargs(mock_send)["html"]) @pytest.mark.asyncio async def test_respects_lang_parameter(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_magic_link({"email": "user@example.com", "token": "tok", "lang": "de"}) html = _call_kwargs(mock_send)["html"] _assert_common_design(html, lang="de") # ── Welcome ────────────────────────────────────────────────────── class TestWelcome: @pytest.mark.asyncio async def test_sends_to_correct_recipient(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_welcome({"email": "new@example.com"}) assert _call_kwargs(mock_send)["to"] == "new@example.com" @pytest.mark.asyncio async def test_subject_not_empty(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_welcome({"email": "new@example.com"}) assert len(_call_kwargs(mock_send)["subject"]) > 5 @pytest.mark.asyncio async def test_html_contains_quickstart_links(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_welcome({"email": "new@example.com"}) html = _call_kwargs(mock_send)["html"] assert "/planner" in html assert "/markets" in html assert "/leads/quote" in html @pytest.mark.asyncio async def test_uses_first_name_when_provided(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_welcome({"email": "new@example.com", "name": "Alice Smith"}) html = _call_kwargs(mock_send)["html"] assert "Alice" in html @pytest.mark.asyncio async def test_fallback_greeting_when_no_name(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_welcome({"email": "new@example.com"}) html = _call_kwargs(mock_send)["html"] # Should use "there" as fallback first_name assert "there" in html.lower() @pytest.mark.asyncio async def test_uses_transactional_from_addr(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_welcome({"email": "new@example.com"}) assert _call_kwargs(mock_send)["from_addr"] == core.EMAIL_ADDRESSES["transactional"] @pytest.mark.asyncio async def test_design_elements_present(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_welcome({"email": "new@example.com"}) _assert_common_design(_call_kwargs(mock_send)["html"]) @pytest.mark.asyncio async def test_german_welcome(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_welcome({"email": "new@example.com", "lang": "de"}) html = _call_kwargs(mock_send)["html"] _assert_common_design(html, lang="de") # ── Quote Verification ─────────────────────────────────────────── class TestQuoteVerification: _BASE_PAYLOAD = { "email": "lead@example.com", "token": "verify_tok", "lead_token": "lead_tok", "contact_name": "Bob Builder", "court_count": 6, "facility_type": "Indoor", "country": "Germany", } @pytest.mark.asyncio async def test_sends_to_contact_email(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_quote_verification(self._BASE_PAYLOAD) assert _call_kwargs(mock_send)["to"] == "lead@example.com" @pytest.mark.asyncio async def test_html_contains_verify_link(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_quote_verification(self._BASE_PAYLOAD) html = _call_kwargs(mock_send)["html"] assert "token=verify_tok" in html assert "lead=lead_tok" in html @pytest.mark.asyncio async def test_html_contains_project_recap(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_quote_verification(self._BASE_PAYLOAD) html = _call_kwargs(mock_send)["html"] assert "6 courts" in html assert "Indoor" in html assert "Germany" in html @pytest.mark.asyncio async def test_uses_first_name_from_contact(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_quote_verification(self._BASE_PAYLOAD) html = _call_kwargs(mock_send)["html"] assert "Bob" in html @pytest.mark.asyncio async def test_handles_minimal_payload(self): """No court_count/facility_type/country — should still send.""" with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_quote_verification({ "email": "lead@example.com", "token": "tok", "lead_token": "ltok", }) mock_send.assert_called_once() @pytest.mark.asyncio async def test_uses_transactional_from_addr(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_quote_verification(self._BASE_PAYLOAD) assert _call_kwargs(mock_send)["from_addr"] == core.EMAIL_ADDRESSES["transactional"] @pytest.mark.asyncio async def test_design_elements_present(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_quote_verification(self._BASE_PAYLOAD) _assert_common_design(_call_kwargs(mock_send)["html"]) # ── Lead Forward (the money email) ────────────────────────────── class TestLeadForward: @pytest.mark.asyncio async def test_sends_to_supplier_email(self, db): lead_id, supplier_id = await _seed_lead_and_supplier(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) assert _call_kwargs(mock_send)["to"] == "supplier@test.com" @pytest.mark.asyncio async def test_subject_contains_heat_and_country(self, db): lead_id, supplier_id = await _seed_lead_and_supplier(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) subject = _call_kwargs(mock_send)["subject"] assert "[HOT]" in subject assert "Germany" in subject assert "4 courts" in subject @pytest.mark.asyncio async def test_html_contains_heat_badge(self, db): lead_id, supplier_id = await _seed_lead_and_supplier(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) html = _call_kwargs(mock_send)["html"] assert "#DC2626" in html # HOT badge color assert "HOT" in html @pytest.mark.asyncio async def test_html_contains_project_brief(self, db): lead_id, supplier_id = await _seed_lead_and_supplier(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) html = _call_kwargs(mock_send)["html"] assert "Indoor" in html assert "Germany" in html @pytest.mark.asyncio async def test_html_contains_contact_info(self, db): lead_id, supplier_id = await _seed_lead_and_supplier(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) html = _call_kwargs(mock_send)["html"] assert "lead@buyer.com" in html assert "mailto:lead@buyer.com" in html assert "John Doe" in html @pytest.mark.asyncio async def test_html_contains_urgency_callout(self, db): lead_id, supplier_id = await _seed_lead_and_supplier(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) html = _call_kwargs(mock_send)["html"] # Urgency callout has yellow background assert "#FEF3C7" in html @pytest.mark.asyncio async def test_html_contains_direct_reply_cta(self, db): lead_id, supplier_id = await _seed_lead_and_supplier(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) html = _call_kwargs(mock_send)["html"] # Direct reply link text should mention the contact email assert "lead@buyer.com" in html @pytest.mark.asyncio async def test_uses_leads_from_addr(self, db): lead_id, supplier_id = await _seed_lead_and_supplier(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) assert _call_kwargs(mock_send)["from_addr"] == core.EMAIL_ADDRESSES["leads"] @pytest.mark.asyncio async def test_updates_email_sent_at(self, db): lead_id, supplier_id = await _seed_lead_and_supplier(db, create_forward=True) with patch("padelnomics.worker.send_email", new_callable=AsyncMock): await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) async with db.execute( "SELECT email_sent_at FROM lead_forwards WHERE lead_id = ? AND supplier_id = ?", (lead_id, supplier_id), ) as cursor: row = await cursor.fetchone() assert row is not None assert row["email_sent_at"] is not None @pytest.mark.asyncio async def test_skips_when_no_supplier_email(self, db): """No email on supplier record — handler exits without sending.""" lead_id, supplier_id = await _seed_lead_and_supplier(db, supplier_email="") with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) mock_send.assert_not_called() @pytest.mark.asyncio async def test_skips_when_lead_not_found(self, db): """Non-existent lead_id — handler exits without sending.""" _, supplier_id = await _seed_lead_and_supplier(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": 99999, "supplier_id": supplier_id}) mock_send.assert_not_called() @pytest.mark.asyncio async def test_design_elements_present(self, db): lead_id, supplier_id = await _seed_lead_and_supplier(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) _assert_common_design(_call_kwargs(mock_send)["html"]) # ── Lead Matched Notification ──────────────────────────────────── class TestLeadMatched: @pytest.mark.asyncio async def test_sends_to_lead_contact_email(self, db): lead_id = await _seed_lead(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_matched_notification({"lead_id": lead_id}) assert _call_kwargs(mock_send)["to"] == "lead@buyer.com" @pytest.mark.asyncio async def test_subject_contains_first_name(self, db): lead_id = await _seed_lead(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_matched_notification({"lead_id": lead_id}) assert "John" in _call_kwargs(mock_send)["subject"] @pytest.mark.asyncio async def test_html_contains_what_happens_next(self, db): lead_id = await _seed_lead(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_matched_notification({"lead_id": lead_id}) html = _call_kwargs(mock_send)["html"] # "What happens next" section and tip callout (blue bg) assert "#F0F9FF" in html # tip callout background @pytest.mark.asyncio async def test_html_contains_project_context(self, db): lead_id = await _seed_lead(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_matched_notification({"lead_id": lead_id}) html = _call_kwargs(mock_send)["html"] assert "Indoor" in html assert "Germany" in html @pytest.mark.asyncio async def test_uses_leads_from_addr(self, db): lead_id = await _seed_lead(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_matched_notification({"lead_id": lead_id}) assert _call_kwargs(mock_send)["from_addr"] == core.EMAIL_ADDRESSES["leads"] @pytest.mark.asyncio async def test_skips_when_lead_not_found(self, db): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_matched_notification({"lead_id": 99999}) mock_send.assert_not_called() @pytest.mark.asyncio async def test_skips_when_no_contact_email(self, db): lead_id = await _seed_lead(db, contact_email="") with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_matched_notification({"lead_id": lead_id}) mock_send.assert_not_called() @pytest.mark.asyncio async def test_design_elements_present(self, db): lead_id = await _seed_lead(db) with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_lead_matched_notification({"lead_id": lead_id}) _assert_common_design(_call_kwargs(mock_send)["html"]) # ── Supplier Enquiry ───────────────────────────────────────────── class TestSupplierEnquiry: _BASE_PAYLOAD = { "supplier_email": "supplier@corp.com", "supplier_name": "PadelBuild GmbH", "contact_name": "Alice Smith", "contact_email": "alice@buyer.com", "message": "I need 4 courts, can you quote?", } @pytest.mark.asyncio async def test_sends_to_supplier_email(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) assert _call_kwargs(mock_send)["to"] == "supplier@corp.com" @pytest.mark.asyncio async def test_subject_contains_contact_name(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) assert "Alice Smith" in _call_kwargs(mock_send)["subject"] @pytest.mark.asyncio async def test_html_contains_message(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) html = _call_kwargs(mock_send)["html"] assert "4 courts" in html assert "alice@buyer.com" in html @pytest.mark.asyncio async def test_html_contains_respond_fast_nudge(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) html = _call_kwargs(mock_send)["html"] # The respond-fast nudge line should be present assert "24" in html # "24 hours" reference @pytest.mark.asyncio async def test_skips_when_no_supplier_email(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_supplier_enquiry_email({**self._BASE_PAYLOAD, "supplier_email": ""}) mock_send.assert_not_called() @pytest.mark.asyncio async def test_uses_transactional_from_addr(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) assert _call_kwargs(mock_send)["from_addr"] == core.EMAIL_ADDRESSES["transactional"] @pytest.mark.asyncio async def test_design_elements_present(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) _assert_common_design(_call_kwargs(mock_send)["html"]) # ── Waitlist (supplement existing test_waitlist.py) ────────────── class TestWaitlistEmails: """Verify design & content for waitlist confirmation emails.""" @pytest.mark.asyncio async def test_general_waitlist_has_preheader(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_waitlist_confirmation({"email": "u@example.com", "intent": "signup"}) html = _call_kwargs(mock_send)["html"] assert "display:none" in html # preheader span @pytest.mark.asyncio async def test_supplier_waitlist_mentions_plan(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_waitlist_confirmation({"email": "s@example.com", "intent": "supplier_growth"}) kw = _call_kwargs(mock_send) assert "growth" in kw["subject"].lower() assert "supplier" in kw["html"].lower() @pytest.mark.asyncio async def test_general_waitlist_design_elements(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_waitlist_confirmation({"email": "u@example.com", "intent": "signup"}) _assert_common_design(_call_kwargs(mock_send)["html"]) @pytest.mark.asyncio async def test_supplier_waitlist_perks_listed(self): with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: await handle_send_waitlist_confirmation({"email": "s@example.com", "intent": "supplier_pro"}) html = _call_kwargs(mock_send)["html"] # Should have
  • perks assert html.count("
  • ") >= 3 # ── DB seed helpers ────────────────────────────────────────────── async def _seed_lead( db, contact_email: str = "lead@buyer.com", contact_name: str = "John Doe", ) -> int: """Insert a lead_request row, return its id.""" now = datetime.now(UTC).isoformat() async with db.execute( """INSERT INTO lead_requests ( lead_type, contact_name, contact_email, contact_phone, contact_company, stakeholder_type, facility_type, court_count, country, location, build_context, glass_type, lighting_type, timeline, budget_estimate, location_status, financing_status, services_needed, additional_info, heat_score, status, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( "quote", contact_name, contact_email, "+49123456", "Padel Corp", "investor", "Indoor", 4, "Germany", "Berlin", "new_build", "tempered", "LED", "6-12 months", 200000, "secured", "seeking", "construction, lighting", "Need turnkey solution", "hot", "verified", now, ), ) as cursor: lead_id = cursor.lastrowid await db.commit() return lead_id async def _seed_supplier( db, contact_email: str = "supplier@test.com", ) -> int: """Insert a supplier row, return its id.""" now = datetime.now(UTC).isoformat() async with db.execute( """INSERT INTO suppliers ( name, slug, country_code, region, category, contact_email, tier, claimed_by, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( "Test Supplier", "test-supplier", "DE", "Europe", "construction", contact_email, "growth", None, now, ), ) as cursor: supplier_id = cursor.lastrowid await db.commit() return supplier_id async def _seed_lead_and_supplier( db, supplier_email: str = "supplier@test.com", create_forward: bool = False, ) -> tuple[int, int]: """Insert a lead + supplier pair, optionally with a lead_forwards row.""" lead_id = await _seed_lead(db) supplier_id = await _seed_supplier(db, contact_email=supplier_email) if create_forward: now = datetime.now(UTC).isoformat() await db.execute( """INSERT INTO lead_forwards (lead_id, supplier_id, credit_cost, created_at) VALUES (?, ?, ?, ?)""", (lead_id, supplier_id, 1, now), ) await db.commit() return lead_id, supplier_id # ── Live Resend integration tests ──────────────────────────────── # # These send real emails to Resend's @resend.dev test addresses, # then retrieve the sent email via resend.Emails.get() to verify the # rendered HTML contains expected design elements and content. # # Skipped unless RESEND_API_KEY is set in the environment. # Run with: RESEND_API_KEY=re_xxx uv run pytest web/tests/test_emails.py -k resend_live -v _has_resend_key = bool(os.environ.get("RESEND_API_KEY")) _skip_no_key = pytest.mark.skipif(not _has_resend_key, reason="RESEND_API_KEY not set") def _send_and_capture(): """Patch resend.Emails.send to capture the returned email ID. Returns (wrapper, captured) where captured is a dict that will contain {"id": ...} after send is called. The real send still executes — this just grabs the return value that core.send_email discards. """ import resend as _resend _original_send = _resend.Emails.send captured = {} def _wrapper(params): result = _original_send(params) captured["id"] = result.id if hasattr(result, "id") else result.get("id") return result return _wrapper, captured def _retrieve_html(captured: dict) -> str | None: """Retrieve the sent email from Resend and return its HTML. Returns None if: - The send failed (rate limit, network error) — captured has no id - The API key lacks read permission (sending_access only) """ import resend as _resend email_id = captured.get("id") if not email_id: return None # send failed (e.g. rate limit) — can't retrieve try: email = _resend.Emails.get(email_id) except Exception: # sending_access key — can't retrieve, that's OK return None return email.html if hasattr(email, "html") else email.get("html", "") @_skip_no_key class TestResendLive: """Send real emails via Resend to @resend.dev test addresses. Uses ``delivered@resend.dev`` which simulates successful delivery. After sending, retrieves the email via resend.Emails.get() and asserts on the rendered HTML (design elements, content, structure). Each test calls the handler WITHOUT mocking send_email, so the full path is exercised: handler → _email_wrap → send_email → Resend API. """ @pytest.fixture(autouse=True) async def _set_resend_key(self): """Ensure config picks up the real API key from env. Adds a 0.6s pause between tests to respect Resend's 2 req/sec rate limit. """ import resend as _resend original = core.config.RESEND_API_KEY core.config.RESEND_API_KEY = os.environ["RESEND_API_KEY"] _resend.api_key = os.environ["RESEND_API_KEY"] yield core.config.RESEND_API_KEY = original await asyncio.sleep(0.6) def _check_html(self, captured: dict, assertions: list[tuple[str, str]]): """Retrieve sent email and run assertions on HTML. If the API key lacks read permission (sending_access only), the retrieve is skipped — the test still passes because the send itself succeeded (no exception from handler). assertions: list of (needle, description) tuples. """ html = _retrieve_html(captured) if html is None: return # sending_access key — can't retrieve, send was enough for needle, desc in assertions: assert needle in html, f"Expected {desc!r} ({needle!r}) in rendered HTML" @pytest.mark.asyncio async def test_magic_link_delivered(self): wrapper, captured = _send_and_capture() with patch("resend.Emails.send", side_effect=wrapper): await handle_send_magic_link({ "email": "delivered@resend.dev", "token": "test_token_magic", "lang": "en", }) self._check_html(captured, [ ("padelnomics", "wordmark"), ("Bricolage Grotesque", "brand font"), ("/auth/verify?token=test_token_magic", "verify link"), ("padelnomics.io", "footer link"), ]) @pytest.mark.asyncio async def test_welcome_delivered(self): wrapper, captured = _send_and_capture() with patch("resend.Emails.send", side_effect=wrapper): await handle_send_welcome({ "email": "delivered@resend.dev", "name": "Test User", "lang": "en", }) self._check_html(captured, [ ("Test", "first name"), ("/planner", "planner link"), ("/markets", "markets link"), ("/leads/quote", "quotes link"), ]) @pytest.mark.asyncio async def test_quote_verification_delivered(self): wrapper, captured = _send_and_capture() with patch("resend.Emails.send", side_effect=wrapper): await handle_send_quote_verification({ "email": "delivered@resend.dev", "token": "test_verify_tok", "lead_token": "test_lead_tok", "contact_name": "Test Buyer", "court_count": 4, "facility_type": "Indoor", "country": "Germany", "lang": "en", }) self._check_html(captured, [ ("4 courts", "court count"), ("Indoor", "facility type"), ("Germany", "country"), ("token=test_verify_tok", "verify token"), ]) @pytest.mark.asyncio async def test_waitlist_general_delivered(self): wrapper, captured = _send_and_capture() with patch("resend.Emails.send", side_effect=wrapper): await handle_send_waitlist_confirmation({ "email": "delivered@resend.dev", "intent": "signup", "lang": "en", }) self._check_html(captured, [ ("padelnomics", "wordmark"), ("
  • ", "perk bullets"), ]) @pytest.mark.asyncio async def test_waitlist_supplier_delivered(self): wrapper, captured = _send_and_capture() with patch("resend.Emails.send", side_effect=wrapper): await handle_send_waitlist_confirmation({ "email": "delivered@resend.dev", "intent": "supplier_growth", "lang": "en", }) self._check_html(captured, [ ("Growth", "plan name"), ]) @pytest.mark.asyncio async def test_lead_forward_delivered(self, db): lead_id, supplier_id = await _seed_lead_and_supplier( db, supplier_email="delivered@resend.dev", ) wrapper, captured = _send_and_capture() with patch("resend.Emails.send", side_effect=wrapper): await handle_send_lead_forward_email({ "lead_id": lead_id, "supplier_id": supplier_id, "lang": "en", }) self._check_html(captured, [ ("#DC2626", "HOT badge color"), ("lead@buyer.com", "contact email"), ("mailto:lead@buyer.com", "mailto link"), ("#FEF3C7", "urgency callout bg"), ]) @pytest.mark.asyncio async def test_lead_matched_delivered(self, db): lead_id = await _seed_lead(db, contact_email="delivered@resend.dev") wrapper, captured = _send_and_capture() with patch("resend.Emails.send", side_effect=wrapper): await handle_send_lead_matched_notification({ "lead_id": lead_id, "lang": "en", }) self._check_html(captured, [ ("#F0F9FF", "tip callout bg"), ("Indoor", "facility type"), ("Germany", "country"), ]) @pytest.mark.asyncio async def test_supplier_enquiry_delivered(self): wrapper, captured = _send_and_capture() with patch("resend.Emails.send", side_effect=wrapper): await handle_send_supplier_enquiry_email({ "supplier_email": "delivered@resend.dev", "supplier_name": "PadelBuild GmbH", "contact_name": "Alice Smith", "contact_email": "alice@buyer.example", "message": "I need 4 courts, can you quote?", "lang": "en", }) self._check_html(captured, [ ("Alice Smith", "contact name"), ("alice@buyer.example", "contact email"), ("4 courts", "message content"), ]) @pytest.mark.asyncio async def test_bounce_handled_gracefully(self): """Sending to bounced@resend.dev should not raise — send_email returns str|None.""" result = await core.send_email( to="bounced@resend.dev", subject="Bounce test", html="

    This should bounce.

    ", from_addr=core.EMAIL_ADDRESSES["transactional"], ) # Resend may return success (delivery fails async) or error; # either way the handler must not crash. # send_email now returns resend_id (str) on success, None on failure. assert result is None or isinstance(result, str)