feat(tests): add/update tests

This commit is contained in:
Ivan
2026-05-02 04:38:28 -05:00
parent 5e5ce253b8
commit a067afaa1f
8 changed files with 254 additions and 9 deletions
@@ -141,3 +141,16 @@ def test_get_announce_count_by_aspect(announce_dao):
assert announce_dao.get_announce_count_by_aspect("lxmf.delivery") == 2
assert announce_dao.get_announce_count_by_aspect("nomadnetwork.node") == 1
assert announce_dao.get_announce_count_by_aspect("lxmf.propagation") == 0
def test_get_favourite_by_destination_hash(announce_dao):
assert announce_dao.get_favourite_by_destination_hash("missing") is None
announce_dao.upsert_favourite("dh1", "Original", "nomadnetwork.node")
row = announce_dao.get_favourite_by_destination_hash("dh1")
assert row["destination_hash"] == "dh1"
assert row["display_name"] == "Original"
assert row["aspect"] == "nomadnetwork.node"
announce_dao.upsert_custom_display_name("dh1", "Renamed")
announce_dao.upsert_favourite("dh1", "Renamed", row["aspect"])
row2 = announce_dao.get_favourite_by_destination_hash("dh1")
assert row2["display_name"] == "Renamed"
+64
View File
@@ -5,6 +5,7 @@ import pytest
from meshchatx.src.backend.http_url_guard import (
UnsafeOutboundUrlError,
normalize_loopback_http_service_base,
normalize_libretranslate_http_service_base,
)
@@ -72,3 +73,66 @@ def test_normalize_accepts_loopback_variants(edge):
def test_normalize_rejects_scheme_or_crlf_injection(bad):
with pytest.raises(UnsafeOutboundUrlError):
normalize_loopback_http_service_base(bad)
def test_normalize_libretranslate_public_https():
assert normalize_libretranslate_http_service_base(
"https://libretranslate.com/path"
) == ("https://libretranslate.com")
def test_normalize_libretranslate_remote_host_port_strip_path():
assert normalize_libretranslate_http_service_base(
"http://superfishy.example:5002/languages",
) == ("http://superfishy.example:5002")
def test_normalize_libretranslate_private_and_loopback_ips():
assert normalize_libretranslate_http_service_base("http://10.20.30.40:5123/") == (
"http://10.20.30.40:5123"
)
assert normalize_libretranslate_http_service_base("http://127.0.0.1:5000") == (
"http://127.0.0.1:5000"
)
@pytest.mark.parametrize(
"bad",
[
"http://169.254.169.254/latest",
"http://239.255.0.1:5000/",
"http://0.0.0.0/",
"http://240.0.0.1:1",
],
)
def test_normalize_libretranslate_rejects_ssrf_lit_ips(bad):
with pytest.raises(UnsafeOutboundUrlError):
normalize_libretranslate_http_service_base(bad)
@pytest.mark.parametrize(
"bad",
[
"ftp://example.com/",
"http://user:pass@192.168.1.1:5000",
],
)
def test_normalize_libretranslate_rejects_scheme_or_creds(bad):
with pytest.raises(UnsafeOutboundUrlError):
normalize_libretranslate_http_service_base(bad)
@pytest.mark.parametrize(
"bad",
["", " ", "http+unix://%2Ftmp%2Fs.sock"],
)
def test_normalize_libretranslate_rejects_scheme_or_empty(bad):
with pytest.raises(UnsafeOutboundUrlError):
normalize_libretranslate_http_service_base(bad)
def test_normalize_libretranslate_rejects_encoded_crlf_in_host():
with pytest.raises(UnsafeOutboundUrlError):
normalize_libretranslate_http_service_base(
"http://127.0.0.1%0d%0a.evil.com:80/"
)
+33
View File
@@ -73,6 +73,38 @@ async def test_update_config_theme(mock_app):
mock_app.config.theme.set.assert_called_with("dark")
@pytest.mark.asyncio
async def test_update_config_libretranslate_api_key(mock_app):
mock_app.send_config_to_websocket_clients = MagicMock(return_value=asyncio.Future())
mock_app.send_config_to_websocket_clients.return_value.set_result(None)
mock_app.config.libretranslate_api_key = MagicMock()
mock_app.translator_handler = MagicMock()
await mock_app.update_config({"libretranslate_api_key": " sek "})
mock_app.config.libretranslate_api_key.set.assert_called_once_with("sek")
assert mock_app.translator_handler.libretranslate_api_key == "sek"
@pytest.mark.asyncio
async def test_update_config_libretranslate_api_key_empty_clears(mock_app):
mock_app.send_config_to_websocket_clients = MagicMock(return_value=asyncio.Future())
mock_app.send_config_to_websocket_clients.return_value.set_result(None)
mock_app.config.libretranslate_api_key = MagicMock()
mock_app.translator_handler = MagicMock()
await mock_app.update_config({"libretranslate_api_key": ""})
mock_app.config.libretranslate_api_key.set.assert_called_once_with(None)
assert mock_app.translator_handler.libretranslate_api_key is None
@pytest.mark.asyncio
async def test_update_config_libretranslate_api_key_length_limit(mock_app):
mock_app.send_config_to_websocket_clients = MagicMock(return_value=asyncio.Future())
mock_app.send_config_to_websocket_clients.return_value.set_result(None)
mock_app.config.libretranslate_api_key = MagicMock()
mock_app.translator_handler = MagicMock()
with pytest.raises(ValueError):
await mock_app.update_config({"libretranslate_api_key": "z" * 513})
def test_get_config_dict_no_context(mock_app):
mock_app.current_context = None
assert mock_app.get_config_dict() == {}
@@ -149,6 +181,7 @@ def test_get_config_dict_basic(mock_app):
"translator_argos_enabled",
"translator_libretranslate_enabled",
"libretranslate_url",
"libretranslate_api_key",
"desktop_open_calls_in_separate_window",
"desktop_hardware_acceleration_enabled",
"blackhole_integration_enabled",
+2
View File
@@ -68,6 +68,8 @@ class TestTranslatorHandler(unittest.TestCase):
result = self.handler.translate_text("Hello", "en", "de", use_argos=False)
self.assertEqual(result["translated_text"], "Hallo")
self.assertEqual(result["source"], "libretranslate")
body = mock_session.post.call_args.kwargs.get("json")
self.assertNotIn("api_key", body)
if __name__ == "__main__":
@@ -4,7 +4,10 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from meshchatx.src.backend.translator_handler import TranslatorHandler
from meshchatx.src.backend.translator_handler import (
TranslatorHandler,
_normalize_optional_libretranslate_api_key,
)
def test_translator_handler_init():
@@ -17,6 +20,11 @@ def test_translator_handler_init():
assert handler.translator_argos_enabled is True
def test_translator_handler_init_optional_api_key_stripped():
handler = TranslatorHandler(libretranslate_api_key=" trim ")
assert handler.libretranslate_api_key == "trim"
def test_get_supported_languages_no_backends():
handler = TranslatorHandler()
handler.has_requests = False
@@ -202,7 +210,7 @@ def test_language_code_to_name():
@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")
def test_get_supported_languages_skips_non_loopback_stored_url(mock_session_cls):
def test_get_supported_languages_skips_link_local_lit_stored_url(mock_session_cls):
handler = TranslatorHandler(
libretranslate_url="http://169.254.169.254/",
translator_libretranslate_enabled=True,
@@ -215,15 +223,30 @@ def test_get_supported_languages_skips_non_loopback_stored_url(mock_session_cls)
mock_session_cls.assert_not_called()
def test_translate_text_rejects_non_loopback_libre_url():
@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")
def test_translate_text_accepts_remote_libre_url(mock_session_cls):
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value={"translatedText": "Hallo"})
mock_post = MagicMock()
mock_post.__aenter__ = AsyncMock(return_value=mock_response)
mock_post.__aexit__ = AsyncMock(return_value=None)
mock_session = MagicMock()
mock_session.post = MagicMock(return_value=mock_post)
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
mock_session.__aexit__ = AsyncMock(return_value=None)
mock_session_cls.return_value = mock_session
handler = TranslatorHandler(
libretranslate_url="http://example.com:5000/",
translator_libretranslate_enabled=True,
translator_argos_enabled=False,
)
handler.has_requests = True
with pytest.raises(ValueError, match="URL host must be"):
handler.translate_text("Hello", "en", "de", use_argos=False)
result = handler.translate_text("Hello", "en", "de", use_argos=False)
assert result["translated_text"] == "Hallo"
called_url = mock_session.post.call_args.args[0]
assert called_url.startswith("http://example.com:5000")
def test_get_translator_languages_response_explicit_bad_override_raises():
@@ -232,7 +255,105 @@ def test_get_translator_languages_response_explicit_bad_override_raises():
translator_libretranslate_enabled=True,
)
handler.has_requests = True
with pytest.raises(ValueError, match="URL host must be"):
with pytest.raises(ValueError, match="IPv4 link-local"):
handler.get_translator_languages_response(
libretranslate_url="http://metadata.example/latest",
libretranslate_url="http://169.254.169.254:5000",
)
def test_normalize_optional_libretranslate_api_key():
assert _normalize_optional_libretranslate_api_key(None) is None
assert _normalize_optional_libretranslate_api_key(" ") is None
assert _normalize_optional_libretranslate_api_key(" xyz ") == "xyz"
with pytest.raises(ValueError):
_normalize_optional_libretranslate_api_key("k" * 513)
@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")
def test_get_supported_languages_sends_api_key_for_languages_when_set(mock_session_cls):
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value=[{"code": "en", "name": "English"}])
mock_get = MagicMock()
mock_get.__aenter__ = AsyncMock(return_value=mock_response)
mock_get.__aexit__ = AsyncMock(return_value=None)
mock_session = MagicMock()
mock_session.get = MagicMock(return_value=mock_get)
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
mock_session.__aexit__ = AsyncMock(return_value=None)
mock_session_cls.return_value = mock_session
handler = TranslatorHandler(
translator_libretranslate_enabled=True,
libretranslate_api_key="srv-key",
)
handler.has_requests = True
handler.has_argos = False
handler.has_argos_lib = False
handler.has_argos_cli = False
handler.get_supported_languages()
kw = mock_session.get.call_args.kwargs
assert kw.get("params") == {"api_key": "srv-key"}
@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")
def test_translate_sends_optional_api_key_in_json_body(mock_session_cls):
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"translatedText": "Bonjour",
"detectedLanguage": {"language": "en"},
},
)
mock_post_ctx = MagicMock()
mock_post_ctx.__aenter__ = AsyncMock(return_value=mock_response)
mock_post_ctx.__aexit__ = AsyncMock(return_value=None)
mock_session = MagicMock()
mock_session.post = MagicMock(return_value=mock_post_ctx)
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
mock_session.__aexit__ = AsyncMock(return_value=None)
mock_session_cls.return_value = mock_session
handler = TranslatorHandler(
translator_libretranslate_enabled=True,
libretranslate_api_key="abc123",
translator_argos_enabled=False,
)
handler.has_requests = True
handler.translate_text("Hello", "en", "fr", use_argos=False)
body = mock_session.post.call_args.kwargs.get("json")
assert body.get("api_key") == "abc123"
@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")
def test_translate_explicit_api_key_overrides_handler_default(mock_session_cls):
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value={"translatedText": "Hi"})
mock_post_ctx = MagicMock()
mock_post_ctx.__aenter__ = AsyncMock(return_value=mock_response)
mock_post_ctx.__aexit__ = AsyncMock(return_value=None)
mock_session = MagicMock()
mock_session.post = MagicMock(return_value=mock_post_ctx)
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
mock_session.__aexit__ = AsyncMock(return_value=None)
mock_session_cls.return_value = mock_session
handler = TranslatorHandler(
translator_libretranslate_enabled=True,
libretranslate_api_key="stored",
translator_argos_enabled=False,
)
handler.has_requests = True
handler.translate_text(
"Hello",
"en",
"de",
use_argos=False,
libretranslate_api_key=" one-off ",
)
body = mock_session.post.call_args.kwargs.get("json")
assert body.get("api_key") == "one-off"
@@ -365,7 +365,7 @@ describe("ConversationViewer.vue button interactions", () => {
"/api/v1/translator/translate",
expect.objectContaining({
text: "hello",
source_lang: "en",
source_lang: "auto",
target_lang: "de",
use_argos: true,
})
+12 -1
View File
@@ -20,6 +20,7 @@ describe("TranslatorPage.vue", () => {
translator_argos_enabled: true,
translator_libretranslate_enabled: true,
libretranslate_url: "http://localhost:5000",
libretranslate_api_key: null,
},
},
});
@@ -49,10 +50,19 @@ describe("TranslatorPage.vue", () => {
});
const mountTranslatorPage = () => {
const tMap = {
"translator.api_server": "LibreTranslate API Server",
"translator.api_server_description":
"Enter the base URL of your LibreTranslate server (e.g., http://localhost:5000)",
"translator.api_key_optional": "LibreTranslate API key (optional)",
"translator.api_key_placeholder": "Leave empty unless your provider requires one",
"translator.api_key_description": "If required, LibreTranslate expects api_key in the JSON translate body.",
"translator.failed_load_languages": "Failed",
};
return mount(TranslatorPage, {
global: {
mocks: {
$t: (key) => key,
$t: (key) => (key in tMap ? tMap[key] : key),
},
stubs: {
MaterialDesignIcon: {
@@ -80,6 +90,7 @@ describe("TranslatorPage.vue", () => {
translator_argos_enabled: false,
translator_libretranslate_enabled: false,
libretranslate_url: "http://127.0.0.1:5000",
libretranslate_api_key: null,
},
},
});
@@ -91,6 +91,7 @@ export function buildFullServerConfig(overrides = {}) {
translator_argos_enabled: false,
translator_libretranslate_enabled: false,
libretranslate_url: "http://localhost:5000",
libretranslate_api_key: null,
desktop_open_calls_in_separate_window: false,
desktop_hardware_acceleration_enabled: true,
blackhole_integration_enabled: true,