# #region Test.BackupPlugin [C:3] [TYPE Module] [SEMANTICS test, backup, plugin, coverage] # @BRIEF Unit tests for BackupPlugin — properties, get_schema, execute, and edge cases. # @RELATION BINDS_TO -> [BackupPlugin] # @TEST_EDGE: missing_env -> Raises KeyError # @TEST_EDGE: no_dashboards -> Returns NO_DASHBOARDS status # @TEST_EDGE: partial_failure -> Returns PARTIAL_SUCCESS with failed_dashboards # @TEST_EDGE: empty_environments -> get_schema returns empty list import pytest from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock from pathlib import Path from requests.exceptions import RequestException from src.plugins.backup import BackupPlugin # ── Helpers ── def _make_env(id_val="env-1", name="Test Env"): env = MagicMock() env.id = id_val env.name = name return env def _make_task_context(): ctx = MagicMock() ctx.logger = MagicMock() ctx.logger.with_source.return_value = MagicMock() return ctx def _make_dashboard(dash_id=1, title="Test Dash"): return {"id": dash_id, "dashboard_title": title} class TestBackupPluginProperties: """Verify static property values.""" def test_id(self): plugin = BackupPlugin() assert plugin.id == "superset-backup" def test_name(self): plugin = BackupPlugin() assert plugin.name == "Superset Dashboard Backup" def test_description(self): plugin = BackupPlugin() assert plugin.description == "Backs up all dashboards from a Superset instance." def test_version(self): plugin = BackupPlugin() assert plugin.version == "1.0.0" def test_ui_route(self): plugin = BackupPlugin() assert plugin.ui_route == "/tools/backups" class TestBackupPluginGetSchema: """Verify get_schema — dynamic schema based on environments.""" def test_get_schema_with_envs(self): plugin = BackupPlugin() mock_cm = MagicMock() mock_cm.get_environments.return_value = [_make_env("e1", "Dev"), _make_env("e2", "Prod")] with patch('src.plugins.backup.get_config_manager', return_value=mock_cm): schema = plugin.get_schema() assert schema["type"] == "object" assert schema["properties"]["env"]["enum"] == ["Dev", "Prod"] assert "env" in schema["required"] def test_get_schema_no_envs(self): """Fallback to empty list when no environments configured.""" plugin = BackupPlugin() mock_cm = MagicMock() mock_cm.get_environments.return_value = [] with patch('src.plugins.backup.get_config_manager', return_value=mock_cm): schema = plugin.get_schema() assert schema["properties"]["env"]["enum"] == [] class TestBackupPluginExecute: """Verify BackupPlugin.execute with various scenarios.""" # ── Missing env → KeyError ── @pytest.mark.asyncio async def test_execute_missing_env(self): """Missing env param raises KeyError.""" plugin = BackupPlugin() with pytest.raises(KeyError, match="env"): await plugin.execute({}) @pytest.mark.asyncio async def test_execute_missing_env_with_ctx(self): """Missing env param raises KeyError even with context.""" plugin = BackupPlugin() ctx = _make_task_context() with pytest.raises(KeyError, match="env"): await plugin.execute({"other": "value"}, context=ctx) # ── No dashboards ── @pytest.mark.asyncio async def test_execute_no_dashboards(self): """Zero dashboards returns NO_DASHBOARDS status.""" plugin = BackupPlugin() env = _make_env("env-1", "Source") mock_cm = MagicMock() mock_cm.has_environments.return_value = True mock_cm.get_environments.return_value = [env] mock_cm.get_environment.return_value = env mock_cfg = MagicMock() mock_cfg.settings.storage.root_path = "/tmp" mock_cm.get_config.return_value = mock_cfg mock_client = MagicMock() mock_client.get_dashboards = AsyncMock(return_value=(0, [])) with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \ patch('src.plugins.backup.SupersetClient', return_value=mock_client): result = await plugin.execute({"env": "Source"}) assert result["status"] == "NO_DASHBOARDS" assert result["total_dashboards"] == 0 assert result["backed_up_dashboards"] == 0 assert result["failed_dashboards"] == 0 # ── Partial success ── @pytest.mark.asyncio async def test_execute_partial_failure(self): """One dashboard fails, one succeeds → PARTIAL_SUCCESS.""" plugin = BackupPlugin() env = _make_env("env-1", "Source") mock_cm = MagicMock() mock_cm.has_environments.return_value = True mock_cm.get_environments.return_value = [env] mock_cm.get_environment.return_value = env mock_cfg = MagicMock() mock_cfg.settings.storage.root_path = "/tmp" mock_cm.get_config.return_value = mock_cfg dashboards = [ _make_dashboard(1, "Good Dash"), _make_dashboard(2, "Bad Dash"), ] mock_client = MagicMock() mock_client.get_dashboards = AsyncMock(return_value=(2, dashboards)) mock_client.export_dashboard = AsyncMock(side_effect=[ (b"zip_content", "good.zip"), RequestException("Export failed"), ]) ctx = _make_task_context() ctx.logger.progress = MagicMock() with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \ patch('src.plugins.backup.SupersetClient', return_value=mock_client), \ patch('src.plugins.backup.run_blocking') as mock_run: mock_run.return_value = None result = await plugin.execute({"env": "Source"}, context=ctx) assert result["status"] == "PARTIAL_SUCCESS" assert result["total_dashboards"] == 2 assert result["backed_up_dashboards"] == 1 assert result["failed_dashboards"] == 1 assert result["dashboards"][0]["title"] == "Good Dash" assert result["failures"][0]["title"] == "Bad Dash" # ── Full success with environment_id resolution ── @pytest.mark.asyncio async def test_execute_with_environment_id(self): """environment_id param resolves to env name.""" plugin = BackupPlugin() env = _make_env("env-abc", "ResolvedEnv") mock_cm = MagicMock() mock_cm.has_environments.return_value = True mock_cm.get_environments.return_value = [env] mock_cm.get_environment.return_value = env mock_cfg = MagicMock() mock_cfg.settings.storage.root_path = "/tmp" mock_cm.get_config.return_value = mock_cfg mock_client = MagicMock() mock_client.get_dashboards = AsyncMock(return_value=(0, [])) with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \ patch('src.plugins.backup.SupersetClient', return_value=mock_client): result = await plugin.execute({"environment_id": "env-abc"}) assert result["status"] == "NO_DASHBOARDS" assert result["environment"] == "ResolvedEnv" # ── Dashboard ID filter ── @pytest.mark.asyncio async def test_execute_dashboard_ids_filter(self): """Filtering by specific dashboard IDs.""" plugin = BackupPlugin() env = _make_env("env-1", "Source") mock_cm = MagicMock() mock_cm.has_environments.return_value = True mock_cm.get_environments.return_value = [env] mock_cm.get_environment.return_value = env mock_cfg = MagicMock() mock_cfg.settings.storage.root_path = "/tmp" mock_cm.get_config.return_value = mock_cfg all_dashboards = [ _make_dashboard(1, "Dash A"), _make_dashboard(2, "Dash B"), _make_dashboard(3, "Dash C"), ] mock_client = MagicMock() mock_client.get_dashboards = AsyncMock(return_value=(3, all_dashboards)) mock_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta.zip")) ctx = _make_task_context() ctx.logger.progress = MagicMock() with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \ patch('src.plugins.backup.SupersetClient', return_value=mock_client), \ patch('src.plugins.backup.run_blocking') as mock_run: mock_run.return_value = None result = await plugin.execute({"env": "Source", "dashboard_ids": ["1", "3"]}, context=ctx) assert result["status"] == "SUCCESS" assert result["total_dashboards"] == 2 assert result["dashboards"][0]["id"] == 1 assert result["dashboards"][1]["id"] == 3 # ── No env configured ── @pytest.mark.asyncio async def test_execute_no_environments_configured(self): """has_environments returns False → raise ValueError.""" plugin = BackupPlugin() mock_cm = MagicMock() mock_cm.has_environments.return_value = False mock_cfg = MagicMock() mock_cfg.settings.storage.root_path = "/tmp" mock_cm.get_config.return_value = mock_cfg with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \ pytest.raises(ValueError, match="No Superset environments configured"): await plugin.execute({"env": "Source"}) # ── Environment not found ── @pytest.mark.asyncio async def test_execute_environment_not_found(self): """get_environment returns None → raise ValueError.""" plugin = BackupPlugin() mock_cm = MagicMock() mock_cm.has_environments.return_value = True mock_cm.get_environments.return_value = [_make_env("other", "Other")] mock_cm.get_environment.return_value = None mock_cfg = MagicMock() mock_cfg.settings.storage.root_path = "/tmp" mock_cm.get_config.return_value = mock_cfg with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \ pytest.raises(ValueError, match="not found"): await plugin.execute({"env": "NonExistent"}) # ── Fatal error wraps OSError ── @pytest.mark.asyncio async def test_execute_oserror_raised(self): """OSError during execution propagates.""" plugin = BackupPlugin() env = _make_env("env-1", "Source") mock_cm = MagicMock() mock_cm.has_environments.return_value = True mock_cm.get_environments.return_value = [env] mock_cm.get_environment.return_value = env mock_cfg = MagicMock() mock_cfg.settings.storage.root_path = "/tmp" mock_cm.get_config.return_value = mock_cfg mock_client = MagicMock() mock_client.get_dashboards = AsyncMock(side_effect=OSError("Disk full")) with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \ patch('src.plugins.backup.SupersetClient', return_value=mock_client), \ pytest.raises(OSError, match="Disk full"): await plugin.execute({"env": "Source"}) # ── Missing dashboard_id in metadata ── @pytest.mark.asyncio async def test_execute_skip_dashboard_without_id(self): """Dashboard without id field is skipped.""" plugin = BackupPlugin() env = _make_env("env-1", "Source") mock_cm = MagicMock() mock_cm.has_environments.return_value = True mock_cm.get_environments.return_value = [env] mock_cm.get_environment.return_value = env mock_cfg = MagicMock() mock_cfg.settings.storage.root_path = "/tmp" mock_cm.get_config.return_value = mock_cfg dashboards = [ {"dashboard_title": "No ID Dash"}, # no 'id' key _make_dashboard(2, "Valid Dash"), ] mock_client = MagicMock() mock_client.get_dashboards = AsyncMock(return_value=(2, dashboards)) mock_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta.zip")) ctx = _make_task_context() ctx.logger.progress = MagicMock() with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \ patch('src.plugins.backup.SupersetClient', return_value=mock_client), \ patch('src.plugins.backup.run_blocking') as mock_run: mock_run.return_value = None result = await plugin.execute({"env": "Source"}, context=ctx) assert result["status"] == "SUCCESS" assert result["total_dashboards"] == 2 assert result["backed_up_dashboards"] == 1 assert result["dashboards"][0]["title"] == "Valid Dash"