240 lines
9.8 KiB
Python
240 lines
9.8 KiB
Python
import uuid
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from django.utils.timezone import now
|
|
from rest_framework import status
|
|
from rest_framework.test import APIClient
|
|
|
|
from accounts.tests.factories import AccountUserFactory
|
|
from notifications.models import (
|
|
DeliveryChannel,
|
|
DeliveryStatus,
|
|
Notification,
|
|
NotificationEventType,
|
|
)
|
|
from notifications.services import notify
|
|
|
|
|
|
@pytest.fixture
|
|
def user():
|
|
return AccountUserFactory()
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_client(user):
|
|
client = APIClient()
|
|
client.force_authenticate(user=user)
|
|
return client, user
|
|
|
|
|
|
# ── notify() service ───────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.django_db
|
|
class TestNotifyService:
|
|
def test_creates_notification_and_deliveries(self, user):
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n = notify(
|
|
recipient=user,
|
|
event_type=NotificationEventType.COURSE_ASSIGNED,
|
|
title="Test",
|
|
body="Body",
|
|
object_type="enrollment",
|
|
object_id="abc",
|
|
)
|
|
assert Notification.objects.filter(pk=n.pk).exists()
|
|
assert n.deliveries.count() == 2 # in_app + email
|
|
|
|
def test_idempotent_on_duplicate(self, user):
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n1 = notify(
|
|
recipient=user,
|
|
event_type=NotificationEventType.CERTIFICATE_ISSUED,
|
|
title="Cert",
|
|
object_type="certificate",
|
|
object_id="xyz",
|
|
)
|
|
n2 = notify(
|
|
recipient=user,
|
|
event_type=NotificationEventType.CERTIFICATE_ISSUED,
|
|
title="Cert",
|
|
object_type="certificate",
|
|
object_id="xyz",
|
|
)
|
|
assert n1.pk == n2.pk
|
|
assert Notification.objects.filter(recipient=user, event_type=NotificationEventType.CERTIFICATE_ISSUED).count() == 1
|
|
|
|
def test_different_objects_create_separate_notifications(self, user):
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n1 = notify(recipient=user, event_type="course_assigned", title="A", object_type="enrollment", object_id="1")
|
|
n2 = notify(recipient=user, event_type="course_assigned", title="B", object_type="enrollment", object_id="2")
|
|
assert n1.pk != n2.pk
|
|
|
|
def test_custom_channels(self, user):
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n = notify(
|
|
recipient=user,
|
|
event_type="course_assigned",
|
|
title="In-App Only",
|
|
channels=[DeliveryChannel.IN_APP],
|
|
)
|
|
assert n.deliveries.count() == 1
|
|
assert n.deliveries.first().channel == DeliveryChannel.IN_APP
|
|
|
|
|
|
# ── deliver_notification_task ──────────────────────────────────────────────────
|
|
|
|
@pytest.mark.django_db
|
|
class TestDeliverNotificationTask:
|
|
def test_email_channel_sends_mail(self, user):
|
|
from notifications.tasks import deliver_notification_task
|
|
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n = notify(
|
|
recipient=user,
|
|
event_type="certificate_issued",
|
|
title="Your cert",
|
|
channels=[DeliveryChannel.EMAIL],
|
|
)
|
|
delivery = n.deliveries.first()
|
|
|
|
with patch("django.core.mail.send_mail") as mock_mail:
|
|
deliver_notification_task(str(delivery.pk))
|
|
|
|
mock_mail.assert_called_once()
|
|
delivery.refresh_from_db()
|
|
assert delivery.status == DeliveryStatus.SENT
|
|
assert delivery.sent_at is not None
|
|
|
|
def test_in_app_channel_marks_sent_without_email(self, user):
|
|
from notifications.tasks import deliver_notification_task
|
|
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n = notify(
|
|
recipient=user,
|
|
event_type="course_assigned",
|
|
title="Enrolled",
|
|
channels=[DeliveryChannel.IN_APP],
|
|
)
|
|
delivery = n.deliveries.first()
|
|
|
|
with patch("django.core.mail.send_mail") as mock_mail:
|
|
deliver_notification_task(str(delivery.pk))
|
|
|
|
mock_mail.assert_not_called()
|
|
delivery.refresh_from_db()
|
|
assert delivery.status == DeliveryStatus.SENT
|
|
|
|
def test_missing_delivery_returns_error(self):
|
|
from notifications.tasks import deliver_notification_task
|
|
|
|
result = deliver_notification_task(str(uuid.uuid4()))
|
|
assert result["error"] == "delivery not found"
|
|
|
|
def test_email_failure_marks_failed(self, user):
|
|
from notifications.tasks import deliver_notification_task
|
|
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n = notify(
|
|
recipient=user,
|
|
event_type="certificate_issued",
|
|
title="Fail",
|
|
channels=[DeliveryChannel.EMAIL],
|
|
)
|
|
delivery = n.deliveries.first()
|
|
|
|
with patch("django.core.mail.send_mail", side_effect=Exception("SMTP down")):
|
|
with pytest.raises(Exception):
|
|
deliver_notification_task(str(delivery.pk))
|
|
|
|
delivery.refresh_from_db()
|
|
assert delivery.status == DeliveryStatus.FAILED
|
|
assert "SMTP down" in delivery.error_detail
|
|
|
|
|
|
# ── NotificationListView ───────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.django_db
|
|
class TestNotificationListView:
|
|
url = "/api/v1/notifications/"
|
|
|
|
def test_unauthenticated_returns_401(self):
|
|
resp = APIClient().get(self.url)
|
|
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
|
|
|
|
def test_returns_only_own_notifications(self, auth_client):
|
|
client, user = auth_client
|
|
other = AccountUserFactory()
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
notify(recipient=user, event_type="course_assigned", title="Mine", object_id="a")
|
|
notify(recipient=other, event_type="course_assigned", title="Other", object_id="b")
|
|
resp = client.get(self.url)
|
|
assert resp.status_code == status.HTTP_200_OK
|
|
assert all(r["title"] == "Mine" for r in resp.data)
|
|
assert len(resp.data) == 1
|
|
|
|
def test_unread_filter(self, auth_client):
|
|
client, user = auth_client
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n = notify(recipient=user, event_type="course_assigned", title="Unread", object_id="x")
|
|
n.read_at = now()
|
|
n.save(update_fields=["read_at"])
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
notify(recipient=user, event_type="certificate_issued", title="Also unread", object_id="y")
|
|
resp = client.get(self.url, {"unread": "true"})
|
|
assert len(resp.data) == 1
|
|
assert resp.data[0]["title"] == "Also unread"
|
|
|
|
|
|
# ── NotificationMarkReadView ───────────────────────────────────────────────────
|
|
|
|
@pytest.mark.django_db
|
|
class TestNotificationMarkReadView:
|
|
def test_marks_notification_read(self, auth_client):
|
|
client, user = auth_client
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n = notify(recipient=user, event_type="course_assigned", title="Read me", object_id="1")
|
|
url = f"/api/v1/notifications/{n.pk}/read/"
|
|
resp = client.post(url)
|
|
assert resp.status_code == status.HTTP_200_OK
|
|
assert resp.data["is_read"] is True
|
|
n.refresh_from_db()
|
|
assert n.read_at is not None
|
|
|
|
def test_idempotent_re_read(self, auth_client):
|
|
client, user = auth_client
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n = notify(recipient=user, event_type="course_assigned", title="Re-read", object_id="2")
|
|
url = f"/api/v1/notifications/{n.pk}/read/"
|
|
client.post(url)
|
|
first_read_at = Notification.objects.get(pk=n.pk).read_at
|
|
client.post(url)
|
|
second_read_at = Notification.objects.get(pk=n.pk).read_at
|
|
assert first_read_at == second_read_at
|
|
|
|
def test_cannot_mark_other_users_notification_read(self, auth_client):
|
|
client, user = auth_client
|
|
other = AccountUserFactory()
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
n = notify(recipient=other, event_type="course_assigned", title="Not yours", object_id="3")
|
|
url = f"/api/v1/notifications/{n.pk}/read/"
|
|
resp = client.post(url)
|
|
assert resp.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
|
|
# ── NotificationMarkAllReadView ────────────────────────────────────────────────
|
|
|
|
@pytest.mark.django_db
|
|
class TestNotificationMarkAllReadView:
|
|
url = "/api/v1/notifications/read-all/"
|
|
|
|
def test_marks_all_unread(self, auth_client):
|
|
client, user = auth_client
|
|
with patch("notifications.tasks.deliver_notification_task.delay"):
|
|
notify(recipient=user, event_type="course_assigned", title="A", object_id="a1")
|
|
notify(recipient=user, event_type="certificate_issued", title="B", object_id="b1")
|
|
resp = client.post(self.url)
|
|
assert resp.status_code == status.HTTP_200_OK
|
|
assert resp.data["marked_read"] == 2
|
|
assert Notification.objects.filter(recipient=user, read_at__isnull=True).count() == 0
|