from unittest.mock import MagicMock, call import pytest from core import notifications def test_get_notification_settings_returns_record_or_false(monkeypatch): selectOne = MagicMock( side_effect=[{"user_uuid": "user-1", "ntfy_enabled": True}, None] ) monkeypatch.setattr(notifications.postgres, "select_one", selectOne) assert notifications.getNotificationSettings("user-1") == { "user_uuid": "user-1", "ntfy_enabled": True, } assert notifications.getNotificationSettings("user-2") is False assert selectOne.call_args_list == [ call("notifications", {"user_uuid": "user-1"}), call("notifications", {"user_uuid": "user-2"}), ] def test_notification_settings_filter_fields_and_update_existing(monkeypatch): update = MagicMock() insert = MagicMock() monkeypatch.setattr( notifications.postgres, "select_one", MagicMock(return_value={"id": "notification-1"}), ) monkeypatch.setattr(notifications.postgres, "update", update) monkeypatch.setattr(notifications.postgres, "insert", insert) result = notifications.setNotificationSettings( "user-1", { "ntfy_topic": "team-alerts", "ntfy_enabled": True, "user_uuid": "another-user", "created_at": "not-allowed", }, ) assert result is True update.assert_called_once_with( "notifications", {"ntfy_topic": "team-alerts", "ntfy_enabled": True}, {"user_uuid": "user-1"}, ) insert.assert_not_called() def test_notification_settings_insert_new_record(monkeypatch): insert = MagicMock() monkeypatch.setattr( notifications.postgres, "select_one", MagicMock(return_value=None), ) monkeypatch.setattr(notifications.postgres, "insert", insert) monkeypatch.setattr(notifications.uuid, "uuid4", lambda: "notification-1") result = notifications.setNotificationSettings( "user-1", {"discord_enabled": False, "ntfy_topic": "personal"}, ) assert result is True insert.assert_called_once_with( "notifications", { "discord_enabled": False, "ntfy_topic": "personal", "id": "notification-1", "user_uuid": "user-1", }, ) @pytest.mark.parametrize( "settings", [None, [], "invalid", {}, {"created_at": "not-allowed"}], ) def test_notification_settings_reject_invalid_or_empty_updates(monkeypatch, settings): selectOne = MagicMock() monkeypatch.setattr(notifications.postgres, "select_one", selectOne) assert notifications.setNotificationSettings("user-1", settings) is False selectOne.assert_not_called() @pytest.mark.parametrize( "webhook", [ "https://discord.com/api/webhooks/123/token", "https://canary.discord.com/api/webhooks/123/token", "https://ptb.discord.com/api/webhooks/123/token", ], ) def test_discord_webhook_validation_accepts_official_https_urls(webhook): assert notifications._validateDiscordWebhook(webhook) == webhook @pytest.mark.parametrize( ("webhook", "error"), [ ( "http://discord.com/api/webhooks/123/token", "official HTTPS Discord host", ), ( "https://discord.com.evil.example/api/webhooks/123/token", "official HTTPS Discord host", ), ("https://discord.com/channels/123", "Invalid Discord webhook path"), ], ) def test_discord_webhook_validation_rejects_unsafe_urls(webhook, error): with pytest.raises(ValueError, match=error): notifications._validateDiscordWebhook(webhook) def test_discord_webhook_delivery_posts_content(monkeypatch): post = MagicMock(return_value=MagicMock(status_code=204)) monkeypatch.setattr(notifications.requests, "post", post) webhook = "https://discord.com/api/webhooks/123/token" assert notifications.discord.send(webhook, 42) is True post.assert_called_once_with( webhook, json={"content": "42"}, timeout=notifications.REQUEST_TIMEOUT, ) @pytest.mark.parametrize("failure", [429, notifications.requests.ConnectionError("offline")]) def test_discord_webhook_delivery_reports_failures(monkeypatch, failure): post = MagicMock() if isinstance(failure, int): post.return_value = MagicMock(status_code=failure) else: post.side_effect = failure monkeypatch.setattr(notifications.requests, "post", post) assert notifications.discord.send( "https://discord.com/api/webhooks/123/token", "hello", ) is False def test_ntfy_encodes_topic_and_sends_bearer_token(monkeypatch): post = MagicMock(return_value=MagicMock(status_code=201)) monkeypatch.setattr(notifications.requests, "post", post) monkeypatch.setenv("NTFY_BASE_URL", "https://notify.example/base/") monkeypatch.setenv("NTFY_TOKEN", "ntfy-secret") assert notifications.ntfy.send(" alerts/team #1 ", 42) is True post.assert_called_once_with( "https://notify.example/base/alerts%2Fteam%20%231", data=b"42", headers={"Authorization": "Bearer ntfy-secret"}, timeout=notifications.REQUEST_TIMEOUT, ) @pytest.mark.parametrize("failure", [500, notifications.requests.Timeout("slow")]) def test_ntfy_reports_http_and_transport_failures(monkeypatch, failure): post = MagicMock() if isinstance(failure, int): post.return_value = MagicMock(status_code=failure) else: post.side_effect = failure monkeypatch.setattr(notifications.requests, "post", post) monkeypatch.delenv("NTFY_TOKEN", raising=False) assert notifications.ntfy.send("alerts", "hello") is False def test_ntfy_rejects_empty_topic_without_request(monkeypatch): post = MagicMock() monkeypatch.setattr(notifications.requests, "post", post) assert notifications.ntfy.send(" ", "hello") is False assert notifications.ntfy.send(None, "hello") is False post.assert_not_called() def test_channel_aggregation_tries_each_enabled_channel(monkeypatch): discordSend = MagicMock(return_value=False) ntfySend = MagicMock(return_value=True) monkeypatch.setattr(notifications.discord, "send", discordSend) monkeypatch.setattr(notifications.ntfy, "send", ntfySend) result = notifications._sendToEnabledChannels( { "discord_enabled": True, "discord_webhook": "https://discord.com/api/webhooks/123/token", "ntfy_enabled": True, "ntfy_topic": "alerts", }, "hello", ) assert result is True discordSend.assert_called_once_with( "https://discord.com/api/webhooks/123/token", "hello", ) ntfySend.assert_called_once_with("alerts", "hello") @pytest.mark.parametrize("settings", [None, {}, {"discord_enabled": False}]) def test_channel_aggregation_skips_unconfigured_channels(monkeypatch, settings): discordSend = MagicMock() ntfySend = MagicMock() monkeypatch.setattr(notifications.discord, "send", discordSend) monkeypatch.setattr(notifications.ntfy, "send", ntfySend) assert notifications._sendToEnabledChannels(settings, "hello") is False discordSend.assert_not_called() ntfySend.assert_not_called()