CRITICAL (CVE-class): - C-4: Remove DEV_MODE fallback with hardcoded postgres:postgres credentials - C-3: WebSocket endpoints now require JWT or API key token (?token=) auth - C-1: Superset environment passwords encrypted via Fernet at rest in DB - H-4: SESSION_SECRET_KEY separated from JWT AUTH_SECRET_KEY - H-1: Log injection via %s-formatting instead of f-strings - H-5: X-Trace-ID header validated as UUID4 to prevent trace poisoning INFRASTRUCTURE: - New src/core/encryption.py — EncryptionManager extracted from llm_provider - Test DB: per-module sqlite:///:memory: with PRAGMA foreign_keys=ON - testcontainers PostgreSQL via TEST_DB=postgres env var - conftest temp-file global engine (no 10GB shared-cache leak) TEST FIXES (44 pre-existing → 0): - test_smoke_plugins: module-level sys.modules mock isolated to per-test fixture - test_migration_engine: EXT:Python:uuid → uuid syntax fix - test_dashboards_api: mock env attributes + correct patch target - test_constants_audit_fixes: sync expected constant names with actual - test_defensive_guards: patch.object instead of module-level Repo mock - test_clean_release_cli: removed empty config.json directory - FK violations: TaskRecord parents in log_persistence, Environment in mapping_service, ReleaseCandidate in candidate_manifest_services ORTHOGONAL TESTS (18 new): - test_security_orthogonal.py: bcrypt 72-byte limit, unicode, API key format invariants, Fernet robustness, encryption key lifecycle, log injection protection, JWT edge cases MODEL FIXES: - MetricSnapshot.job_id: nullable with SET NULL (was broken FK for aggregate prune snapshots, hidden by SQLite) CLEANUP: - Removed stale :memory:test_main/test_auth/test_tasks file databases - Removed duplicate #endregion in encryption_key.py
151 lines
5.5 KiB
Python
151 lines
5.5 KiB
Python
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from git.exc import InvalidGitRepositoryError
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
|
|
from src.core.config_models import Environment
|
|
from src.core.superset_client import SupersetClient
|
|
from src.services.git_service import GitService
|
|
|
|
|
|
# #region test_git_service_get_repo_path_guard [C:2] [TYPE Function]
|
|
def test_git_service_get_repo_path_guard():
|
|
"""Verify that _get_repo_path raises ValueError if dashboard_id is None."""
|
|
service = GitService(base_path="test_repos")
|
|
with pytest.raises(ValueError, match="dashboard_id cannot be None"):
|
|
service._get_repo_path(None)
|
|
|
|
|
|
# #endregion test_git_service_get_repo_path_guard
|
|
|
|
# #region test_git_service_get_repo_path_recreates_base_dir [C:2] [TYPE Function]
|
|
def test_git_service_get_repo_path_recreates_base_dir():
|
|
"""Verify _get_repo_path recreates missing base directory before returning repo path."""
|
|
service = GitService(base_path="test_repos_runtime_recreate")
|
|
shutil.rmtree(service.base_path, ignore_errors=True)
|
|
|
|
repo_path = service._get_repo_path(42)
|
|
|
|
assert Path(service.base_path).is_dir()
|
|
assert repo_path == str(Path(service.base_path) / "42")
|
|
|
|
# #endregion test_git_service_get_repo_path_recreates_base_dir
|
|
|
|
# #region test_superset_client_import_dashboard_guard [C:2] [TYPE Function]
|
|
def test_superset_client_import_dashboard_guard():
|
|
"""Verify that import_dashboard raises ValueError if file_name is None."""
|
|
mock_env = Environment(
|
|
id="test",
|
|
name="test",
|
|
url="http://localhost:8088",
|
|
username="admin",
|
|
password="admin"
|
|
)
|
|
client = SupersetClient(mock_env)
|
|
with pytest.raises(ValueError, match="file_name cannot be None"):
|
|
client.import_dashboard(None)
|
|
|
|
|
|
# #endregion test_superset_client_import_dashboard_guard
|
|
|
|
# #region test_git_service_init_repo_reclones_when_path_is_not_a_git_repo [C:2] [TYPE Function]
|
|
def test_git_service_init_repo_reclones_when_path_is_not_a_git_repo():
|
|
"""Verify init_repo reclones when target path exists but is not a valid Git repository."""
|
|
service = GitService(base_path="test_repos_invalid_repo")
|
|
target_dir = Path(service.base_path)
|
|
if target_dir.exists():
|
|
shutil.rmtree(target_dir, ignore_errors=True)
|
|
target_path = target_dir / "covid"
|
|
target_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
clone_result = MagicMock()
|
|
with patch.object(service, "_clone_with_auth", return_value=clone_result) as mock_clone:
|
|
result = service.init_repo(10, "https://example.com/org/repo.git", "token", repo_key="covid")
|
|
|
|
assert result is clone_result
|
|
mock_clone.assert_called_once()
|
|
assert not target_path.exists()
|
|
|
|
|
|
# #endregion test_git_service_init_repo_reclones_when_path_is_not_a_git_repo
|
|
|
|
# #region test_git_service_ensure_gitflow_branches_creates_and_pushes_missing_defaults [C:2] [TYPE Function]
|
|
def test_git_service_ensure_gitflow_branches_creates_and_pushes_missing_defaults():
|
|
"""Verify _ensure_gitflow_branches creates dev/preprod locally and pushes them to origin."""
|
|
service = GitService(base_path="test_repos_gitflow_defaults")
|
|
|
|
class FakeRemoteRef:
|
|
def __init__(self, remote_head):
|
|
self.remote_head = remote_head
|
|
|
|
class FakeHead:
|
|
def __init__(self, name, commit):
|
|
self.name = name
|
|
self.commit = commit
|
|
|
|
class FakeOrigin:
|
|
def __init__(self):
|
|
self.refs = [FakeRemoteRef("main")]
|
|
self.pushed = []
|
|
|
|
def fetch(self):
|
|
return []
|
|
|
|
def push(self, refspec=None):
|
|
self.pushed.append(refspec)
|
|
return []
|
|
|
|
class FakeHeadPointer:
|
|
def __init__(self, commit):
|
|
self.commit = commit
|
|
|
|
class FakeRepo:
|
|
def __init__(self):
|
|
self.head = FakeHeadPointer("main-commit")
|
|
self.heads = [FakeHead("main", "main-commit")]
|
|
self.origin = FakeOrigin()
|
|
|
|
def create_head(self, name, commit):
|
|
head = FakeHead(name, commit)
|
|
self.heads.append(head)
|
|
return head
|
|
|
|
def remote(self, name="origin"):
|
|
if name != "origin":
|
|
raise ValueError("unknown remote")
|
|
return self.origin
|
|
|
|
repo = FakeRepo()
|
|
service._ensure_gitflow_branches(repo, dashboard_id=10)
|
|
|
|
local_branch_names = {head.name for head in repo.heads}
|
|
assert {"main", "dev", "preprod"}.issubset(local_branch_names)
|
|
assert "dev:dev" in repo.origin.pushed
|
|
assert "preprod:preprod" in repo.origin.pushed
|
|
|
|
|
|
# #endregion test_git_service_ensure_gitflow_branches_creates_and_pushes_missing_defaults
|
|
|
|
# #region test_git_service_configure_identity_updates_repo_local_config [C:2] [TYPE Function]
|
|
def test_git_service_configure_identity_updates_repo_local_config():
|
|
"""Verify configure_identity writes repository-local user.name/user.email."""
|
|
service = GitService(base_path="test_repos_identity")
|
|
|
|
config_writer_context = MagicMock()
|
|
config_writer = config_writer_context.__enter__.return_value
|
|
fake_repo = MagicMock()
|
|
fake_repo.config_writer.return_value = config_writer_context
|
|
|
|
with patch.object(service, "get_repo", return_value=fake_repo):
|
|
service.configure_identity(42, "user_1", "user1@mail.ru")
|
|
|
|
fake_repo.config_writer.assert_called_once_with(config_level="repository")
|
|
config_writer.set_value.assert_any_call("user", "name", "user_1")
|
|
config_writer.set_value.assert_any_call("user", "email", "user1@mail.ru")
|
|
# #endregion test_git_service_configure_identity_updates_repo_local_config
|