mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-14 17:35:06 +00:00
feat(tests): add/update tests
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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/"
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user