Files
gh-christianlouis-inboxconv…/backend/tests/unit/test_notification_service.py
T

216 lines
8.2 KiB
Python

"""
Unit tests for the notification service (services/notification_service.py).
All external dependencies (database, Apprise) are mocked so no real
infrastructure is needed.
"""
from unittest.mock import AsyncMock, MagicMock, patch
from app.services.notification_service import (
send_user_notification,
send_admin_notification,
test_notification as _test_notification,
_send_apprise,
)
# ── helpers ──────────────────────────────────────────────────────────────────
def _make_notification_config(
*,
id: int = 1,
user_id: int = 42,
apprise_url: str = "json://localhost",
is_enabled: bool = True,
notify_on_errors: bool = True,
notify_on_success: bool = False,
):
cfg = MagicMock()
cfg.id = id
cfg.user_id = user_id
cfg.apprise_url = apprise_url
cfg.is_enabled = is_enabled
cfg.notify_on_errors = notify_on_errors
cfg.notify_on_success = notify_on_success
return cfg
def _make_db(configs=None):
"""Return an AsyncMock db with execute returning the given config list."""
db = AsyncMock()
result = MagicMock()
scalars = MagicMock()
scalars.all.return_value = configs or []
result.scalars.return_value = scalars
db.execute = AsyncMock(return_value=result)
return db
# ── send_user_notification ────────────────────────────────────────────────────
class TestSendUserNotification:
async def test_returns_zero_when_no_configs(self):
db = _make_db(configs=[])
count = await send_user_notification(db, user_id=1, title="T", body="B")
assert count == 0
async def test_sends_to_error_channel(self):
cfg = _make_notification_config(notify_on_errors=True, notify_on_success=False)
db = _make_db(configs=[cfg])
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(return_value=True),
) as mock_send:
count = await send_user_notification(
db, user_id=42, title="Err", body="msg", notify_on_error=True
)
assert count == 1
mock_send.assert_awaited_once()
async def test_skips_error_channel_for_success_notification(self):
cfg = _make_notification_config(notify_on_errors=True, notify_on_success=False)
db = _make_db(configs=[cfg])
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(return_value=True),
) as mock_send:
count = await send_user_notification(
db, user_id=42, title="Ok", body="msg", notify_on_error=False
)
assert count == 0
mock_send.assert_not_awaited()
async def test_sends_to_success_channel(self):
cfg = _make_notification_config(notify_on_errors=False, notify_on_success=True)
db = _make_db(configs=[cfg])
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(return_value=True),
) as mock_send:
count = await send_user_notification(
db, user_id=42, title="Ok", body="msg", notify_on_error=False
)
assert count == 1
mock_send.assert_awaited_once()
async def test_failed_apprise_not_counted(self):
cfg = _make_notification_config(notify_on_errors=True)
db = _make_db(configs=[cfg])
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(return_value=False),
):
count = await send_user_notification(db, user_id=42, title="T", body="B")
assert count == 0
async def test_exception_in_apprise_swallowed(self):
cfg = _make_notification_config(notify_on_errors=True)
db = _make_db(configs=[cfg])
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(side_effect=Exception("boom")),
):
count = await send_user_notification(db, user_id=42, title="T", body="B")
assert count == 0
async def test_multiple_channels_counted_individually(self):
cfg1 = _make_notification_config(id=1, notify_on_errors=True)
cfg2 = _make_notification_config(id=2, notify_on_errors=True)
db = _make_db(configs=[cfg1, cfg2])
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(return_value=True),
):
count = await send_user_notification(db, user_id=42, title="T", body="B")
assert count == 2
# ── send_admin_notification ───────────────────────────────────────────────────
class TestSendAdminNotification:
async def test_returns_zero_when_no_configs(self):
db = _make_db(configs=[])
count = await send_admin_notification(db, title="T", body="B")
assert count == 0
async def test_sends_to_enabled_channel(self):
cfg = MagicMock()
cfg.id = 1
cfg.apprise_url = "json://localhost"
db = _make_db(configs=[cfg])
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(return_value=True),
):
count = await send_admin_notification(db, title="T", body="B")
assert count == 1
async def test_exception_in_channel_swallowed(self):
cfg = MagicMock()
cfg.id = 1
cfg.apprise_url = "json://localhost"
db = _make_db(configs=[cfg])
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(side_effect=RuntimeError("oops")),
):
count = await send_admin_notification(db, title="T", body="B")
assert count == 0
# ── test_notification ─────────────────────────────────────────────────────────
class TestTestNotification:
async def test_success(self):
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(return_value=True),
):
ok, msg = await _test_notification("json://localhost")
assert ok is True
assert "success" in msg.lower()
async def test_failure_from_apprise(self):
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(return_value=False),
):
ok, msg = await _test_notification("json://localhost")
assert ok is False
async def test_exception_returns_false(self):
with patch(
"app.services.notification_service._send_apprise",
new=AsyncMock(side_effect=Exception("network error")),
):
ok, msg = await _test_notification("json://localhost")
assert ok is False
assert "error" in msg.lower()
# ── _send_apprise internal helper ─────────────────────────────────────────────
class TestSendApprise:
async def test_invalid_url_returns_false(self):
# Apprise.add() returns False for unrecognised schemes
with patch("app.services.notification_service.apprise") as mock_apprise_module:
ap_instance = MagicMock()
ap_instance.add.return_value = False
mock_apprise_module.Apprise.return_value = ap_instance
result = await _send_apprise("not-a-valid-url://", "T", "B")
assert result is False
async def test_valid_url_returns_true(self):
with patch("app.services.notification_service.apprise") as mock_apprise_module:
ap_instance = MagicMock()
ap_instance.add.return_value = True
ap_instance.async_notify = AsyncMock(return_value=True)
mock_apprise_module.Apprise.return_value = ap_instance
result = await _send_apprise("json://localhost", "T", "B")
assert result is True