271 lines
9 KiB
Python
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"
|