# #region Test.Maintenance.ChartManager [C:3] [TYPE Module] [SEMANTICS test, maintenance, chart, banner, edge] # @BRIEF Tests for maintenance chart manager — ensure_banner_chart, process_dashboards, process_states. # @RELATION BINDS_TO -> [MaintenanceChartManager] import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src")) from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.models.maintenance import ( MaintenanceDashboardBanner, MaintenanceDashboardBannerStatus, MaintenanceDashboardState, MaintenanceDashboardStateStatus, MaintenanceEvent, MaintenanceEventStatus, MaintenanceSettings, DashboardScope, ) @pytest.fixture def mock_db(): db = MagicMock() db.query.return_value.filter.return_value.first.return_value = None db.query.return_value.filter.return_value.count.return_value = 0 db.query.return_value.filter.return_value.all.return_value = [] return db @pytest.fixture def mock_superset(): client = AsyncMock() client.get_chart.return_value = {"id": 123, "result": {"viz_type": "markdown"}} client.create_markdown_chart.return_value = 456 client.update_dashboard_layout.return_value = {"result": "ok"} client.update_banner_on_dashboard.return_value = {"result": "ok"} client.remove_chart_from_layout.return_value = {"result": "ok"} client.delete_chart.return_value = {"result": "ok"} return client class TestEnsureBannerChart: @pytest.mark.asyncio async def test_creates_new_banner(self, mock_db, mock_superset): """No existing banner → creates chart, updates layout, persists banner.""" from src.services.maintenance._chart_manager import ensure_banner_chart result = await ensure_banner_chart(1, "env1", mock_superset, mock_db) assert result.chart_id == 456 assert result.status == MaintenanceDashboardBannerStatus.ACTIVE mock_superset.create_markdown_chart.assert_called_once_with(1, "*Maintenance banner placeholder*") mock_superset.update_dashboard_layout.assert_called_once_with(1, 456) mock_db.add.assert_called_once() mock_db.flush.assert_called() @pytest.mark.asyncio async def test_reuses_existing_banner(self, mock_db, mock_superset): """Existing active banner → verifies chart in Superset and reuses.""" from src.services.maintenance._chart_manager import ensure_banner_chart existing = MaintenanceDashboardBanner( id="b1", dashboard_id=1, environment_id="env1", chart_id=123, status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text="" ) mock_db.query.return_value.filter.return_value.first.return_value = existing mock_superset.get_chart.return_value = {"id": 123} result = await ensure_banner_chart(1, "env1", mock_superset, mock_db) assert result is existing mock_superset.get_chart.assert_called_once_with(123) mock_superset.create_markdown_chart.assert_not_called() @pytest.mark.asyncio async def test_stale_banner_removed_and_recreated(self, mock_db, mock_superset): """Existing but stale banner → marked REMOVED, new one created.""" from src.services.maintenance._chart_manager import ensure_banner_chart existing = MaintenanceDashboardBanner( id="b1", dashboard_id=1, environment_id="env1", chart_id=123, status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text="" ) mock_db.query.return_value.filter.return_value.first.return_value = existing mock_superset.get_chart.side_effect = Exception("Chart not found") result = await ensure_banner_chart(1, "env1", mock_superset, mock_db) assert existing.status == MaintenanceDashboardBannerStatus.REMOVED assert result.chart_id == 456 # new chart mock_superset.create_markdown_chart.assert_called_once() @pytest.mark.asyncio async def test_create_chart_failure(self, mock_db, mock_superset): """Failure to create chart → exception propagates.""" from src.services.maintenance._chart_manager import ensure_banner_chart mock_superset.create_markdown_chart.side_effect = Exception("API error") with pytest.raises(Exception, match="API error"): await ensure_banner_chart(1, "env1", mock_superset, mock_db) @pytest.mark.asyncio async def test_layout_update_failure_cleans_up_chart(self, mock_db, mock_superset): """Failure to update layout → deletes chart and raises.""" from src.services.maintenance._chart_manager import ensure_banner_chart mock_superset.update_dashboard_layout.side_effect = Exception("Layout error") with pytest.raises(Exception, match="Layout error"): await ensure_banner_chart(1, "env1", mock_superset, mock_db) mock_superset.delete_chart.assert_called_once_with(456) @pytest.mark.asyncio async def test_layout_update_failure_cleanup_ignores_delete_error(self, mock_db, mock_superset): """Delete chart failure during cleanup does not mask original error.""" from src.services.maintenance._chart_manager import ensure_banner_chart mock_superset.update_dashboard_layout.side_effect = Exception("Layout error") mock_superset.delete_chart.side_effect = Exception("Delete also failed") with pytest.raises(Exception, match="Layout error"): await ensure_banner_chart(1, "env1", mock_superset, mock_db) class TestProcessDashboardsForStart: @pytest.mark.asyncio async def test_successful_processing(self, mock_db, mock_superset): """All dashboards processed successfully.""" from src.services.maintenance._chart_manager import _process_dashboards_for_start mock_superset.get_dashboards.return_value = (1, [{"id": 1, "dashboard_title": "Test"}]) successful, failed, banners = await _process_dashboards_for_start( [1], "evt1", "env1", "Template: {message}", "UTC", mock_superset, mock_db ) assert len(successful) == 1 assert len(failed) == 0 assert len(banners) == 1 @pytest.mark.asyncio async def test_banner_update_failure_handled(self, mock_db, mock_superset): """Failed banner update → records as APPLY_FAILED.""" from src.services.maintenance._chart_manager import _process_dashboards_for_start mock_superset.update_banner_on_dashboard.side_effect = Exception("Update failed") mock_superset.get_dashboards.return_value = (1, [{"id": 1, "dashboard_title": "Test"}]) successful, failed, banners = await _process_dashboards_for_start( [1], "evt1", "env1", "Template", "UTC", mock_superset, mock_db ) assert len(successful) == 0 assert len(failed) == 1 assert len(banners) == 1 @pytest.mark.asyncio async def test_top_level_exception_handling(self, mock_db, mock_superset): """Exception outside banner creation → recorded as failed.""" from src.services.maintenance._chart_manager import _process_dashboards_for_start mock_superset.create_markdown_chart.side_effect = Exception("Chart creation failed") successful, failed, banners = await _process_dashboards_for_start( [1], "evt1", "env1", "Template", "UTC", mock_superset, mock_db ) assert len(successful) == 0 assert len(failed) == 1 class TestProcessStatesForEnd: @pytest.mark.asyncio async def test_no_banner_id(self, mock_db, mock_superset): """State with no banner_id → marked REMOVED.""" from src.services.maintenance._chart_manager import _process_states_for_end state = MaintenanceDashboardState( event_id="evt1", dashboard_id=1, status=MaintenanceDashboardStateStatus.ACTIVE ) state.banner_id = None removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db) assert removed == 1 assert state.status == MaintenanceDashboardStateStatus.REMOVED @pytest.mark.asyncio async def test_other_events_still_active_rebuilds(self, mock_db, mock_superset): """Other active events exist → rebuild banner.""" from src.services.maintenance._chart_manager import _process_states_for_end banner = MaintenanceDashboardBanner( id="b1", dashboard_id=1, chart_id=123, status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text="old" ) state = MaintenanceDashboardState( event_id="evt1", dashboard_id=1, banner_id="b1", status=MaintenanceDashboardStateStatus.ACTIVE ) # Mock query: other active events exist mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.count.return_value = 1 mock_db.query.return_value.filter.return_value.first.return_value = banner removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db) assert removed == 1 assert state.status == MaintenanceDashboardStateStatus.REMOVED @pytest.mark.asyncio async def test_no_other_events_removes_banner(self, mock_db, mock_superset): """No other active events → remove banner fully.""" from src.services.maintenance._chart_manager import _process_states_for_end banner = MaintenanceDashboardBanner( id="b1", dashboard_id=1, chart_id=123, status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text="old" ) state = MaintenanceDashboardState( event_id="evt1", dashboard_id=1, banner_id="b1", status=MaintenanceDashboardStateStatus.ACTIVE ) mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.count.return_value = 0 mock_db.query.return_value.filter.return_value.first.return_value = banner removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db) assert removed == 1 assert banner.status == MaintenanceDashboardBannerStatus.REMOVED assert state.status == MaintenanceDashboardStateStatus.REMOVED mock_superset.remove_chart_from_layout.assert_called_once_with(1, 123) mock_superset.delete_chart.assert_called_once_with(123) @pytest.mark.asyncio async def test_remove_chart_failure(self, mock_db, mock_superset): """Failure to remove chart from layout → REMOVAL_FAILED.""" from src.services.maintenance._chart_manager import _process_states_for_end banner = MaintenanceDashboardBanner( id="b1", dashboard_id=1, chart_id=123, status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text="old" ) state = MaintenanceDashboardState( event_id="evt1", dashboard_id=1, banner_id="b1", status=MaintenanceDashboardStateStatus.ACTIVE ) mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.count.return_value = 0 mock_db.query.return_value.filter.return_value.first.return_value = banner mock_superset.remove_chart_from_layout.side_effect = Exception("Remove failed") removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db) assert removed == 0 assert len(failed) == 1 assert state.status == MaintenanceDashboardStateStatus.REMOVAL_FAILED @pytest.mark.asyncio async def test_delete_chart_failure(self, mock_db, mock_superset): """Failure to delete chart → REMOVAL_FAILED.""" from src.services.maintenance._chart_manager import _process_states_for_end banner = MaintenanceDashboardBanner( id="b1", dashboard_id=1, chart_id=123, status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text="old" ) state = MaintenanceDashboardState( event_id="evt1", dashboard_id=1, banner_id="b1", status=MaintenanceDashboardStateStatus.ACTIVE ) mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.count.return_value = 0 mock_db.query.return_value.filter.return_value.first.return_value = banner mock_superset.delete_chart.side_effect = Exception("Delete failed") removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db) assert removed == 0 assert len(failed) == 1 assert state.status == MaintenanceDashboardStateStatus.REMOVAL_FAILED @pytest.mark.asyncio async def test_no_banner_record(self, mock_db, mock_superset): """State with banner_id but no banner record → REMOVED.""" from src.services.maintenance._chart_manager import _process_states_for_end state = MaintenanceDashboardState( event_id="evt1", dashboard_id=1, banner_id="nonexistent", status=MaintenanceDashboardStateStatus.ACTIVE ) mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.count.return_value = 0 mock_db.query.return_value.filter.return_value.first.return_value = None removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db) assert removed == 1 assert state.status == MaintenanceDashboardStateStatus.REMOVED @pytest.mark.asyncio async def test_top_level_exception(self, mock_db, mock_superset): """Exception at top level → REMOVAL_FAILED.""" from src.services.maintenance._chart_manager import _process_states_for_end state = MaintenanceDashboardState( event_id="evt1", dashboard_id=1, banner_id="b1", status=MaintenanceDashboardStateStatus.ACTIVE ) mock_db.query.side_effect = Exception("DB error") removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db) assert removed == 0 assert len(failed) == 1 # #endregion Test.Maintenance.ChartManager