Co-authored-by: Michael Chihlas <michael@resolutionflow.com> Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
135 lines
4.5 KiB
Python
135 lines
4.5 KiB
Python
"""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
|