From 162e633c627378a76732cc27a4a1e683e94fe839 Mon Sep 17 00:00:00 2001 From: Deeman Date: Mon, 2 Mar 2026 09:40:52 +0100 Subject: [PATCH] refactor(tests): compress admin_client + mock_send_email into conftest Lift admin_client fixture from 7 duplicate definitions into conftest.py. Add mock_send_email fixture, replacing 60 inline patch() blocks across test_emails.py, test_waitlist.py, and test_businessplan.py. Net -197 lines. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 3 + web/tests/conftest.py | 26 ++ web/tests/test_businessplan.py | 39 ++- web/tests/test_content.py | 20 -- web/tests/test_email_templates.py | 21 -- web/tests/test_emails.py | 497 +++++++++++++----------------- web/tests/test_feature_flags.py | 20 -- web/tests/test_outreach.py | 20 -- web/tests/test_pipeline.py | 21 -- web/tests/test_pseo.py | 21 -- web/tests/test_seo.py | 20 -- web/tests/test_waitlist.py | 74 +++-- 12 files changed, 294 insertions(+), 488 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6602797..0177829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **SQLMesh macros** (`transform/macros/__init__.py`): 5 new macros compress repeated country code patterns across 7 SQL models: `@country_name`, `@country_slug`, `@normalize_eurostat_country`, `@normalize_eurostat_nuts`, `@infer_country_from_coords`. - **Extract helpers** (`extract/utils.py`): `skip_if_current()` compresses cursor-check + early-return pattern (3 extractors); `write_jsonl_atomic()` compresses working-file → JSONL → compress pattern (2 extractors). - **Coding philosophy updated** (`~/.claude/coding_philosophy.md`): added `` section documenting the workflow, the test ("Did this abstraction make the total codebase smaller?"), and distinction from premature DRY. +- **Test suite compression pass** — applied same compression workflow to `web/tests/` (30 files, 13,949 lines). Net result: -197 lines across 11 files. + - **`admin_client` fixture** lifted from 7 duplicate definitions into `conftest.py`. + - **`mock_send_email` fixture** added to `conftest.py`, replacing 60 inline `with patch("padelnomics.worker.send_email", ...)` blocks across `test_emails.py` (51), `test_waitlist.py` (4), `test_businessplan.py` (2). Each refactored test drops one indentation level. ### Fixed - **Admin: empty confirm dialog on auto-poll** — `htmx:confirm` handler now guards with `if (!evt.detail.question) return` so auto-poll requests (`hx-trigger="every 5s"`, no `hx-confirm` attribute) no longer trigger an empty dialog every 5 seconds. diff --git a/web/tests/conftest.py b/web/tests/conftest.py index 8f8eb52..bdfde3d 100644 --- a/web/tests/conftest.py +++ b/web/tests/conftest.py @@ -125,6 +125,32 @@ async def auth_client(app, test_user): yield c +@pytest.fixture +async def admin_client(app, db): + """Test client with an admin user pre-loaded in session.""" + now = datetime.now(UTC).isoformat() + async with db.execute( + "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", + ("admin@test.com", "Admin", now), + ) as cursor: + user_id = cursor.lastrowid + await db.execute( + "INSERT INTO user_roles (user_id, role) VALUES (?, 'admin')", (user_id,) + ) + await db.commit() + async with app.test_client() as c: + async with c.session_transaction() as sess: + sess["user_id"] = user_id + yield c + + +@pytest.fixture +def mock_send_email(): + """Patch padelnomics.worker.send_email for the duration of the test.""" + with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock: + yield mock + + # ── Subscriptions ──────────────────────────────────────────── @pytest.fixture diff --git a/web/tests/test_businessplan.py b/web/tests/test_businessplan.py index 508d0e5..cd8510d 100644 --- a/web/tests/test_businessplan.py +++ b/web/tests/test_businessplan.py @@ -9,7 +9,6 @@ Covers: """ from datetime import UTC, datetime from pathlib import Path -from unittest.mock import AsyncMock, patch import pytest from padelnomics.businessplan import generate_business_plan, get_plan_sections @@ -184,19 +183,18 @@ async def _insert_export(db, user_id: int, scenario_id: int, status: str = "pend @requires_weasyprint class TestWorkerHandler: - async def test_happy_path_generates_pdf_and_updates_status(self, db, scenario): + async def test_happy_path_generates_pdf_and_updates_status(self, db, scenario, mock_send_email): from padelnomics.worker import handle_generate_business_plan export = await _insert_export(db, scenario["user_id"], scenario["id"]) output_file = None - with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_email: - await handle_generate_business_plan({ - "export_id": export["id"], - "user_id": scenario["user_id"], - "scenario_id": scenario["id"], - "language": "en", - }) + await handle_generate_business_plan({ + "export_id": export["id"], + "user_id": scenario["user_id"], + "scenario_id": scenario["id"], + "language": "en", + }) # Status should be 'ready' from padelnomics.core import fetch_one @@ -214,14 +212,14 @@ class TestWorkerHandler: assert output_file.read_bytes()[:4] == b"%PDF" # Email should have been sent - mock_email.assert_called_once() - assert "to" in mock_email.call_args.kwargs - assert "subject" in mock_email.call_args.kwargs + mock_send_email.assert_called_once() + assert "to" in mock_send_email.call_args.kwargs + assert "subject" in mock_send_email.call_args.kwargs finally: if output_file and output_file.exists(): output_file.unlink() - async def test_marks_failed_on_bad_scenario(self, db, scenario): + async def test_marks_failed_on_bad_scenario(self, db, scenario, mock_send_email): """Handler marks export failed when user_id doesn't match scenario owner.""" from padelnomics.worker import handle_generate_business_plan @@ -229,14 +227,13 @@ class TestWorkerHandler: wrong_user_id = scenario["user_id"] + 9999 export = await _insert_export(db, scenario["user_id"], scenario["id"]) - with patch("padelnomics.worker.send_email", new_callable=AsyncMock): - with pytest.raises(ValueError): - await handle_generate_business_plan({ - "export_id": export["id"], - "user_id": wrong_user_id, - "scenario_id": scenario["id"], - "language": "en", - }) + with pytest.raises(ValueError): + await handle_generate_business_plan({ + "export_id": export["id"], + "user_id": wrong_user_id, + "scenario_id": scenario["id"], + "language": "en", + }) from padelnomics.core import fetch_one row = await fetch_one( diff --git a/web/tests/test_content.py b/web/tests/test_content.py index c70063c..ecbe09f 100644 --- a/web/tests/test_content.py +++ b/web/tests/test_content.py @@ -938,26 +938,6 @@ class TestRouteRegistration: # Admin routes (require admin session) # ════════════════════════════════════════════════════════════ -@pytest.fixture -async def admin_client(app, db): - """Test client with admin user (has admin role).""" - now = utcnow_iso() - async with db.execute( - "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", - ("admin@test.com", "Admin", now), - ) as cursor: - admin_id = cursor.lastrowid - await db.execute( - "INSERT INTO user_roles (user_id, role) VALUES (?, 'admin')", (admin_id,) - ) - await db.commit() - - async with app.test_client() as c: - async with c.session_transaction() as sess: - sess["user_id"] = admin_id - yield c - - class TestAdminTemplates: async def test_template_list_requires_admin(self, client): resp = await client.get("/admin/templates") diff --git a/web/tests/test_email_templates.py b/web/tests/test_email_templates.py index 9c110ff..0d97e5e 100644 --- a/web/tests/test_email_templates.py +++ b/web/tests/test_email_templates.py @@ -9,7 +9,6 @@ Admin gallery tests: access control, list page, preview page, error handling. """ import pytest -from padelnomics.core import utcnow_iso from padelnomics.email_templates import EMAIL_TEMPLATE_REGISTRY, render_email_template # ── render_email_template() ────────────────────────────────────────────────── @@ -124,26 +123,6 @@ class TestRenderEmailTemplate: # ── Admin gallery routes ────────────────────────────────────────────────────── -@pytest.fixture -async def admin_client(app, db): - """Test client with a user that has the admin role.""" - now = utcnow_iso() - async with db.execute( - "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", - ("gallery_admin@test.com", "Gallery Admin", now), - ) as cursor: - admin_id = cursor.lastrowid - await db.execute( - "INSERT INTO user_roles (user_id, role) VALUES (?, 'admin')", (admin_id,) - ) - await db.commit() - - async with app.test_client() as c: - async with c.session_transaction() as sess: - sess["user_id"] = admin_id - yield c - - class TestEmailGalleryRoutes: @pytest.mark.asyncio async def test_gallery_requires_auth(self, client): diff --git a/web/tests/test_emails.py b/web/tests/test_emails.py index eaac104..4598fb8 100644 --- a/web/tests/test_emails.py +++ b/web/tests/test_emails.py @@ -50,59 +50,51 @@ def _assert_common_design(html: str, lang: str = "en"): 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" + async def test_sends_to_correct_recipient(self, mock_send_email): + await handle_send_magic_link({"email": "user@example.com", "token": "abc123"}) + kw = _call_kwargs(mock_send_email) + 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() + async def test_subject_contains_app_name(self, mock_send_email): + await handle_send_magic_link({"email": "user@example.com", "token": "abc123"}) + kw = _call_kwargs(mock_send_email) + 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"] + async def test_html_contains_verify_link(self, mock_send_email): + await handle_send_magic_link({"email": "user@example.com", "token": "abc123"}) + kw = _call_kwargs(mock_send_email) + 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 + async def test_html_contains_fallback_link_text(self, mock_send_email): + await handle_send_magic_link({"email": "user@example.com", "token": "tok"}) + html = _call_kwargs(mock_send_email)["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"] + async def test_uses_transactional_from_addr(self, mock_send_email): + await handle_send_magic_link({"email": "user@example.com", "token": "tok"}) + assert _call_kwargs(mock_send_email)["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 + async def test_preheader_mentions_expiry(self, mock_send_email): + await handle_send_magic_link({"email": "user@example.com", "token": "tok"}) + html = _call_kwargs(mock_send_email)["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"]) + async def test_design_elements_present(self, mock_send_email): + await handle_send_magic_link({"email": "user@example.com", "token": "tok"}) + _assert_common_design(_call_kwargs(mock_send_email)["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") + async def test_respects_lang_parameter(self, mock_send_email): + await handle_send_magic_link({"email": "user@example.com", "token": "tok", "lang": "de"}) + html = _call_kwargs(mock_send_email)["html"] + _assert_common_design(html, lang="de") # ── Welcome ────────────────────────────────────────────────────── @@ -110,59 +102,51 @@ class TestMagicLink: 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" + async def test_sends_to_correct_recipient(self, mock_send_email): + await handle_send_welcome({"email": "new@example.com"}) + assert _call_kwargs(mock_send_email)["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 + async def test_subject_not_empty(self, mock_send_email): + await handle_send_welcome({"email": "new@example.com"}) + assert len(_call_kwargs(mock_send_email)["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 + async def test_html_contains_quickstart_links(self, mock_send_email): + await handle_send_welcome({"email": "new@example.com"}) + html = _call_kwargs(mock_send_email)["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 + async def test_uses_first_name_when_provided(self, mock_send_email): + await handle_send_welcome({"email": "new@example.com", "name": "Alice Smith"}) + html = _call_kwargs(mock_send_email)["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() + async def test_fallback_greeting_when_no_name(self, mock_send_email): + await handle_send_welcome({"email": "new@example.com"}) + html = _call_kwargs(mock_send_email)["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"] + async def test_uses_transactional_from_addr(self, mock_send_email): + await handle_send_welcome({"email": "new@example.com"}) + assert _call_kwargs(mock_send_email)["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"]) + async def test_design_elements_present(self, mock_send_email): + await handle_send_welcome({"email": "new@example.com"}) + _assert_common_design(_call_kwargs(mock_send_email)["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") + async def test_german_welcome(self, mock_send_email): + await handle_send_welcome({"email": "new@example.com", "lang": "de"}) + html = _call_kwargs(mock_send_email)["html"] + _assert_common_design(html, lang="de") # ── Quote Verification ─────────────────────────────────────────── @@ -180,57 +164,50 @@ class TestQuoteVerification: } @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" + async def test_sends_to_contact_email(self, mock_send_email): + await handle_send_quote_verification(self._BASE_PAYLOAD) + assert _call_kwargs(mock_send_email)["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 + async def test_html_contains_verify_link(self, mock_send_email): + await handle_send_quote_verification(self._BASE_PAYLOAD) + html = _call_kwargs(mock_send_email)["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 + async def test_html_contains_project_recap(self, mock_send_email): + await handle_send_quote_verification(self._BASE_PAYLOAD) + html = _call_kwargs(mock_send_email)["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 + async def test_uses_first_name_from_contact(self, mock_send_email): + await handle_send_quote_verification(self._BASE_PAYLOAD) + html = _call_kwargs(mock_send_email)["html"] + assert "Bob" in html @pytest.mark.asyncio - async def test_handles_minimal_payload(self): + async def test_handles_minimal_payload(self, mock_send_email): """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() + await handle_send_quote_verification({ + "email": "lead@example.com", + "token": "tok", + "lead_token": "ltok", + }) + mock_send_email.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"] + async def test_uses_transactional_from_addr(self, mock_send_email): + await handle_send_quote_verification(self._BASE_PAYLOAD) + assert _call_kwargs(mock_send_email)["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"]) + async def test_design_elements_present(self, mock_send_email): + await handle_send_quote_verification(self._BASE_PAYLOAD) + _assert_common_design(_call_kwargs(mock_send_email)["html"]) # ── Lead Forward (the money email) ────────────────────────────── @@ -238,89 +215,71 @@ class TestQuoteVerification: class TestLeadForward: @pytest.mark.asyncio - async def test_sends_to_supplier_email(self, db): + async def test_sends_to_supplier_email(self, db, mock_send_email): 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" + await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) + assert _call_kwargs(mock_send_email)["to"] == "supplier@test.com" @pytest.mark.asyncio - async def test_subject_contains_heat_and_country(self, db): + async def test_subject_contains_heat_and_country(self, db, mock_send_email): 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 + await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) + subject = _call_kwargs(mock_send_email)["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): + async def test_html_contains_heat_badge(self, db, mock_send_email): 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 + await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) + html = _call_kwargs(mock_send_email)["html"] + assert "#DC2626" in html # HOT badge color + assert "HOT" in html @pytest.mark.asyncio - async def test_html_contains_project_brief(self, db): + async def test_html_contains_project_brief(self, db, mock_send_email): 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 + await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) + html = _call_kwargs(mock_send_email)["html"] + assert "Indoor" in html + assert "Germany" in html @pytest.mark.asyncio - async def test_html_contains_contact_info(self, db): + async def test_html_contains_contact_info(self, db, mock_send_email): 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 + await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) + html = _call_kwargs(mock_send_email)["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): + async def test_html_contains_urgency_callout(self, db, mock_send_email): 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 + await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) + html = _call_kwargs(mock_send_email)["html"] + # Urgency callout has yellow background + assert "#FEF3C7" in html @pytest.mark.asyncio - async def test_html_contains_direct_reply_cta(self, db): + async def test_html_contains_direct_reply_cta(self, db, mock_send_email): 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 + await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) + html = _call_kwargs(mock_send_email)["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): + async def test_uses_leads_from_addr(self, db, mock_send_email): 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"] + await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) + assert _call_kwargs(mock_send_email)["from_addr"] == core.EMAIL_ADDRESSES["leads"] @pytest.mark.asyncio - async def test_updates_email_sent_at(self, db): + async def test_updates_email_sent_at(self, db, mock_send_email): 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}) + 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 = ?", @@ -331,30 +290,24 @@ class TestLeadForward: assert row["email_sent_at"] is not None @pytest.mark.asyncio - async def test_skips_when_no_supplier_email(self, db): + async def test_skips_when_no_supplier_email(self, db, mock_send_email): """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() + await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) + mock_send_email.assert_not_called() @pytest.mark.asyncio - async def test_skips_when_lead_not_found(self, db): + async def test_skips_when_lead_not_found(self, db, mock_send_email): """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() + await handle_send_lead_forward_email({"lead_id": 99999, "supplier_id": supplier_id}) + mock_send_email.assert_not_called() @pytest.mark.asyncio - async def test_design_elements_present(self, db): + async def test_design_elements_present(self, db, mock_send_email): 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"]) + await handle_send_lead_forward_email({"lead_id": lead_id, "supplier_id": supplier_id}) + _assert_common_design(_call_kwargs(mock_send_email)["html"]) # ── Lead Matched Notification ──────────────────────────────────── @@ -362,70 +315,55 @@ class TestLeadForward: class TestLeadMatched: @pytest.mark.asyncio - async def test_sends_to_lead_contact_email(self, db): + async def test_sends_to_lead_contact_email(self, db, mock_send_email): 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" + await handle_send_lead_matched_notification({"lead_id": lead_id}) + assert _call_kwargs(mock_send_email)["to"] == "lead@buyer.com" @pytest.mark.asyncio - async def test_subject_contains_first_name(self, db): + async def test_subject_contains_first_name(self, db, mock_send_email): 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"] + await handle_send_lead_matched_notification({"lead_id": lead_id}) + assert "John" in _call_kwargs(mock_send_email)["subject"] @pytest.mark.asyncio - async def test_html_contains_what_happens_next(self, db): + async def test_html_contains_what_happens_next(self, db, mock_send_email): 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 + await handle_send_lead_matched_notification({"lead_id": lead_id}) + html = _call_kwargs(mock_send_email)["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): + async def test_html_contains_project_context(self, db, mock_send_email): 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 + await handle_send_lead_matched_notification({"lead_id": lead_id}) + html = _call_kwargs(mock_send_email)["html"] + assert "Indoor" in html + assert "Germany" in html @pytest.mark.asyncio - async def test_uses_leads_from_addr(self, db): + async def test_uses_leads_from_addr(self, db, mock_send_email): 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"] + await handle_send_lead_matched_notification({"lead_id": lead_id}) + assert _call_kwargs(mock_send_email)["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() + async def test_skips_when_lead_not_found(self, db, mock_send_email): + await handle_send_lead_matched_notification({"lead_id": 99999}) + mock_send_email.assert_not_called() @pytest.mark.asyncio - async def test_skips_when_no_contact_email(self, db): + async def test_skips_when_no_contact_email(self, db, mock_send_email): 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() + await handle_send_lead_matched_notification({"lead_id": lead_id}) + mock_send_email.assert_not_called() @pytest.mark.asyncio - async def test_design_elements_present(self, db): + async def test_design_elements_present(self, db, mock_send_email): 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"]) + await handle_send_lead_matched_notification({"lead_id": lead_id}) + _assert_common_design(_call_kwargs(mock_send_email)["html"]) # ── Supplier Enquiry ───────────────────────────────────────────── @@ -441,50 +379,43 @@ class TestSupplierEnquiry: } @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" + async def test_sends_to_supplier_email(self, mock_send_email): + await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) + assert _call_kwargs(mock_send_email)["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"] + async def test_subject_contains_contact_name(self, mock_send_email): + await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) + assert "Alice Smith" in _call_kwargs(mock_send_email)["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 + async def test_html_contains_message(self, mock_send_email): + await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) + html = _call_kwargs(mock_send_email)["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 + async def test_html_contains_respond_fast_nudge(self, mock_send_email): + await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) + html = _call_kwargs(mock_send_email)["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() + async def test_skips_when_no_supplier_email(self, mock_send_email): + await handle_send_supplier_enquiry_email({**self._BASE_PAYLOAD, "supplier_email": ""}) + mock_send_email.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"] + async def test_uses_transactional_from_addr(self, mock_send_email): + await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) + assert _call_kwargs(mock_send_email)["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"]) + async def test_design_elements_present(self, mock_send_email): + await handle_send_supplier_enquiry_email(self._BASE_PAYLOAD) + _assert_common_design(_call_kwargs(mock_send_email)["html"]) # ── Waitlist (supplement existing test_waitlist.py) ────────────── @@ -494,33 +425,29 @@ 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 + async def test_general_waitlist_has_preheader(self, mock_send_email): + await handle_send_waitlist_confirmation({"email": "u@example.com", "intent": "signup"}) + html = _call_kwargs(mock_send_email)["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() + async def test_supplier_waitlist_mentions_plan(self, mock_send_email): + await handle_send_waitlist_confirmation({"email": "s@example.com", "intent": "supplier_growth"}) + kw = _call_kwargs(mock_send_email) + 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"]) + async def test_general_waitlist_design_elements(self, mock_send_email): + await handle_send_waitlist_confirmation({"email": "u@example.com", "intent": "signup"}) + _assert_common_design(_call_kwargs(mock_send_email)["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 + async def test_supplier_waitlist_perks_listed(self, mock_send_email): + await handle_send_waitlist_confirmation({"email": "s@example.com", "intent": "supplier_pro"}) + html = _call_kwargs(mock_send_email)["html"] + # Should have
  • perks + assert html.count("
  • ") >= 3 # ── DB seed helpers ────────────────────────────────────────────── diff --git a/web/tests/test_feature_flags.py b/web/tests/test_feature_flags.py index df822bb..6b519bd 100644 --- a/web/tests/test_feature_flags.py +++ b/web/tests/test_feature_flags.py @@ -10,7 +10,6 @@ import sqlite3 from unittest.mock import AsyncMock, patch import pytest -from padelnomics.core import utcnow_iso from padelnomics.migrations.migrate import migrate from padelnomics import core @@ -25,25 +24,6 @@ def mock_csrf_validation(): yield -@pytest.fixture -async def admin_client(app, db): - """Test client with an admin-role user session (module-level, follows test_content.py).""" - now = utcnow_iso() - async with db.execute( - "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", - ("flags_admin@test.com", "Flags Admin", now), - ) as cursor: - admin_id = cursor.lastrowid - await db.execute( - "INSERT INTO user_roles (user_id, role) VALUES (?, 'admin')", (admin_id,) - ) - await db.commit() - async with app.test_client() as c: - async with c.session_transaction() as sess: - sess["user_id"] = admin_id - yield c - - async def _set_flag(db, name: str, enabled: bool, description: str = ""): """Insert or replace a flag in the test DB.""" await db.execute( diff --git a/web/tests/test_outreach.py b/web/tests/test_outreach.py index 9013214..bbf6817 100644 --- a/web/tests/test_outreach.py +++ b/web/tests/test_outreach.py @@ -46,26 +46,6 @@ def _bypass_csrf(): yield -@pytest.fixture -async def admin_client(app, db): - """Test client with an admin user pre-loaded in session.""" - now = datetime.now(UTC).isoformat() - async with db.execute( - "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", - ("admin@example.com", "Admin User", now), - ) as cursor: - user_id = cursor.lastrowid - await db.execute( - "INSERT INTO user_roles (user_id, role) VALUES (?, 'admin')", (user_id,) - ) - await db.commit() - - async with app.test_client() as c: - async with c.session_transaction() as sess: - sess["user_id"] = user_id - yield c - - async def _insert_supplier( db, name: str = "Test Supplier", diff --git a/web/tests/test_pipeline.py b/web/tests/test_pipeline.py index e4352f3..bdd4b96 100644 --- a/web/tests/test_pipeline.py +++ b/web/tests/test_pipeline.py @@ -14,31 +14,10 @@ from unittest.mock import AsyncMock, MagicMock, patch import padelnomics.admin.pipeline_routes as pipeline_mod import pytest -from padelnomics.core import utcnow_iso # ── Fixtures ────────────────────────────────────────────────────────────────── -@pytest.fixture -async def admin_client(app, db): - """Authenticated admin test client.""" - now = utcnow_iso() - async with db.execute( - "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", - ("pipeline-admin@test.com", "Pipeline Admin", now), - ) as cursor: - admin_id = cursor.lastrowid - await db.execute( - "INSERT INTO user_roles (user_id, role) VALUES (?, 'admin')", (admin_id,) - ) - await db.commit() - - async with app.test_client() as c: - async with c.session_transaction() as sess: - sess["user_id"] = admin_id - yield c - - @pytest.fixture def state_db_dir(): """Temp directory with a seeded .state.sqlite for testing.""" diff --git a/web/tests/test_pseo.py b/web/tests/test_pseo.py index 45627eb..8bef849 100644 --- a/web/tests/test_pseo.py +++ b/web/tests/test_pseo.py @@ -10,7 +10,6 @@ Covers: import json from unittest.mock import patch -import pytest from padelnomics.content.health import ( check_broken_scenario_refs, check_hreflang_orphans, @@ -27,26 +26,6 @@ from padelnomics import core # ── Fixtures ────────────────────────────────────────────────────────────────── -@pytest.fixture -async def admin_client(app, db): - """Authenticated admin test client.""" - now = utcnow_iso() - async with db.execute( - "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", - ("pseo-admin@test.com", "pSEO Admin", now), - ) as cursor: - admin_id = cursor.lastrowid - await db.execute( - "INSERT INTO user_roles (user_id, role) VALUES (?, 'admin')", (admin_id,) - ) - await db.commit() - - async with app.test_client() as c: - async with c.session_transaction() as sess: - sess["user_id"] = admin_id - yield c - - # ── DB helpers ──────────────────────────────────────────────────────────────── diff --git a/web/tests/test_seo.py b/web/tests/test_seo.py index 31e8a1a..faff5d1 100644 --- a/web/tests/test_seo.py +++ b/web/tests/test_seo.py @@ -89,26 +89,6 @@ async def articles_data(db, seo_data): await db.commit() -@pytest.fixture -async def admin_client(app, db): - """Authenticated admin client.""" - now = utcnow_iso() - async with db.execute( - "INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)", - ("admin@test.com", "Admin", now), - ) as cursor: - admin_id = cursor.lastrowid - await db.execute( - "INSERT INTO user_roles (user_id, role) VALUES (?, 'admin')", (admin_id,) - ) - await db.commit() - - async with app.test_client() as c: - async with c.session_transaction() as sess: - sess["user_id"] = admin_id - yield c - - # ── Query function tests ───────────────────────────────────── class TestSearchPerformance: diff --git a/web/tests/test_waitlist.py b/web/tests/test_waitlist.py index cef4250..9762992 100644 --- a/web/tests/test_waitlist.py +++ b/web/tests/test_waitlist.py @@ -188,59 +188,55 @@ class TestWorkerTask: """Test send_waitlist_confirmation worker task.""" @pytest.mark.asyncio - async def test_sends_entrepreneur_confirmation(self): + async def test_sends_entrepreneur_confirmation(self, mock_send_email): """Task sends confirmation email for entrepreneur signup.""" - with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: - await handle_send_waitlist_confirmation({ - "email": "entrepreneur@example.com", - "intent": "signup", - }) + await handle_send_waitlist_confirmation({ + "email": "entrepreneur@example.com", + "intent": "signup", + }) - mock_send.assert_called_once() - call_args = mock_send.call_args - assert call_args.kwargs["to"] == "entrepreneur@example.com" - assert "notify you at launch" in call_args.kwargs["subject"].lower() - assert "waitlist" in call_args.kwargs["html"].lower() + mock_send_email.assert_called_once() + call_args = mock_send_email.call_args + assert call_args.kwargs["to"] == "entrepreneur@example.com" + assert "notify you at launch" in call_args.kwargs["subject"].lower() + assert "waitlist" in call_args.kwargs["html"].lower() @pytest.mark.asyncio - async def test_sends_supplier_confirmation(self): + async def test_sends_supplier_confirmation(self, mock_send_email): """Task sends confirmation email for supplier signup.""" - with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: - await handle_send_waitlist_confirmation({ - "email": "supplier@example.com", - "intent": "supplier_growth", - }) + await handle_send_waitlist_confirmation({ + "email": "supplier@example.com", + "intent": "supplier_growth", + }) - mock_send.assert_called_once() - call_args = mock_send.call_args - assert call_args.kwargs["to"] == "supplier@example.com" - assert "growth" in call_args.kwargs["subject"].lower() - assert "supplier" in call_args.kwargs["html"].lower() + mock_send_email.assert_called_once() + call_args = mock_send_email.call_args + assert call_args.kwargs["to"] == "supplier@example.com" + assert "growth" in call_args.kwargs["subject"].lower() + assert "supplier" in call_args.kwargs["html"].lower() @pytest.mark.asyncio - async def test_supplier_email_includes_plan_name(self): + async def test_supplier_email_includes_plan_name(self, mock_send_email): """Supplier confirmation should mention the specific plan.""" - with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: - await handle_send_waitlist_confirmation({ - "email": "supplier@example.com", - "intent": "supplier_pro", - }) + await handle_send_waitlist_confirmation({ + "email": "supplier@example.com", + "intent": "supplier_pro", + }) - call_args = mock_send.call_args - html = call_args.kwargs["html"] - assert "pro" in html.lower() + call_args = mock_send_email.call_args + html = call_args.kwargs["html"] + assert "pro" in html.lower() @pytest.mark.asyncio - async def test_uses_transactional_email_address(self): + async def test_uses_transactional_email_address(self, mock_send_email): """Task should use transactional sender address.""" - with patch("padelnomics.worker.send_email", new_callable=AsyncMock) as mock_send: - await handle_send_waitlist_confirmation({ - "email": "test@example.com", - "intent": "signup", - }) + await handle_send_waitlist_confirmation({ + "email": "test@example.com", + "intent": "signup", + }) - call_args = mock_send.call_args - assert call_args.kwargs["from_addr"] == core.EMAIL_ADDRESSES["transactional"] + call_args = mock_send_email.call_args + assert call_args.kwargs["from_addr"] == core.EMAIL_ADDRESSES["transactional"] # ── TestAuthRoutes ────────────────────────────────────────────────