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

262 lines
9.6 KiB
Python

# #region Test.Api.TranslateRunRoutes [C:3] [TYPE Module] [SEMANTICS test,translate,runs,execution,api]
# @BRIEF Tests for _run_routes.py — run execution, retry, cancel.
# @RELATION BINDS_TO -> [TranslateRunRoutesModule]
# @TEST_EDGE: run_value_error -> 400
# @TEST_EDGE: run_generic_error -> 502
# @TEST_EDGE: retry_value_error -> 400
# @TEST_EDGE: retry_generic_error -> 502
import os
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from unittest.mock import AsyncMock
_src = str(Path(__file__).resolve().parent.parent.parent / "src")
if _src not in sys.path:
sys.path.insert(0, _src)
def _make_client(overrides: dict | None = None) -> TestClient:
from src.api.routes.translate._run_routes import router
from src.dependencies import (
get_config_manager, get_current_user, get_db,
has_permission, require_feature,
)
from src.schemas.auth import User, RoleSchema
app = FastAPI()
app.include_router(router)
mock_user = User(
id="user-1", username="admin", email="admin@x.com",
auth_source="LOCAL",
created_at=__import__("datetime").datetime.now(),
roles=[RoleSchema(id="r1", name="Admin", description="", permissions=[])],
)
defaults = {
get_current_user: lambda: mock_user,
get_config_manager: lambda: MagicMock(),
get_db: lambda: MagicMock(),
has_permission: lambda *a, **kw: lambda: None,
require_feature: lambda *a, **kw: lambda: None,
}
if overrides:
defaults.update(overrides)
for dep, mock_fn in defaults.items():
app.dependency_overrides[dep] = mock_fn
return TestClient(app)
def _mock_run(id="run-1", job_id="job-1", status="PENDING"):
r = MagicMock()
r.id = id
r.job_id = job_id
r.status = status
r.trigger_type = "manual"
r.started_at = None
r.completed_at = None
r.error_message = None
r.total_records = 0
r.successful_records = 0
r.failed_records = 0
r.skipped_records = 0
r.cache_hits = 0
r.insert_status = None
r.superset_execution_id = None
r.config_snapshot = None
r.key_hash = None
r.config_hash = None
r.dict_snapshot_hash = None
r.created_by = "admin"
r.created_at = __import__("datetime").datetime(2026, 1, 1)
return r
# =============================================================================
# Run Translation
# =============================================================================
class TestRunTranslation:
"""POST /api/translate/jobs/{job_id}/run"""
def test_run_translation_success(self):
mock_orch = MagicMock()
mock_orch.start_run.return_value = _mock_run()
with (
patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch),
patch("src.api.routes.translate._run_routes.asyncio.create_task"),
):
client = _make_client()
resp = client.post("/api/translate/jobs/job-1/run")
assert resp.status_code == 201
data = resp.json()
assert data["id"] == "run-1"
assert data["status"] == "PENDING"
def test_run_translation_full(self):
mock_orch = MagicMock()
mock_orch.start_run.return_value = _mock_run()
with (
patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch),
patch("src.api.routes.translate._run_routes.asyncio.create_task"),
):
client = _make_client()
resp = client.post("/api/translate/jobs/job-1/run?full_translation=true")
assert resp.status_code == 201
# Verify start_run was called with full_translation=True
mock_orch.start_run.assert_called_once_with(
job_id="job-1", is_scheduled=False, full_translation=True
)
def test_run_translation_value_error_400(self):
mock_orch = MagicMock()
mock_orch.start_run.side_effect = ValueError("Job not found")
with (
patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch),
patch("src.api.routes.translate._run_routes.asyncio.create_task"),
):
client = _make_client()
resp = client.post("/api/translate/jobs/job-missing/run")
assert resp.status_code == 400
assert "Job not found" in resp.text
def test_run_translation_generic_error_502(self):
mock_orch = MagicMock()
mock_orch.start_run.side_effect = RuntimeError("Unexpected error")
with (
patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch),
patch("src.api.routes.translate._run_routes.asyncio.create_task"),
):
client = _make_client()
resp = client.post("/api/translate/jobs/job-1/run")
assert resp.status_code == 502
assert "Run failed" in resp.text
# =============================================================================
# Retry Run
# =============================================================================
class TestRetryRun:
"""POST /api/translate/runs/{run_id}/retry"""
def test_retry_success(self):
mock_orch = MagicMock()
mock_orch.retry_failed_batches = AsyncMock(return_value=_mock_run(status="RUNNING"))
with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch):
client = _make_client()
resp = client.post("/api/translate/runs/run-1/retry")
assert resp.status_code == 200
assert resp.json()["status"] == "RUNNING"
def test_retry_value_error_400(self):
mock_orch = MagicMock()
mock_orch.retry_failed_batches.side_effect = ValueError("Run not retriable")
with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch):
client = _make_client()
resp = client.post("/api/translate/runs/run-1/retry")
assert resp.status_code == 400
def test_retry_generic_error_502(self):
mock_orch = MagicMock()
mock_orch.retry_failed_batches.side_effect = RuntimeError("Internal error")
with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch):
client = _make_client()
resp = client.post("/api/translate/runs/run-1/retry")
assert resp.status_code == 502
assert "Retry failed" in resp.text
# =============================================================================
# Retry Insert
# =============================================================================
class TestRetryInsert:
"""POST /api/translate/runs/{run_id}/retry-insert"""
def test_retry_insert_success(self):
mock_orch = MagicMock()
mock_orch.retry_insert = AsyncMock(return_value=_mock_run(status="COMPLETED"))
with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch):
client = _make_client()
resp = client.post("/api/translate/runs/run-1/retry-insert")
assert resp.status_code == 200
assert resp.json()["status"] == "COMPLETED"
def test_retry_insert_value_error_400(self):
mock_orch = MagicMock()
mock_orch.retry_insert.side_effect = ValueError("Insert not needed")
with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch):
client = _make_client()
resp = client.post("/api/translate/runs/run-1/retry-insert")
assert resp.status_code == 400
def test_retry_insert_generic_error_502(self):
mock_orch = MagicMock()
mock_orch.retry_insert.side_effect = RuntimeError("Insert failed")
with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch):
client = _make_client()
resp = client.post("/api/translate/runs/run-1/retry-insert")
assert resp.status_code == 502
assert "Retry insert failed" in resp.text
# =============================================================================
# Cancel Run
# =============================================================================
class TestCancelRun:
"""POST /api/translate/runs/{run_id}/cancel"""
def test_cancel_success(self):
mock_orch = MagicMock()
mock_orch.cancel_run.return_value = _mock_run(status="CANCELLED")
with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch):
client = _make_client()
resp = client.post("/api/translate/runs/run-1/cancel")
assert resp.status_code == 200
assert resp.json()["status"] == "CANCELLED"
def test_cancel_value_error_400(self):
mock_orch = MagicMock()
mock_orch.cancel_run.side_effect = ValueError("Run not found")
with patch("src.api.routes.translate._run_routes.TranslationOrchestrator",
return_value=mock_orch):
client = _make_client()
resp = client.post("/api/translate/runs/run-missing/cancel")
assert resp.status_code == 400
# #endregion Test.Api.TranslateRunRoutes