# #region Test.Api.TranslateDictionaryRoutes [C:3] [TYPE Module] [SEMANTICS test,translate,dictionary,api,crud] # @BRIEF Tests for _dictionary_routes.py — terminology dictionary CRUD, entries, and import. # @RELATION BINDS_TO -> [TranslateDictionaryRoutesModule] # @TEST_EDGE: dictionary_not_found -> 404 # @TEST_EDGE: dictionary_conflict -> 409 (blocked by active jobs) # @TEST_EDGE: entry_already_exists -> 409 # @TEST_EDGE: entry_not_found -> 404 # @TEST_EDGE: import_invalid_on_conflict -> 422 # @TEST_EDGE: import_value_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._dictionary_routes import router from src.dependencies import 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_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) # Shared mock dictionary ORM row def _mock_dict( id="dict-1", name="Test Dict", description="A test dictionary", is_active=True, created_by="admin", created_at=None, updated_at=None, ): d = MagicMock() d.id = id d.name = name d.description = description d.is_active = is_active d.created_by = created_by d.created_at = created_at or __import__("datetime").datetime(2026, 1, 1) d.updated_at = updated_at return d def _mock_entry( id="entry-1", dictionary_id="dict-1", source_term="hello", source_term_normalized="hello", target_term="hola", source_language="en", target_language="es", context_notes=None, context_data=None, usage_notes=None, has_context=False, is_regex=False, context_source=None, origin_source_language=None, created_at=None, updated_at=None, ): e = MagicMock() e.id = id e.dictionary_id = dictionary_id e.source_term = source_term e.source_term_normalized = source_term_normalized e.target_term = target_term e.source_language = source_language e.target_language = target_language e.context_notes = context_notes e.context_data = context_data e.usage_notes = usage_notes e.has_context = has_context e.is_regex = is_regex e.context_source = context_source e.origin_source_language = origin_source_language e.created_at = created_at or __import__("datetime").datetime(2026, 1, 1) e.updated_at = updated_at return e # ============================================================================= # Dictionaries CRUD # ============================================================================= class TestListDictionaries: """GET /api/translate/dictionaries""" def test_list_success(self): mock_mgr = MagicMock() mock_mgr.list_dictionaries.return_value = ([_mock_dict()], 1) with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.get("/api/translate/dictionaries") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 assert len(data["items"]) == 1 assert data["items"][0]["id"] == "dict-1" assert data["items"][0]["name"] == "Test Dict" def test_list_empty(self): mock_mgr = MagicMock() mock_mgr.list_dictionaries.return_value = ([], 0) with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.get("/api/translate/dictionaries") assert resp.status_code == 200 data = resp.json() assert data["total"] == 0 assert data["items"] == [] def test_list_pagination_params(self): mock_mgr = MagicMock() mock_mgr.list_dictionaries.return_value = ([_mock_dict()], 1) with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.get("/api/translate/dictionaries?page=2&page_size=10") assert resp.status_code == 200 data = resp.json() assert data["page"] == 2 assert data["page_size"] == 10 class TestGetDictionary: """GET /api/translate/dictionaries/{dictionary_id}""" def test_get_success(self): mock_dict = _mock_dict() mock_mgr = MagicMock() mock_mgr.get_dictionary.return_value = mock_dict mock_db = MagicMock() mock_db.query.return_value.filter.return_value.count.return_value = 5 with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): from src.dependencies import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.get("/api/translate/dictionaries/dict-1") assert resp.status_code == 200 data = resp.json() assert data["id"] == "dict-1" assert data["entry_count"] == 5 def test_get_not_found_404(self): mock_mgr = MagicMock() mock_mgr.get_dictionary.side_effect = ValueError("Dictionary not found") with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.get("/api/translate/dictionaries/dict-missing") assert resp.status_code == 404 assert "Dictionary not found" in resp.text class TestCreateDictionary: """POST /api/translate/dictionaries""" def test_create_success(self): mock_dict = _mock_dict(id="dict-new", name="New Dict") mock_mgr = MagicMock() mock_mgr.create_dictionary.return_value = mock_dict with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.post("/api/translate/dictionaries", json={ "name": "New Dict", "description": "A new dictionary", "is_active": True, }) assert resp.status_code == 201 data = resp.json() assert data["id"] == "dict-new" assert data["entry_count"] == 0 def test_create_missing_name_422(self): client = _make_client() resp = client.post("/api/translate/dictionaries", json={ "description": "Missing name", }) assert resp.status_code == 422 assert "name" in resp.text class TestUpdateDictionary: """PUT /api/translate/dictionaries/{dictionary_id}""" def test_update_success(self): mock_dict = _mock_dict(id="dict-1", name="Updated Dict") mock_mgr = MagicMock() mock_mgr.update_dictionary.return_value = mock_dict mock_db = MagicMock() mock_db.query.return_value.filter.return_value.count.return_value = 3 with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): from src.dependencies import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.put("/api/translate/dictionaries/dict-1", json={ "name": "Updated Dict", "is_active": False, }) assert resp.status_code == 200 data = resp.json() assert data["name"] == "Updated Dict" assert data["entry_count"] == 3 def test_update_not_found_404(self): mock_mgr = MagicMock() mock_mgr.update_dictionary.side_effect = ValueError("Dictionary not found") with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.put("/api/translate/dictionaries/dict-missing", json={ "name": "Nope", "is_active": True, }) assert resp.status_code == 404 class TestDeleteDictionary: """DELETE /api/translate/dictionaries/{dictionary_id}""" def test_delete_success(self): mock_mgr = MagicMock() mock_mgr.delete_dictionary.return_value = None with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.delete("/api/translate/dictionaries/dict-1") assert resp.status_code == 204 def test_delete_not_found_404(self): mock_mgr = MagicMock() mock_mgr.delete_dictionary.side_effect = ValueError("Dictionary not found") with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.delete("/api/translate/dictionaries/dict-missing") assert resp.status_code == 404 def test_delete_conflict_409(self): mock_mgr = MagicMock() mock_mgr.delete_dictionary.side_effect = ValueError( "Dictionary has active/scheduled jobs attached" ) with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.delete("/api/translate/dictionaries/dict-busy") assert resp.status_code == 409 assert "active/scheduled" in resp.text # ============================================================================= # Dictionary Entries # ============================================================================= class TestListDictionaryEntries: """GET /api/translate/dictionaries/{dictionary_id}/entries""" def test_list_entries_success(self): entry = _mock_entry(id="e1") mock_mgr = MagicMock() mock_mgr.list_entries.return_value = ([entry], 1) with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.get("/api/translate/dictionaries/dict-1/entries") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 assert data["items"][0]["id"] == "e1" assert data["items"][0]["source_term"] == "hello" def test_list_entries_with_language_filters(self): entry_en_es = _mock_entry(id="e1", source_language="en", target_language="es") entry_fr_de = _mock_entry(id="e2", source_language="fr", target_language="de") mock_mgr = MagicMock() mock_mgr.list_entries.return_value = ([entry_en_es, entry_fr_de], 2) with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.get( "/api/translate/dictionaries/dict-1/entries" "?source_language=en&target_language=es" ) assert resp.status_code == 200 data = resp.json() # Only en→es passes filter assert data["total"] == 1 assert data["items"][0]["id"] == "e1" def test_list_entries_only_target_language_filter(self): """Filter by target_language only — covers source skip at line 193 then target skip at line 195.""" entry_en_es = _mock_entry(id="e1", source_language="en", target_language="es") entry_fr_de = _mock_entry(id="e2", source_language="fr", target_language="de") mock_mgr = MagicMock() mock_mgr.list_entries.return_value = ([entry_en_es, entry_fr_de], 2) with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.get( "/api/translate/dictionaries/dict-1/entries?target_language=de" ) assert resp.status_code == 200 data = resp.json() # Only fr→de passes target_language filter assert data["total"] == 1 assert data["items"][0]["id"] == "e2" def test_list_entries_not_found_404(self): mock_mgr = MagicMock() mock_mgr.list_entries.side_effect = ValueError("Dictionary not found") with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.get("/api/translate/dictionaries/dict-missing/entries") assert resp.status_code == 404 class TestAddDictionaryEntry: """POST /api/translate/dictionaries/{dictionary_id}/entries""" def test_add_entry_success(self): entry = _mock_entry(id="e-new", dictionary_id="dict-1") mock_mgr = MagicMock() mock_mgr.add_entry.return_value = entry with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.post("/api/translate/dictionaries/dict-1/entries", json={ "source_term": "hello", "target_term": "hola", }) assert resp.status_code == 201 data = resp.json() assert data["id"] == "e-new" assert data["source_term"] == "hello" assert data["target_term"] == "hola" def test_add_entry_conflict_409(self): mock_mgr = MagicMock() mock_mgr.add_entry.side_effect = ValueError("Entry already exists") with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.post("/api/translate/dictionaries/dict-1/entries", json={ "source_term": "hello", "target_term": "hola", }) assert resp.status_code == 409 assert "already exists" in resp.text def test_add_entry_not_found_404(self): mock_mgr = MagicMock() mock_mgr.add_entry.side_effect = ValueError("not found") with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.post("/api/translate/dictionaries/dict-missing/entries", json={ "source_term": "hello", "target_term": "hola", }) assert resp.status_code == 404 def test_add_entry_422_missing_source(self): client = _make_client() resp = client.post("/api/translate/dictionaries/dict-1/entries", json={ "target_term": "hola", }) assert resp.status_code == 422 class TestEditDictionaryEntry: """PUT /api/translate/dictionaries/{dictionary_id}/entries/{entry_id}""" def test_edit_entry_success(self): entry = _mock_entry(id="e1", target_term="updated_hola") mock_mgr = MagicMock() mock_mgr.edit_entry.return_value = entry with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.put( "/api/translate/dictionaries/dict-1/entries/e1", json={ "source_term": "hello", "target_term": "updated_hola", } ) assert resp.status_code == 200 assert resp.json()["target_term"] == "updated_hola" def test_edit_entry_conflict_409(self): mock_mgr = MagicMock() mock_mgr.edit_entry.side_effect = ValueError("Entry already exists") with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.put( "/api/translate/dictionaries/dict-1/entries/e1", json={ "source_term": "hello", "target_term": "hola", } ) assert resp.status_code == 409 def test_edit_entry_not_found_404(self): mock_mgr = MagicMock() mock_mgr.edit_entry.side_effect = ValueError("Entry not found") with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.put( "/api/translate/dictionaries/dict-1/entries/e-missing", json={ "source_term": "hello", "target_term": "hola", } ) assert resp.status_code == 404 class TestDeleteDictionaryEntry: """DELETE /api/translate/dictionaries/{dictionary_id}/entries/{entry_id}""" def test_delete_entry_success(self): mock_mgr = MagicMock() mock_mgr.delete_entry.return_value = None with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.delete("/api/translate/dictionaries/dict-1/entries/e1") assert resp.status_code == 204 def test_delete_entry_not_found_404(self): mock_mgr = MagicMock() mock_mgr.delete_entry.side_effect = ValueError("Entry not found") with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.delete("/api/translate/dictionaries/dict-1/entries/e-missing") assert resp.status_code == 404 # ============================================================================= # Import # ============================================================================= class TestImportDictionaryEntries: """POST /api/translate/dictionaries/{dictionary_id}/import""" def test_import_success(self): mock_mgr = MagicMock() mock_mgr.import_entries.return_value = { "total": 2, "created": 2, "updated": 0, "skipped": 0, "errors": [], } with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.post("/api/translate/dictionaries/dict-1/import", json={ "content": "source,target\nhello,hola\nbye,adiós", "on_conflict": "overwrite", }) assert resp.status_code == 200 data = resp.json() assert data["total"] == 2 assert data["created"] == 2 def test_import_invalid_on_conflict_422(self): client = _make_client() resp = client.post("/api/translate/dictionaries/dict-1/import", json={ "content": "source,target\nhello,hola", "on_conflict": "invalid_option", }) assert resp.status_code == 422 assert "on_conflict" in resp.text def test_import_value_error_400(self): mock_mgr = MagicMock() mock_mgr.import_entries.side_effect = ValueError("Invalid CSV format") with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.post("/api/translate/dictionaries/dict-1/import", json={ "content": "broken content", "on_conflict": "overwrite", }) assert resp.status_code == 400 assert "Invalid CSV format" in resp.text def test_import_preview_only(self): mock_mgr = MagicMock() mock_mgr.import_entries.return_value = { "total": 1, "created": 0, "updated": 0, "skipped": 0, "errors": [], "preview": [{"source_term": "hello"}], } with patch("src.api.routes.translate._dictionary_routes.DictionaryManager", mock_mgr): client = _make_client() resp = client.post("/api/translate/dictionaries/dict-1/import", json={ "content": "source,target\nhello,hola", "on_conflict": "keep_existing", "preview_only": True, }) assert resp.status_code == 200 data = resp.json() assert "preview" in data assert data["preview"][0]["source_term"] == "hello" # #endregion Test.Api.TranslateDictionaryRoutes