Files
training-software/notifications/tests/test_notifications.py
Paperclip CTO 01661bf5a8
Some checks failed
CI / lint (push) Successful in 6s
CI / test (push) Failing after 7s
CI / build-container (push) Has been skipped
fix: restore CI by fixing lint, test collection, and notification syntax
2026-05-18 14:53:29 +02:00

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