mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-09-10 21:26:48 +00:00
catalog() joined the requested language code straight into the UI and content catalogue paths, and read_catalog logged those paths. Both the negotiated Accept-Language code and the /<lang>/ route only ever pass supported codes, but that guarantee lived in the callers, so CodeQL reported path injection and log injection on the request value. catalog() now resolves the code through a table of the supported languages and builds the file names from the table's value, so an unsupported code returns an empty catalogue and never becomes a path. A unit test holds "../content/de" to that without reading any file, and the translate_tree fixture moves from the made-up code "xx" to "de" because unsupported codes now translate to English by design. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
238 lines
8.3 KiB
Python
238 lines
8.3 KiB
Python
import re
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import yaml
|
|
|
|
from app.utils import i18n
|
|
|
|
|
|
class TestNegotiate(unittest.TestCase):
|
|
def test_regional_tag_beats_a_lower_ranked_exact_match(self):
|
|
self.assertEqual(i18n.negotiate([("de-DE", 1.0), ("en", 0.8)]), "de")
|
|
|
|
def test_an_uppercase_tag_is_accepted(self):
|
|
self.assertEqual(i18n.negotiate([("DE-DE", 1.0)]), "de")
|
|
|
|
def test_underscore_separated_tag_is_accepted(self):
|
|
self.assertEqual(i18n.negotiate([("pt_BR", 1.0)]), "pt")
|
|
|
|
def test_highest_quality_supported_tag_wins(self):
|
|
self.assertEqual(
|
|
i18n.negotiate([("xx", 1.0), ("fr", 0.9), ("es", 0.5)]),
|
|
"fr",
|
|
)
|
|
|
|
def test_unsupported_tags_fall_back_to_the_default(self):
|
|
self.assertEqual(i18n.negotiate([("xx", 1.0)]), "en")
|
|
self.assertEqual(i18n.negotiate([], default="de"), "de")
|
|
|
|
def test_a_refused_language_is_not_selected(self):
|
|
self.assertEqual(i18n.negotiate([("de", 0.0)]), "en")
|
|
|
|
def test_the_first_of_two_equal_tags_wins(self):
|
|
self.assertEqual(i18n.negotiate([("fr", 0.9), ("es", 0.9)]), "fr")
|
|
|
|
|
|
class TestDirection(unittest.TestCase):
|
|
def test_every_right_to_left_language_is_marked(self):
|
|
for code in ("ar", "dv", "fa", "he", "ks", "ps", "sd", "ug", "ur", "yi"):
|
|
with self.subTest(code=code):
|
|
self.assertEqual(i18n.direction(code), "rtl")
|
|
|
|
def test_other_languages_are_left_to_right(self):
|
|
for code in set(i18n.LANGUAGES) - set(i18n.RTL_LANGUAGES):
|
|
with self.subTest(code=code):
|
|
self.assertEqual(i18n.direction(code), "ltr")
|
|
|
|
|
|
class TestTranslateTree(unittest.TestCase):
|
|
def setUp(self):
|
|
self.addCleanup(i18n._catalogs.clear)
|
|
i18n._catalogs["de"] = {"A card": "Eine Karte", "Pictures": "Bilder"}
|
|
|
|
def test_only_translatable_keys_are_replaced(self):
|
|
tree = {
|
|
"cards": [
|
|
{
|
|
"title": "Pictures",
|
|
"text": "A card",
|
|
"url": "A card",
|
|
"icon": {"class": "Pictures"},
|
|
}
|
|
]
|
|
}
|
|
|
|
translated = i18n.translate_tree(tree, "de")
|
|
|
|
card = translated["cards"][0]
|
|
self.assertEqual(card["title"], "Bilder")
|
|
self.assertEqual(card["text"], "Eine Karte")
|
|
self.assertEqual(card["url"], "A card")
|
|
self.assertEqual(card["icon"]["class"], "Pictures")
|
|
|
|
def test_strings_inside_a_list_are_translated(self):
|
|
translated = i18n.translate_tree({"text": ["A card", "Pictures"]}, "de")
|
|
|
|
self.assertEqual(translated["text"], ["Eine Karte", "Bilder"])
|
|
|
|
def test_unknown_strings_keep_their_source_value(self):
|
|
translated = i18n.translate_tree({"description": "Untranslated"}, "de")
|
|
|
|
self.assertEqual(translated["description"], "Untranslated")
|
|
|
|
def test_source_tree_is_left_untouched(self):
|
|
tree = {"name": "Pictures"}
|
|
|
|
i18n.translate_tree(tree, "de")
|
|
|
|
self.assertEqual(tree["name"], "Pictures")
|
|
|
|
def test_non_string_leaves_survive(self):
|
|
tree = {"name": 1, "text": None, "info": True}
|
|
|
|
self.assertEqual(i18n.translate_tree(tree, "de"), tree)
|
|
|
|
|
|
class TestReadCatalog(unittest.TestCase):
|
|
def setUp(self):
|
|
self.directory = Path(tempfile.mkdtemp())
|
|
self.addCleanup(shutil.rmtree, self.directory, True)
|
|
self.path = self.directory / "de.yaml"
|
|
|
|
def _read(self, text):
|
|
self.path.write_text(text, encoding="utf-8")
|
|
with self.assertLogs(level="WARNING"):
|
|
return i18n.read_catalog(self.path)
|
|
|
|
def test_absent_file_is_an_empty_catalog(self):
|
|
self.assertEqual(i18n.read_catalog(self.directory / "missing.yaml"), {})
|
|
|
|
def test_unparsable_yaml_is_an_empty_catalog(self):
|
|
self.assertEqual(self._read("Close: [unclosed"), {})
|
|
|
|
def test_tab_indentation_is_an_empty_catalog(self):
|
|
self.assertEqual(self._read("a:\n\tb: 1"), {})
|
|
|
|
def test_non_mapping_document_is_an_empty_catalog(self):
|
|
self.assertEqual(self._read("- a\n- b"), {})
|
|
|
|
def test_a_non_utf8_catalog_is_an_empty_catalog(self):
|
|
self.path.write_bytes(b"\xffClose: Schliessen\n")
|
|
|
|
with self.assertLogs(level="WARNING"):
|
|
self.assertEqual(i18n.read_catalog(self.path), {})
|
|
|
|
def test_a_directory_at_the_catalog_path_is_an_empty_catalog(self):
|
|
(self.directory / "sub.yaml").mkdir()
|
|
|
|
with self.assertLogs(level="WARNING"):
|
|
self.assertEqual(i18n.read_catalog(self.directory / "sub.yaml"), {})
|
|
|
|
def test_non_string_entries_are_dropped(self):
|
|
self.path.write_text(
|
|
"Close: 42\nOpen:\nCopy: yes\n123: Zahl\nyes: Ja\nImprint: Impressum\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
self.assertEqual(i18n.read_catalog(self.path), {"Imprint": "Impressum"})
|
|
|
|
def test_a_missing_catalog_is_silent(self):
|
|
with self.assertNoLogs(level="WARNING"):
|
|
i18n.read_catalog(self.directory / "absent.yaml")
|
|
|
|
|
|
class TestCatalogMerge(unittest.TestCase):
|
|
def setUp(self):
|
|
directory = Path(tempfile.mkdtemp())
|
|
self.addCleanup(shutil.rmtree, directory, True)
|
|
(directory / "ui").mkdir()
|
|
(directory / "content").mkdir()
|
|
|
|
self.addCleanup(setattr, i18n, "UI_DIR", i18n.UI_DIR)
|
|
self.addCleanup(setattr, i18n, "CONTENT_DIR", i18n.CONTENT_DIR)
|
|
self.addCleanup(i18n.clear_catalogs)
|
|
i18n.UI_DIR = directory / "ui"
|
|
i18n.CONTENT_DIR = directory / "content"
|
|
i18n.clear_catalogs()
|
|
|
|
def test_the_content_catalog_overrides_the_shipped_interface_string(self):
|
|
(i18n.UI_DIR / "de.yaml").write_text(
|
|
"Close: Schliessen\nImprint: Impressum\n", encoding="utf-8"
|
|
)
|
|
(i18n.CONTENT_DIR / "de.yaml").write_text("Close: Zumachen\n", encoding="utf-8")
|
|
|
|
self.assertEqual(
|
|
i18n.catalog("de"), {"Close": "Zumachen", "Imprint": "Impressum"}
|
|
)
|
|
|
|
def test_an_unsupported_code_never_becomes_a_path(self):
|
|
with mock.patch.object(i18n, "read_catalog") as read:
|
|
self.assertEqual(i18n.catalog("../content/de"), {})
|
|
|
|
read.assert_not_called()
|
|
|
|
|
|
class TestShippedCatalogs(unittest.TestCase):
|
|
def test_every_iso_639_1_language_is_offered(self):
|
|
self.assertEqual(len(i18n.LANGUAGES), 184)
|
|
self.assertEqual(i18n.SOURCE_LANGUAGE, next(iter(i18n.LANGUAGES)))
|
|
self.assertGreaterEqual(
|
|
set(i18n.LANGUAGES), {"en", "de", "zh", "ar", "he", "vo", "za", "cu"}
|
|
)
|
|
|
|
def test_every_display_name_is_filled_in(self):
|
|
blank = [code for code, name in i18n.LANGUAGES.items() if not name.strip()]
|
|
|
|
self.assertFalse(blank, f"No display name for: {blank}")
|
|
|
|
def test_every_code_is_usable_in_the_route_converter(self):
|
|
offenders = [
|
|
code for code in i18n.LANGUAGES if not re.fullmatch(r"[a-z]+", code)
|
|
]
|
|
|
|
self.assertFalse(offenders, f"Not usable in the route converter: {offenders}")
|
|
|
|
def test_every_shipped_catalog_names_a_known_language(self):
|
|
stray = [
|
|
path.stem
|
|
for path in i18n.UI_DIR.glob("*.yaml")
|
|
if path.stem not in i18n.LANGUAGES or path.stem == i18n.SOURCE_LANGUAGE
|
|
]
|
|
|
|
self.assertFalse(stray, f"Catalogue for an unknown language: {stray}")
|
|
|
|
def test_ui_catalogs_cover_exactly_the_interface_strings(self):
|
|
expected = set(i18n.UI_STRINGS)
|
|
mismatched = {}
|
|
|
|
for path in sorted(i18n.UI_DIR.glob("*.yaml")):
|
|
code = path.stem
|
|
entries = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
if set(entries) != expected:
|
|
mismatched[code] = {
|
|
"missing": sorted(expected - set(entries)),
|
|
"unexpected": sorted(set(entries) - expected),
|
|
}
|
|
|
|
self.assertFalse(mismatched, f"UI catalogues out of sync: {mismatched}")
|
|
|
|
def test_ui_strings_of_the_source_language_are_the_source(self):
|
|
self.assertEqual(
|
|
i18n.ui_strings("en"),
|
|
{source: source for source in i18n.UI_STRINGS},
|
|
)
|
|
|
|
def test_ui_strings_are_translated_for_a_shipped_language(self):
|
|
strings = i18n.ui_strings("de")
|
|
|
|
self.assertEqual(strings["Close"], "Schließen")
|
|
self.assertEqual(strings["Imprint"], "Impressum")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|