Files
ss-tools/backend/tests/api/test_settings.py

752 lines
28 KiB
Python

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