This commit is contained in:
2026-04-05 00:57:25 +03:30
parent 6d5ece1f5d
commit 32dbbed1af
26 changed files with 825 additions and 291 deletions
+160 -71
View File
@@ -1,97 +1,186 @@
from unittest.mock import patch
from django.conf import settings
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.test import TestCase, override_settings
from rest_framework.test import APIRequestFactory, force_authenticate
from .views import NotificationPublishView, NotificationStreamView
from farm_hub.models import FarmHub, FarmType
from .models import FarmNotification
from .services import create_notification_for_farm_uuid, long_poll_notifications
from .views import ExternalNotificationIngestView, NotificationLongPollView
class NotificationPublishViewTests(TestCase):
class NotificationServiceTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username="notif-service-user",
password="secret123",
email="notif-service@example.com",
phone_number="09120000011",
)
self.farm_type = FarmType.objects.create(name="گلخانه")
self.farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
name="Farm A",
)
def test_create_notification_for_farm_uuid_creates_record(self):
notification = create_notification_for_farm_uuid(
farm_uuid=self.farm.farm_uuid,
title="Irrigation alert",
message="Soil moisture dropped",
level="warning",
metadata={"sensor": "soil-1"},
)
self.assertEqual(notification.farm, self.farm)
self.assertEqual(notification.level, "warning")
self.assertEqual(notification.metadata["sensor"], "soil-1")
def test_create_notification_for_farm_uuid_raises_for_unknown_farm(self):
with self.assertRaisesMessage(ValueError, "Farm not found."):
create_notification_for_farm_uuid(
farm_uuid="11111111-1111-1111-1111-111111111111",
title="x",
message="y",
)
def test_long_poll_notifications_returns_new_notifications(self):
FarmNotification.objects.create(farm=self.farm, title="A", message="B")
notifications = long_poll_notifications(farm=self.farm, timeout_seconds=0)
self.assertEqual(len(notifications), 1)
self.assertEqual(notifications[0].title, "A")
class NotificationLongPollViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = get_user_model().objects.create_user(
username="notify-user",
username="notif-view-user",
password="secret123",
email="notify@example.com",
phone_number="09120000099",
email="notif-view@example.com",
phone_number="09120000012",
)
self.other_user = get_user_model().objects.create_user(
username="notif-other-user",
password="secret123",
email="notif-other@example.com",
phone_number="09120000013",
)
self.farm_type = FarmType.objects.create(name="دامداری")
self.farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
name="Farm B",
)
@patch("notifications.views.publish_notification")
def test_publish_calls_service_and_returns_payload(self, mock_publish_notification):
mock_publish_notification.return_value = {"id": "1", "event": "notification", "message": "hello"}
def test_long_poll_view_returns_notifications_for_owned_farm(self):
FarmNotification.objects.create(farm=self.farm, title="Alert", message="Check sensor")
request = self.factory.get(f"/api/notifications/long-poll/?farm_uuid={self.farm.farm_uuid}&timeout=0")
force_authenticate(request, user=self.user)
response = NotificationLongPollView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data["data"]), 1)
self.assertEqual(response.data["data"][0]["title"], "Alert")
def test_long_poll_view_returns_404_for_unowned_farm(self):
request = self.factory.get(f"/api/notifications/long-poll/?farm_uuid={self.farm.farm_uuid}&timeout=0")
force_authenticate(request, user=self.other_user)
response = NotificationLongPollView.as_view()(request)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.data["msg"], "Farm not found.")
@patch("notifications.views.long_poll_notifications")
def test_long_poll_view_passes_since_id(self, mocked_long_poll):
mocked_long_poll.return_value = []
request = self.factory.get(
f"/api/notifications/long-poll/?farm_uuid={self.farm.farm_uuid}&since_id=5&timeout=0"
)
force_authenticate(request, user=self.user)
response = NotificationLongPollView.as_view()(request)
self.assertEqual(response.status_code, 200)
mocked_long_poll.assert_called_once()
self.assertEqual(mocked_long_poll.call_args.kwargs["since_id"], 5)
@override_settings(EXTERNAL_NOTIFICATION_API_KEY="12345")
class ExternalNotificationIngestViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = get_user_model().objects.create_user(
username="notif-external-user",
password="secret123",
email="notif-external@example.com",
phone_number="09120000014",
)
self.farm_type = FarmType.objects.create(name="آبی")
self.farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
name="Farm C",
)
def test_external_ingest_requires_api_key(self):
request = self.factory.post(
"/api/notifications/publish/",
"/api/notifications/external/ingest/",
{
"channel": "user-1",
"title": "Test",
"message": "hello",
"level": "info",
"farm_uuid": str(self.farm.farm_uuid),
"title": "external",
"message": "payload",
},
format="json",
)
force_authenticate(request, user=self.user)
response = NotificationPublishView.as_view()(request)
response = ExternalNotificationIngestView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["code"], 200)
mock_publish_notification.assert_called_once()
self.assertEqual(response.status_code, 401)
class _FakePubSub:
def __init__(self):
self.calls = 0
def subscribe(self, _channel):
return None
def get_message(self, ignore_subscribe_messages=True, timeout=15.0):
self.calls += 1
if self.calls == 1:
return {"type": "message", "data": '{"event":"notification","message":"hi"}'}
return None
def close(self):
return None
class _FakeRedis:
def __init__(self):
self._pubsub = _FakePubSub()
def pubsub(self):
return self._pubsub
class NotificationStreamViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = get_user_model().objects.create_user(
username="stream-user",
password="secret123",
email="stream@example.com",
phone_number="09120000098",
def test_external_ingest_creates_notification_with_valid_api_key(self):
request = self.factory.post(
"/api/notifications/external/ingest/",
{
"farm_uuid": str(self.farm.farm_uuid),
"title": "Pump alert",
"message": "Pump disconnected",
"level": "critical",
"metadata": {"source": "external-service"},
},
format="json",
HTTP_X_API_KEY=settings.EXTERNAL_NOTIFICATION_API_KEY,
)
@patch("notifications.views.get_notifications_redis_client")
def test_stream_returns_event_stream_response(self, mock_redis_client):
mock_redis_client.return_value = _FakeRedis()
request = self.factory.get("/api/notifications/stream/?channel=user-1")
force_authenticate(request, user=self.user)
response = ExternalNotificationIngestView.as_view()(request)
response = NotificationStreamView.as_view()(request)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data["data"]["title"], "Pump alert")
self.assertTrue(
FarmNotification.objects.filter(farm=self.farm, title="Pump alert", level="critical").exists()
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "text/event-stream")
iterator = iter(response.streaming_content)
first_chunk = self._to_text(next(iterator))
second_chunk = self._to_text(next(iterator))
self.assertIn("connected", first_chunk)
self.assertIn("event: notification", second_chunk)
def test_external_ingest_returns_404_for_unknown_farm(self):
request = self.factory.post(
"/api/notifications/external/ingest/",
{
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"title": "Pump alert",
"message": "Pump disconnected",
},
format="json",
HTTP_X_API_KEY=settings.EXTERNAL_NOTIFICATION_API_KEY,
)
@staticmethod
def _to_text(value):
if isinstance(value, bytes):
return value.decode()
return str(value)
response = ExternalNotificationIngestView.as_view()(request)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.data["msg"], "Farm not found.")