262 lines
9.6 KiB
Python
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
|