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)
333 lines
12 KiB
Python
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
|