Close the gaps between the documented contract (API-STANDARD, TENANCY, ENTITLEMENTS) and the implementation, and remove committed build artifacts. Security: - verify + process inbound webhooks (HMAC/handler verify, size limit, timestamp tolerance, idempotency via InboundEvent.external_id) - real 2FA login flow (pre-auth challenge; tokens only after verify/recovery) - wire HybridPermission into security/audit views; add API-key rotate and users/<id>/permissions|roles endpoints - tenant-scoped throttling on by default; webhook replay protection - verify MercadoPago webhook signatures - login brute-force guard, configurable password policy, real ALTCHA PoW Correctness: - apply verified billing webhooks idempotently (subscription/entitlement/ invoice/PaymentTransaction); scheduled payment lifecycle jobs - capture audit update diffs automatically; add audit retention purge - working notification retries, per-channel rate limits, log retention - pluggable virus scanner, upload-size limit, pipeline toggle - feature rollout %/environment targeting; settings-driven registrations - workflow guards (instance cap, route depth, self-assignment, clone on re-entry) - wire every previously-dead INFRASYNTH_* setting; drop truly dead ones Delivery: - README + CHANGELOG; CI format check + coverage gate - keep test media out of the tree; untrack .coverage, __pycache__, egg-info, docs/ and invoice artifacts
140 lines
4.9 KiB
Python
140 lines
4.9 KiB
Python
import pytest
|
|
|
|
from infrasynth.notifications.models import (
|
|
ChannelConfig,
|
|
NotificationDispatch,
|
|
NotificationTemplate,
|
|
)
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
TEMPLATES_URL = "/api/v1/notifications/templates/"
|
|
DISPATCHES_URL = "/api/v1/notifications/dispatches/"
|
|
CHANNELS_URL = "/api/v1/notifications/channels/"
|
|
|
|
|
|
@pytest.fixture
|
|
def template():
|
|
return NotificationTemplate.objects.create(
|
|
slug="welcome",
|
|
name="Welcome",
|
|
channel="email",
|
|
subject_template="Hi",
|
|
body_template="Hello",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def dispatch(template, user):
|
|
return NotificationDispatch.objects.create(
|
|
template=template,
|
|
recipient="a@b.com",
|
|
channel="email",
|
|
subject="Hi",
|
|
body="Hello",
|
|
status=NotificationDispatch.Status.SENT,
|
|
)
|
|
|
|
|
|
class TestNotificationTemplateViewSet:
|
|
def test_list_templates(self, authenticated_client, template):
|
|
response = authenticated_client.get(TEMPLATES_URL)
|
|
assert response.status_code == 200
|
|
assert response.data["count"] == 1
|
|
assert response.data["results"][0]["slug"] == "welcome"
|
|
|
|
def test_requires_auth(self, api_client, template):
|
|
assert api_client.get(TEMPLATES_URL).status_code == 401
|
|
|
|
def test_create_template(self, authenticated_client):
|
|
response = authenticated_client.post(
|
|
TEMPLATES_URL,
|
|
{
|
|
"slug": "order_shipped",
|
|
"name": "Order shipped",
|
|
"channel": "email",
|
|
"subject_template": "Your order shipped",
|
|
"body_template": "Tracking: {{ tracking }}",
|
|
},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 201
|
|
assert NotificationTemplate.objects.filter(slug="order_shipped").exists()
|
|
|
|
def test_retrieve_and_update_template(self, authenticated_client, template):
|
|
response = authenticated_client.get(f"{TEMPLATES_URL}{template.id}/")
|
|
assert response.status_code == 200
|
|
response = authenticated_client.patch(
|
|
f"{TEMPLATES_URL}{template.id}/",
|
|
{"name": "Renamed"},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 200
|
|
template.refresh_from_db()
|
|
assert template.name == "Renamed"
|
|
|
|
def test_delete_template(self, authenticated_client, template):
|
|
response = authenticated_client.delete(f"{TEMPLATES_URL}{template.id}/")
|
|
assert response.status_code == 204
|
|
assert not NotificationTemplate.objects.filter(pk=template.pk).exists()
|
|
|
|
|
|
class TestNotificationDispatchViewSet:
|
|
def test_list_dispatches(self, authenticated_client, dispatch):
|
|
response = authenticated_client.get(DISPATCHES_URL)
|
|
assert response.status_code == 200
|
|
assert response.data["count"] == 1
|
|
assert response.data["results"][0]["recipient"] == "a@b.com"
|
|
|
|
def test_retrieve_dispatch(self, authenticated_client, dispatch):
|
|
response = authenticated_client.get(f"{DISPATCHES_URL}{dispatch.id}/")
|
|
assert response.status_code == 200
|
|
assert response.data["template_info"]["slug"] == "welcome"
|
|
|
|
def test_list_does_not_allow_create(self, authenticated_client):
|
|
response = authenticated_client.post(
|
|
DISPATCHES_URL,
|
|
{"recipient": "x@y.com", "channel": "email", "subject": "S", "body": "B"},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 405
|
|
|
|
def test_requires_auth(self, api_client, dispatch):
|
|
assert api_client.get(DISPATCHES_URL).status_code == 401
|
|
|
|
def test_filter_by_status(self, authenticated_client, dispatch):
|
|
response = authenticated_client.get(DISPATCHES_URL, {"status": "sent"})
|
|
assert response.data["count"] == 1
|
|
response = authenticated_client.get(DISPATCHES_URL, {"status": "failed"})
|
|
assert response.data["count"] == 0
|
|
|
|
|
|
class TestChannelConfigViewSet:
|
|
def test_list_channels(self, authenticated_client):
|
|
ChannelConfig.objects.create(
|
|
slug="email",
|
|
channel_type="email",
|
|
display_name="Email",
|
|
config={"host": "smtp.x.com"},
|
|
)
|
|
response = authenticated_client.get(CHANNELS_URL)
|
|
assert response.status_code == 200
|
|
assert response.data["count"] == 1
|
|
assert response.data["results"][0]["display_name"] == "Email"
|
|
|
|
def test_create_channel(self, authenticated_client):
|
|
response = authenticated_client.post(
|
|
CHANNELS_URL,
|
|
{
|
|
"slug": "sms",
|
|
"channel_type": "sms",
|
|
"display_name": "SMS",
|
|
"config": {"account_sid": "sid"},
|
|
},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 201
|
|
assert ChannelConfig.objects.filter(slug="sms").exists()
|
|
|
|
def test_requires_auth(self, api_client):
|
|
assert api_client.get(CHANNELS_URL).status_code == 401
|