Files
ss-tools/backend/tests/test_translate_corrections.py
busya 4205618ee6 chore: eliminate all deprecation warnings from tests and linter
Warnings fixed:
- datetime.utcnow() → datetime.now(UTC) across 48+ files (src/ + tests/)
- datetime.utcnow (callback ref) → lambda: datetime.now(UTC) in model fields (18 files)
- Pydantic class Config → model_config = ConfigDict(...) (16 files)
- Pydantic .dict() → .model_dump() (8 files)
- ConfigDict(allow_population_by_field_name=True) → validate_by_name=True
- SQLAlchemy declarative_base() import path updated
- FastAPI on_event → lifespan context manager (app.py)
- Import sorting (ruff I001) auto-fixed across all files
- Fixed broken re-export chains that ruff F401 cleanup broke:
  _validate_bcp47: service.py now imports from dictionary_validation directly
  job_to_response: _job_routes.py and test imports from service_utils directly
  fetch_datasource_metadata: restored re-export in service.py
- Added missing TranslateJobService import in _job_routes.py (was deleted by F401)
- Added ConfigDict(protected_namespaces=()) for DashboardDatasetItem schema field
- pytest.ini: replaced deprecated importmode with asyncio_mode

All 440 tests pass with zero deprecation warnings.
2026-05-26 19:18:28 +03:00

250 lines
8.4 KiB
Python

# #region TranslateCorrectionTests [C:2] [TYPE Module]
# @SEMANTICS: tests, translate, corrections, dictionary
# @PURPOSE: Tests for term correction API endpoints and DictionaryManager correction methods.
# @LAYER Tests
# @RELATION BINDS_TO -> [DictionaryManagerModule]
# @RELATION BINDS_TO -> [TranslateRoutes]
#
# @TEST_CONTRACT: CorrectionFlow ->
# {
# required_fields: {source_term, incorrect_target_term, corrected_target_term, dictionary_id},
# invariants: [
# "POST /api/translate/corrections creates or updates entry",
# "Conflict detection works for existing entries",
# "POST /api/translate/corrections/bulk atomic all-or-nothing",
# "Origin tracking fields are populated"
# ]
# }
# @TEST_FIXTURE: valid_dictionary -> created via DictionaryManager
# @TEST_EDGE: missing_dictionary_id -> 422
# @TEST_EDGE: conflict_detected -> conflict response returned
# @TEST_EDGE: bulk_correction_all_succeed -> atomic commit
# @TEST_EDGE: bulk_correction_with_conflicts -> conflicts listed
# @TEST_INVARIANT: correction_flow -> VERIFIED_BY: [valid_dictionary, conflict_detected]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
import pytest
from unittest.mock import MagicMock
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.app import app
from src.core.database import Base
from src.dependencies import get_config_manager, get_current_user, get_db
from src.plugins.translate.dictionary import DictionaryManager
# 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)
# #region test_submit_correction_creates_entry [C:2] [TYPE Function]
# @PURPOSE: Verify that a correction creates a new dictionary entry.
def test_submit_correction_creates_entry(db_session):
"""Test that submitting a correction creates a new entry."""
dict_obj = DictionaryManager.create_dictionary(
db_session, name="Test Dict", source_dialect="en", target_dialect="ru",
created_by="testuser",
)
result = DictionaryManager.submit_correction(
db_session,
dict_id=dict_obj.id,
source_term="hello",
incorrect_target_term="privet",
corrected_target_term="zdravstvuyte",
origin_run_id="run-123",
origin_row_key="row-1",
origin_user_id="testuser",
)
assert result["action"] == "created"
assert result["entry_id"] is not None
entries, total = DictionaryManager.list_entries(db_session, dict_obj.id)
assert total == 1
assert entries[0].source_term == "hello"
assert entries[0].target_term == "zdravstvuyte"
assert entries[0].origin_run_id == "run-123"
assert entries[0].origin_row_key == "row-1"
assert entries[0].origin_user_id == "testuser"
# #endregion test_submit_correction_creates_entry
# #region test_submit_correction_conflict_detected [C:2] [TYPE Function]
# @PURPOSE: Verify conflict detection when entry already exists.
def test_submit_correction_conflict_detected(db_session):
"""Test that conflict is detected when entry already exists."""
dict_obj = DictionaryManager.create_dictionary(
db_session, name="Dict", source_dialect="en", target_dialect="ru",
)
# Create initial entry
DictionaryManager.add_entry(db_session, dict_obj.id, "hello", "privet")
# Submit correction with conflict
result = DictionaryManager.submit_correction(
db_session,
dict_id=dict_obj.id,
source_term="hello",
incorrect_target_term="privet",
corrected_target_term="zdravstvuyte",
on_conflict="keep_existing",
)
assert result["action"] == "conflict_detected"
assert result["conflict"] is not None
assert result["conflict"]["existing_target_term"] == "privet"
# #endregion test_submit_correction_conflict_detected
# #region test_submit_correction_overwrite [C:2] [TYPE Function]
# @PURPOSE: Verify that correction overwrites existing entry.
def test_submit_correction_overwrite(db_session):
"""Test that correction overwrites existing entry."""
dict_obj = DictionaryManager.create_dictionary(
db_session, name="Dict", source_dialect="en", target_dialect="ru",
)
DictionaryManager.add_entry(db_session, dict_obj.id, "hello", "privet")
result = DictionaryManager.submit_correction(
db_session,
dict_id=dict_obj.id,
source_term="hello",
incorrect_target_term="privet",
corrected_target_term="hi_there",
on_conflict="overwrite",
)
assert result["action"] == "updated"
entries, _ = DictionaryManager.list_entries(db_session, dict_obj.id)
assert entries[0].target_term == "hi_there"
# #endregion test_submit_correction_overwrite
# #region test_bulk_corrections_atomic [C:2] [TYPE Function]
# @PURPOSE: Verify bulk corrections are applied atomically.
def test_bulk_corrections_atomic(db_session):
"""Test that bulk corrections are applied atomically."""
dict_obj = DictionaryManager.create_dictionary(
db_session, name="Bulk Dict", source_dialect="en", target_dialect="ru",
)
corrections = [
{"source_term": "cat", "incorrect_target_term": "kot", "corrected_target_term": "koshka"},
{"source_term": "dog", "incorrect_target_term": "sobaka", "corrected_target_term": "pyos"},
]
result = DictionaryManager.submit_bulk_corrections(
db_session,
dict_id=dict_obj.id,
corrections=corrections,
origin_user_id="testuser",
)
assert result["status"] == "completed"
assert result["applied"] == 2
assert len(result["conflicts"]) == 0
entries, total = DictionaryManager.list_entries(db_session, dict_obj.id)
assert total == 2
# #endregion test_bulk_corrections_atomic
# #region test_api_correction_missing_dict [C:2] [TYPE Function]
# @PURPOSE: Verify POST corrections without dictionary_id returns 422.
def test_api_correction_missing_dict(client):
"""Test correction without dict_id returns 422."""
response = client.post("/api/translate/corrections", json={
"source_term": "hello",
"incorrect_target_term": "privet",
"corrected_target_term": "zdravstvuyte",
})
assert response.status_code == 422
# #endregion test_api_correction_missing_dict
# #region test_api_bulk_corrections [C:2] [TYPE Function]
# @PURPOSE: Verify POST /corrections/bulk works.
def test_api_bulk_corrections(client, mock_api_deps):
"""Test bulk corrections endpoint."""
session = mock_api_deps["session"]
dict_obj = DictionaryManager.create_dictionary(
session, name="API Dict", source_dialect="en", target_dialect="ru",
)
response = client.post("/api/translate/corrections/bulk", json={
"dictionary_id": dict_obj.id,
"corrections": [
{"source_term": "hello", "incorrect_target_term": "privet", "corrected_target_term": "hi"},
],
})
assert response.status_code == 200
data = response.json()
assert data["status"] == "completed"
# #endregion test_api_bulk_corrections
# #endregion TranslateCorrectionTests