""" Tests for the standalone email template renderer and admin gallery routes. render_email_template() tests: each registry entry renders without error, produces a valid DOCTYPE document, includes the wordmark, and supports both EN and DE languages. 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() ────────────────────────────────────────────────── class TestRenderEmailTemplate: """render_email_template() produces valid HTML for all registry entries.""" @pytest.mark.parametrize("slug", list(EMAIL_TEMPLATE_REGISTRY.keys())) def test_all_templates_render_en(self, slug): entry = EMAIL_TEMPLATE_REGISTRY[slug] sample = entry["sample_data"]("en") html = render_email_template(entry["template"], lang="en", **sample) assert "" in html assert "padelnomics" in html.lower() assert 'lang="en"' in html @pytest.mark.parametrize("slug", list(EMAIL_TEMPLATE_REGISTRY.keys())) def test_all_templates_render_de(self, slug): entry = EMAIL_TEMPLATE_REGISTRY[slug] sample = entry["sample_data"]("de") html = render_email_template(entry["template"], lang="de", **sample) assert "" in html assert "padelnomics" in html.lower() assert 'lang="de"' in html def test_magic_link_contains_verify_link(self): entry = EMAIL_TEMPLATE_REGISTRY["magic_link"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) assert "/auth/verify?token=" in html def test_magic_link_has_preheader(self): entry = EMAIL_TEMPLATE_REGISTRY["magic_link"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) assert "display:none" in html # hidden preheader span def test_lead_forward_has_heat_badge(self): entry = EMAIL_TEMPLATE_REGISTRY["lead_forward"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) assert "HOT" in html assert "#DC2626" in html # HOT badge color def test_lead_forward_has_brief_rows(self): entry = EMAIL_TEMPLATE_REGISTRY["lead_forward"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) # Brief rows table is rendered (e.g. "Facility" label) assert "Facility" in html def test_lead_forward_has_contact_info(self): entry = EMAIL_TEMPLATE_REGISTRY["lead_forward"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) assert "ceo@padelclub.es" in html assert "Carlos Rivera" in html def test_weekly_digest_loops_over_leads(self): entry = EMAIL_TEMPLATE_REGISTRY["weekly_digest"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) # Sample data has 3 leads — all 3 countries should appear assert "Germany" in html assert "Austria" in html assert "Switzerland" in html def test_weekly_digest_has_heat_badges(self): entry = EMAIL_TEMPLATE_REGISTRY["weekly_digest"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) assert "HOT" in html assert "WARM" in html assert "COOL" in html def test_welcome_has_quickstart_links(self): entry = EMAIL_TEMPLATE_REGISTRY["welcome"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) assert "/planner" in html assert "/markets" in html def test_admin_compose_renders_body_html(self): entry = EMAIL_TEMPLATE_REGISTRY["admin_compose"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) assert "test message" in html.lower() def test_business_plan_has_download_link(self): entry = EMAIL_TEMPLATE_REGISTRY["business_plan"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) assert "/planner/export/" in html def test_invalid_lang_raises(self): entry = EMAIL_TEMPLATE_REGISTRY["magic_link"] with pytest.raises(AssertionError, match="Unsupported lang"): render_email_template(entry["template"], lang="fr", **entry["sample_data"]("en")) def test_non_emails_prefix_raises(self): with pytest.raises(AssertionError, match="Expected emails/ prefix"): render_email_template("base.html", lang="en") def test_common_design_elements_present(self): """Branded shell must include font + blue accent across all templates.""" entry = EMAIL_TEMPLATE_REGISTRY["magic_link"] html = render_email_template(entry["template"], lang="en", **entry["sample_data"]("en")) assert "Bricolage Grotesque" in html assert "#1D4ED8" in html assert "padelnomics.io" in html def test_registry_has_required_keys(self): for slug, entry in EMAIL_TEMPLATE_REGISTRY.items(): assert "template" in entry, f"{slug}: missing 'template'" assert "label" in entry, f"{slug}: missing 'label'" assert "description" in entry, f"{slug}: missing 'description'" assert callable(entry.get("sample_data")), f"{slug}: sample_data must be callable" assert entry["template"].startswith("emails/"), f"{slug}: template must start with emails/" # ── 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): resp = await client.get("/admin/emails/gallery") assert resp.status_code == 302 @pytest.mark.asyncio async def test_gallery_list_returns_200(self, admin_client): resp = await admin_client.get("/admin/emails/gallery") assert resp.status_code == 200 @pytest.mark.asyncio async def test_gallery_list_shows_all_template_labels(self, admin_client): resp = await admin_client.get("/admin/emails/gallery") html = (await resp.get_data(as_text=True)) for entry in EMAIL_TEMPLATE_REGISTRY.values(): assert entry["label"] in html, f"Expected label {entry['label']!r} on gallery page" @pytest.mark.asyncio async def test_gallery_preview_magic_link_en(self, admin_client): resp = await admin_client.get("/admin/emails/gallery/magic_link") assert resp.status_code == 200 html = (await resp.get_data(as_text=True)) assert "srcdoc" in html # sandboxed iframe is present assert "Magic Link" in html @pytest.mark.asyncio async def test_gallery_preview_magic_link_de(self, admin_client): resp = await admin_client.get("/admin/emails/gallery/magic_link?lang=de") assert resp.status_code == 200 html = (await resp.get_data(as_text=True)) assert 'lang="de"' in html or "de" in html # lang toggle shows active state @pytest.mark.asyncio async def test_gallery_preview_lead_forward(self, admin_client): resp = await admin_client.get("/admin/emails/gallery/lead_forward") assert resp.status_code == 200 html = (await resp.get_data(as_text=True)) assert "Lead Forward" in html assert "srcdoc" in html @pytest.mark.asyncio async def test_gallery_preview_weekly_digest(self, admin_client): resp = await admin_client.get("/admin/emails/gallery/weekly_digest") assert resp.status_code == 200 @pytest.mark.asyncio async def test_gallery_preview_nonexistent_slug_redirects(self, admin_client): resp = await admin_client.get("/admin/emails/gallery/does-not-exist") assert resp.status_code == 302 @pytest.mark.asyncio async def test_gallery_preview_invalid_lang_falls_back(self, admin_client): resp = await admin_client.get("/admin/emails/gallery/magic_link?lang=fr") assert resp.status_code == 200 # invalid lang → falls back to "en" @pytest.mark.asyncio async def test_gallery_preview_requires_auth(self, client): resp = await client.get("/admin/emails/gallery/magic_link") assert resp.status_code == 302 @pytest.mark.asyncio async def test_gallery_list_has_preview_links(self, admin_client): resp = await admin_client.get("/admin/emails/gallery") html = (await resp.get_data(as_text=True)) # Each card links to the preview page for slug in EMAIL_TEMPLATE_REGISTRY: assert f"/admin/emails/gallery/{slug}" in html @pytest.mark.asyncio async def test_compose_preview_plain_body(self, admin_client): """POST to compose/preview with wrap=0 returns plain HTML body.""" resp = await admin_client.post( "/admin/emails/compose/preview", form={"body": "Hello world", "wrap": "0"}, ) assert resp.status_code == 200 html = (await resp.get_data(as_text=True)) assert "Hello world" in html @pytest.mark.asyncio async def test_compose_preview_wrapped_body(self, admin_client): """POST to compose/preview with wrap=1 wraps body in branded layout.""" resp = await admin_client.post( "/admin/emails/compose/preview", form={"body": "Test preview content", "wrap": "1"}, ) assert resp.status_code == 200 html = (await resp.get_data(as_text=True)) assert "Test preview content" in html # Branded wrapper includes padelnomics wordmark assert "padelnomics" in html.lower() @pytest.mark.asyncio async def test_compose_preview_empty_body(self, admin_client): """Empty body returns an empty but valid partial.""" resp = await admin_client.post( "/admin/emails/compose/preview", form={"body": "", "wrap": "1"}, ) assert resp.status_code == 200