536 lines
20 KiB
Python
536 lines
20 KiB
Python
# #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
|