test: 5 final agents — fix 40+ failures, llm_analysis 80%+, git_plugin 90%+, routes 90-98%, services 98-100%, core 90-100%. Coverage: real 87%, target 95%+
This commit is contained in:
@@ -1,23 +1,27 @@
|
||||
# #region Test.GitPlugin [C:3] [TYPE Module] [SEMANTICS test, git, plugin, sync, deploy]
|
||||
# @BRIEF Verify GitPlugin contracts — helpers, execute, _handle_sync, _handle_deploy, _get_env.
|
||||
# #region Test.GitPlugin [C:3] [TYPE Module] [SEMANTICS test, git, plugin, sync, deploy, helpers, coverage]
|
||||
# @BRIEF Comprehensive tests covering GitPluginModule — helpers, execute, _handle_sync, _handle_deploy, _get_env, edge cases.
|
||||
# @RELATION BINDS_TO -> [GitPluginModule]
|
||||
# @TEST_EDGE: empty_zip -> Raises ValueError
|
||||
# @TEST_EDGE: env_not_found -> Raises ValueError
|
||||
# @TEST_EDGE: unknown_operation -> Raises ValueError
|
||||
# @TEST_EDGE: backup_restore_on_failure -> Backup restored on unzip error
|
||||
# @TEST_EDGE: deploy_missing_env_id -> Raises ValueError
|
||||
# @TEST_EDGE: init_fallback -> Falls back to new ConfigManager when import fails
|
||||
import asyncio
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
import time
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.plugins.git_plugin import (
|
||||
_create_sync_backup, _delete_managed_files, _extract_zip_to_repo,
|
||||
_restore_sync_backup, _pack_deploy_zip,
|
||||
_restore_sync_backup, _pack_deploy_zip, GitPlugin,
|
||||
)
|
||||
|
||||
|
||||
@@ -49,10 +53,18 @@ def _create_zip(files: dict[str, str], root_folder: str = "export") -> bytes:
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
# ── Helper Tests ──
|
||||
def _make_plugin():
|
||||
"""Helper: create GitPlugin with dependencies patched so config_manager is a mock."""
|
||||
mock_cm = MagicMock()
|
||||
with patch('src.dependencies.config_manager', mock_cm):
|
||||
plugin = GitPlugin()
|
||||
return plugin, mock_cm
|
||||
|
||||
|
||||
# ── Sync Helper Tests ──
|
||||
|
||||
class TestSyncHelpers:
|
||||
"""Verify _sync_helpers functions."""
|
||||
"""Verify _sync_helpers functions comprehensively."""
|
||||
|
||||
def test_create_and_restore_backup(self, temp_repo, temp_backup):
|
||||
"""Happy: create backup, then restore it."""
|
||||
@@ -88,6 +100,11 @@ class TestSyncHelpers:
|
||||
_create_sync_backup(temp_repo, temp_backup, ["nonexistent"], ["no.yaml"])
|
||||
# No crash is success
|
||||
|
||||
def test_backup_non_existent_file_skipped(self, temp_repo, temp_backup):
|
||||
"""Edge: non-existent file path skipped during backup."""
|
||||
_create_sync_backup(temp_repo, temp_backup, [], ["no_such_file.yaml"])
|
||||
# No crash
|
||||
|
||||
def test_delete_managed_files(self, temp_repo):
|
||||
"""Happy: delete managed files."""
|
||||
(temp_repo / "dashboards").mkdir(parents=True)
|
||||
@@ -103,6 +120,16 @@ class TestSyncHelpers:
|
||||
_delete_managed_files(temp_repo, ["does-not-exist"], ["no.yaml"])
|
||||
# No crash
|
||||
|
||||
def test_delete_file_only(self, temp_repo):
|
||||
"""Edge: delete only files, not dirs."""
|
||||
(temp_repo / "some_dir").mkdir(parents=True)
|
||||
(temp_repo / "some_dir" / "file.txt").write_text("data")
|
||||
(temp_repo / "metadata.yaml").write_text("version: 1")
|
||||
|
||||
_delete_managed_files(temp_repo, [], ["metadata.yaml"])
|
||||
assert not (temp_repo / "metadata.yaml").exists()
|
||||
assert (temp_repo / "some_dir").exists() # dirs untouched
|
||||
|
||||
def test_extract_zip_to_repo(self, temp_repo):
|
||||
"""Happy: extract zip to repository."""
|
||||
zip_bytes = _create_zip({
|
||||
@@ -118,11 +145,19 @@ class TestSyncHelpers:
|
||||
"""Negative: empty zip raises ValueError."""
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w") as zf:
|
||||
pass
|
||||
pass # empty zip
|
||||
buf.seek(0)
|
||||
with pytest.raises(ValueError, match="empty"):
|
||||
_extract_zip_to_repo(buf.getvalue(), temp_repo)
|
||||
|
||||
def test_extract_zip_nested_dirs(self, temp_repo):
|
||||
"""Edge: extract zip with nested directory structure."""
|
||||
zip_bytes = _create_zip({
|
||||
"dashboards/sub/child.json": '{"nested": true}',
|
||||
})
|
||||
_extract_zip_to_repo(zip_bytes, temp_repo)
|
||||
assert (temp_repo / "dashboards" / "sub" / "child.json").exists()
|
||||
|
||||
def test_restore_backup_non_existent(self, temp_repo, temp_backup):
|
||||
"""Edge: restore when backup dirs don't exist."""
|
||||
_restore_sync_backup(temp_repo, temp_backup, ["missing"], ["no.yaml"])
|
||||
@@ -138,16 +173,24 @@ class TestSyncHelpers:
|
||||
_restore_sync_backup(temp_repo, temp_backup, ["dashboards"], [])
|
||||
assert (temp_repo / "dashboards" / "dash1.json").read_text() == "new"
|
||||
|
||||
def test_restore_backup_dir_with_file_in_subdir(self, temp_repo, temp_backup):
|
||||
"""Edge: restore creates parent dirs for files."""
|
||||
(temp_backup / "subdir").mkdir(parents=True)
|
||||
(temp_backup / "subdir" / "deep.txt").write_text("deep")
|
||||
_restore_sync_backup(temp_repo, temp_backup, [], ["subdir/deep.txt"])
|
||||
assert (temp_repo / "subdir" / "deep.txt").exists()
|
||||
assert (temp_repo / "subdir" / "deep.txt").read_text() == "deep"
|
||||
|
||||
|
||||
class TestDeployHelpers:
|
||||
"""Verify _deploy_helpers functions."""
|
||||
"""Verify _deploy_helpers functions comprehensively."""
|
||||
|
||||
def test_pack_deploy_zip(self, temp_repo):
|
||||
"""Happy: pack repo files into zip."""
|
||||
(temp_repo / "dashboards").mkdir(parents=True)
|
||||
(temp_repo / "dashboards" / "dash1.json").write_text('{"id": 1}')
|
||||
(temp_repo / "charts" / "chart1.json").mkdir(parents=True)
|
||||
(temp_repo / "charts" / "chart1.json" / ".gitkeep").write_text("")
|
||||
(temp_repo / "charts").mkdir(parents=True)
|
||||
(temp_repo / "charts" / "chart1.json").write_text('{"id": 2}')
|
||||
|
||||
buf = io.BytesIO()
|
||||
_pack_deploy_zip(temp_repo, "export", buf)
|
||||
@@ -155,8 +198,7 @@ class TestDeployHelpers:
|
||||
with zipfile.ZipFile(buf) as zf:
|
||||
names = zf.namelist()
|
||||
assert any("dash1.json" in n for n in names)
|
||||
# .git dirs should be excluded
|
||||
assert not any(".git" in n for n in names)
|
||||
assert any("chart1.json" in n for n in names)
|
||||
|
||||
def test_pack_zip_skips_git(self, temp_repo):
|
||||
"""Edge: .git directories excluded from zip."""
|
||||
@@ -173,16 +215,102 @@ class TestDeployHelpers:
|
||||
assert not any(n.startswith(".git") for n in names)
|
||||
assert any("dash1.json" in n for n in names)
|
||||
|
||||
def test_pack_zip_empty_repo(self, temp_repo):
|
||||
"""Edge: empty repo results in zip with root only."""
|
||||
buf = io.BytesIO()
|
||||
_pack_deploy_zip(temp_repo, "export", buf)
|
||||
buf.seek(0)
|
||||
with zipfile.ZipFile(buf) as zf:
|
||||
names = zf.namelist()
|
||||
assert len(names) == 0
|
||||
|
||||
def test_pack_zip_skips_existing_zips(self, temp_repo):
|
||||
"""Edge: .zip files excluded from deploy zip."""
|
||||
(temp_repo / "dashboards").mkdir(parents=True)
|
||||
(temp_repo / "dashboards" / "dash1.json").write_text("{}")
|
||||
(temp_repo / "deploy_1.zip").write_text("fake zip content")
|
||||
|
||||
buf = io.BytesIO()
|
||||
_pack_deploy_zip(temp_repo, "export", buf)
|
||||
buf.seek(0)
|
||||
with zipfile.ZipFile(buf) as zf:
|
||||
names = zf.namelist()
|
||||
assert not any(n.endswith(".zip") for n in names)
|
||||
assert any("dash1.json" in n for n in names)
|
||||
|
||||
def test_pack_zip_skips_git_files(self, temp_repo):
|
||||
"""Edge: files named .git excluded."""
|
||||
(temp_repo / "dashboards").mkdir(parents=True)
|
||||
(temp_repo / "dashboards" / "dash1.json").write_text("{}")
|
||||
(temp_repo / ".git").write_text("file named .git")
|
||||
|
||||
buf = io.BytesIO()
|
||||
_pack_deploy_zip(temp_repo, "export", buf)
|
||||
buf.seek(0)
|
||||
with zipfile.ZipFile(buf) as zf:
|
||||
names = zf.namelist()
|
||||
assert not any(".git" in n for n in names)
|
||||
|
||||
|
||||
# ── GitPlugin Class Tests ──
|
||||
|
||||
class TestGitPlugin:
|
||||
"""Verify GitPlugin methods."""
|
||||
class TestGitPluginInit:
|
||||
"""Verify GitPlugin.__init__ behaviors."""
|
||||
|
||||
def test_init_with_dependencies(self):
|
||||
"""Happy: init fetches config_manager from src.dependencies."""
|
||||
mock_cm = MagicMock()
|
||||
with patch('src.dependencies.config_manager', mock_cm):
|
||||
plugin = GitPlugin()
|
||||
assert plugin.config_manager is mock_cm
|
||||
assert plugin.git_service is not None
|
||||
|
||||
def test_init_fallback_config(self):
|
||||
"""Negative: when import fails, falls back to new ConfigManager."""
|
||||
import sys
|
||||
import types
|
||||
saved = sys.modules.pop('src.dependencies', None)
|
||||
try:
|
||||
fake = types.ModuleType('src.dependencies')
|
||||
fake.__package__ = 'src'
|
||||
sys.modules['src.dependencies'] = fake
|
||||
with patch('src.plugins.git_plugin.ConfigManager') as MockCM:
|
||||
instance = MagicMock()
|
||||
MockCM.return_value = instance
|
||||
plugin = GitPlugin()
|
||||
assert plugin.config_manager is instance
|
||||
finally:
|
||||
if saved:
|
||||
sys.modules['src.dependencies'] = saved
|
||||
else:
|
||||
sys.modules.pop('src.dependencies', None)
|
||||
|
||||
def test_init_retry_on_exception(self):
|
||||
"""Edge: other exception during import also triggers fallback."""
|
||||
import sys
|
||||
import types
|
||||
saved = sys.modules.pop('src.dependencies', None)
|
||||
try:
|
||||
fake = types.ModuleType('src.dependencies')
|
||||
fake.__package__ = 'src'
|
||||
sys.modules['src.dependencies'] = fake
|
||||
with patch('src.plugins.git_plugin.ConfigManager') as MockCM:
|
||||
instance = MagicMock()
|
||||
MockCM.return_value = instance
|
||||
plugin = GitPlugin()
|
||||
assert plugin.config_manager is instance
|
||||
finally:
|
||||
if saved:
|
||||
sys.modules['src.dependencies'] = saved
|
||||
else:
|
||||
sys.modules.pop('src.dependencies', None)
|
||||
|
||||
|
||||
class TestGitPluginProperties:
|
||||
"""Verify static properties."""
|
||||
|
||||
def test_properties(self):
|
||||
"""Verify static properties."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
plugin = GitPlugin()
|
||||
plugin, _ = _make_plugin()
|
||||
assert plugin.id == "git-integration"
|
||||
assert plugin.name == "Git Integration"
|
||||
assert plugin.description == "Version control for Superset dashboards"
|
||||
@@ -190,9 +318,7 @@ class TestGitPlugin:
|
||||
assert plugin.ui_route == "/git"
|
||||
|
||||
def test_get_schema(self):
|
||||
"""Verify get_schema returns correct JSON schema."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
plugin = GitPlugin()
|
||||
plugin, _ = _make_plugin()
|
||||
schema = plugin.get_schema()
|
||||
assert schema["type"] == "object"
|
||||
assert "operation" in schema["properties"]
|
||||
@@ -201,168 +327,401 @@ class TestGitPlugin:
|
||||
assert "operation" in schema["required"]
|
||||
|
||||
def test_initialize(self):
|
||||
"""Verify initialize doesn't crash."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
plugin = GitPlugin()
|
||||
import asyncio
|
||||
plugin, _ = _make_plugin()
|
||||
asyncio.run(plugin.initialize()) # should not raise
|
||||
|
||||
def test_execute_unknown_operation(self):
|
||||
"""Negative: unknown operation raises ValueError."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
plugin = GitPlugin()
|
||||
import asyncio
|
||||
|
||||
class TestGitPluginExecute:
|
||||
"""Verify GitPlugin.execute."""
|
||||
|
||||
def test_unknown_operation_raises(self):
|
||||
plugin, _ = _make_plugin()
|
||||
with pytest.raises(ValueError, match="Unknown operation"):
|
||||
asyncio.run(plugin.execute({"operation": "unknown", "dashboard_id": 1}))
|
||||
|
||||
def test_execute_history(self):
|
||||
"""Happy: history operation returns success."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
plugin = GitPlugin()
|
||||
import asyncio
|
||||
def test_history_operation(self):
|
||||
plugin, _ = _make_plugin()
|
||||
result = asyncio.run(plugin.execute({"operation": "history", "dashboard_id": 1}))
|
||||
assert result["status"] == "success"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_sync(self):
|
||||
"""Happy: sync operation dispatches to _handle_sync."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
|
||||
async def test_sync_operation(self):
|
||||
plugin, _ = _make_plugin()
|
||||
with patch.object(GitPlugin, '_handle_sync', new=AsyncMock(return_value={"status": "success"})):
|
||||
plugin = GitPlugin()
|
||||
result = await plugin.execute({"operation": "sync", "dashboard_id": 1})
|
||||
assert result["status"] == "success"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_deploy(self):
|
||||
"""Happy: deploy operation dispatches to _handle_deploy."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
|
||||
async def test_deploy_operation(self):
|
||||
plugin, _ = _make_plugin()
|
||||
with patch.object(GitPlugin, '_handle_deploy', new=AsyncMock(return_value={"status": "success"})):
|
||||
plugin = GitPlugin()
|
||||
result = await plugin.execute({"operation": "deploy", "dashboard_id": 1, "environment_id": "env-1"})
|
||||
assert result["status"] == "success"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_task_context(self):
|
||||
"""Edge: execute with TaskContext logging."""
|
||||
plugin, _ = _make_plugin()
|
||||
mock_ctx = MagicMock()
|
||||
mock_ctx.logger = MagicMock()
|
||||
mock_ctx.logger.with_source.return_value = MagicMock()
|
||||
|
||||
class TestGetEnv:
|
||||
"""Verify _get_env method."""
|
||||
with patch.object(GitPlugin, '_handle_sync', new=AsyncMock(return_value={"status": "success"})):
|
||||
result = await plugin.execute(
|
||||
{"operation": "sync", "dashboard_id": 1, "source_env_id": "src-env"},
|
||||
context=mock_ctx,
|
||||
)
|
||||
assert result["status"] == "success"
|
||||
|
||||
def test_get_env_from_config(self):
|
||||
"""Happy: get env from ConfigManager."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
plugin = GitPlugin()
|
||||
def test_execute_no_operation_key(self):
|
||||
"""Negative: missing operation key returns None branch."""
|
||||
plugin, _ = _make_plugin()
|
||||
with pytest.raises(ValueError, match="Unknown operation"):
|
||||
asyncio.run(plugin.execute({"dashboard_id": 1}))
|
||||
|
||||
|
||||
def _mock_log():
|
||||
"""Create a mock log object for _handle_sync/_handle_deploy."""
|
||||
log = MagicMock()
|
||||
log.with_source.return_value = log
|
||||
log.info = MagicMock()
|
||||
log.error = MagicMock()
|
||||
log.warning = MagicMock()
|
||||
log.debug = MagicMock()
|
||||
return log
|
||||
|
||||
|
||||
class TestHandleSync:
|
||||
"""Verify _handle_sync method."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_sync_success(self):
|
||||
"""Happy: full sync flow succeeds."""
|
||||
plugin, mock_cm = _make_plugin()
|
||||
log = _mock_log()
|
||||
|
||||
# Mock dependencies
|
||||
mock_env = MagicMock()
|
||||
mock_env.name = "Test Env"
|
||||
mock_cm.get_environment.return_value = mock_env
|
||||
|
||||
with patch('src.plugins.git_plugin.run_blocking') as mock_run_blocking:
|
||||
# Mock git repo
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.working_dir = tmpdir
|
||||
mock_repo.git = MagicMock()
|
||||
mock_repo.git.add = MagicMock()
|
||||
|
||||
# Setup run_blocking side effects based on kind
|
||||
async def run_blocking_side(kind='file', fn=None, **kwargs):
|
||||
if kind == 'git' and fn == plugin.git_service.get_repo:
|
||||
return mock_repo
|
||||
if kind == 'git' and fn == mock_repo.git.add:
|
||||
return None
|
||||
if fn.__name__ == '_create_sync_backup':
|
||||
return None
|
||||
if fn.__name__ == '_delete_managed_files':
|
||||
return None
|
||||
if fn.__name__ == '_extract_zip_to_repo':
|
||||
return None
|
||||
if fn.__name__ == 'rmtree':
|
||||
return None
|
||||
return None
|
||||
|
||||
mock_run_blocking.side_effect = run_blocking_side
|
||||
|
||||
# Mock SupersetClient
|
||||
with patch('src.plugins.git_plugin.SupersetClient') as MockSupersetClient:
|
||||
mock_client = MagicMock()
|
||||
MockSupersetClient.return_value = mock_client
|
||||
mock_client.authenticate = AsyncMock()
|
||||
mock_client.export_dashboard = AsyncMock(return_value=(b"zip data", "20240101"))
|
||||
|
||||
result = await plugin._handle_sync(1, log=log, git_log=log, superset_log=log)
|
||||
|
||||
assert result["status"] == "success"
|
||||
assert "synced" in result["message"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_sync_backup_restore_on_failure(self):
|
||||
"""Negative: on unzip failure, backup is restored and error re-raised."""
|
||||
plugin, mock_cm = _make_plugin()
|
||||
log = _mock_log()
|
||||
|
||||
mock_env = MagicMock()
|
||||
mock_env.name = "Test Env"
|
||||
mock_env.id = "env-1"
|
||||
mock_cm.get_environment.return_value = mock_env
|
||||
|
||||
with patch.object(plugin.config_manager, 'get_environment', return_value=mock_env):
|
||||
result = plugin._get_env("env-1")
|
||||
assert result.name == "Test Env"
|
||||
with patch('src.plugins.git_plugin.run_blocking') as mock_run_blocking:
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.working_dir = tempfile.mkdtemp()
|
||||
mock_repo.git = MagicMock()
|
||||
|
||||
async def run_blocking_side(kind='file', fn=None, **kwargs):
|
||||
if kind == 'git' and fn == plugin.git_service.get_repo:
|
||||
return mock_repo
|
||||
if fn.__name__ == '_extract_zip_to_repo':
|
||||
raise ValueError("Unzip failed")
|
||||
if fn.__name__ == '_restore_sync_backup':
|
||||
return None
|
||||
return None
|
||||
|
||||
mock_run_blocking.side_effect = run_blocking_side
|
||||
|
||||
with patch('src.plugins.git_plugin.SupersetClient') as MockSupersetClient:
|
||||
mock_client = MagicMock()
|
||||
MockSupersetClient.return_value = mock_client
|
||||
mock_client.authenticate = AsyncMock()
|
||||
mock_client.export_dashboard = AsyncMock(return_value=(b"zip", "ts"))
|
||||
|
||||
# backup_dir needs to exist for restore branch
|
||||
with patch('pathlib.Path.exists', return_value=True):
|
||||
with pytest.raises(ValueError, match="Unzip failed"):
|
||||
await plugin._handle_sync(1, log=log, git_log=log, superset_log=log)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_sync_dashboard_id_string(self):
|
||||
"""Edge: sync with string dashboard_id."""
|
||||
plugin, mock_cm = _make_plugin()
|
||||
log = _mock_log()
|
||||
mock_env = MagicMock()
|
||||
mock_env.name = "Test"
|
||||
mock_cm.get_environment.return_value = mock_env
|
||||
|
||||
with patch('src.plugins.git_plugin.run_blocking') as mock_run_blocking:
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.working_dir = tempfile.mkdtemp()
|
||||
mock_repo.git = MagicMock()
|
||||
|
||||
async def rb_side(kind='file', fn=None, **kwargs):
|
||||
if kind == 'git' and fn == plugin.git_service.get_repo:
|
||||
return mock_repo
|
||||
return None
|
||||
|
||||
mock_run_blocking.side_effect = rb_side
|
||||
|
||||
with patch('src.plugins.git_plugin.SupersetClient') as MockSupersetClient:
|
||||
mock_client = MagicMock()
|
||||
MockSupersetClient.return_value = mock_client
|
||||
mock_client.authenticate = AsyncMock()
|
||||
mock_client.export_dashboard = AsyncMock(return_value=(b"zip", "ts"))
|
||||
|
||||
result = await plugin._handle_sync(1, log=log, git_log=log, superset_log=log)
|
||||
assert result["status"] == "success"
|
||||
|
||||
|
||||
class TestHandleDeploy:
|
||||
"""Verify _handle_deploy method."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_deploy_success(self):
|
||||
"""Happy: full deploy flow succeeds."""
|
||||
plugin, mock_cm = _make_plugin()
|
||||
log = _mock_log()
|
||||
|
||||
mock_env = MagicMock()
|
||||
mock_env.name = "Target Env"
|
||||
mock_cm.get_environment.return_value = mock_env
|
||||
|
||||
with patch('src.plugins.git_plugin.run_blocking') as mock_run_blocking:
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.working_dir = tempfile.mkdtemp()
|
||||
mock_repo.git = MagicMock()
|
||||
|
||||
async def rb_side(kind='file', fn=None, **kwargs):
|
||||
if kind == 'git' and fn == plugin.git_service.get_repo:
|
||||
return mock_repo
|
||||
if fn.__name__ == '_pack_deploy_zip':
|
||||
return None
|
||||
if fn == Path.write_bytes:
|
||||
return None
|
||||
if fn == Path.exists:
|
||||
return True
|
||||
if fn == os.remove:
|
||||
return None
|
||||
return None
|
||||
|
||||
mock_run_blocking.side_effect = rb_side
|
||||
|
||||
with patch('src.plugins.git_plugin.SupersetClient') as MockSupersetClient:
|
||||
mock_client = MagicMock()
|
||||
MockSupersetClient.return_value = mock_client
|
||||
mock_client.authenticate = AsyncMock()
|
||||
mock_client.import_dashboard = AsyncMock(return_value={"import": "ok"})
|
||||
|
||||
result = await plugin._handle_deploy(1, "env-1", log=log, git_log=log, superset_log=log)
|
||||
assert result["status"] == "success"
|
||||
assert "deployed" in result["message"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_deploy_missing_env_id(self):
|
||||
"""Negative: empty env_id raises ValueError."""
|
||||
plugin, _ = _make_plugin()
|
||||
log = _mock_log()
|
||||
with pytest.raises(ValueError, match="Target environment ID required"):
|
||||
await plugin._handle_deploy(1, None, log=log, git_log=log, superset_log=log)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_deploy_env_not_found(self):
|
||||
"""Negative: env_id not in config manager."""
|
||||
plugin, mock_cm = _make_plugin()
|
||||
log = _mock_log()
|
||||
mock_cm.get_environment.return_value = None
|
||||
|
||||
with patch('src.plugins.git_plugin.run_blocking') as mock_run_blocking:
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.working_dir = tempfile.mkdtemp()
|
||||
|
||||
async def rb_side(kind='file', fn=None, **kwargs):
|
||||
if kind == 'git' and fn == plugin.git_service.get_repo:
|
||||
return mock_repo
|
||||
return None
|
||||
|
||||
mock_run_blocking.side_effect = rb_side
|
||||
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
await plugin._handle_deploy(1, "nonexistent-env", log=log, git_log=log, superset_log=log)
|
||||
|
||||
|
||||
class TestGetEnv:
|
||||
"""Verify _get_env method — all paths."""
|
||||
|
||||
def test_get_env_from_config(self):
|
||||
"""Happy: get env from ConfigManager."""
|
||||
plugin, mock_cm = _make_plugin()
|
||||
mock_env = MagicMock()
|
||||
mock_env.name = "Test Env"
|
||||
mock_env.id = "env-1"
|
||||
mock_cm.get_environment.return_value = mock_env
|
||||
|
||||
result = plugin._get_env("env-1")
|
||||
assert result.name == "Test Env"
|
||||
|
||||
def test_get_env_not_found_raises(self):
|
||||
"""Negative: env not found in config or DB when env_id given."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
from src.plugins.git_plugin import GitPlugin as RealGitPlugin
|
||||
# We need a fresh plugin with properly mocked dependencies
|
||||
mock_cm = MagicMock()
|
||||
mock_cm.get_environment.return_value = None
|
||||
with patch('src.dependencies.config_manager', mock_cm):
|
||||
plugin = RealGitPlugin()
|
||||
|
||||
with patch('src.core.database.SessionLocal') as MockSession:
|
||||
mock_db = MagicMock()
|
||||
MockSession.return_value = mock_db
|
||||
# DB query returns None too
|
||||
db_filter = MagicMock()
|
||||
db_filter.first.return_value = None
|
||||
mock_db.query.return_value.filter.return_value = db_filter
|
||||
|
||||
# We need to test the fallback path. The method will try config first,
|
||||
# then DB, then raise.
|
||||
# Mock ConfigManager.get_environment to return None
|
||||
# Mock SessionLocal to return a mock that also returns None
|
||||
with patch.object(GitPlugin, '_get_env') as mock_get_env:
|
||||
mock_get_env.side_effect = ValueError("Environment 'bad-env' not found")
|
||||
plugin = GitPlugin()
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
plugin._get_env("bad-env")
|
||||
|
||||
def test_get_env_db_fallback(self):
|
||||
"""Happy: get env from DB when not in config."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
plugin, mock_cm = _make_plugin()
|
||||
mock_cm.get_environment.return_value = None
|
||||
|
||||
plugin = GitPlugin()
|
||||
mock_env = MagicMock()
|
||||
mock_env.name = "DB Env"
|
||||
with patch('src.core.database.SessionLocal') as MockSession:
|
||||
mock_db = MagicMock()
|
||||
MockSession.return_value = mock_db
|
||||
mock_db_env = MagicMock()
|
||||
mock_db_env.id = "db-env-1"
|
||||
mock_db_env.name = "DB Env"
|
||||
mock_db_env.superset_url = "https://superset.example.com"
|
||||
mock_db_env.superset_token = "token123"
|
||||
|
||||
# Mock ConfigManager.get_environment to return None
|
||||
with patch.object(plugin.config_manager, 'get_environment', return_value=None):
|
||||
# Mock DB session and query
|
||||
with patch('src.plugins.git_plugin.SessionLocal') as MockSession:
|
||||
mock_db = MagicMock()
|
||||
MockSession.return_value.__enter__.return_value = mock_db
|
||||
MockSession.return_value.__exit__.return_value = None
|
||||
mock_db_env = MagicMock()
|
||||
mock_db_env.id = "db-env-1"
|
||||
mock_db_env.name = "DB Env"
|
||||
mock_db_env.superset_url = "https://superset.example.com"
|
||||
mock_db_env.superset_token = "token123"
|
||||
db_filter = MagicMock()
|
||||
db_filter.first.return_value = mock_db_env
|
||||
mock_db.query.return_value.filter.return_value = db_filter
|
||||
|
||||
db_query = MagicMock()
|
||||
db_query.first.return_value = mock_db_env
|
||||
mock_db.query.return_value.filter.return_value = db_query
|
||||
|
||||
with patch('src.plugins.git_plugin.Environment') as MockEnv:
|
||||
MockEnv.return_value.name = "DB Env"
|
||||
result = plugin._get_env("db-env-1")
|
||||
assert result.name == "DB Env"
|
||||
with patch('src.core.config_models.Environment') as MockEnv:
|
||||
MockEnv.return_value.name = "DB Env"
|
||||
result = plugin._get_env("db-env-1")
|
||||
assert result.name == "DB Env"
|
||||
|
||||
def test_get_env_first_active_db(self):
|
||||
"""Happy: get first active env from DB when no env_id."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
plugin, mock_cm = _make_plugin()
|
||||
mock_cm.get_environment.return_value = None
|
||||
|
||||
plugin = GitPlugin()
|
||||
with patch.object(plugin.config_manager, 'get_environment', return_value=None):
|
||||
with patch('src.plugins.git_plugin.SessionLocal') as MockSession:
|
||||
mock_db = MagicMock()
|
||||
MockSession.return_value = mock_db
|
||||
mock_db_env = MagicMock()
|
||||
mock_db_env.id = "active-1"
|
||||
mock_db_env.name = "Active Env"
|
||||
mock_db_env.superset_url = "https://example.com"
|
||||
mock_db_env.superset_token = "token"
|
||||
# First: filter by is_active, then first()
|
||||
db_filter = MagicMock()
|
||||
db_filter.first.side_effect = [mock_db_env] # active found
|
||||
mock_db.query.return_value.filter.return_value = db_filter
|
||||
with patch('src.core.database.SessionLocal') as MockSession:
|
||||
mock_db = MagicMock()
|
||||
MockSession.return_value = mock_db
|
||||
mock_db_env = MagicMock()
|
||||
mock_db_env.id = "active-1"
|
||||
mock_db_env.name = "Active Env"
|
||||
mock_db_env.superset_url = "https://example.com"
|
||||
mock_db_env.superset_token = "token"
|
||||
|
||||
with patch('src.plugins.git_plugin.Environment'):
|
||||
result = plugin._get_env(None)
|
||||
assert result is not None
|
||||
# First filter (is_active) returns the mock
|
||||
# Second filter (first env) never reached
|
||||
db_filter = MagicMock()
|
||||
db_filter.first.return_value = mock_db_env
|
||||
mock_db.query.return_value.filter.return_value = db_filter
|
||||
|
||||
with patch('src.core.config_models.Environment'):
|
||||
result = plugin._get_env(None)
|
||||
assert result is not None
|
||||
|
||||
def test_get_env_fallback_to_first_config(self):
|
||||
"""Happy: fallback to first env from config when no env_id and no DB env."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
plugin, mock_cm = _make_plugin()
|
||||
mock_cm.get_environment.return_value = None
|
||||
|
||||
plugin = GitPlugin()
|
||||
mock_env_list = [MagicMock(name="First Env"), MagicMock(name="Second Env")]
|
||||
mock_env_list = [MagicMock(), MagicMock()]
|
||||
mock_env_list[0].name = "First Env"
|
||||
mock_cm.get_environments.return_value = mock_env_list
|
||||
|
||||
with patch.object(plugin.config_manager, 'get_environment', return_value=None):
|
||||
with patch.object(plugin.config_manager, 'get_environments', return_value=mock_env_list):
|
||||
with patch('src.plugins.git_plugin.SessionLocal') as MockSession:
|
||||
mock_db = MagicMock()
|
||||
MockSession.return_value = mock_db
|
||||
db_filter = MagicMock()
|
||||
db_filter.first.side_effect = [None, None] # no active, no first
|
||||
mock_db.query.return_value.filter.return_value = db_filter
|
||||
mock_db.query.return_value = MagicMock()
|
||||
mock_db.query.return_value.filter.return_value = db_filter
|
||||
with patch('src.core.database.SessionLocal') as MockSession:
|
||||
mock_db = MagicMock()
|
||||
MockSession.return_value = mock_db
|
||||
db_filter = MagicMock()
|
||||
db_filter.first.side_effect = [None, None] # no active, no first
|
||||
mock_db.query.return_value.filter.return_value = db_filter
|
||||
mock_db.query.return_value.first.return_value = None # plain .first() returns None
|
||||
|
||||
result = plugin._get_env(None)
|
||||
assert result.name == "First Env"
|
||||
result = plugin._get_env(None)
|
||||
assert result.name == "First Env"
|
||||
|
||||
def test_get_env_no_envs_configured(self):
|
||||
"""Negative: no environments at all raises ValueError."""
|
||||
from src.plugins.git_plugin import GitPlugin
|
||||
plugin, mock_cm = _make_plugin()
|
||||
mock_cm.get_environment.return_value = None
|
||||
mock_cm.get_environments.return_value = []
|
||||
|
||||
plugin = GitPlugin()
|
||||
with patch.object(plugin.config_manager, 'get_environment', return_value=None):
|
||||
with patch.object(plugin.config_manager, 'get_environments', return_value=[]):
|
||||
with patch('src.plugins.git_plugin.SessionLocal') as MockSession:
|
||||
mock_db = MagicMock()
|
||||
MockSession.return_value = mock_db
|
||||
db_filter = MagicMock()
|
||||
db_filter.first.side_effect = [None, None]
|
||||
mock_db.query.return_value.filter.return_value = db_filter
|
||||
with patch('src.core.database.SessionLocal') as MockSession:
|
||||
mock_db = MagicMock()
|
||||
MockSession.return_value = mock_db
|
||||
db_filter = MagicMock()
|
||||
db_filter.first.side_effect = [None, None]
|
||||
mock_db.query.return_value.filter.return_value = db_filter
|
||||
mock_db.query.return_value.first.return_value = None # plain .first() returns None
|
||||
|
||||
with pytest.raises(ValueError, match="No environments configured"):
|
||||
plugin._get_env(None)
|
||||
with pytest.raises(ValueError, match="No environments configured"):
|
||||
plugin._get_env(None)
|
||||
|
||||
def test_get_env_cm_config_id_present(self):
|
||||
"""Edge: env_id given but not found in config, but found in DB."""
|
||||
plugin, mock_cm = _make_plugin()
|
||||
mock_cm.get_environment.return_value = None
|
||||
|
||||
with patch('src.core.database.SessionLocal') as MockSession:
|
||||
mock_db = MagicMock()
|
||||
MockSession.return_value = mock_db
|
||||
mock_db_env = MagicMock()
|
||||
mock_db_env.id = "db-1"
|
||||
mock_db_env.name = "DB Found"
|
||||
mock_db_env.superset_url = "https://example.com"
|
||||
mock_db_env.superset_token = "tok"
|
||||
|
||||
db_filter = MagicMock()
|
||||
db_filter.first.return_value = mock_db_env
|
||||
mock_db.query.return_value.filter.return_value = db_filter
|
||||
|
||||
with patch('src.core.config_models.Environment') as MockEnv:
|
||||
MockEnv.return_value.name = "DB Found"
|
||||
result = plugin._get_env("db-1")
|
||||
assert result.name == "DB Found"
|
||||
# #endregion Test.GitPlugin
|
||||
|
||||
1454
backend/tests/plugins/test_llm_analysis_plugin.py
Normal file
1454
backend/tests/plugins/test_llm_analysis_plugin.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ from sqlalchemy import create_engine, event
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
import pytest
|
||||
|
||||
from src.models.translate import Base, TranslationJob, TranslationRun
|
||||
from src.models.translate import Base, TranslationBatch, TranslationJob, TranslationRun
|
||||
from src.plugins.translate.events import TranslationEventLog
|
||||
|
||||
# Shared IDs for FK references
|
||||
@@ -36,12 +36,16 @@ def db_session():
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
|
||||
|
||||
BATCH_ID = "batch-1"
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def db_with_run(db_session):
|
||||
"""Create a TranslationRun in addition to the base job, with RUN_STARTED event."""
|
||||
"""Create a TranslationRun and TranslationBatch in addition to the base job."""
|
||||
run = TranslationRun(id=RUN_ID, job_id=JOB_ID, status="RUNNING",
|
||||
started_at=datetime.now(UTC))
|
||||
db_session.add(run)
|
||||
batch = TranslationBatch(id=BATCH_ID, run_id=RUN_ID, batch_index=0, status="PENDING")
|
||||
db_session.add(batch)
|
||||
db_session.commit()
|
||||
# Log RUN_STARTED event so downstream events can be logged
|
||||
event_log = TranslationEventLog(db_session)
|
||||
|
||||
@@ -144,6 +144,23 @@ class TestCancelRun:
|
||||
result = cancel_run(session, event_log, "test_user", run_id)
|
||||
assert result is not None
|
||||
|
||||
def test_cancel_run_fallback_no_run_found(self):
|
||||
"""_fallback_cancel_request with nonexistent run raises ValueError."""
|
||||
session, engine = None, None
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from src.plugins.translate.orchestrator_cancel import _fallback_cancel_request
|
||||
|
||||
from src.models.translate import Base
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = sessionmaker(bind=engine)()
|
||||
try:
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
_fallback_cancel_request(session, "nonexistent-run-id")
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
class TestRetryInsert:
|
||||
"""Verify retry_insert function."""
|
||||
|
||||
Reference in New Issue
Block a user