# #region Test.Api.TranslateRunEditRoutes [C:3] [TYPE Module] [SEMANTICS test,translate,run,edit,bulk,override,api] # @BRIEF Tests for _run_edit_routes.py — language override, inline edit, bulk find-replace. # @RELATION BINDS_TO -> [TranslateRunEditRoutesModule] # @TEST_EDGE: override_record_not_found -> 404 # @TEST_EDGE: override_lang_entry_not_found -> 404 # @TEST_EDGE: override_generic_error -> 400 # @TEST_EDGE: inline_edit_value_error -> 400 # @TEST_EDGE: inline_edit_generic_error -> 400 # @TEST_EDGE: bulk_replace_value_error -> 400 # @TEST_EDGE: bulk_replace_generic_error -> 400 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 _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_edit_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_record(id="rec-1", run_id="run-1"): r = MagicMock() r.id = id r.run_id = run_id return r def _mock_lang_entry(record_id="rec-1", language_code="es"): e = MagicMock() e.record_id = record_id e.language_code = language_code e.source_language_detected = "en" e.language_overridden = False e.needs_review = False # Explicit None for string fields to avoid MagicMock auto-creation e.translated_value = None e.user_edit = None e.final_value = None e.status = "pending" return e # ============================================================================= # Override Detected Language # ============================================================================= class TestOverrideDetectedLanguage: """PUT /api/translate/runs/{run_id}/records/{record_id}/languages/{language_code}/override-language""" def test_override_success(self): mock_db = MagicMock() mock_record = _mock_record() mock_lang = _mock_lang_entry() # First query returns record, second returns lang entry mock_db.query.return_value.filter.return_value.first.side_effect = [ mock_record, mock_lang, ] from src.dependencies import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.put( "/api/translate/runs/run-1/records/rec-1/languages/es/override-language", json={"source_language": "fr"}, ) assert resp.status_code == 200 data = resp.json() assert data["source_language_detected"] == "fr" assert data["language_overridden"] is True # Verify language_overridden was set assert mock_lang.language_overridden is True assert mock_lang.source_language_detected == "fr" def test_override_record_not_found_404(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.side_effect = [ None, # record not found ] from src.dependencies import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.put( "/api/translate/runs/run-1/records/rec-missing/languages/es/override-language", json={"source_language": "fr"}, ) assert resp.status_code == 404 assert "not found" in resp.text def test_override_lang_entry_not_found_404(self): mock_db = MagicMock() mock_record = _mock_record() mock_db.query.return_value.filter.return_value.first.side_effect = [ mock_record, # record found None, # lang entry not found ] from src.dependencies import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.put( "/api/translate/runs/run-1/records/rec-1/languages/missing/override-language", json={"source_language": "fr"}, ) assert resp.status_code == 404 assert "not found" in resp.text def test_override_generic_error_400(self): mock_db = MagicMock() mock_db.query.return_value.filter.side_effect = Exception("DB error") from src.dependencies import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.put( "/api/translate/runs/run-1/records/rec-1/languages/es/override-language", json={"source_language": "fr"}, ) assert resp.status_code == 400 # ============================================================================= # Inline Edit Translation # ============================================================================= class TestInlineEditTranslation: """PUT /api/translate/runs/{run_id}/records/{record_id}/languages/{language_code}""" def test_inline_edit_success(self): with patch( "src.plugins.translate.service.InlineCorrectionService" ) as MockService: MockService.apply_inline_edit.return_value = { "status": "updated", "final_value": "corregido", } client = _make_client() resp = client.put( "/api/translate/runs/run-1/records/rec-1/languages/es", json={"final_value": "corregido"}, ) assert resp.status_code == 200 data = resp.json() assert data["status"] == "updated" def test_inline_edit_with_dictionary_submit(self): with patch( "src.plugins.translate.service.InlineCorrectionService" ) as MockService: MockService.apply_inline_edit.return_value = { "status": "updated", "dictionary_entry_id": "entry-1", } client = _make_client() resp = client.put( "/api/translate/runs/run-1/records/rec-1/languages/es", json={ "final_value": "corregido", "submit_to_dictionary": True, "dictionary_id": "dict-1", }, ) assert resp.status_code == 200 MockService.apply_inline_edit.assert_called_once() call_kwargs = MockService.apply_inline_edit.call_args.kwargs assert call_kwargs["submit_to_dictionary"] is True assert call_kwargs["dictionary_id"] == "dict-1" def test_inline_edit_value_error_400(self): with patch( "src.plugins.translate.service.InlineCorrectionService" ) as MockService: MockService.apply_inline_edit.side_effect = ValueError("Record not found") client = _make_client() resp = client.put( "/api/translate/runs/run-1/records/rec-missing/languages/es", json={"final_value": "corregido"}, ) assert resp.status_code == 400 def test_inline_edit_generic_error_400(self): with patch( "src.plugins.translate.service.InlineCorrectionService" ) as MockService: MockService.apply_inline_edit.side_effect = RuntimeError("Unexpected") client = _make_client() resp = client.put( "/api/translate/runs/run-1/records/rec-1/languages/es", json={"final_value": "corregido"}, ) assert resp.status_code == 400 def test_inline_edit_422_missing_final_value(self): client = _make_client() resp = client.put( "/api/translate/runs/run-1/records/rec-1/languages/es", json={}, ) assert resp.status_code == 422 assert "final_value" in resp.text # ============================================================================= # Bulk Find Replace # ============================================================================= class TestBulkFindReplace: """POST /api/translate/runs/{run_id}/bulk-replace""" def test_bulk_preview_success(self): with patch( "src.plugins.translate.service.BulkFindReplaceService" ) as MockService: MockService.preview.return_value = [ {"record_id": "rec-1", "current_value": "hola", "new_value": "hello"}, ] client = _make_client() resp = client.post("/api/translate/runs/run-1/bulk-replace", json={ "find_pattern": "hola", "replacement_text": "hello", "target_language": "es", "preview": True, }) assert resp.status_code == 200 data = resp.json() assert data["rows_affected"] == 1 assert data["corrections_submitted"] == 0 assert len(data["preview"]) == 1 def test_bulk_apply_success(self): with patch( "src.plugins.translate.service.BulkFindReplaceService" ) as MockService: MockService.apply.return_value = { "rows_affected": 5, "corrections_submitted": 5, "preview": [], } client = _make_client() resp = client.post("/api/translate/runs/run-1/bulk-replace", json={ "find_pattern": "hola", "replacement_text": "hello", "target_language": "es", "preview": False, }) assert resp.status_code == 200 data = resp.json() assert data["rows_affected"] == 5 assert data["corrections_submitted"] == 5 def test_bulk_replace_value_error_400(self): with patch( "src.plugins.translate.service.BulkFindReplaceService" ) as MockService: MockService.preview.side_effect = ValueError("Run not found") client = _make_client() resp = client.post("/api/translate/runs/run-1/bulk-replace", json={ "find_pattern": "hola", "replacement_text": "hello", "target_language": "es", "preview": True, }) assert resp.status_code == 400 def test_bulk_replace_generic_error_400(self): with patch( "src.plugins.translate.service.BulkFindReplaceService" ) as MockService: MockService.apply.side_effect = RuntimeError("Bulk replace crashed") client = _make_client() resp = client.post("/api/translate/runs/run-1/bulk-replace", json={ "find_pattern": "hola", "replacement_text": "hello", "target_language": "es", "preview": False, }) assert resp.status_code == 400 def test_bulk_replace_422_missing_fields(self): client = _make_client() resp = client.post("/api/translate/runs/run-1/bulk-replace", json={ "target_language": "es", }) assert resp.status_code == 422 assert "find_pattern" in resp.text def test_bulk_replace_invalid_regex_422(self): """Pattern longer than 500 chars should be rejected.""" client = _make_client() resp = client.post("/api/translate/runs/run-1/bulk-replace", json={ "find_pattern": "x" * 501, "is_regex": True, "replacement_text": "y", "target_language": "es", "preview": True, }) assert resp.status_code == 422 # #endregion Test.Api.TranslateRunEditRoutes