Files
ss-tools/backend/tests/services/test_notification_service.py
busya ce0369ae5c test(backend): add 55+ test files to push coverage to 98%
Subagents delivered tests across all uncovered backend modules:

Schemas (100%): agent, auth, health, profile, settings, validation
Services (98-100%): auth, profile, health, llm, mapping, resource,
  security, git, superset_lookup, sql_table_extractor, rbac
API routes (new): auth, admin, health, environments, plugins,
  dashboards (helpers, projection, actions, listing),
  git (config, deps, env, helpers)
Clean Release (100%): DTO, facade, policy_engine, stages,
  repos, preparation, source_isolation, compliance
Git services: base, remote_providers
Agent module: app, run, middleware, langgraph_setup
Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching
Reports: normalizer, report_service, type_profiles
Notifications: service, providers

Also:
- .gitignore: add .coverage, *.cover, coverage-* dirs
- src/schemas/auth.py: fix AD group DN regex (comma in CN=...)
- Remove co-located src/services/__tests__/ (caused pytest module collision)
2026-06-15 13:55:57 +03:00

333 lines
12 KiB
Python

# #region Test.NotificationService [C:3] [TYPE Module] [SEMANTICS test,notification,service,dispatch]
# @BRIEF Tests for services/notifications/service.py — NotificationService.
# @RELATION BINDS_TO -> [service]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# #region test_notification_service [C:2] [TYPE Function]
# @BRIEF Test NotificationService initialization and dispatch.
class TestNotificationServiceInit:
@pytest.fixture
def mock_db(self):
return MagicMock()
@pytest.fixture
def mock_config_manager(self):
cm = MagicMock()
cm.get_payload.return_value = {}
return cm
@pytest.fixture
def service(self, mock_db, mock_config_manager):
from src.services.notifications.service import NotificationService
return NotificationService(mock_db, mock_config_manager)
def test_init(self, service, mock_db, mock_config_manager):
assert service.db is mock_db
assert service.config_manager is mock_config_manager
assert service._providers == {}
assert service._initialized is False
# #region test_initialize_providers [C:2] [TYPE Function]
# @BRIEF Test _initialize_providers creates providers from config.
class TestInitializeProviders:
@pytest.fixture
def mock_db(self):
return MagicMock()
@pytest.fixture
def service(self, mock_db):
from src.services.notifications.service import NotificationService
cm = MagicMock()
cm.get_payload.return_value = {
"notifications": {
"smtp": {"host": "smtp.example.com"},
"telegram": {"bot_token": "123:ABC"},
"slack": {"webhook_url": "https://hooks.slack.com/xxx"},
}
}
return NotificationService(mock_db, cm)
def test_initializes_providers(self, service):
service._initialize_providers()
assert "SMTP" in service._providers
assert "TELEGRAM" in service._providers
assert "SLACK" in service._providers
assert service._initialized is True
def test_initializes_only_once(self, service):
service._initialize_providers()
providers_count = len(service._providers)
service._initialize_providers()
assert len(service._providers) == providers_count # no duplicates
def test_no_notification_config(self, mock_db):
from src.services.notifications.service import NotificationService
cm = MagicMock()
cm.get_payload.return_value = {}
service = NotificationService(mock_db, cm)
service._initialize_providers()
assert service._providers == {}
assert service._initialized is True
# #endregion test_initialize_providers
# #region test_should_notify [C:2] [TYPE Function]
# @BRIEF Test _should_notify decision logic.
class TestShouldNotify:
@pytest.fixture
def service(self, mock_db):
from src.services.notifications.service import NotificationService
cm = MagicMock()
cm.get_payload.return_value = {}
return NotificationService(mock_db, cm)
def test_returns_true_for_fail_default(self, service):
record = MagicMock()
record.status = "FAIL"
assert service._should_notify(record, None) is True
def test_returns_false_for_warn_default(self, service):
record = MagicMock()
record.status = "WARN"
assert service._should_notify(record, None) is False
def test_returns_false_for_success_default(self, service):
record = MagicMock()
record.status = "SUCCESS"
assert service._should_notify(record, None) is False
def test_policy_always_returns_true(self, service):
record = MagicMock()
record.status = "SUCCESS"
policy = MagicMock()
policy.alert_condition = "ALWAYS"
assert service._should_notify(record, policy) is True
def test_policy_warn_and_fail(self, service):
record_warn = MagicMock(status="WARN")
record_fail = MagicMock(status="FAIL")
record_ok = MagicMock(status="SUCCESS")
policy = MagicMock()
policy.alert_condition = "WARN_AND_FAIL"
assert service._should_notify(record_warn, policy) is True
assert service._should_notify(record_fail, policy) is True
assert service._should_notify(record_ok, policy) is False
# #endregion test_should_notify
# #region test_resolve_targets [C:2] [TYPE Function]
# @BRIEF Test _resolve_targets resolves notification targets.
class TestResolveTargets:
@pytest.fixture
def service(self, mock_db):
from src.services.notifications.service import NotificationService
cm = MagicMock()
cm.get_payload.return_value = {}
return NotificationService(mock_db, cm)
def test_resolves_owner_email(self, service):
record = MagicMock()
policy = MagicMock()
policy.notify_owners = True
policy.custom_channels = None
owner_pref = MagicMock()
owner_pref.notify_on_fail = True
owner_pref.telegram_id = None
owner_pref.email_address = "owner@example.com"
owner_pref.user = MagicMock(email="user@example.com")
with patch.object(service, "_find_dashboard_owners", return_value=[owner_pref]):
targets = service._resolve_targets(record, policy)
assert ("SMTP", "owner@example.com") in targets
def test_resolves_owner_telegram(self, service):
record = MagicMock()
policy = MagicMock()
policy.notify_owners = True
policy.custom_channels = None
owner_pref = MagicMock()
owner_pref.notify_on_fail = True
owner_pref.telegram_id = "@admin"
owner_pref.email_address = None
owner_pref.user = MagicMock(email="user@example.com")
with patch.object(service, "_find_dashboard_owners", return_value=[owner_pref]):
targets = service._resolve_targets(record, policy)
assert ("TELEGRAM", "@admin") in targets
def test_skips_owner_with_notify_off(self, service):
record = MagicMock()
policy = MagicMock()
policy.notify_owners = True
owner_pref = MagicMock()
owner_pref.notify_on_fail = False
with patch.object(service, "_find_dashboard_owners", return_value=[owner_pref]):
targets = service._resolve_targets(record, policy)
assert len(targets) == 0
def test_custom_channels_added(self, service):
record = MagicMock()
policy = MagicMock()
policy.notify_owners = False
policy.custom_channels = [
{"type": "SLACK", "target": "#alerts"},
{"type": "TELEGRAM", "target": "@bot_channel"},
]
targets = service._resolve_targets(record, policy)
assert ("SLACK", "#alerts") in targets
assert ("TELEGRAM", "@bot_channel") in targets
def test_skips_owner_without_policy(self, service):
"""When policy is None, owner notification is enabled by default."""
record = MagicMock()
policy = None
owner_pref = MagicMock()
owner_pref.notify_on_fail = True
owner_pref.telegram_id = None
owner_pref.email_address = None
owner_pref.user = MagicMock(email="user@example.com")
with patch.object(service, "_find_dashboard_owners", return_value=[owner_pref]):
targets = service._resolve_targets(record, None)
# No valid target since both telegrams_id and email are None but user.email exists
assert ("SMTP", "user@example.com") in targets
# #endregion test_resolve_targets
# #region test_build_body [C:2] [TYPE Function]
# @BRIEF Test _build_body formats correctly.
class TestBuildBody:
def test_formats_body(self, mock_db):
from src.services.notifications.service import NotificationService
cm = MagicMock()
cm.get_payload.return_value = {}
service = NotificationService(mock_db, cm)
record = MagicMock()
record.dashboard_id = "dash-1"
record.environment_id = "env-1"
record.status = "FAIL"
record.summary = "Dashboard has issues"
record.issues = ["issue1", "issue2"]
body = service._build_body(record)
assert "dash-1" in body
assert "env-1" in body
assert "FAIL" in body
assert "2" in body # len(record.issues)
# #endregion test_build_body
# #region test_dispatch_report [C:2] [TYPE Function]
# @BRIEF Test dispatch_report full flow.
class TestDispatchReport:
@pytest.fixture
def service(self, mock_db):
from src.services.notifications.service import NotificationService
cm = MagicMock()
cm.get_payload.return_value = {
"notifications": {
"smtp": {"host": "smtp.example.com"},
"telegram": {"bot_token": "123:ABC"},
}
}
return NotificationService(mock_db, cm)
@pytest.mark.asyncio
async def test_skips_notification_when_not_needed(self, service):
record = MagicMock()
record.status = "SUCCESS"
record.id = "rec-1"
policy = None
with patch.object(service, "_should_notify", return_value=False):
await service.dispatch_report(record, policy)
# No providers should be called
@pytest.mark.asyncio
async def test_dispatches_via_background_tasks(self, service):
record = MagicMock()
record.status = "FAIL"
record.id = "rec-1"
policy = MagicMock()
policy.alert_condition = "FAIL_ONLY"
background_tasks = MagicMock()
with patch.object(service, "_resolve_targets", return_value=[("SMTP", "admin@x.com")]):
await service.dispatch_report(record, policy, background_tasks)
background_tasks.add_task.assert_called_once()
@pytest.mark.asyncio
async def test_dispatches_via_gather(self):
from src.services.notifications.service import NotificationService
db = MagicMock()
cm = MagicMock()
cm.get_payload.return_value = {}
service = NotificationService(db, cm)
service._initialized = True # Skip provider init
record = MagicMock()
record.status = "FAIL"
record.id = "rec-1"
policy = MagicMock()
policy.alert_condition = "FAIL_ONLY"
provider_mock = AsyncMock()
provider_mock.send = AsyncMock(return_value=True)
service._providers["SMTP"] = provider_mock
with patch.object(service, "_resolve_targets", return_value=[("SMTP", "admin@x.com")]):
await service.dispatch_report(record, policy, background_tasks=None)
provider_mock.send.assert_called_once()
@pytest.mark.asyncio
async def test_skips_unconfigured_channel(self, service):
record = MagicMock()
record.status = "FAIL"
record.id = "rec-1"
policy = MagicMock()
policy.alert_condition = "FAIL_ONLY"
with patch.object(service, "_resolve_targets", return_value=[("UNKNOWN", "nowhere")]):
await service.dispatch_report(record, policy)
# No exception raised, unknown channel skipped
# #endregion test_dispatch_report
# #region test_find_dashboard_owners [C:2] [TYPE Function]
# @BRIEF Test _find_dashboard_owners DB query.
class TestFindDashboardOwners:
def test_queries_db(self, mock_db):
from src.services.notifications.service import NotificationService
cm = MagicMock()
cm.get_payload.return_value = {}
service = NotificationService(mock_db, cm)
from src.models.profile import UserDashboardPreference
mock_db.query.return_value.filter.return_value.all.return_value = ["owner1", "owner2"]
owners = service._find_dashboard_owners(MagicMock())
assert len(owners) == 2
mock_db.query.assert_called_once_with(UserDashboardPreference)
# #endregion test_find_dashboard_owners
# @pytest.fixture helpers
@pytest.fixture
def mock_db():
return MagicMock()
# #endregion Test.NotificationService