test: add 18 e2e tests for billing, checkout, supplier signup/dashboard, export

- Pricing page (EN/DE, plan cards, no-auth access)
- Checkout success (auth required, renders for authed user)
- Supplier signup wizard (step 1, plan cards, DE variant, success page)
- Supplier dashboard (overview stats, boosts/credit packs, listing, leads tabs)
- Business plan export (auth required, form renders)

Also fixes:
- E2e server init_db mock scope — before_serving was calling real init_db
  outside the patch context, overwriting the in-memory DB (fixes 3
  pre-existing failures: markets_hub, markets_results, signup_page)
- Add _seed_billing_data() for supplier + feature flags in e2e server
- Mock RESEND_API_KEY="" in conftest + e2e server to prevent real emails

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-23 18:34:39 +01:00
parent 8558fd6b40
commit e35e01edb1
2 changed files with 233 additions and 5 deletions

View File

@@ -189,6 +189,7 @@ def patch_config():
test_values = { test_values = {
"PADDLE_API_KEY": "test_api_key_123", "PADDLE_API_KEY": "test_api_key_123",
"PADDLE_WEBHOOK_SECRET": "whsec_test_secret", "PADDLE_WEBHOOK_SECRET": "whsec_test_secret",
"RESEND_API_KEY": "", # never send real emails in tests
"BASE_URL": "http://localhost:5000", "BASE_URL": "http://localhost:5000",
"DEBUG": True, "DEBUG": True,
"WAITLIST_MODE": False, "WAITLIST_MODE": False,

View File

@@ -36,6 +36,33 @@ BASE = f"http://127.0.0.1:{PORT}"
# Server / Browser Fixtures # Server / Browser Fixtures
# ============================================================================= # =============================================================================
async def _seed_billing_data(conn):
"""Seed a supplier + feature flags for e2e billing tests."""
# Create user for supplier dashboard
await conn.execute(
"INSERT INTO users (id, email, created_at)"
" VALUES (999, 'supplier@test.com', datetime('now'))"
)
# Create pro-tier supplier claimed by that user
await conn.execute(
"INSERT INTO suppliers"
" (id, name, slug, tier, claimed_by, claimed_at,"
" country_code, region, category, credit_balance,"
" monthly_credits, contact_name, contact_email, created_at)"
" VALUES (1, 'Test Supplier GmbH', 'test-supplier', 'pro', 999,"
" datetime('now'), 'DE', 'Europe', 'construction', 50,"
" 10, 'Test User', 'supplier@test.com', datetime('now'))"
)
# Enable feature flags needed for billing/signup/export tests
for flag in ("supplier_signup", "markets", "payments",
"planner_export", "lead_unlock"):
await conn.execute(
"INSERT OR REPLACE INTO feature_flags (name, enabled)"
" VALUES (?, 1)", (flag,)
)
await conn.commit()
def _run_server(ready_event): def _run_server(ready_event):
"""Run the Quart dev server in a subprocess with in-memory SQLite.""" """Run the Quart dev server in a subprocess with in-memory SQLite."""
import aiosqlite import aiosqlite
@@ -59,11 +86,27 @@ def _run_server(ready_event):
conn.row_factory = aiosqlite.Row conn.row_factory = aiosqlite.Row
await conn.execute("PRAGMA foreign_keys=ON") await conn.execute("PRAGMA foreign_keys=ON")
await conn.executescript(schema_ddl) await conn.executescript(schema_ddl)
await conn.commit() # Safety: ensure feature_flags table exists (may be missed by
# executescript if an earlier DDL statement involving FTS5 fails)
await conn.execute(
"CREATE TABLE IF NOT EXISTS feature_flags"
" (name TEXT PRIMARY KEY, enabled INTEGER NOT NULL DEFAULT 0,"
" description TEXT,"
" updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')))"
)
await _seed_billing_data(conn)
core._db = conn core._db = conn
with patch.object(core, "init_db", new_callable=AsyncMock), \ # Never send real emails from the e2e server
patch.object(core, "close_db", new_callable=AsyncMock): core.config.RESEND_API_KEY = ""
# Patch init_db/close_db at the *app* module level (where before_serving
# imports them via `from .core import init_db`). Patching core.init_db
# alone won't affect the already-bound reference in app.py's globals.
with patch("padelnomics.app.init_db", new_callable=AsyncMock), \
patch("padelnomics.app.close_db", new_callable=AsyncMock), \
patch("padelnomics.app.open_analytics_db"), \
patch("padelnomics.app.close_analytics_db"):
app = create_app() app = create_app()
app.config["TESTING"] = True app.config["TESTING"] = True
@@ -511,3 +554,187 @@ def test_planner_tooltips_present(live_server, page):
# After clicking returns tab, look for tooltip info spans # After clicking returns tab, look for tooltip info spans
ti_spans = page.locator(".ti") ti_spans = page.locator(".ti")
assert ti_spans.count() >= 1, "No tooltip spans (.ti) found on results tab" assert ti_spans.count() >= 1, "No tooltip spans (.ti) found on results tab"
# =============================================================================
# J. Pricing Page
# =============================================================================
def test_pricing_page_loads_en(live_server, page):
"""Pricing page renders in English."""
resp = page.goto(live_server + "/billing/pricing")
assert resp.ok, f"/billing/pricing returned {resp.status}"
expect(page.locator("h1").first).to_be_visible()
def test_pricing_page_loads_de(live_server, page):
"""Pricing page renders in German when lang cookie is set."""
page.context.add_cookies([{
"name": "lang", "value": "de",
"domain": "127.0.0.1", "path": "/"
}])
resp = page.goto(live_server + "/billing/pricing")
assert resp.ok, f"/billing/pricing returned {resp.status}"
expect(page.locator("h1").first).to_be_visible()
def test_pricing_page_has_plan_cards(live_server, page):
"""Pricing page shows at least 2 plan cards with CTAs."""
page.goto(live_server + "/billing/pricing")
page.wait_for_load_state("networkidle")
cards = page.locator(".card-header")
assert cards.count() >= 2, f"Expected >=2 plan cards, found {cards.count()}"
# Each card should have a CTA button/link
ctas = page.locator("a.btn, a.btn-outline")
assert ctas.count() >= 2, f"Expected >=2 CTAs, found {ctas.count()}"
def test_pricing_page_no_auth_required(live_server, page):
"""Pricing page is accessible without authentication."""
resp = page.goto(live_server + "/billing/pricing")
assert resp.ok
# Should NOT have redirected to login
assert "login" not in page.url and "auth" not in page.url
# =============================================================================
# K. Checkout Success
# =============================================================================
def test_checkout_success_requires_auth(live_server, page):
"""Checkout success page redirects unauthenticated users to login."""
page.goto(live_server + "/billing/success", wait_until="networkidle")
assert "login" in page.url or "auth" in page.url, (
f"Expected redirect to login, got: {page.url}"
)
def test_checkout_success_renders_for_authed_user(live_server, page):
"""Checkout success page renders for authenticated user."""
dev_login(page, live_server, "checkout@example.com")
resp = page.goto(live_server + "/billing/success")
assert resp.ok
expect(page.locator("h1").first).to_be_visible()
# Should have a CTA button (back to dashboard or similar)
expect(page.locator("a.btn").first).to_be_visible()
# =============================================================================
# L. Supplier Signup Wizard
# =============================================================================
def test_supplier_signup_step1_loads(live_server, page):
"""Supplier signup wizard step 1 renders."""
resp = page.goto(live_server + "/en/suppliers/signup")
assert resp.ok, f"/en/suppliers/signup returned {resp.status}"
# Wait for HTMX to load step content
page.wait_for_load_state("networkidle")
expect(page.locator("[data-step='1']").first).to_be_visible()
def test_supplier_signup_step1_has_plan_cards(live_server, page):
"""Step 1 shows plan cards for selecting a tier."""
page.goto(live_server + "/en/suppliers/signup")
page.wait_for_load_state("networkidle")
plan_cards = page.locator(".s-plan-card")
assert plan_cards.count() >= 2, (
f"Expected >=2 plan cards, found {plan_cards.count()}"
)
# Should have a next button
expect(page.locator(".s-btn-next").first).to_be_visible()
def test_supplier_signup_step1_de_loads(live_server, page):
"""German variant of supplier signup wizard loads."""
resp = page.goto(live_server + "/de/suppliers/signup")
assert resp.ok, f"/de/suppliers/signup returned {resp.status}"
page.wait_for_load_state("networkidle")
expect(page.locator("[data-step='1']").first).to_be_visible()
def test_supplier_signup_success_page_loads(live_server, page):
"""Supplier signup success page renders."""
resp = page.goto(live_server + "/en/suppliers/signup/success")
assert resp.ok, f"signup success returned {resp.status}"
expect(page.locator("h1, h2").first).to_be_visible()
# =============================================================================
# M. Supplier Dashboard (seeded supplier data)
# =============================================================================
def test_supplier_dashboard_loads(live_server, page):
"""Dashboard loads for authenticated supplier."""
dev_login(page, live_server, "supplier@test.com")
resp = page.goto(live_server + "/en/suppliers/dashboard")
assert resp.ok, f"dashboard returned {resp.status}"
expect(page.locator(".dash").first).to_be_visible()
# Sidebar should show supplier name
expect(page.locator(".dash-sidebar__name").first).to_be_visible()
def test_supplier_dashboard_overview_tab(live_server, page):
"""Overview tab renders with stats grid."""
dev_login(page, live_server, "supplier@test.com")
page.goto(live_server + "/en/suppliers/dashboard")
page.wait_for_load_state("networkidle")
# Wait for HTMX to load overview content
page.wait_for_timeout(1000)
stats = page.locator(".ov-stat")
assert stats.count() >= 2, f"Expected >=2 stat cards, found {stats.count()}"
def test_supplier_dashboard_boosts_tab(live_server, page):
"""Boosts tab renders with boost options."""
dev_login(page, live_server, "supplier@test.com")
resp = page.goto(live_server + "/en/suppliers/dashboard/boosts")
assert resp.ok, f"boosts tab returned {resp.status}"
def test_supplier_dashboard_boosts_has_credit_packs(live_server, page):
"""Boosts tab shows credit pack purchase options."""
dev_login(page, live_server, "supplier@test.com")
page.goto(live_server + "/en/suppliers/dashboard/boosts")
page.wait_for_load_state("networkidle")
content = page.content()
# Credit pack amounts should be visible (25, 50, 100, 250)
assert "25" in content or "50" in content or "100" in content, (
"No credit pack amounts found on boosts page"
)
def test_supplier_dashboard_listing_tab(live_server, page):
"""Listing tab renders for authenticated supplier."""
dev_login(page, live_server, "supplier@test.com")
resp = page.goto(live_server + "/en/suppliers/dashboard/listing")
assert resp.ok, f"listing tab returned {resp.status}"
def test_supplier_dashboard_leads_tab(live_server, page):
"""Leads tab renders for pro-tier supplier."""
dev_login(page, live_server, "supplier@test.com")
resp = page.goto(live_server + "/en/suppliers/dashboard/leads")
assert resp.ok, f"leads tab returned {resp.status}"
# =============================================================================
# N. Business Plan Export
# =============================================================================
def test_export_page_requires_auth(live_server, page):
"""Export page redirects unauthenticated users to login."""
page.goto(live_server + "/en/planner/export", wait_until="networkidle")
assert "login" in page.url or "auth" in page.url, (
f"Expected redirect to login, got: {page.url}"
)
def test_export_page_loads_for_authed_user(live_server, page):
"""Export page renders for authenticated user with form."""
dev_login(page, live_server, "export@example.com")
resp = page.goto(live_server + "/en/planner/export")
assert resp.ok, f"export page returned {resp.status}"
expect(page.locator("h1").first).to_be_visible()
# Should have the export form with scenario select and buy button
expect(page.locator("#export-form").first).to_be_visible()
expect(page.locator("#export-buy-btn").first).to_be_visible()