from types import SimpleNamespace import pytest from apps.communications.push_notifications import PushNotificationService pytestmark = pytest.mark.unit def test_push_delivery_passes_payload_to_gateway(mocker): gateway = mocker.patch("apps.communications.push_notifications.webpush") service = PushNotificationService() assert service.send_notification({"endpoint": "https://push.test"}, {"title": "News"}) assert gateway.call_args.kwargs["ttl"] == 86400 assert '"title": "News"' in gateway.call_args.kwargs["data"] def test_push_batch_counts_success_and_failures(mocker): service = PushNotificationService() mocker.patch.object(service, "send_notification", side_effect=[True, False]) devices = [SimpleNamespace(endpoint="a", p256dh_key="key", auth_key="auth"), SimpleNamespace(endpoint="b", p256dh_key="key", auth_key="auth")] assert service.send_to_multiple(devices, {"title": "News"}) == {"sent": 1, "failed": 1} def test_push_delivery_handles_gateway_failure(mocker): gateway = mocker.patch("apps.communications.push_notifications.webpush", side_effect=RuntimeError("offline")) service = PushNotificationService() assert not service.send_notification({"endpoint": "https://push.test"}, {"title": "News"}) gateway.assert_called_once() def test_push_announcement_and_reminder_build_expected_payloads(mocker): service = PushNotificationService() sender = mocker.patch.object(service, "send_to_multiple", return_value={"sent": 1, "failed": 0}) device = SimpleNamespace(endpoint="push", p256dh_key="key", auth_key="auth") announcement = SimpleNamespace(id=7, title="Notice", content="x" * 110) assert service.send_announcement_notification(announcement, [device]) == {"sent": 1, "failed": 0} assert sender.call_args.args[0] == [device] assert sender.call_args.args[1]["body"] == "x" * 100 + "..." event = SimpleNamespace(id=9, title="Workshop") service.send_event_reminder_notification(event, [device]) assert sender.call_args.args[1]["data"]["url"] == "/events/9/" @pytest.mark.parametrize("audience,expected", [("all", {"is_active": True}), ("members", {"user__is_member": True, "is_active": True}), ("committee", {"user__is_committee_member": True, "is_active": True})]) def test_push_announcement_selects_audience(mocker, audience, expected): service = PushNotificationService() devices = mocker.patch("apps.communications.push_notifications.PushNotificationDevice.objects.filter") sender = mocker.patch.object(service, "send_to_multiple", return_value={"sent": 0, "failed": 0}) announcement = SimpleNamespace(id=1, title="Alert", content="Short", audience=audience) service.send_announcement_notification(announcement) devices.assert_called_once_with(**expected) sender.assert_called_once() def test_push_reminder_selects_registered_devices(mocker): service = PushNotificationService() registrations = mocker.patch("apps.communications.push_notifications.Registration.objects.filter") registrations.return_value.values_list.return_value = [1, 2] devices = mocker.patch("apps.communications.push_notifications.PushNotificationDevice.objects.filter") mocker.patch.object(service, "send_to_multiple", return_value={"sent": 0, "failed": 0}) event = SimpleNamespace(id=3, title="Event") service.send_event_reminder_notification(event) registrations.assert_called_once_with(event=event, status="confirmed") devices.assert_called_once_with(user_id__in=[1, 2], is_active=True) def test_invalid_push_subscription_is_removed(mocker): service = PushNotificationService() deleted = mocker.patch("apps.communications.push_notifications.PushNotificationDevice.objects.filter") service._remove_invalid_subscription({"endpoint": "https://push.test"}) deleted.assert_called_once_with(endpoint="https://push.test") deleted.return_value.delete.assert_called_once()