feat: auto-create Resend audiences per blueprint
Removes RESEND_AUDIENCE_WAITLIST env var. capture_waitlist_email() now derives audience name from request.blueprints[0] (e.g. waitlist-auth, waitlist-suppliers), lazily creates it via Resend API on first signup, and caches the ID in a new resend_audiences table. Zero config beyond RESEND_API_KEY — adding @waitlist_gate to any new blueprint auto-creates its audience on first use. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,11 +79,6 @@ class TestConfiguration:
|
||||
assert hasattr(core.config, "WAITLIST_MODE")
|
||||
assert isinstance(core.config.WAITLIST_MODE, bool)
|
||||
|
||||
def test_resend_audience_waitlist_exists(self):
|
||||
"""RESEND_AUDIENCE_WAITLIST config should exist."""
|
||||
assert hasattr(core.config, "RESEND_AUDIENCE_WAITLIST")
|
||||
assert isinstance(core.config.RESEND_AUDIENCE_WAITLIST, str)
|
||||
|
||||
def test_waitlist_mode_can_be_enabled(self):
|
||||
"""WAITLIST_MODE can be set to True via config."""
|
||||
with patch.object(core.config, "WAITLIST_MODE", True):
|
||||
@@ -385,22 +380,95 @@ class TestAuthRoutes:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_adds_to_resend_audience_when_configured(self, client, db):
|
||||
"""POST adds email to Resend audience if RESEND_AUDIENCE_WAITLIST is set."""
|
||||
"""POST auto-creates Resend audience per blueprint and adds the contact."""
|
||||
with patch.object(core.config, "WAITLIST_MODE", True), \
|
||||
patch.object(core.config, "RESEND_AUDIENCE_WAITLIST", "aud_test123"), \
|
||||
patch.object(core.config, "RESEND_API_KEY", "re_test_key"), \
|
||||
patch("padelnomics.worker.enqueue", new_callable=AsyncMock), \
|
||||
patch("resend.Contacts.create") as mock_resend:
|
||||
patch("resend.Audiences.create", return_value={"id": "aud_test"}) as mock_create_aud, \
|
||||
patch("resend.Contacts.create") as mock_create_contact:
|
||||
|
||||
await client.post("/auth/signup", form={
|
||||
"csrf_token": "test_token",
|
||||
"email": "test@example.com",
|
||||
})
|
||||
|
||||
mock_resend.assert_called_once()
|
||||
call_args = mock_resend.call_args[0][0]
|
||||
mock_create_aud.assert_called_once_with({"name": "waitlist-auth"})
|
||||
mock_create_contact.assert_called_once()
|
||||
call_args = mock_create_contact.call_args[0][0]
|
||||
assert call_args["email"] == "test@example.com"
|
||||
assert call_args["audience_id"] == "aud_test123"
|
||||
assert call_args["audience_id"] == "aud_test"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resend_audience_cached_on_second_signup(self, client, db):
|
||||
"""Second signup for same blueprint reuses cached audience — no extra API call."""
|
||||
with patch.object(core.config, "WAITLIST_MODE", True), \
|
||||
patch.object(core.config, "RESEND_API_KEY", "re_test_key"), \
|
||||
patch("padelnomics.worker.enqueue", new_callable=AsyncMock), \
|
||||
patch("resend.Audiences.create", return_value={"id": "aud_cached"}) as mock_create_aud, \
|
||||
patch("resend.Contacts.create"):
|
||||
|
||||
await client.post("/auth/signup", form={
|
||||
"csrf_token": "test_token",
|
||||
"email": "first@example.com",
|
||||
})
|
||||
await client.post("/auth/signup", form={
|
||||
"csrf_token": "test_token",
|
||||
"email": "second@example.com",
|
||||
})
|
||||
|
||||
# Audience created only once; second call reads from DB cache
|
||||
mock_create_aud.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_supplier_and_auth_signups_go_to_different_audiences(self, client, db):
|
||||
"""Auth and supplier signups are added to separate Resend audiences."""
|
||||
with patch.object(core.config, "WAITLIST_MODE", True), \
|
||||
patch.object(core.config, "RESEND_API_KEY", "re_test_key"), \
|
||||
patch("padelnomics.worker.enqueue", new_callable=AsyncMock), \
|
||||
patch("resend.Audiences.create", side_effect=[
|
||||
{"id": "aud_auth"},
|
||||
{"id": "aud_suppliers"},
|
||||
]) as mock_create_aud, \
|
||||
patch("resend.Contacts.create") as mock_create_contact:
|
||||
|
||||
await client.post("/auth/signup", form={
|
||||
"csrf_token": "test_token",
|
||||
"email": "entrepreneur@example.com",
|
||||
})
|
||||
await client.post("/suppliers/signup/waitlist", form={
|
||||
"csrf_token": "test_token",
|
||||
"email": "supplier@example.com",
|
||||
"plan": "supplier_growth",
|
||||
})
|
||||
|
||||
audience_names = [
|
||||
call[0][0]["name"] for call in mock_create_aud.call_args_list
|
||||
]
|
||||
assert "waitlist-auth" in audience_names
|
||||
assert "waitlist-suppliers" in audience_names
|
||||
|
||||
contact_audience_ids = [
|
||||
call[0][0]["audience_id"] for call in mock_create_contact.call_args_list
|
||||
]
|
||||
assert "aud_auth" in contact_audience_ids
|
||||
assert "aud_suppliers" in contact_audience_ids
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resend_audience_create_failure_still_succeeds(self, client, db):
|
||||
"""Audiences.create failure is silent — signup still returns success page."""
|
||||
with patch.object(core.config, "WAITLIST_MODE", True), \
|
||||
patch.object(core.config, "RESEND_API_KEY", "re_test_key"), \
|
||||
patch("padelnomics.worker.enqueue", new_callable=AsyncMock), \
|
||||
patch("resend.Audiences.create", side_effect=Exception("Resend down")):
|
||||
|
||||
response = await client.post("/auth/signup", form={
|
||||
"csrf_token": "test_token",
|
||||
"email": "test@example.com",
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
html = await response.get_data(as_text=True)
|
||||
assert "test@example.com" in html
|
||||
|
||||
|
||||
# ── TestSupplierRoutes ────────────────────────────────────────────
|
||||
@@ -589,8 +657,9 @@ class TestEdgeCases:
|
||||
async def test_resend_api_error_handled_gracefully(self, client, db):
|
||||
"""Resend API errors don't break the flow."""
|
||||
with patch.object(core.config, "WAITLIST_MODE", True), \
|
||||
patch.object(core.config, "RESEND_AUDIENCE_WAITLIST", "aud_test"), \
|
||||
patch.object(core.config, "RESEND_API_KEY", "re_test_key"), \
|
||||
patch("padelnomics.worker.enqueue", new_callable=AsyncMock), \
|
||||
patch("resend.Audiences.create", return_value={"id": "aud_test"}), \
|
||||
patch("resend.Contacts.create", side_effect=Exception("API error")):
|
||||
|
||||
response = await client.post("/auth/signup", form={
|
||||
|
||||
Reference in New Issue
Block a user