"""Integration tests for the public Talk-to-Sales endpoint. POST /api/v1/sales-leads — no auth, rate-limited 5/hour per IP. """ from unittest.mock import AsyncMock, patch import pytest import sqlalchemy as sa @pytest.mark.asyncio async def test_sales_lead_creates_row_and_sends_notification_email(client, test_db): """Happy path: row inserted, notification email fired, 201 returned.""" payload = { "email": "buyer@acme.example", "name": "Pat Buyer", "company": "Acme MSP", "team_size": "11-50", "message": "We're evaluating ResolutionFlow for our NOC team.", "source": "pricing_page", "posthog_distinct_id": "ph_distinct_123", } with patch( "app.api.endpoints.sales_leads.EmailService.send_sales_lead_notification", new=AsyncMock(return_value=True), ) as mock_email: response = await client.post("/api/v1/sales-leads", json=payload) assert response.status_code == 201, response.text body = response.json() assert body["status"] == "received" assert "id" in body # Notification email was attempted (asyncio.create_task — give it a tick). import asyncio await asyncio.sleep(0) await asyncio.sleep(0) assert mock_email.await_count == 1 kwargs = mock_email.await_args.kwargs assert kwargs["to_email"] # default placeholder until cutover assert kwargs["lead"].email == "buyer@acme.example" assert kwargs["lead"].source == "pricing_page" # Row was inserted with normalized email + all fields preserved. result = await test_db.execute( sa.text("SELECT email, name, company, team_size, message, source, posthog_distinct_id, status FROM sales_leads") ) rows = result.all() assert len(rows) == 1 row = rows[0] assert row.email == "buyer@acme.example" assert row.name == "Pat Buyer" assert row.company == "Acme MSP" assert row.team_size == "11-50" assert row.message == "We're evaluating ResolutionFlow for our NOC team." assert row.source == "pricing_page" assert row.posthog_distinct_id == "ph_distinct_123" assert row.status == "new" @pytest.mark.asyncio async def test_sales_lead_email_failure_does_not_fail_request(client, test_db): """If the email send raises, the API still returns 201 and the row persists.""" payload = { "email": "buyer2@acme.example", "name": "Sam Lead", "company": "Acme MSP", "source": "register_footer", } with patch( "app.api.endpoints.sales_leads.EmailService.send_sales_lead_notification", new=AsyncMock(side_effect=RuntimeError("resend exploded")), ): response = await client.post("/api/v1/sales-leads", json=payload) assert response.status_code == 201, response.text # Row must still be persisted even though email failed. import asyncio await asyncio.sleep(0) result = await test_db.execute( sa.text("SELECT count(*) FROM sales_leads WHERE email = 'buyer2@acme.example'") ) assert result.scalar() == 1 @pytest.mark.asyncio async def test_sales_lead_rate_limited_after_5_per_hour(client): """The 6th submission within an hour from the same IP returns 429. The default `limiter` is disabled in tests (DEBUG=true). We re-enable it for this test, then reset its state on teardown so other tests aren't affected. """ from app.core.rate_limit import limiter was_enabled = limiter.enabled limiter.enabled = True try: limiter.reset() with patch( "app.api.endpoints.sales_leads.EmailService.send_sales_lead_notification", new=AsyncMock(return_value=True), ): for i in range(5): payload = { "email": f"lead{i}@acme.example", "name": f"Lead {i}", "company": "Acme MSP", "source": "landing_page", } resp = await client.post("/api/v1/sales-leads", json=payload) assert resp.status_code == 201, f"submission {i}: {resp.text}" # 6th should be rate-limited. resp = await client.post( "/api/v1/sales-leads", json={ "email": "lead6@acme.example", "name": "Lead 6", "company": "Acme MSP", "source": "landing_page", }, ) assert resp.status_code == 429, resp.text finally: limiter.reset() limiter.enabled = was_enabled