# #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