# [DEF:TranslateHistoryTests:Module] # @SEMANTICS: tests, translate, history, metrics # @PURPOSE: Tests for run history list/detail endpoints and metrics aggregation. # @LAYER: Test # @RELATION: BINDS_TO -> [TranslateRoutes:Module] # @RELATION: BINDS_TO -> [TranslationMetrics:Module] # # @TEST_CONTRACT: HistoryFlow -> # { # invariants: [ # "GET /api/translate/runs returns paginated list", # "GET /api/translate/runs supports filters (job_id, status, trigger_type)", # "GET /api/translate/runs/{id}/detail returns config_snapshot, records, events", # "GET /api/translate/jobs/{id}/metrics returns aggregated data", # "Metrics include run counts, record counts, duration" # ] # } # @TEST_FIXTURE: completed_run -> run with COMPLETED status # @TEST_EDGE: filter_by_job_id -> returns only that job's runs # @TEST_EDGE: filter_by_status -> returns only matching status # @TEST_EDGE: metrics_empty_job -> returns zero counts # @TEST_INVARIANT: history_list_and_detail -> VERIFIED_BY: [completed_run, filter_by_status] import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import pytest from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from datetime import datetime, timezone import uuid import json from src.core.database import Base from src.app import app from src.dependencies import get_current_user, get_config_manager, has_permission, get_db from src.plugins.translate.metrics import TranslationMetrics from src.models.translate import TranslationJob, TranslationRun, TranslationEvent, MetricSnapshot # In-memory SQLite SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base.metadata.create_all(bind=engine) mock_user = MagicMock() mock_user.username = "testuser" mock_user.roles = [] admin_role = MagicMock() admin_role.name = "Admin" mock_user.roles.append(admin_role) @pytest.fixture def db_session(): connection = engine.connect() transaction = connection.begin() session = TestingSessionLocal(bind=connection) yield session session.close() transaction.rollback() connection.close() @pytest.fixture def mock_api_deps(): config_manager = MagicMock() config_manager.get_environments.return_value = [] connection = engine.connect() transaction = connection.begin() session = TestingSessionLocal(bind=connection) def override_get_db(): try: yield session finally: pass overrides = { get_current_user: lambda: mock_user, get_config_manager: lambda: config_manager, get_db: override_get_db, } for dep, override in overrides.items(): app.dependency_overrides[dep] = override yield {"config_manager": config_manager, "session": session} app.dependency_overrides.clear() session.close() transaction.rollback() connection.close() @pytest.fixture def client(mock_api_deps): return TestClient(app) def _create_test_job(db_session) -> TranslationJob: job = TranslationJob( id=str(uuid.uuid4()), name="Test Job", source_dialect="postgresql", target_dialect="clickhouse", status="ACTIVE", ) db_session.add(job) db_session.flush() return job def _create_test_run(db_session, job_id: str, status: str = "COMPLETED", trigger: str = "manual") -> TranslationRun: run = TranslationRun( id=str(uuid.uuid4()), job_id=job_id, status=status, trigger_type=trigger, total_records=100, successful_records=80, failed_records=10, skipped_records=10, config_snapshot={"test": True}, config_hash="abc123", dict_snapshot_hash="def456", key_hash="ghi789", created_by="testuser", created_at=datetime.now(timezone.utc), ) db_session.add(run) db_session.flush() return run # [DEF:test_list_runs_empty:Function] # @PURPOSE: Verify runs list returns empty result initially. def test_list_runs_empty(client): """Test runs list initially returns empty.""" response = client.get("/api/translate/runs") assert response.status_code == 200 data = response.json() assert data["total"] == 0 assert data["items"] == [] # [/DEF:test_list_runs_empty:Function] # [DEF:test_list_runs_with_data:Function] # @PURPOSE: Verify runs list returns data when runs exist. def test_list_runs_with_data(client, mock_api_deps): """Test runs list with existing runs.""" session = mock_api_deps["session"] job = _create_test_job(session) _create_test_run(session, job.id) session.commit() response = client.get("/api/translate/runs") assert response.status_code == 200 data = response.json() assert data["total"] >= 1 assert len(data["items"]) >= 1 assert data["items"][0]["job_id"] == job.id # [/DEF:test_list_runs_with_data:Function] # [DEF:test_list_runs_filter_job_id:Function] # @PURPOSE: Verify filtering runs by job_id works. def test_list_runs_filter_job_id(client, mock_api_deps): """Test filtering runs by job_id.""" session = mock_api_deps["session"] job1 = _create_test_job(session) job2 = _create_test_job(session) _create_test_run(session, job1.id) _create_test_run(session, job2.id) session.commit() response = client.get(f"/api/translate/runs?job_id={job1.id}") assert response.status_code == 200 data = response.json() assert data["total"] >= 1 for item in data["items"]: assert item["job_id"] == job1.id # [/DEF:test_list_runs_filter_job_id:Function] # [DEF:test_list_runs_filter_status:Function] # @PURPOSE: Verify filtering runs by status works. def test_list_runs_filter_status(client, mock_api_deps): """Test filtering runs by status.""" session = mock_api_deps["session"] job = _create_test_job(session) _create_test_run(session, job.id, status="COMPLETED") _create_test_run(session, job.id, status="FAILED") session.commit() response = client.get(f"/api/translate/runs?status=FAILED") assert response.status_code == 200 data = response.json() for item in data["items"]: assert item["status"] == "FAILED" # [/DEF:test_list_runs_filter_status:Function] # [DEF:test_get_run_detail:Function] # @PURPOSE: Verify run detail returns config_snapshot, records, events. def test_get_run_detail(client, mock_api_deps): """Test run detail endpoint.""" session = mock_api_deps["session"] job = _create_test_job(session) run = _create_test_run(session, job.id) # Add an event event = TranslationEvent( id=str(uuid.uuid4()), job_id=job.id, run_id=run.id, event_type="RUN_STARTED", event_data={}, created_by="testuser", created_at=datetime.now(timezone.utc), ) session.add(event) session.commit() response = client.get(f"/api/translate/runs/{run.id}/detail") assert response.status_code == 200 data = response.json() assert data["id"] == run.id assert data["config_snapshot"] == {"test": True} assert data["config_hash"] == "abc123" assert len(data["events"]) >= 1 assert data["events"][0]["event_type"] == "RUN_STARTED" # [/DEF:test_get_run_detail:Function] # [DEF:test_get_job_metrics:Function] # @PURPOSE: Verify metrics endpoint returns aggregated data. def test_get_job_metrics(client, mock_api_deps): """Test job metrics endpoint.""" session = mock_api_deps["session"] job = _create_test_job(session) _create_test_run(session, job.id, status="COMPLETED") _create_test_run(session, job.id, status="FAILED") session.commit() response = client.get(f"/api/translate/jobs/{job.id}/metrics") assert response.status_code == 200 data = response.json() assert data["job_id"] == job.id assert data["total_runs"] >= 2 assert data["successful_runs"] >= 1 assert data["failed_runs"] >= 1 # [/DEF:test_get_job_metrics:Function] # [DEF:test_get_all_metrics:Function] # @PURPOSE: Verify global metrics endpoint returns data. def test_get_all_metrics(client, mock_api_deps): """Test global metrics endpoint.""" session = mock_api_deps["session"] job = _create_test_job(session) _create_test_run(session, job.id) session.commit() response = client.get("/api/translate/metrics") assert response.status_code == 200 data = response.json() assert isinstance(data, list) assert len(data) >= 1 # [/DEF:test_get_all_metrics:Function] # [DEF:test_metrics_empty_job:Function] # @PURPOSE: Verify metrics for job with no runs returns zeros. def test_metrics_empty_job(client, mock_api_deps): """Test metrics for job with no runs.""" session = mock_api_deps["session"] job = _create_test_job(session) session.commit() response = client.get(f"/api/translate/jobs/{job.id}/metrics") assert response.status_code == 200 data = response.json() assert data["total_runs"] == 0 # [/DEF:test_metrics_empty_job:Function] # [DEF:test_run_detail_not_found:Function] # @PURPOSE: Verify 404 on non-existent run detail. def test_run_detail_not_found(client): """Test run detail returns 404 for non-existent run.""" response = client.get("/api/translate/runs/non-existent/detail") assert response.status_code == 404 # [/DEF:test_run_detail_not_found:Function] # [/DEF:TranslateHistoryTests:Module]