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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 <li> perks
|
||||
assert html.count("<li>") >= 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 <li> perks
|
||||
assert html.count("<li>") >= 3
|
||||
|
||||
|
||||
# ── DB seed helpers ──────────────────────────────────────────────
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user