Files
ss-tools/backend/tests/api/test_translate_dictionary_routes.py

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