test: 8 parallel agents — fix 150+ failures, add 30+ test files. schemas/models 99-100%, core/client_registry ~98%, maintenance 95-98%, git edges 97%, translate 98-100%, dashboard routes 95-96%, dataset_review 100%
This commit is contained in:
@@ -32,10 +32,11 @@ def _make_client(overrides: dict | None = None) -> TestClient:
|
||||
app.include_router(router)
|
||||
app.include_router(agent_router)
|
||||
|
||||
_NOW = __import__("datetime").datetime.now()
|
||||
mock_user = User(
|
||||
id="user-1", username="testuser", email="test@x.com",
|
||||
auth_source="LOCAL",
|
||||
created_at=__import__("datetime").datetime.now(),
|
||||
created_at=_NOW,
|
||||
roles=[RoleSchema(id="r1", name="User", description="", permissions=[])],
|
||||
)
|
||||
|
||||
@@ -59,10 +60,11 @@ class TestListConversations:
|
||||
mock_db.query.return_value = mock_query
|
||||
mock_query.filter.return_value = mock_query
|
||||
mock_query.count.return_value = 2
|
||||
_NOW = __import__("datetime").datetime.now()
|
||||
mock_conv = MagicMock()
|
||||
mock_conv.id = "conv-1"
|
||||
mock_conv.title = "Test Conversation"
|
||||
mock_conv.updated_at = None
|
||||
mock_conv.updated_at = _NOW
|
||||
mock_conv.messages = [MagicMock()]
|
||||
mock_query.order_by.return_value.offset.return_value.limit.return_value.all.return_value = [mock_conv, mock_conv]
|
||||
|
||||
@@ -130,8 +132,9 @@ class TestSaveConversation:
|
||||
|
||||
def test_update_existing(self):
|
||||
existing_conv = MagicMock()
|
||||
existing_conv.updated_at = __import__("datetime").datetime.now()
|
||||
mock_db = MagicMock()
|
||||
mock_db.query.return_value.filter.return_value.first.side_effect = [existing_conv, None]
|
||||
mock_db.query.return_value.filter.return_value.first.side_effect = [existing_conv, None, None]
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
@@ -152,8 +155,10 @@ class TestSaveConversation:
|
||||
|
||||
def test_skip_duplicate_message(self):
|
||||
existing_msg = MagicMock()
|
||||
existing_msg.id = "msg-2"
|
||||
mock_db = MagicMock()
|
||||
mock_db.query.return_value.filter.return_value.first.side_effect = [None, existing_msg]
|
||||
# 3 .first() calls: 1 for conv lookup → None, msg-1 lookup → None, msg-2 lookup → existing_msg
|
||||
mock_db.query.return_value.filter.return_value.first.side_effect = [None, None, existing_msg]
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
@@ -168,6 +173,7 @@ class TestGetHistory:
|
||||
|
||||
def test_success(self):
|
||||
mock_db = MagicMock()
|
||||
_NOW = __import__("datetime").datetime.now()
|
||||
mock_conv = MagicMock()
|
||||
mock_conv.id = "conv-1"
|
||||
mock_msg = MagicMock()
|
||||
@@ -177,7 +183,7 @@ class TestGetHistory:
|
||||
mock_msg.text = "Hello"
|
||||
mock_msg.tool_calls = None
|
||||
mock_msg.attachments = None
|
||||
mock_msg.created_at = None
|
||||
mock_msg.created_at = _NOW
|
||||
mock_conv.messages = [mock_msg]
|
||||
mock_db.query.return_value.filter.return_value.first.return_value = mock_conv
|
||||
|
||||
|
||||
@@ -305,4 +305,163 @@ class TestGetDashboards:
|
||||
client = _make_client(base_mocks)
|
||||
resp = client.get("/api/dashboards?env_id=env-1&override_show_all=true")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_get_dashboards_git_filter_no_match(self, base_mocks):
|
||||
"""Git filter excludes non-matching dashboards."""
|
||||
mock_rs = base_mocks["resource_service"]
|
||||
mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support")
|
||||
mock_rs.get_dashboards_with_status.return_value = [
|
||||
{"id": 1, "title": "Main", "slug": "main", "owners": [],
|
||||
"git_status": {"sync_status": "DIFF", "has_repo": True}},
|
||||
{"id": 2, "title": "Other", "slug": "other", "owners": [],
|
||||
"git_status": {"sync_status": "OK", "has_repo": True}},
|
||||
]
|
||||
|
||||
client = _make_client(base_mocks)
|
||||
resp = client.get("/api/dashboards?env_id=env-1&filter_git_status=ok")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 1
|
||||
assert data["dashboards"][0]["id"] == 2
|
||||
|
||||
def test_get_dashboards_actor_filter_non_list_owner(self, base_mocks):
|
||||
"""Actor filter with non-list owner value (string) doesn't crash."""
|
||||
mock_rs = base_mocks["resource_service"]
|
||||
mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support")
|
||||
mock_rs.get_dashboards_with_status.return_value = [
|
||||
{"id": 1, "title": "Main", "slug": "main", "owners": "admin"},
|
||||
]
|
||||
|
||||
client = _make_client(base_mocks)
|
||||
resp = client.get("/api/dashboards?env_id=env-1&filter_actor=admin")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_get_dashboards_full_scan_with_search(self, base_mocks):
|
||||
"""Search in full scan path (triggered by column filter)."""
|
||||
mock_rs = base_mocks["resource_service"]
|
||||
mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support")
|
||||
mock_rs.get_dashboards_with_status.return_value = [
|
||||
{"id": 1, "title": "Revenue Dashboard", "slug": "revenue", "owners": [], "git_status": {"sync_status": "OK", "has_repo": True}},
|
||||
{"id": 2, "title": "Sales Report", "slug": "sales", "owners": [], "git_status": {"sync_status": "DIFF", "has_repo": True}},
|
||||
]
|
||||
|
||||
# Use filter_git_status to force full_scan path + search narrows results
|
||||
client = _make_client(base_mocks)
|
||||
resp = client.get("/api/dashboards?env_id=env-1&search=revenue&filter_git_status=ok")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 1
|
||||
|
||||
def test_get_dashboards_slug_filter_full_scan(self, base_mocks):
|
||||
"""Slug-only filter applied in full scan path."""
|
||||
mock_rs = base_mocks["resource_service"]
|
||||
mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support")
|
||||
mock_rs.get_dashboards_with_status.return_value = [
|
||||
{"id": 1, "title": "No Slug", "slug": "", "owners": []},
|
||||
{"id": 2, "title": "Has Slug", "slug": "has-slug", "owners": []},
|
||||
]
|
||||
|
||||
mocks = dict(base_mocks)
|
||||
profile_svc = MagicMock()
|
||||
profile_svc.get_dashboard_filter_binding.return_value = {
|
||||
"superset_username": "",
|
||||
"superset_username_normalized": "",
|
||||
"show_only_my_dashboards": False,
|
||||
"show_only_slug_dashboards": True,
|
||||
}
|
||||
profile_svc.matches_dashboard_actor.return_value = False
|
||||
with patch("src.api.routes.dashboards._listing_routes.ProfileService", return_value=profile_svc) as MockPS:
|
||||
MockPS.__module__ = "unittest.mock"
|
||||
client = _make_client(mocks)
|
||||
resp = client.get("/api/dashboards?env_id=env-1&page_context=dashboards_main&apply_profile_default=true")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 1
|
||||
assert data["dashboards"][0]["slug"] == "has-slug"
|
||||
|
||||
def test_get_dashboards_profile_filter_with_actor(self, base_mocks):
|
||||
"""Profile filter with actor alias matching."""
|
||||
mock_rs = base_mocks["resource_service"]
|
||||
mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support")
|
||||
mock_rs.get_dashboards_with_status.return_value = [
|
||||
{"id": 1, "title": "My Dash", "slug": "my-dash", "owners": [{"username": "testuser"}], "modified_by": None},
|
||||
{"id": 2, "title": "Other Dash", "slug": "other", "owners": [{"username": "other"}], "modified_by": None},
|
||||
]
|
||||
|
||||
mocks = dict(base_mocks)
|
||||
profile_svc = MagicMock()
|
||||
profile_svc.get_dashboard_filter_binding.return_value = {
|
||||
"superset_username": "testuser",
|
||||
"superset_username_normalized": "testuser",
|
||||
"show_only_my_dashboards": True,
|
||||
"show_only_slug_dashboards": False,
|
||||
}
|
||||
profile_svc.matches_dashboard_actor.side_effect = lambda bound_username, owners, modified_by: (
|
||||
any("testuser" in str(o) for o in (owners or []))
|
||||
)
|
||||
with patch("src.api.routes.dashboards._listing_routes.ProfileService", return_value=profile_svc) as MockPS:
|
||||
MockPS.__module__ = "unittest.mock"
|
||||
client = _make_client(mocks)
|
||||
resp = client.get("/api/dashboards?env_id=env-1&page_context=dashboards_main&apply_profile_default=true")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 1
|
||||
|
||||
def test_get_dashboards_profile_error_fallback(self, base_mocks):
|
||||
"""Profile service error falls back gracefully."""
|
||||
mock_rs = base_mocks["resource_service"]
|
||||
mock_rs.get_dashboards_page_with_status.return_value = {
|
||||
"dashboards": [{"id": 1, "title": "Main", "slug": "main", "owners": []}],
|
||||
"total": 1,
|
||||
"total_pages": 1,
|
||||
}
|
||||
mocks = dict(base_mocks)
|
||||
with patch("src.api.routes.dashboards._listing_routes.ProfileService") as MockPS:
|
||||
MockPS.side_effect = Exception("Profile error")
|
||||
client = _make_client(mocks)
|
||||
resp = client.get("/api/dashboards?env_id=env-1")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["total"] == 1
|
||||
|
||||
def test_get_dashboards_actor_filter_no_match(self, base_mocks):
|
||||
"""Actor filter with no matching owners."""
|
||||
mock_rs = base_mocks["resource_service"]
|
||||
mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support")
|
||||
mock_rs.get_dashboards_with_status.return_value = [
|
||||
{"id": 1, "title": "Main", "slug": "main", "owners": ["alice", "bob"]},
|
||||
]
|
||||
|
||||
client = _make_client(base_mocks)
|
||||
resp = client.get("/api/dashboards?env_id=env-1&filter_actor=nonexistent")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 0
|
||||
|
||||
def test_get_dashboards_full_scan_profile_and_slug(self, base_mocks):
|
||||
"""Both profile filter and slug filter active."""
|
||||
mock_rs = base_mocks["resource_service"]
|
||||
mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support")
|
||||
mock_rs.get_dashboards_with_status.return_value = [
|
||||
{"id": 1, "title": "My Dash", "slug": "my-dash", "owners": [{"username": "testuser"}], "modified_by": None},
|
||||
{"id": 2, "title": "No Slug", "slug": "", "owners": [{"username": "testuser"}], "modified_by": None},
|
||||
]
|
||||
|
||||
mocks = dict(base_mocks)
|
||||
profile_svc = MagicMock()
|
||||
profile_svc.get_dashboard_filter_binding.return_value = {
|
||||
"superset_username": "testuser",
|
||||
"superset_username_normalized": "testuser",
|
||||
"show_only_my_dashboards": True,
|
||||
"show_only_slug_dashboards": True,
|
||||
}
|
||||
profile_svc.matches_dashboard_actor.return_value = True
|
||||
with patch("src.api.routes.dashboards._listing_routes.ProfileService", return_value=profile_svc) as MockPS:
|
||||
MockPS.__module__ = "unittest.mock"
|
||||
client = _make_client(mocks)
|
||||
resp = client.get("/api/dashboards?env_id=env-1&page_context=dashboards_main&apply_profile_default=true")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
# Only dash-1 has a slug
|
||||
assert data["total"] == 1
|
||||
assert data["dashboards"][0]["slug"] == "my-dash"
|
||||
# #endregion Test.Api.DashboardListingRoutes
|
||||
|
||||
@@ -51,6 +51,9 @@ from src.api.routes.dataset_review_pkg._dependencies import (
|
||||
_commit_owned_session_mutation,
|
||||
_enforce_session_version,
|
||||
_get_latest_clarification_session_or_404,
|
||||
_get_clarification_engine,
|
||||
_get_orchestrator,
|
||||
_get_repository,
|
||||
_get_owned_field_or_404,
|
||||
_get_owned_mapping_or_404,
|
||||
_get_owned_session_or_404,
|
||||
@@ -1232,4 +1235,44 @@ def test_serialize_empty_clarification_state():
|
||||
#endregion
|
||||
|
||||
|
||||
# ── Dependency injection functions ──────────────────────────────────────
|
||||
|
||||
|
||||
#region test_get_repository [C:2]
|
||||
# @BRIEF _get_repository wraps a Session into DatasetReviewSessionRepository.
|
||||
def test_get_repository():
|
||||
mock_db = MagicMock()
|
||||
repo = _get_repository(db=mock_db)
|
||||
from src.services.dataset_review.repositories.session_repository import (
|
||||
DatasetReviewSessionRepository,
|
||||
)
|
||||
assert isinstance(repo, DatasetReviewSessionRepository)
|
||||
assert repo.db is mock_db
|
||||
#endregion
|
||||
|
||||
|
||||
#region test_get_orchestrator [C:2]
|
||||
# @BRIEF _get_orchestrator builds DatasetReviewOrchestrator with deps.
|
||||
def test_get_orchestrator():
|
||||
mock_repo = MagicMock()
|
||||
mock_cm = MagicMock()
|
||||
mock_tm = MagicMock()
|
||||
orch = _get_orchestrator(repository=mock_repo, config_manager=mock_cm, task_manager=mock_tm)
|
||||
from src.services.dataset_review.orchestrator import DatasetReviewOrchestrator
|
||||
assert isinstance(orch, DatasetReviewOrchestrator)
|
||||
assert orch.repository is mock_repo
|
||||
#endregion
|
||||
|
||||
|
||||
#region test_get_clarification_engine [C:2]
|
||||
# @BRIEF _get_clarification_engine builds ClarificationEngine with repo.
|
||||
def test_get_clarification_engine():
|
||||
mock_repo = MagicMock()
|
||||
engine = _get_clarification_engine(repository=mock_repo)
|
||||
from src.services.dataset_review.clarification_engine import ClarificationEngine
|
||||
assert isinstance(engine, ClarificationEngine)
|
||||
assert engine.repository is mock_repo
|
||||
#endregion
|
||||
|
||||
|
||||
#endregion Test.DatasetReview.DepsUnit
|
||||
|
||||
@@ -301,7 +301,10 @@ class TestDeleteProvider:
|
||||
|
||||
def test_blocked_by_active_tasks(self):
|
||||
mock_db = MagicMock()
|
||||
mock_db.query.return_value.filter.return_value.all.return_value = [MagicMock(id="t-1", name="Task")]
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = "t-1"
|
||||
mock_task.name = "Task"
|
||||
mock_db.query.return_value.filter.return_value.all.return_value = [mock_task]
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
@@ -412,7 +415,7 @@ class TestProbeMaxImages:
|
||||
from src.core.database import get_db
|
||||
with (
|
||||
patch("src.api.routes.llm.LLMProviderService", return_value=mock_svc),
|
||||
patch("src.api.routes.llm.AsyncOpenAI", return_value=mock_openai),
|
||||
patch("openai.AsyncOpenAI", return_value=mock_openai),
|
||||
):
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.post(f"{P}/providers/prov-1/probe-max-images")
|
||||
@@ -466,7 +469,7 @@ class TestProbeMaxImages:
|
||||
from src.core.database import get_db
|
||||
with (
|
||||
patch("src.api.routes.llm.LLMProviderService", return_value=mock_svc),
|
||||
patch("src.api.routes.llm.AsyncOpenAI", return_value=mock_openai),
|
||||
patch("openai.AsyncOpenAI", return_value=mock_openai),
|
||||
):
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.post(f"{P}/providers/prov-1/probe-max-images")
|
||||
@@ -486,7 +489,7 @@ class TestProbeMaxImages:
|
||||
from src.core.database import get_db
|
||||
with (
|
||||
patch("src.api.routes.llm.LLMProviderService", return_value=mock_svc),
|
||||
patch("src.api.routes.llm.AsyncOpenAI", return_value=mock_openai),
|
||||
patch("openai.AsyncOpenAI", return_value=mock_openai),
|
||||
):
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.post(f"{P}/providers/prov-1/probe-max-images")
|
||||
|
||||
@@ -42,7 +42,7 @@ def _make_client(overrides: dict | None = None) -> TestClient:
|
||||
from src.schemas.auth import User, RoleSchema
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
app.include_router(router, prefix="/api/mappings")
|
||||
|
||||
mock_user = User(
|
||||
id="admin-1", username="admin", email="admin@x.com",
|
||||
@@ -74,7 +74,7 @@ class TestGetMappings:
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.get("/mappings")
|
||||
resp = client.get("/api/mappings")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) == 2
|
||||
@@ -89,7 +89,7 @@ class TestGetMappings:
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.get("/mappings?source_env_id=env-1")
|
||||
resp = client.get("/api/mappings?source_env_id=env-1")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_filter_by_target(self):
|
||||
@@ -101,7 +101,7 @@ class TestGetMappings:
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.get("/mappings?target_env_id=env-2")
|
||||
resp = client.get("/api/mappings?target_env_id=env-2")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_empty(self):
|
||||
@@ -110,7 +110,7 @@ class TestGetMappings:
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.get("/mappings")
|
||||
resp = client.get("/api/mappings")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == []
|
||||
|
||||
@@ -136,7 +136,7 @@ class TestCreateMapping:
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.post("/mappings", json=self.CREATE_PAYLOAD)
|
||||
resp = client.post("/api/mappings", json=self.CREATE_PAYLOAD)
|
||||
assert resp.status_code == 200
|
||||
mock_db.add.assert_called_once()
|
||||
mock_db.commit.assert_called_once()
|
||||
@@ -148,7 +148,7 @@ class TestCreateMapping:
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.post("/mappings", json=self.CREATE_PAYLOAD)
|
||||
resp = client.post("/api/mappings", json=self.CREATE_PAYLOAD)
|
||||
assert resp.status_code == 200
|
||||
mock_db.add.assert_not_called()
|
||||
mock_db.commit.assert_called_once()
|
||||
@@ -168,10 +168,10 @@ class TestSuggestMappingsApi:
|
||||
from src.dependencies import get_config_manager
|
||||
with patch("src.api.routes.mappings.MappingService") as MockMappingSvc:
|
||||
instance = MagicMock()
|
||||
instance.get_suggestions = MagicMock(return_value=mock_suggestions)
|
||||
instance.get_suggestions = AsyncMock(return_value=mock_suggestions)
|
||||
MockMappingSvc.return_value = instance
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/mappings/suggest", json={
|
||||
resp = client.post("/api/mappings/suggest", json={
|
||||
"source_env_id": "env-1", "target_env_id": "env-2",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
@@ -182,10 +182,10 @@ class TestSuggestMappingsApi:
|
||||
from src.dependencies import get_config_manager
|
||||
with patch("src.api.routes.mappings.MappingService") as MockMappingSvc:
|
||||
instance = MagicMock()
|
||||
instance.get_suggestions = MagicMock(side_effect=RuntimeError("suggestion failed"))
|
||||
instance.get_suggestions = AsyncMock(side_effect=RuntimeError("suggestion failed"))
|
||||
MockMappingSvc.return_value = instance
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/mappings/suggest", json={
|
||||
resp = client.post("/api/mappings/suggest", json={
|
||||
"source_env_id": "env-1", "target_env_id": "env-2",
|
||||
})
|
||||
assert resp.status_code == 500
|
||||
|
||||
@@ -59,7 +59,7 @@ def _make_client(overrides: dict | None = None) -> TestClient:
|
||||
# ── get_dashboards ──
|
||||
|
||||
class TestGetDashboards:
|
||||
"""GET /environments/{env_id}/dashboards"""
|
||||
"""GET /api/environments/{env_id}/dashboards"""
|
||||
|
||||
def test_success(self):
|
||||
mock_config = MagicMock()
|
||||
@@ -72,7 +72,7 @@ class TestGetDashboards:
|
||||
from src.dependencies import get_config_manager
|
||||
with patch("src.api.routes.migration.AsyncSupersetClient", return_value=mock_client):
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.get("/environments/env-1/dashboards")
|
||||
resp = client.get("/api/environments/env-1/dashboards")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) == 1
|
||||
@@ -84,7 +84,7 @@ class TestGetDashboards:
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.get("/environments/bad-env/dashboards")
|
||||
resp = client.get("/api/environments/bad-env/dashboards")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class TestExecuteMigration:
|
||||
get_config_manager: lambda: mock_config,
|
||||
get_task_manager: lambda: mock_tm,
|
||||
})
|
||||
resp = client.post("/migration/execute", json=self.SELECTION_PAYLOAD)
|
||||
resp = client.post("/api/migration/execute", json=self.SELECTION_PAYLOAD)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["task_id"] == "task-123"
|
||||
@@ -126,7 +126,7 @@ class TestExecuteMigration:
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/migration/execute", json=self.SELECTION_PAYLOAD)
|
||||
resp = client.post("/api/migration/execute", json=self.SELECTION_PAYLOAD)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_invalid_target_env(self):
|
||||
@@ -135,7 +135,16 @@ class TestExecuteMigration:
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/migration/execute", json=self.SELECTION_PAYLOAD)
|
||||
resp = client.post("/api/migration/execute", json=self.SELECTION_PAYLOAD)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_invalid_target_env(self):
|
||||
mock_config = MagicMock()
|
||||
mock_config.get_environments.return_value = [_make_env("env-2")]
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/api/migration/execute", json=self.SELECTION_PAYLOAD)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_task_creation_fails(self):
|
||||
@@ -152,7 +161,7 @@ class TestExecuteMigration:
|
||||
get_config_manager: lambda: mock_config,
|
||||
get_task_manager: lambda: mock_tm,
|
||||
})
|
||||
resp = client.post("/migration/execute", json=self.SELECTION_PAYLOAD)
|
||||
resp = client.post("/api/migration/execute", json=self.SELECTION_PAYLOAD)
|
||||
assert resp.status_code == 500
|
||||
|
||||
|
||||
@@ -181,7 +190,7 @@ class TestDryRunMigration:
|
||||
from src.dependencies import get_config_manager
|
||||
with patch("src.api.routes.migration.MigrationDryRunService", return_value=mock_dry_run):
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/migration/dry-run", json=self.SELECTION_PAYLOAD)
|
||||
resp = client.post("/api/migration/dry-run", json=self.SELECTION_PAYLOAD)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["summary"] == "ok"
|
||||
|
||||
@@ -192,7 +201,7 @@ class TestDryRunMigration:
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/migration/dry-run", json={
|
||||
resp = client.post("/api/migration/dry-run", json={
|
||||
"source_env_id": "env-1", "target_env_id": "env-1", "selected_ids": [1],
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
@@ -203,7 +212,7 @@ class TestDryRunMigration:
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/migration/dry-run", json=self.SELECTION_PAYLOAD)
|
||||
resp = client.post("/api/migration/dry-run", json=self.SELECTION_PAYLOAD)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_no_dashboards_selected(self):
|
||||
@@ -214,7 +223,7 @@ class TestDryRunMigration:
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/migration/dry-run", json={
|
||||
resp = client.post("/api/migration/dry-run", json={
|
||||
"source_env_id": "env-1", "target_env_id": "env-2", "selected_ids": [],
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
@@ -231,7 +240,7 @@ class TestDryRunMigration:
|
||||
from src.dependencies import get_config_manager
|
||||
with patch("src.api.routes.migration.MigrationDryRunService", return_value=mock_dry_run):
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/migration/dry-run", json=self.SELECTION_PAYLOAD)
|
||||
resp = client.post("/api/migration/dry-run", json=self.SELECTION_PAYLOAD)
|
||||
assert resp.status_code == 500
|
||||
|
||||
|
||||
@@ -248,7 +257,7 @@ class TestGetMigrationSettings:
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.get("/migration/settings")
|
||||
resp = client.get("/api/migration/settings")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["cron"] == "0 2 * * *"
|
||||
|
||||
@@ -265,7 +274,7 @@ class TestUpdateMigrationSettings:
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.put("/migration/settings", json={"cron": "0 3 * * *"})
|
||||
resp = client.put("/api/migration/settings", json={"cron": "0 3 * * *"})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["cron"] == "0 3 * * *"
|
||||
@@ -276,7 +285,7 @@ class TestUpdateMigrationSettings:
|
||||
mock_config = MagicMock()
|
||||
from src.dependencies import get_config_manager
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.put("/migration/settings", json={})
|
||||
resp = client.put("/api/migration/settings", json={})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@@ -300,7 +309,7 @@ class TestGetResourceMappings:
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.get("/migration/mappings-data")
|
||||
resp = client.get("/api/migration/mappings-data")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 2
|
||||
@@ -316,7 +325,7 @@ class TestGetResourceMappings:
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.get("/migration/mappings-data?env_id=env-1&resource_type=database&search=test&skip=0&limit=10")
|
||||
resp = client.get("/api/migration/mappings-data?env_id=env-1&resource_type=database&search=test&skip=0&limit=10")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@@ -345,7 +354,7 @@ class TestTriggerSyncNow:
|
||||
get_config_manager: lambda: mock_config,
|
||||
get_db: lambda: mock_db,
|
||||
})
|
||||
resp = client.post("/migration/sync-now")
|
||||
resp = client.post("/api/migration/sync-now")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "completed"
|
||||
@@ -383,7 +392,7 @@ class TestTriggerSyncNow:
|
||||
get_config_manager: lambda: mock_config,
|
||||
get_db: lambda: mock_db,
|
||||
})
|
||||
resp = client.post("/migration/sync-now")
|
||||
resp = client.post("/api/migration/sync-now")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["synced_count"] == 1
|
||||
|
||||
@@ -58,16 +58,43 @@ def _make_client(overrides: dict | None = None) -> TestClient:
|
||||
|
||||
# ── 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_cfg = MagicMock()
|
||||
mock_cfg.settings.llm = {}
|
||||
mock_cfg.settings.features = MagicMock()
|
||||
mock_cfg.environments = []
|
||||
mock_config.get_config.return_value = mock_cfg
|
||||
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})
|
||||
@@ -75,19 +102,23 @@ class TestGetSettings:
|
||||
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_cfg = MagicMock()
|
||||
mock_cfg.settings.llm = {}
|
||||
mock_cfg.settings.features = MagicMock()
|
||||
env = _make_env()
|
||||
mock_cfg.environments = [env]
|
||||
mock_config.get_config.return_value = mock_cfg
|
||||
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
|
||||
assert mock_cfg.environments[0].password == "********"
|
||||
data = resp.json()
|
||||
masked_env = data["environments"][0]
|
||||
assert masked_env["password"] == "********"
|
||||
|
||||
|
||||
class TestGetFeatures:
|
||||
@@ -144,9 +175,17 @@ 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 = 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
|
||||
@@ -202,8 +241,13 @@ class TestGetEnvironments:
|
||||
"""GET /settings/environments"""
|
||||
|
||||
def test_success(self):
|
||||
from src.core.config_models import Environment
|
||||
mock_config = MagicMock()
|
||||
mock_config.get_environments.return_value = [_make_env()]
|
||||
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})
|
||||
@@ -211,7 +255,7 @@ class TestGetEnvironments:
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["id"] == "env-1"
|
||||
assert data[0]["id"] == "env-1""
|
||||
|
||||
|
||||
# ── add_environment ──
|
||||
|
||||
@@ -68,7 +68,7 @@ class TestListFiles:
|
||||
mock_loader.get_plugin.return_value = mock_plugin
|
||||
resp = client.get(f"{P}/files?category=BACKUPS&path=/test&recursive=true")
|
||||
assert resp.status_code == 200
|
||||
mock_plugin.list_files.assert_called_once_with("BACKUPS", "/test", True)
|
||||
mock_plugin.list_files.assert_called_once_with("backups", "/test", True)
|
||||
|
||||
def test_plugin_not_loaded(self):
|
||||
client, mock_loader = _make_client()
|
||||
@@ -85,7 +85,7 @@ class TestUploadFile:
|
||||
mock_plugin = MagicMock()
|
||||
mock_plugin.save_file = AsyncMock(return_value={"name": "test.txt"})
|
||||
mock_loader.get_plugin.return_value = mock_plugin
|
||||
resp = client.post(f"{P}/upload", data={"category": "BACKUPS"}, files={"file": ("test.txt", b"hello")})
|
||||
resp = client.post(f"{P}/upload", data={"category": "backups"}, files={"file": ("test.txt", b"hello")})
|
||||
assert resp.status_code == 201
|
||||
|
||||
def test_value_error(self):
|
||||
@@ -93,13 +93,13 @@ class TestUploadFile:
|
||||
mock_plugin = MagicMock()
|
||||
mock_plugin.save_file = AsyncMock(side_effect=ValueError("bad"))
|
||||
mock_loader.get_plugin.return_value = mock_plugin
|
||||
resp = client.post(f"{P}/upload", data={"category": "BACKUPS"}, files={"file": ("test.txt", b"data")})
|
||||
resp = client.post(f"{P}/upload", data={"category": "backups"}, files={"file": ("test.txt", b"data")})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_plugin_not_loaded(self):
|
||||
client, mock_loader = _make_client()
|
||||
mock_loader.get_plugin.return_value = None
|
||||
resp = client.post(f"{P}/upload", data={"category": "BACKUPS"}, files={"file": ("test.txt", b"data")})
|
||||
resp = client.post(f"{P}/upload", data={"category": "backups"}, files={"file": ("test.txt", b"data")})
|
||||
assert resp.status_code == 500
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,16 @@ class TestCreateTask:
|
||||
|
||||
def test_success(self):
|
||||
mock_tm = MagicMock()
|
||||
mock_tm.create_task = AsyncMock(return_value=MagicMock(id="task-1", plugin_id="backup", status="PENDING", params={}, created_at=None))
|
||||
from src.core.task_manager import Task
|
||||
mock_task = MagicMock(spec=Task)
|
||||
mock_task.id = "task-1"
|
||||
mock_task.plugin_id = "backup"
|
||||
mock_task.status = "PENDING"
|
||||
mock_task.params = {}
|
||||
mock_task.created_at = None
|
||||
mock_task.user_id = "admin"
|
||||
mock_task.input_request = None
|
||||
mock_tm.create_task = AsyncMock(return_value=mock_task)
|
||||
|
||||
from src.dependencies import get_task_manager
|
||||
client = _make_client({get_task_manager: lambda: mock_tm})
|
||||
@@ -132,7 +141,16 @@ class TestGetTask:
|
||||
|
||||
def test_success(self):
|
||||
mock_tm = MagicMock()
|
||||
mock_tm.get_task.return_value = MagicMock(id="task-123", plugin_id="backup", status="PENDING", params={}, created_at=None)
|
||||
from src.core.task_manager import Task
|
||||
mock_task = MagicMock(spec=Task)
|
||||
mock_task.id = "task-123"
|
||||
mock_task.plugin_id = "backup"
|
||||
mock_task.status = "PENDING"
|
||||
mock_task.params = {}
|
||||
mock_task.created_at = None
|
||||
mock_task.user_id = "admin"
|
||||
mock_task.input_request = None
|
||||
mock_tm.get_task.return_value = mock_task
|
||||
|
||||
from src.dependencies import get_task_manager
|
||||
client = _make_client({get_task_manager: lambda: mock_tm})
|
||||
@@ -158,7 +176,7 @@ class TestGetTaskLogs:
|
||||
def test_success(self):
|
||||
mock_tm = MagicMock()
|
||||
mock_tm.get_task.return_value = MagicMock(id="task-123")
|
||||
mock_tm.get_task_logs.return_value = [{"level": "INFO", "message": "test"}]
|
||||
mock_tm.get_task_logs.return_value = [MagicMock(level="INFO", message="test", timestamp=None, source="test")]
|
||||
|
||||
from src.dependencies import get_task_manager
|
||||
client = _make_client({get_task_manager: lambda: mock_tm})
|
||||
@@ -266,7 +284,10 @@ class TestResolveTask:
|
||||
def test_success(self):
|
||||
mock_tm = MagicMock()
|
||||
mock_tm.resolve_task = AsyncMock()
|
||||
mock_tm.get_task.return_value = MagicMock(id="task-123")
|
||||
from src.core.task_manager import Task
|
||||
mock_task = MagicMock(spec=Task)
|
||||
mock_task.id = "task-123"
|
||||
mock_tm.get_task.return_value = mock_task
|
||||
|
||||
from src.dependencies import get_task_manager
|
||||
client = _make_client({get_task_manager: lambda: mock_tm})
|
||||
@@ -291,7 +312,10 @@ class TestResumeTask:
|
||||
def test_success(self):
|
||||
mock_tm = MagicMock()
|
||||
mock_tm.resume_task_with_password = MagicMock()
|
||||
mock_tm.get_task.return_value = MagicMock(id="task-123")
|
||||
from src.core.task_manager import Task
|
||||
mock_task = MagicMock(spec=Task)
|
||||
mock_task.id = "task-123"
|
||||
mock_tm.get_task.return_value = mock_task
|
||||
|
||||
from src.dependencies import get_task_manager
|
||||
client = _make_client({get_task_manager: lambda: mock_tm})
|
||||
|
||||
@@ -277,21 +277,40 @@ class TestCreateJobErrors:
|
||||
# =============================================================================
|
||||
|
||||
def _mock_job(id="job-1", name="Test Job", **kw):
|
||||
"""Create a mock TranslationJob ORM object."""
|
||||
"""Create a mock TranslationJob ORM object with all fields job_to_response needs."""
|
||||
dt = __import__("datetime").datetime(2026, 1, 1)
|
||||
j = MagicMock()
|
||||
j.id = id
|
||||
j.name = name
|
||||
j.description = kw.get("description")
|
||||
j.source_dialect = kw.get("source_dialect", "postgresql")
|
||||
j.target_dialect = kw.get("target_dialect", "clickhouse")
|
||||
j.database_dialect = kw.get("database_dialect")
|
||||
j.source_datasource_id = kw.get("source_datasource_id")
|
||||
j.source_table = kw.get("source_table")
|
||||
j.target_schema = kw.get("target_schema")
|
||||
j.target_table = kw.get("target_table")
|
||||
j.source_key_cols = kw.get("source_key_cols", ["id"])
|
||||
j.target_key_cols = kw.get("target_key_cols", ["id"])
|
||||
j.translation_column = kw.get("translation_column", "name")
|
||||
j.target_column = kw.get("target_column")
|
||||
j.target_language_column = kw.get("target_language_column")
|
||||
j.target_source_column = kw.get("target_source_column")
|
||||
j.target_source_language_column = kw.get("target_source_language_column")
|
||||
j.context_columns = kw.get("context_columns")
|
||||
j.target_languages = kw.get("target_languages")
|
||||
j.provider_id = kw.get("provider_id")
|
||||
j.batch_size = kw.get("batch_size", 50)
|
||||
j.disable_reasoning = kw.get("disable_reasoning", False)
|
||||
j.upsert_strategy = kw.get("upsert_strategy", "MERGE")
|
||||
j.status = kw.get("status", "DRAFT")
|
||||
j.created_by = kw.get("created_by", "admin")
|
||||
j.created_at = kw.get("created_at", __import__("datetime").datetime(2026, 1, 1))
|
||||
j.created_at = kw.get("created_at", dt)
|
||||
j.updated_at = kw.get("updated_at")
|
||||
j.environment_id = kw.get("environment_id")
|
||||
j.target_database_id = kw.get("target_database_id")
|
||||
j.insert_method = kw.get("insert_method", "sqllab")
|
||||
j.connection_id = kw.get("connection_id")
|
||||
return j
|
||||
|
||||
|
||||
@@ -338,7 +357,7 @@ class TestUpdateJob:
|
||||
"""Covers success path lines 141-142."""
|
||||
mock_job = _mock_job(name="Updated Job")
|
||||
mock_service = MagicMock()
|
||||
mock_service.update_job.return_value = mock_job
|
||||
mock_service.update_job = AsyncMock(return_value=mock_job)
|
||||
mock_service.get_job_dictionary_ids.return_value = ["dict-1"]
|
||||
|
||||
with patch(
|
||||
@@ -367,6 +386,88 @@ class TestUpdateJob:
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
class TestListJobsSuccess:
|
||||
"""GET /api/translate/jobs — success path (lines 52-66)."""
|
||||
|
||||
def test_list_jobs_success(self):
|
||||
"""Covers list_jobs success path including dict_ids loop."""
|
||||
mock_job_1 = _mock_job(id="job-1", name="Job One")
|
||||
mock_job_2 = _mock_job(id="job-2", name="Job Two")
|
||||
mock_service = MagicMock()
|
||||
mock_service.list_jobs.return_value = (2, [mock_job_1, mock_job_2])
|
||||
mock_service.get_job_dictionary_ids.side_effect = [["dict-a"], ["dict-b"]]
|
||||
|
||||
from src.plugins.translate.service_utils import job_to_response
|
||||
with patch(
|
||||
"src.api.routes.translate._job_routes.TranslateJobService",
|
||||
return_value=mock_service,
|
||||
):
|
||||
client = _make_client()
|
||||
resp = client.get("/api/translate/jobs")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) == 2
|
||||
assert data[0]["id"] == "job-1"
|
||||
assert data[1]["id"] == "job-2"
|
||||
assert mock_service.get_job_dictionary_ids.call_count == 2
|
||||
|
||||
|
||||
class TestCreateJobSuccess:
|
||||
"""POST /api/translate/jobs — success path (lines 114-115)."""
|
||||
|
||||
def test_create_job_success(self):
|
||||
"""Covers create_job success path with dict_ids return."""
|
||||
mock_job = _mock_job(id="job-new", name="New Job")
|
||||
mock_service = MagicMock()
|
||||
mock_service.create_job = AsyncMock(return_value=mock_job)
|
||||
mock_service.get_job_dictionary_ids.return_value = ["dict-1"]
|
||||
|
||||
with patch(
|
||||
"src.api.routes.translate._job_routes.TranslateJobService",
|
||||
return_value=mock_service,
|
||||
):
|
||||
client = _make_client()
|
||||
resp = client.post("/api/translate/jobs", json={
|
||||
"name": "New Job",
|
||||
"source_dialect": "postgresql",
|
||||
"target_dialect": "clickhouse",
|
||||
})
|
||||
assert resp.status_code == 201
|
||||
data = resp.json()
|
||||
assert data["id"] == "job-new"
|
||||
assert data["name"] == "New Job"
|
||||
|
||||
|
||||
class TestDeleteJob:
|
||||
"""DELETE /api/translate/jobs/{job_id} — lines 162-167."""
|
||||
|
||||
def test_delete_job_success(self):
|
||||
"""Covers delete_job success (204)."""
|
||||
mock_service = MagicMock()
|
||||
|
||||
with patch(
|
||||
"src.api.routes.translate._job_routes.TranslateJobService",
|
||||
return_value=mock_service,
|
||||
):
|
||||
client = _make_client()
|
||||
resp = client.delete("/api/translate/jobs/job-1")
|
||||
assert resp.status_code == 204
|
||||
|
||||
def test_delete_job_404(self):
|
||||
"""Covers delete_job ValueError -> 404."""
|
||||
mock_service = MagicMock()
|
||||
mock_service.delete_job.side_effect = ValueError("Job not found")
|
||||
|
||||
with patch(
|
||||
"src.api.routes.translate._job_routes.TranslateJobService",
|
||||
return_value=mock_service,
|
||||
):
|
||||
client = _make_client()
|
||||
resp = client.delete("/api/translate/jobs/job-missing")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
class TestDuplicateJob:
|
||||
"""POST /api/translate/jobs/{job_id}/duplicate"""
|
||||
|
||||
|
||||
@@ -231,6 +231,141 @@ class TestRetryInsert:
|
||||
assert "Retry insert failed" in resp.text
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Background Execute — covers _background_execute inner function (lines 57-120)
|
||||
# =============================================================================
|
||||
|
||||
class TestBackgroundExecute:
|
||||
"""Execute the _background_execute coroutine via real asyncio.create_task."""
|
||||
|
||||
def test_bg_run_not_found(self):
|
||||
"""Run not found in background: early return (lines 71-77)."""
|
||||
mock_orch = MagicMock()
|
||||
mock_orch.start_run.return_value = _mock_run()
|
||||
|
||||
mock_bg_db = MagicMock()
|
||||
mock_bg_db.query.return_value.filter.return_value.first.return_value = None
|
||||
|
||||
with (
|
||||
patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
|
||||
return_value=mock_orch),
|
||||
patch("src.api.routes.translate._run_routes.SessionLocal",
|
||||
return_value=mock_bg_db),
|
||||
patch("src.api.routes.translate._run_routes.seed_trace_id"),
|
||||
):
|
||||
client = _make_client()
|
||||
resp = client.post("/api/translate/jobs/job-1/run")
|
||||
assert resp.status_code == 201
|
||||
mock_bg_db.close.assert_called_once()
|
||||
|
||||
def test_bg_execute_success(self):
|
||||
"""Background execute succeeds (lines 65-85)."""
|
||||
mock_run = _mock_run(status="COMPLETED")
|
||||
mock_run.completed_at = __import__("datetime").datetime(2026, 1, 1)
|
||||
mock_orch = MagicMock()
|
||||
mock_orch.start_run.return_value = _mock_run()
|
||||
mock_orch.execute_run = AsyncMock()
|
||||
|
||||
mock_bg_db = MagicMock()
|
||||
# Two queries: first to find run, second to verify
|
||||
mock_bg_db.query.return_value.filter.return_value.first.side_effect = [mock_run, mock_run]
|
||||
|
||||
with (
|
||||
patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
|
||||
return_value=mock_orch),
|
||||
patch("src.api.routes.translate._run_routes.SessionLocal",
|
||||
return_value=mock_bg_db),
|
||||
patch("src.api.routes.translate._run_routes.seed_trace_id"),
|
||||
patch("src.api.routes.translate._run_routes.get_trace_id",
|
||||
return_value="trace-abc-123"),
|
||||
):
|
||||
client = _make_client()
|
||||
resp = client.post("/api/translate/jobs/job-1/run")
|
||||
assert resp.status_code == 201
|
||||
mock_orch.execute_run.assert_awaited_once()
|
||||
mock_bg_db.close.assert_called_once()
|
||||
|
||||
def test_bg_execute_not_terminal_marks_failed(self):
|
||||
"""Run not in terminal state after commit: marked FAILED (lines 86-95)."""
|
||||
mock_run_pending = _mock_run(status="RUNNING")
|
||||
mock_orch = MagicMock()
|
||||
mock_orch.start_run.return_value = _mock_run()
|
||||
mock_orch.execute_run = AsyncMock()
|
||||
|
||||
mock_bg_db = MagicMock()
|
||||
mock_bg_db.query.return_value.filter.return_value.first.side_effect = [mock_run_pending, mock_run_pending]
|
||||
|
||||
with (
|
||||
patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
|
||||
return_value=mock_orch),
|
||||
patch("src.api.routes.translate._run_routes.SessionLocal",
|
||||
return_value=mock_bg_db),
|
||||
patch("src.api.routes.translate._run_routes.seed_trace_id"),
|
||||
):
|
||||
client = _make_client()
|
||||
resp = client.post("/api/translate/jobs/job-1/run")
|
||||
assert resp.status_code == 201
|
||||
# Run should have been marked FAILED
|
||||
assert mock_run_pending.status == "FAILED"
|
||||
mock_bg_db.commit.assert_called()
|
||||
mock_bg_db.close.assert_called_once()
|
||||
|
||||
def test_bg_execute_exception_marks_failed(self):
|
||||
"""Exception in background marks run FAILED via fallback (lines 97-120)."""
|
||||
mock_run = _mock_run()
|
||||
mock_orch = MagicMock()
|
||||
mock_orch.start_run.return_value = mock_run
|
||||
mock_orch.execute_run = AsyncMock(side_effect=RuntimeError("LLM timeout"))
|
||||
|
||||
mock_bg_db = MagicMock()
|
||||
mock_bg_db.query.return_value.filter.return_value.first.side_effect = [mock_run]
|
||||
|
||||
# Second session for fallback
|
||||
mock_fb_db = MagicMock()
|
||||
mock_fb_db.query.return_value.filter.return_value.first.return_value = mock_run
|
||||
|
||||
with (
|
||||
patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
|
||||
return_value=mock_orch),
|
||||
patch("src.api.routes.translate._run_routes.SessionLocal",
|
||||
side_effect=[mock_bg_db, mock_fb_db]),
|
||||
patch("src.api.routes.translate._run_routes.seed_trace_id"),
|
||||
):
|
||||
client = _make_client()
|
||||
resp = client.post("/api/translate/jobs/job-1/run")
|
||||
assert resp.status_code == 201
|
||||
assert mock_run.status == "FAILED"
|
||||
assert mock_run.error_message is not None
|
||||
mock_fb_db.close.assert_called_once()
|
||||
mock_bg_db.close.assert_called_once()
|
||||
|
||||
def test_bg_execute_exception_fallback_fails(self):
|
||||
"""Exception in background AND fallback: handled gracefully (lines 115-117)."""
|
||||
mock_run = _mock_run()
|
||||
mock_orch = MagicMock()
|
||||
mock_orch.start_run.return_value = mock_run
|
||||
mock_orch.execute_run = AsyncMock(side_effect=RuntimeError("LLM timeout"))
|
||||
|
||||
mock_bg_db = MagicMock()
|
||||
mock_bg_db.query.return_value.filter.return_value.first.return_value = mock_run
|
||||
|
||||
# Fallback db also raises
|
||||
bad_fb_db = MagicMock()
|
||||
bad_fb_db.query.side_effect = RuntimeError("DB closed")
|
||||
|
||||
with (
|
||||
patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
|
||||
return_value=mock_orch),
|
||||
patch("src.api.routes.translate._run_routes.SessionLocal",
|
||||
side_effect=[mock_bg_db, bad_fb_db]),
|
||||
patch("src.api.routes.translate._run_routes.seed_trace_id"),
|
||||
):
|
||||
client = _make_client()
|
||||
resp = client.post("/api/translate/jobs/job-1/run")
|
||||
assert resp.status_code == 201
|
||||
mock_bg_db.close.assert_called_once()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Cancel Run
|
||||
# =============================================================================
|
||||
|
||||
@@ -10,6 +10,7 @@ os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")
|
||||
os.environ.setdefault("DEV_MODE", "true")
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -29,7 +30,7 @@ def _make_client(overrides: dict | None = None) -> TestClient:
|
||||
from src.schemas.auth import User, RoleSchema
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
app.include_router(router, prefix="/validation-tasks")
|
||||
|
||||
mock_user = User(
|
||||
id="admin-1", username="admin", email="admin@x.com",
|
||||
@@ -57,7 +58,11 @@ class TestListTasks:
|
||||
|
||||
def test_success(self):
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.list_tasks.return_value = (1, [{"id": "task-1", "name": "Test"}])
|
||||
mock_svc.list_tasks.return_value = (1, [{
|
||||
"id": "task-1", "name": "Test", "environment_id": "env-1",
|
||||
"is_active": True, "dashboard_ids": ["dash-1"],
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
}])
|
||||
|
||||
from src.api.routes.validation_tasks import _get_task_service
|
||||
client = _make_client({_get_task_service: lambda: mock_svc})
|
||||
@@ -99,8 +104,14 @@ class TestCreateTask:
|
||||
}
|
||||
|
||||
def test_success(self):
|
||||
from src.schemas.validation import ValidationTaskResponse
|
||||
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.create_task.return_value = {"id": "task-new", "name": "New Task"}
|
||||
mock_svc.create_task.return_value = ValidationTaskResponse(
|
||||
id="task-new", name="New Task", environment_id="env-1",
|
||||
is_active=True, dashboard_ids=["dash-1"],
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
mock_sched = MagicMock()
|
||||
|
||||
from src.api.routes.validation_tasks import _get_task_service
|
||||
@@ -130,7 +141,11 @@ class TestGetTask:
|
||||
|
||||
def test_success(self):
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.get_task.return_value = {"id": "task-1", "name": "Test Task"}
|
||||
mock_svc.get_task.return_value = {
|
||||
"id": "task-1", "name": "Test Task", "environment_id": "env-1",
|
||||
"is_active": True, "dashboard_ids": ["dash-1"],
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
}
|
||||
|
||||
from src.api.routes.validation_tasks import _get_task_service
|
||||
client = _make_client({_get_task_service: lambda: mock_svc})
|
||||
@@ -156,8 +171,14 @@ class TestUpdateTask:
|
||||
UPDATE_PAYLOAD = {"name": "Updated Task"}
|
||||
|
||||
def test_success(self):
|
||||
from src.schemas.validation import ValidationTaskResponse
|
||||
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.update_task.return_value = {"id": "task-1", "name": "Updated Task"}
|
||||
mock_svc.update_task.return_value = ValidationTaskResponse(
|
||||
id="task-1", name="Updated Task", environment_id="env-1",
|
||||
is_active=True, dashboard_ids=["dash-1"],
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
mock_sched = MagicMock()
|
||||
|
||||
from src.api.routes.validation_tasks import _get_task_service
|
||||
@@ -187,7 +208,11 @@ class TestToggleTaskStatus:
|
||||
|
||||
def test_activate(self):
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.update_task.return_value = {"id": "task-1", "is_active": True}
|
||||
mock_svc.update_task.return_value = {
|
||||
"id": "task-1", "name": "Task", "environment_id": "env-1",
|
||||
"is_active": True, "dashboard_ids": ["dash-1"],
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
}
|
||||
mock_sched = MagicMock()
|
||||
|
||||
from src.api.routes.validation_tasks import _get_task_service
|
||||
@@ -202,7 +227,11 @@ class TestToggleTaskStatus:
|
||||
|
||||
def test_deactivate(self):
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.update_task.return_value = {"id": "task-1", "is_active": False}
|
||||
mock_svc.update_task.return_value = {
|
||||
"id": "task-1", "name": "Task", "environment_id": "env-1",
|
||||
"is_active": False, "dashboard_ids": ["dash-1"],
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
}
|
||||
mock_sched = MagicMock()
|
||||
|
||||
from src.api.routes.validation_tasks import _get_task_service
|
||||
@@ -239,7 +268,8 @@ class TestTriggerRun:
|
||||
resp = client.post("/validation-tasks/task-1/run")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["spawned_task_id"] == "task-1"
|
||||
# spawned_task_id comes from result["task_id"] (the mock returns "t-1")
|
||||
assert data["spawned_task_id"] == "t-1"
|
||||
assert data["status"] == "PENDING"
|
||||
|
||||
def test_value_error(self):
|
||||
@@ -259,7 +289,9 @@ class TestListAllRuns:
|
||||
|
||||
def test_success(self):
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.list_all_runs.return_value = (1, [{"id": "run-1"}])
|
||||
mock_svc.list_all_runs.return_value = (1, [{
|
||||
"id": "run-1", "dashboard_id": "dash-1", "status": "SUCCESS",
|
||||
}])
|
||||
|
||||
from src.api.routes.validation_tasks import _get_task_service
|
||||
client = _make_client({_get_task_service: lambda: mock_svc})
|
||||
@@ -377,7 +409,7 @@ class TestParseSupersetUrl:
|
||||
mock_extractor.client = mock_client
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
with patch("src.api.routes.validation_tasks.SupersetContextExtractor", return_value=mock_extractor):
|
||||
with patch("src.core.utils.superset_context_extractor.SupersetContextExtractor", return_value=mock_extractor):
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/validation-tasks/parse-url", json=self.PARSE_PAYLOAD)
|
||||
assert resp.status_code == 200
|
||||
@@ -400,7 +432,7 @@ class TestParseSupersetUrl:
|
||||
mock_extractor.client = MagicMock()
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
with patch("src.api.routes.validation_tasks.SupersetContextExtractor", return_value=mock_extractor):
|
||||
with patch("src.core.utils.superset_context_extractor.SupersetContextExtractor", return_value=mock_extractor):
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/validation-tasks/parse-url", json=self.PARSE_PAYLOAD)
|
||||
assert resp.status_code == 200
|
||||
@@ -423,7 +455,7 @@ class TestParseSupersetUrl:
|
||||
mock_extractor.parse_superset_link = AsyncMock(side_effect=ValueError("Invalid URL"))
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
with patch("src.api.routes.validation_tasks.SupersetContextExtractor", return_value=mock_extractor):
|
||||
with patch("src.core.utils.superset_context_extractor.SupersetContextExtractor", return_value=mock_extractor):
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/validation-tasks/parse-url", json=self.PARSE_PAYLOAD)
|
||||
assert resp.status_code == 422
|
||||
@@ -436,7 +468,7 @@ class TestParseSupersetUrl:
|
||||
mock_extractor.parse_superset_link = AsyncMock(side_effect=RuntimeError("Network error"))
|
||||
|
||||
from src.dependencies import get_config_manager
|
||||
with patch("src.api.routes.validation_tasks.SupersetContextExtractor", return_value=mock_extractor):
|
||||
with patch("src.core.utils.superset_context_extractor.SupersetContextExtractor", return_value=mock_extractor):
|
||||
client = _make_client({get_config_manager: lambda: mock_config})
|
||||
resp = client.post("/validation-tasks/parse-url", json=self.PARSE_PAYLOAD)
|
||||
assert resp.status_code == 502
|
||||
@@ -457,7 +489,7 @@ class TestLegacyLlmReportRedirect:
|
||||
|
||||
from src.core.database import get_db
|
||||
client = _make_client({get_db: lambda: mock_db})
|
||||
resp = client.get("/validation-tasks/reports/llm/old-task")
|
||||
resp = client.get("/validation-tasks/reports/llm/old-task", follow_redirects=False)
|
||||
assert resp.status_code == 302
|
||||
assert "/validation-tasks/policy-1/runs/run-1" in resp.headers["location"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user