Build reusable bot framework
This commit is contained in:
223
tests/unit/test_notifications.py
Normal file
223
tests/unit/test_notifications.py
Normal file
@@ -0,0 +1,223 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user