Files
ss-tools/backend/tests/integration/test_translate_status_fk.py
busya 50d6b9226e test(translate): add tests for ConnectionService, DbExecutor, orchestrator direct DB dispatch
- 9 new enhancement test files: test_connection_service.py,
  test_db_executor.py, test_orchestrator_direct_db.py, test_batch_insert.py,
  test_lang_stats.py, test_response_field_coverage.py, test_retry.py,
  test_run_service.py, test_sql_insert_service.py
- 5 new integration tests: test_superset_sqllab_e2e.py,
  test_translate_clickhouse.py, test_translate_corrections.py,
  test_translate_schedules.py, test_translate_status_fk.py
- Updated existing tests for insert_method/connection_id fields
2026-06-11 19:12:45 +03:00

246 lines
9.6 KiB
Python

# #region TestTranslateStatusAndFK [C:3] [TYPE Module] [SEMANTICS test,translate,integration,status,fk,constraints]
# @BRIEF Integration tests for job status transitions, run state machine, FK constraint enforcement
# with real PostgreSQL via Testcontainers.
# @RELATION BINDS_TO -> [TranslateJobService]
# @RELATION BINDS_TO -> [TranslationEventLog]
#
# @TEST_CONTRACT JobStatusTransitions ->
# DRAFT job: create -> delete works
# non-existent: get/update/delete raises ValueError
# long name: persists
#
# @TEST_CONTRACT RunStateMachine ->
# PENDING -> COMPLETED: via cancel_run
# COMPLETED -> cancel raises ValueError
# CANCELLED -> cancel raises ValueError
#
# @TEST_EDGE: delete_non_existent_job -> ValueError
# @TEST_EDGE: cancel_non_existent_run -> ValueError
# @TEST_EDGE: cancel_cancelled_run -> ValueError
import pytest
import uuid
from sqlalchemy import text
from sqlalchemy.orm import Session
from src.models.translate import (
TranslationJob,
TranslationRun,
TranslationBatch,
TranslationRecord,
TranslationLanguage,
TranslationEvent,
)
from src.plugins.translate.service import TranslateJobService
from src.schemas.translate import TranslateJobCreate
# #region TestJobStatusTransitions [C:3] [TYPE Class]
@pytest.mark.asyncio
class TestJobStatusTransitions:
"""Verify job status lifecycle transitions with real PostgreSQL."""
async def test_create_draft_job(self, db_session: Session, mock_config_manager):
service = TranslateJobService(db_session, mock_config_manager, "test_user")
payload = TranslateJobCreate(
name="Draft Test Job",
source_dialect="postgresql",
target_dialect="clickhouse",
translation_column="name",
target_languages=["ru"],
)
job = await service.create_job(payload)
assert job.status == "DRAFT"
async def test_delete_job(self, db_session: Session, mock_config_manager):
service = TranslateJobService(db_session, mock_config_manager, "test_user")
payload = TranslateJobCreate(
name="Delete Test", source_dialect="postgresql",
target_dialect="clickhouse", translation_column="name",
target_languages=["ru"],
)
job = await service.create_job(payload)
service.delete_job(job.id)
with pytest.raises(ValueError, match="not found"):
service.get_job(job.id)
async def test_delete_non_existent_job(self, db_session: Session, mock_config_manager):
service = TranslateJobService(db_session, mock_config_manager, "test_user")
with pytest.raises(ValueError, match="not found"):
service.delete_job("non-existent-id")
async def test_get_non_existent_job(self, db_session: Session, mock_config_manager):
service = TranslateJobService(db_session, mock_config_manager, "test_user")
with pytest.raises(ValueError, match="not found"):
service.get_job("non-existent-id")
async def test_create_job_max_name_length(self, db_session: Session, mock_config_manager):
service = TranslateJobService(db_session, mock_config_manager, "test_user")
long_name = "A" * 500
payload = TranslateJobCreate(
name=long_name,
source_dialect="postgresql",
target_dialect="clickhouse",
translation_column="name",
target_languages=["ru"],
)
job = await service.create_job(payload)
assert len(job.name) == 500
# #endregion TestJobStatusTransitions
# #region TestFKConstraintEnforcement [C:3] [TYPE Class]
class TestFKConstraintEnforcement:
"""Verify PostgreSQL FK constraints catch orphaned data."""
def _assert_fk_violation(self, db: Session, obj):
"""Helper: try adding an orphaned object and expect FK violation."""
db.add(obj)
try:
db.flush()
db.rollback()
pytest.fail("Expected FK violation was not raised")
except Exception:
db.rollback()
def test_orphan_run_rejected(self, db_session: Session):
run = TranslationRun(job_id=str(uuid.uuid4()), status="PENDING", trigger_type="manual")
self._assert_fk_violation(db_session, run)
def test_orphan_batch_rejected(self, db_session: Session):
batch = TranslationBatch(run_id=str(uuid.uuid4()), batch_index=0, status="PENDING")
self._assert_fk_violation(db_session, batch)
def test_valid_run_with_job_succeeds(self, db_session: Session):
job = TranslationJob(
name="FK Test Job", status="ACTIVE",
source_dialect="postgresql", target_dialect="clickhouse",
created_by="test_user",
)
db_session.add(job)
db_session.flush()
db_session.refresh(job)
run = TranslationRun(job_id=job.id, status="PENDING", trigger_type="manual")
db_session.add(run)
db_session.flush()
assert run.job_id == job.id
# #endregion TestFKConstraintEnforcement
# #region TestRunRecovery [C:3] [TYPE Class]
class TestRunRecovery:
"""Verify run cancel behavior with real PostgreSQL."""
def _create_run(self, db: Session, status: str = "PENDING") -> TranslationRun:
job = TranslationJob(
name="Cancel Test", status="ACTIVE",
source_dialect="postgresql", target_dialect="clickhouse",
created_by="test_user",
)
db.add(job)
db.flush()
db.refresh(job)
run = TranslationRun(job_id=job.id, status=status, trigger_type="manual")
db.add(run)
db.flush()
db.refresh(run)
return run
def test_cancel_pending_run(self, db_session: Session):
run = self._create_run(db_session, "PENDING")
from src.plugins.translate.orchestrator_cancel import cancel_run
from src.plugins.translate.events import TranslationEventLog
event_log = TranslationEventLog(db_session)
result = cancel_run(db_session, event_log, "test_user", run.id)
assert result.status == "CANCELLED"
assert result.completed_at is not None
def test_cancel_failed_run(self, db_session: Session):
run = self._create_run(db_session, "FAILED")
from src.plugins.translate.orchestrator_cancel import cancel_run
from src.plugins.translate.events import TranslationEventLog
event_log = TranslationEventLog(db_session)
with pytest.raises(ValueError, match="Cannot cancel"):
cancel_run(db_session, event_log, "test_user", run.id)
def test_cancel_completed_run_raises(self, db_session: Session):
run = self._create_run(db_session, "COMPLETED")
from src.plugins.translate.orchestrator_cancel import cancel_run
from src.plugins.translate.events import TranslationEventLog
event_log = TranslationEventLog(db_session)
with pytest.raises(ValueError, match="Cannot cancel"):
cancel_run(db_session, event_log, "test_user", run.id)
def test_cancel_cancelled_run_raises(self, db_session: Session):
run = self._create_run(db_session, "CANCELLED")
from src.plugins.translate.orchestrator_cancel import cancel_run
from src.plugins.translate.events import TranslationEventLog
event_log = TranslationEventLog(db_session)
with pytest.raises(ValueError, match="Cannot cancel"):
cancel_run(db_session, event_log, "test_user", run.id)
def test_cancel_non_existent_run(self, db_session: Session):
from src.plugins.translate.orchestrator_cancel import cancel_run
from src.plugins.translate.events import TranslationEventLog
event_log = TranslationEventLog(db_session)
with pytest.raises((ValueError, KeyError)):
cancel_run(db_session, event_log, "test_user", str(uuid.uuid4()))
# #endregion TestRunRecovery
# #region TestEventLogStateMachine [C:3] [TYPE Class]
class TestEventLogStateMachine:
"""Verify event log state machine rules with real PostgreSQL."""
def _create_job_and_run(self, db: Session) -> tuple:
job = TranslationJob(
name="Event Log Job", status="ACTIVE",
source_dialect="postgresql", target_dialect="clickhouse",
created_by="test_user",
)
db.add(job)
db.flush()
db.refresh(job)
run = TranslationRun(job_id=job.id, status="RUNNING", trigger_type="manual")
db.add(run)
db.flush()
db.refresh(run)
return job, run
def test_event_log_persistence(self, db_session: Session):
job, run = self._create_job_and_run(db_session)
from src.plugins.translate.events import TranslationEventLog
event_log = TranslationEventLog(db_session)
event_log.log_event(job.id, "RUN_STARTED", {"strategy": "incremental"}, run.id)
event_log.log_event(job.id, "BATCH_STARTED", {"batch_index": 0}, run.id)
event_log.log_event(job.id, "BATCH_COMPLETED", {"batch_index": 0, "records": 10}, run.id)
event_log.log_event(job.id, "RUN_COMPLETED", {"total_records": 10}, run.id)
events = (
db_session.query(TranslationEvent)
.filter(TranslationEvent.run_id == run.id)
.order_by(TranslationEvent.created_at)
.all()
)
assert len(events) == 4
def test_invalid_event_type_raises(self, db_session: Session):
job, run = self._create_job_and_run(db_session)
from src.plugins.translate.events import TranslationEventLog
event_log = TranslationEventLog(db_session)
with pytest.raises(ValueError, match="Invalid event_type"):
event_log.log_event(job.id, run.id, "INVALID_EVENT", {})
# #endregion TestEventLogStateMachine
# #endregion TestTranslateStatusAndFK