infrasynth-backend-kit/tests/test_notifications/test_services.py
2026-08-28 14:38:47 -05:00

271 lines
9 KiB
Python

import pytest
from infrasynth.notifications.channels.base import Attachment
from infrasynth.notifications.models import (
ChannelConfig,
NotificationDispatch,
NotificationTemplate,
)
from infrasynth.notifications.resolvers import VariableResolverRegistry
from infrasynth.notifications.services import NotificationService
pytestmark = pytest.mark.django_db
@pytest.fixture
def sync_dispatch(settings):
settings.INFRASYNTH_NOTIFICATIONS = {
**settings.INFRASYNTH_NOTIFICATIONS,
"DISPATCH_BACKEND": "sync",
}
@pytest.fixture
def email_channel_config():
return ChannelConfig.objects.create(
slug="email",
channel_type="email",
display_name="Email",
config={
"host": "smtp.example.com",
"backend": "django.core.mail.backends.locmem.EmailBackend",
},
)
@pytest.fixture
def sms_channel_config():
return ChannelConfig.objects.create(
slug="sms",
channel_type="sms",
display_name="SMS",
config={},
)
@pytest.fixture
def no_retries(settings):
settings.INFRASYNTH_NOTIFICATIONS = {
**settings.INFRASYNTH_NOTIFICATIONS,
"MAX_RETRIES": 0,
}
class TestSend:
def test_creates_dispatch_and_sends(self, user, sync_dispatch, email_channel_config):
dispatch = NotificationService().send(
recipient="a@b.com",
subject="Welcome",
body="Hello!",
channel="email",
)
dispatch.refresh_from_db()
assert dispatch.status == NotificationDispatch.Status.SENT
assert dispatch.recipient == "a@b.com"
assert dispatch.subject == "Welcome"
assert dispatch.body == "Hello!"
assert dispatch.completed_at is not None
def test_renders_template(self, sync_dispatch, email_channel_config):
template = NotificationTemplate.objects.create(
slug="welcome",
name="Welcome",
channel="email",
subject_template="Hi {{ user_name }}",
body_template="Hello {{ user_name }}, welcome!",
is_html=False,
)
dispatch = NotificationService().send(
recipient="a@b.com",
subject="",
body="",
channel="email",
template=template,
context={"user_name": "John"},
)
dispatch.refresh_from_db()
assert dispatch.subject == "Hi John"
assert dispatch.body == "Hello John, welcome!"
def test_emits_notification_sent_signal(self, sync_dispatch, email_channel_config):
from infrasynth.notifications.signals import notification_sent
sent = []
receiver = lambda **kw: sent.append(kw) # noqa: E731
notification_sent.connect(receiver, weak=False)
dispatch = NotificationService().send(
recipient="a@b.com",
subject="S",
body="B",
channel="email",
)
assert sent
assert sent[0]["dispatch_id"] == dispatch.id
assert sent[0]["recipient"] == "a@b.com"
assert sent[0]["status"] == "sent"
notification_sent.disconnect(receiver)
def test_no_channel_config_schedules_retry(self, user, sync_dispatch):
dispatch = NotificationService().send(
recipient="a@b.com",
subject="S",
body="B",
channel="email",
)
dispatch.refresh_from_db()
assert dispatch.status == NotificationDispatch.Status.RETRYING
assert "SMTP host not configured" in dispatch.error_message
assert dispatch.next_retry_at is not None
def test_celery_backend_delivers_eagerly(self, user, email_channel_config):
dispatch = NotificationService().send(
recipient="a@b.com",
subject="S",
body="B",
channel="email",
)
dispatch.refresh_from_db()
assert dispatch.status == NotificationDispatch.Status.SENT
def test_snapshot_stringifies_non_json_values(self, sync_dispatch, email_channel_config):
class _Obj:
def __str__(self):
return "obj-string"
dispatch = NotificationService().send(
recipient="a@b.com",
subject="S",
body="B",
channel="email",
context={"plain": "x", "obj": _Obj()},
)
assert dispatch.context_snapshot["plain"] == "x"
assert dispatch.context_snapshot["obj"] == "obj-string"
class TestSendWithFailover:
def test_primary_fails_fallback_succeeds(self, user, sync_dispatch, email_channel_config, sms_channel_config):
dispatch = NotificationService().send_with_failover(
recipient="+573001234567",
subject="S",
body="B",
channels=["sms", "email"],
)
dispatch.refresh_from_db()
assert dispatch.status == NotificationDispatch.Status.SENT
assert dispatch.channel == "email"
assert dispatch.attempt == 1
def test_all_channels_fail(self, user, sync_dispatch, sms_channel_config, no_retries):
dispatch = NotificationService().send_with_failover(
recipient="+573001234567",
subject="S",
body="B",
channels=["sms"],
)
dispatch.refresh_from_db()
assert dispatch.status == NotificationDispatch.Status.FAILED
assert "Twilio credentials not configured" in dispatch.error_message
def test_emits_failed_signal(self, user, sync_dispatch, sms_channel_config, no_retries):
from infrasynth.notifications.signals import notification_failed
failed = []
receiver = lambda **kw: failed.append(kw) # noqa: E731
notification_failed.connect(receiver, weak=False)
dispatch = NotificationService().send_with_failover(
recipient="+573001234567",
subject="S",
body="B",
channels=["sms"],
)
assert failed
assert failed[0]["dispatch_id"] == dispatch.id
assert "credentials" in failed[0]["error"]
notification_failed.disconnect(receiver)
class TestGetTemplate:
def test_returns_template_by_slug(self):
NotificationTemplate.objects.create(
slug="welcome",
name="Welcome",
channel="email",
subject_template="",
body_template="Hello",
namespace="",
)
template = NotificationService().get_template("welcome")
assert template is not None
assert template.slug == "welcome"
def test_namespace_scoping(self):
NotificationTemplate.objects.create(
slug="welcome",
name="Welcome",
channel="email",
subject_template="",
body_template="Global",
namespace="",
)
NotificationTemplate.objects.create(
slug="welcome",
name="Welcome NS",
channel="email",
subject_template="",
body_template="Namespaced",
namespace="helpdesk",
)
service = NotificationService()
assert service.get_template("welcome").body_template == "Global"
assert service.get_template("welcome", namespace="helpdesk").body_template == "Namespaced"
assert service.get_template("missing") is None
class TestVariableResolversInTemplates:
def test_namespace_resolver_used_in_render(self, sync_dispatch, email_channel_config):
def resolve_agent(recipient, context, request=None):
return "Agent Smith"
VariableResolverRegistry.register("agent_name", namespace="helpdesk")(resolve_agent)
try:
template = NotificationTemplate.objects.create(
slug="assigned",
name="Assigned",
channel="email",
subject_template="Ticket {{ ticket_id }}",
body_template="Assigned to {{ agent_name }}",
namespace="helpdesk",
is_html=False,
)
dispatch = NotificationService().send(
recipient="a@b.com",
subject="",
body="",
channel="email",
template=template,
context={"ticket_id": 7},
)
dispatch.refresh_from_db()
assert dispatch.subject == "Ticket 7"
assert dispatch.body == "Assigned to Agent Smith"
finally:
VariableResolverRegistry._resolvers.clear()
class TestAttachmentsPassThrough:
def test_send_with_attachment(self, sync_dispatch, email_channel_config):
from infrasynth.notifications.services import NotificationService
dispatch = NotificationService().send(
recipient="a@b.com",
subject="S",
body="B",
channel="email",
)
dispatch.refresh_from_db()
assert dispatch.status == NotificationDispatch.Status.SENT
def test_attachment_dataclass_usable(self):
attachment = Attachment("a.txt", b"x", "text/plain")
assert attachment.filename == "a.txt"