# #region Test.Api.Settings [C:3] [TYPE Module] [SEMANTICS test,settings,routes] # @BRIEF Tests for settings API routes — get, update, storage, environments, logging, connections, policies, schedules. # @RELATION BINDS_TO -> [SettingsRouter] import os os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests") os.environ.setdefault("DEV_MODE", "true") import sys from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient _src = str(Path(__file__).resolve().parent.parent.parent / "src") if _src not in sys.path: sys.path.insert(0, _src) def _make_env(id_val="env-1", name="Test Env", url="https://example.com"): env = MagicMock() env.id = id_val env.name = name env.url = url env.password = "real-password" return env def _make_client(overrides: dict | None = None) -> TestClient: from src.api.routes.settings import router from src.core.database import get_db from src.dependencies import get_config_manager, get_current_user from src.schemas.auth import User, RoleSchema app = FastAPI() app.include_router(router, prefix="/api/settings") mock_user = User( id="admin-1", username="admin", email="admin@x.com", auth_source="LOCAL", created_at=__import__("datetime").datetime.now(), roles=[RoleSchema(id="r1", name="Admin", description="", permissions=[])], ) app.dependency_overrides[get_db] = lambda: MagicMock() app.dependency_overrides[get_current_user] = lambda: mock_user app.dependency_overrides[get_config_manager] = lambda: MagicMock() if overrides: for dep, fn in overrides.items(): app.dependency_overrides[dep] = fn return TestClient(app) # ── get_settings ── def _make_real_config(environments=None): """Build a real AppConfig with default settings for mock testing.""" from src.core.config_models import ( AppConfig, GlobalSettings, StorageConfig, LoggingConfig, CleanReleaseConfig, ) gs = GlobalSettings( storage=StorageConfig( root_path="/tmp/storage", backup_path="/tmp/backup", repo_path="/tmp/repo", backup_structure_pattern="{env_id}/{backup_id}", repo_structure_pattern="{env_id}/{repo_id}", filename_pattern="{name}_{timestamp}", ), logging=LoggingConfig( level="INFO", task_log_level="DEBUG", file_path="/tmp/log.log", enable_belief_state=True, ), clean_release=CleanReleaseConfig( active_policy_id="", active_registry_id="", ), default_environment_id="", migration_sync_cron="0 0 * * *", app_timezone="UTC", ) return AppConfig(settings=gs, environments=environments or []) class TestGetSettings: """GET /settings""" def test_success(self): mock_config = MagicMock() mock_config.get_config.return_value = _make_real_config() from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.get("/api/settings") assert resp.status_code == 200 def test_password_masked(self): from src.core.config_models import Environment env = Environment( id="env-1", name="Test Env", url="https://example.com", username="admin", password="real-password", stage="dev", ) real_config = _make_real_config(environments=[env]) mock_config = MagicMock() mock_config.get_config.return_value = real_config from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.get("/api/settings") assert resp.status_code == 200 data = resp.json() masked_env = data["environments"][0] assert masked_env["password"] == "********" class TestGetFeatures: """GET /api/settings/features""" def test_success(self): mock_config = MagicMock() mock_cfg = MagicMock() mock_cfg.settings.features.model_dump.return_value = {"translate": True, "migration": False} mock_config.get_config.return_value = mock_cfg from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.get("/api/settings/features") assert resp.status_code == 200 assert resp.json()["translate"] is True # ── get_allowed_languages ── class TestGetAllowedLanguages: """GET /settings/allowed-languages""" def test_success(self): mock_config = MagicMock() mock_cfg = MagicMock() mock_cfg.settings.allowed_languages = ["en", "ru"] mock_config.get_config.return_value = mock_cfg from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.get("/api/settings/allowed-languages") assert resp.status_code == 200 assert resp.json() == ["en", "ru"] # ── update_global_settings ── class TestUpdateGlobalSettings: """PATCH /settings/global""" def test_success(self): mock_config = MagicMock() from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.patch("/api/settings/global", json={"app_name": "Test", "log_level": "INFO"}) assert resp.status_code == 200 # ── get_storage_settings ── class TestGetStorageSettings: """GET /settings/storage""" def test_success(self): from src.core.config_models import StorageConfig mock_config = MagicMock() mock_cfg = MagicMock() mock_cfg.settings.storage = StorageConfig( root_path="/tmp/storage", backup_path="/tmp/backup", repo_path="/tmp/repo", backup_structure_pattern="{env_id}/{backup_id}", repo_structure_pattern="{env_id}/{repo_id}", filename_pattern="{name}_{timestamp}", ) mock_config.get_config.return_value = mock_cfg from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.get("/api/settings/storage") assert resp.status_code == 200 # ── update_storage_settings ── class TestUpdateStorageSettings: """PUT /settings/storage""" def test_success(self): mock_config = MagicMock() mock_cfg = MagicMock() mock_cfg.settings = MagicMock() mock_cfg.settings.storage = MagicMock() mock_config.get_config.return_value = mock_cfg mock_config.validate_path.return_value = (True, "") from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.put("/api/settings/storage", json={ "root_path": "/data/storage", "max_file_size_mb": 100, "allowed_extensions": [".txt"], "backup_retention_days": 30, }) assert resp.status_code == 200 def test_invalid_path(self): mock_config = MagicMock() mock_cfg = MagicMock() mock_cfg.settings = MagicMock() mock_config.get_config.return_value = mock_cfg mock_config.validate_path.return_value = (False, "Path is invalid") from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.put("/api/settings/storage", json={ "root_path": "/invalid", "max_file_size_mb": 100, "allowed_extensions": [], "backup_retention_days": 30, }) assert resp.status_code == 400 # ── get_environments ── class TestGetEnvironments: """GET /settings/environments""" def test_success(self): from src.core.config_models import Environment mock_config = MagicMock() real_env = Environment( id="env-1", name="Test Env", url="https://example.com", username="admin", password="secret", stage="dev", ) mock_config.get_environments.return_value = [real_env] from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.get("/api/settings/environments") assert resp.status_code == 200 data = resp.json() assert len(data) == 1 assert data[0]["id"] == "env-1" # ── add_environment ── class TestAddEnvironment: """POST /settings/environments""" ENV_PAYLOAD = { "id": "env-new", "name": "New Env", "url": "https://superset.example.com", "password": "secret", } def test_success(self): mock_config = MagicMock() with patch("src.api.routes.settings.AsyncSupersetClient") as mock_client_cls: mock_client = MagicMock() mock_client.authenticate = AsyncMock() mock_client.get_dashboards_page = AsyncMock(return_value=(0, [])) mock_client_cls.return_value = mock_client from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.post("/api/settings/environments", json=self.ENV_PAYLOAD) assert resp.status_code == 200 mock_config.add_environment.assert_called_once() def test_connection_failure(self): mock_config = MagicMock() with patch("src.api.routes.settings.AsyncSupersetClient") as mock_client_cls: mock_client = MagicMock() mock_client.authenticate = AsyncMock(side_effect=RuntimeError("Connection refused")) mock_client_cls.return_value = mock_client from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.post("/api/settings/environments", json=self.ENV_PAYLOAD) assert resp.status_code == 400 # ── update_environment ── class TestUpdateEnvironment: """PUT /settings/environments/{id}""" ENV_PAYLOAD = { "id": "env-1", "name": "Updated Env", "url": "https://superset.example.com", "password": "new-password", } def test_success(self): mock_config = MagicMock() mock_config.get_environments.return_value = [_make_env("env-1")] mock_config.update_environment.return_value = True with patch("src.api.routes.settings.AsyncSupersetClient") as mock_client_cls: mock_client = MagicMock() mock_client.authenticate = AsyncMock() mock_client.get_dashboards_page = AsyncMock(return_value=(0, [])) mock_client_cls.return_value = mock_client from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.put("/api/settings/environments/env-1", json=self.ENV_PAYLOAD) assert resp.status_code == 200 def test_success_masked_password(self): """When password is '********', use the existing password for validation.""" mock_config = MagicMock() old_env = _make_env("env-1") old_env.password = "existing-real-pwd" mock_config.get_environments.return_value = [old_env] mock_config.update_environment.return_value = True with patch("src.api.routes.settings.AsyncSupersetClient") as mock_client_cls: mock_client = MagicMock() mock_client.authenticate = AsyncMock() mock_client.get_dashboards_page = AsyncMock(return_value=(0, [])) mock_client_cls.return_value = mock_client from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.put("/api/settings/environments/env-1", json={ "id": "env-1", "name": "Updated", "url": "https://x.com", "password": "********", }) assert resp.status_code == 200 def test_connection_failure(self): mock_config = MagicMock() mock_config.get_environments.return_value = [_make_env("env-1")] with patch("src.api.routes.settings.AsyncSupersetClient") as mock_client_cls: mock_client = MagicMock() mock_client.authenticate = AsyncMock(side_effect=RuntimeError("fail")) mock_client_cls.return_value = mock_client from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.put("/api/settings/environments/env-1", json=self.ENV_PAYLOAD) assert resp.status_code == 400 def test_not_found(self): mock_config = MagicMock() mock_config.get_environments.return_value = [_make_env("env-1")] with patch("src.api.routes.settings.AsyncSupersetClient") as mock_client_cls: mock_client = MagicMock() mock_client.authenticate = AsyncMock() mock_client.get_dashboards_page = AsyncMock(return_value=(0, [])) mock_client_cls.return_value = mock_client from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.put("/api/settings/environments/env-999", json=self.ENV_PAYLOAD) assert resp.status_code == 404 # ── delete_environment ── class TestDeleteEnvironment: """DELETE /settings/environments/{id}""" def test_success(self): mock_config = MagicMock() from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.delete("/api/settings/environments/env-1") assert resp.status_code == 200 data = resp.json() assert "deleted" in data["message"] mock_config.delete_environment.assert_called_once_with("env-1") # ── test_environment_connection ── class TestTestEnvironmentConnection: """POST /settings/environments/{id}/test""" def test_success(self): mock_config = MagicMock() mock_config.get_environments.return_value = [_make_env("env-1")] with patch("src.api.routes.settings.AsyncSupersetClient") as mock_client_cls: mock_client = MagicMock() mock_client.authenticate = AsyncMock() mock_client.get_dashboards_page = AsyncMock(return_value=(0, [])) mock_client_cls.return_value = mock_client from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.post("/api/settings/environments/env-1/test") assert resp.status_code == 200 assert resp.json()["status"] == "success" def test_not_found(self): mock_config = MagicMock() mock_config.get_environments.return_value = [] from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.post("/api/settings/environments/env-unknown/test") assert resp.status_code == 404 def test_connection_error(self): mock_config = MagicMock() mock_config.get_environments.return_value = [_make_env("env-1")] with patch("src.api.routes.settings.AsyncSupersetClient") as mock_client_cls: mock_client = MagicMock() mock_client.authenticate = AsyncMock(side_effect=RuntimeError("Connection failed")) mock_client_cls.return_value = mock_client from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.post("/api/settings/environments/env-1/test") assert resp.status_code == 200 assert resp.json()["status"] == "error" # ── get_logging_config ── class TestGetLoggingConfig: """GET /settings/logging""" def test_success(self): mock_config = MagicMock() mock_cfg = MagicMock() mock_logging = MagicMock() mock_logging.level = "INFO" mock_logging.task_log_level = "DEBUG" mock_logging.enable_belief_state = True mock_cfg.settings.logging = mock_logging mock_config.get_config.return_value = mock_cfg from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.get("/api/settings/logging") assert resp.status_code == 200 data = resp.json() assert data["level"] == "INFO" assert data["task_log_level"] == "DEBUG" assert data["enable_belief_state"] is True # ── update_logging_config ── class TestUpdateLoggingConfig: """PATCH /settings/logging""" def test_success(self): mock_config = MagicMock() mock_cfg = MagicMock() mock_cfg.settings = MagicMock() mock_config.get_config.return_value = mock_cfg from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.patch("/api/settings/logging", json={ "level": "WARNING", "task_log_level": "INFO", "enable_belief_state": False, }) assert resp.status_code == 200 data = resp.json() assert data["level"] == "WARNING" assert data["task_log_level"] == "INFO" # ── get_validation_policies ── class TestGetValidationPolicies: """GET /settings/automation/policies""" def test_success(self): mock_db = MagicMock() mock_db.query.return_value.all.return_value = [MagicMock(id="p1")] from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.get("/api/settings/automation/policies") assert resp.status_code == 200 # ── create_validation_policy ── class TestCreateValidationPolicy: """POST /settings/automation/policies""" def test_success(self): mock_db = MagicMock() from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.post("/api/settings/automation/policies", json={ "name": "Test Policy", "environment_id": "env-1", "dashboard_ids": [], }) assert resp.status_code == 200 mock_db.add.assert_called_once() mock_db.commit.assert_called_once() # ── update_validation_policy ── class TestUpdateValidationPolicy: """PATCH /settings/automation/policies/{id}""" def test_success(self): mock_db = MagicMock() mock_policy = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = mock_policy from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.patch("/api/settings/automation/policies/p1", json={"name": "Updated"}) assert resp.status_code == 200 mock_db.commit.assert_called_once() def test_not_found(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.patch("/api/settings/automation/policies/unknown", json={"name": "X"}) assert resp.status_code == 404 # ── delete_validation_policy ── class TestDeleteValidationPolicy: """DELETE /settings/automation/policies/{id}""" def test_success(self): mock_db = MagicMock() mock_policy = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = mock_policy from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.delete("/api/settings/automation/policies/p1") assert resp.status_code == 200 mock_db.delete.assert_called_once() mock_db.commit.assert_called_once() def test_not_found(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.delete("/api/settings/automation/policies/unknown") assert resp.status_code == 404 # ── get_translation_schedules ── class TestGetTranslationSchedules: """GET /settings/automation/translation-schedules""" def test_success(self): mock_db = MagicMock() mock_ts = MagicMock() mock_ts.id = "sched-1" mock_ts.job_id = "job-1" mock_ts.cron_expression = "0 * * * *" mock_ts.timezone = "UTC" mock_ts.is_active = True mock_ts.last_run_at = None mock_ts.next_run_at = None mock_ts.created_by = "admin" mock_ts.created_at = None mock_db.query.return_value.outerjoin.return_value.all.return_value = [(mock_ts, "Test Job")] from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.get("/api/settings/automation/translation-schedules") assert resp.status_code == 200 data = resp.json() assert len(data) == 1 assert data[0]["schedule_id"] == "sched-1" assert data[0]["job_name"] == "Test Job" # ── list_connections ── class TestListConnections: """GET /settings/connections""" def test_success(self): mock_conn_svc = MagicMock() mock_conn_svc.list_connections.return_value = [{"id": "conn-1", "name": "Test"}] from src.dependencies import get_config_manager with patch("src.api.routes.settings._get_connection_service", return_value=mock_conn_svc): client = _make_client({get_config_manager: lambda: MagicMock()}) resp = client.get("/api/settings/connections") assert resp.status_code == 200 # ── get_connection ── class TestGetConnection: """GET /settings/connections/{connection_id}""" def test_success(self): mock_conn_svc = MagicMock() mock_conn = MagicMock() mock_conn.model_dump.return_value = {"id": "conn-1", "name": "Test", "password": "real-pwd"} mock_conn_svc.get_connection.return_value = mock_conn mock_conn_svc._count_job_references.return_value = 2 from src.dependencies import get_config_manager with patch("src.api.routes.settings._get_connection_service", return_value=mock_conn_svc): client = _make_client({get_config_manager: lambda: MagicMock()}) resp = client.get("/api/settings/connections/conn-1") assert resp.status_code == 200 data = resp.json() assert data["password"] == "********" assert data["used_by"] == 2 def test_not_found(self): mock_conn_svc = MagicMock() mock_conn_svc.get_connection.return_value = None from src.dependencies import get_config_manager with patch("src.api.routes.settings._get_connection_service", return_value=mock_conn_svc): client = _make_client({get_config_manager: lambda: MagicMock()}) resp = client.get("/api/settings/connections/unknown") assert resp.status_code == 404 # ── create_connection ── class TestCreateConnection: """POST /settings/connections""" def test_success(self): mock_conn_svc = MagicMock() mock_conn_svc.create_connection.return_value = {"id": "conn-new", "name": "New"} from src.dependencies import get_config_manager with patch("src.api.routes.settings._get_connection_service", return_value=mock_conn_svc): client = _make_client({get_config_manager: lambda: MagicMock()}) resp = client.post("/api/settings/connections", json={ "name": "New", "host": "localhost", "port": 5432, "database": "db", "username": "u", "password": "p", "dialect": "postgresql", }) assert resp.status_code == 201 def test_value_error(self): mock_conn_svc = MagicMock() mock_conn_svc.create_connection.side_effect = ValueError("Invalid") from src.dependencies import get_config_manager with patch("src.api.routes.settings._get_connection_service", return_value=mock_conn_svc): client = _make_client({get_config_manager: lambda: MagicMock()}) resp = client.post("/api/settings/connections", json={"name": "Bad"}) assert resp.status_code == 422 # ── update_connection ── class TestUpdateConnection: """PUT /settings/connections/{connection_id}""" def test_success(self): mock_conn_svc = MagicMock() mock_conn_svc.update_connection.return_value = {"id": "conn-1", "name": "Updated"} from src.dependencies import get_config_manager with patch("src.api.routes.settings._get_connection_service", return_value=mock_conn_svc): client = _make_client({get_config_manager: lambda: MagicMock()}) resp = client.put("/api/settings/connections/conn-1", json={"name": "Updated"}) assert resp.status_code == 200 def test_value_error(self): mock_conn_svc = MagicMock() mock_conn_svc.update_connection.side_effect = ValueError("Invalid") from src.dependencies import get_config_manager with patch("src.api.routes.settings._get_connection_service", return_value=mock_conn_svc): client = _make_client({get_config_manager: lambda: MagicMock()}) resp = client.put("/api/settings/connections/conn-1", json={"name": ""}) assert resp.status_code == 422 # ── delete_connection ── class TestDeleteConnection: """DELETE /settings/connections/{connection_id}""" def test_success(self): mock_conn_svc = MagicMock() mock_conn_svc.delete_connection.return_value = {"success": True} from src.dependencies import get_config_manager with patch("src.api.routes.settings._get_connection_service", return_value=mock_conn_svc): client = _make_client({get_config_manager: lambda: MagicMock()}) resp = client.delete("/api/settings/connections/conn-1") assert resp.status_code == 200 def test_in_use(self): mock_conn_svc = MagicMock() mock_conn_svc.delete_connection.return_value = { "success": False, "blocking_jobs": ["job-1"], } from src.dependencies import get_config_manager with patch("src.api.routes.settings._get_connection_service", return_value=mock_conn_svc): client = _make_client({get_config_manager: lambda: MagicMock()}) resp = client.delete("/api/settings/connections/conn-1") assert resp.status_code == 409 # ── test_connection_endpoint ── class TestTestConnectionEndpoint: """POST /settings/connections/{connection_id}/test""" def test_success(self): mock_conn_svc = MagicMock() mock_conn_svc.test_connection = AsyncMock(return_value={"success": True}) from src.dependencies import get_config_manager with patch("src.api.routes.settings._get_connection_service", return_value=mock_conn_svc): client = _make_client({get_config_manager: lambda: MagicMock()}) resp = client.post("/api/settings/connections/conn-1/test") assert resp.status_code == 200 # #endregion Test.Api.Settings