"""Tests for i18n infrastructure. Covers TranslationService, locale detection helpers, and the convenience _() function. """ from types import SimpleNamespace from unittest.mock import MagicMock import pytest from app.infrastructure.i18n.translator import ( DEFAULT_LOCALE, TranslationService, _, ) from app.presentation.web.locale import ( _get_best_locale, _parse_accept_language, setup_locale_manager, ) def _make_mock_request( cookies: dict[str, str] | None = None, headers: dict[str, str] | None = None, ) -> MagicMock: """Create a mock FastAPI Request with controlled state, cookies and headers.""" request = MagicMock() request.state = SimpleNamespace() request.cookies = cookies or {} request.headers = headers or {} return request class TestTranslationService: """Test TranslationService get_text resolution and fallback chain.""" def test_get_text_returns_translation_for_existing_key(self) -> None: """Test get_text returns the correct translation for a known key.""" ts = TranslationService() result = ts.get_text("nav.home", "ru") assert result == "Главная" def test_get_text_returns_english_fallback_for_missing_key(self) -> None: """Test get_text falls back to English when locale lacks the key.""" ts = TranslationService() result = ts.get_text("nav.home", "fr") assert result == "Accueil" def test_get_text_returns_key_when_neither_locale_nor_en_has_it(self) -> None: """Test get_text returns the key itself when no translation exists anywhere.""" ts = TranslationService() result = ts.get_text("nonexistent.key", "de") assert result == "nonexistent.key" def test_get_text_returns_english_fallback_for_unknown_locale(self) -> None: """Test get_text returns English when the requested locale does not exist.""" ts = TranslationService() result = ts.get_text("nav.home", "zz") assert result == "Home" def test_get_text_returns_en_when_requested_locale_is_en(self) -> None: """Test get_text returns English string when locale is en.""" ts = TranslationService() result = ts.get_text("nav.home", "en") assert result == "Home" def test_get_text_returns_en_fallback_when_requested_locale_missing_key(self) -> None: """Test get_text falls back to English when locale is unknown and key exists in en.""" ts = TranslationService() result = ts.get_text("about.signed_in", "zz") assert result == "Signed in as {username}." def test_singleton_returns_same_instance(self) -> None: """Test TranslationService.get_instance always returns the same instance.""" instance_a = TranslationService.get_instance() instance_b = TranslationService.get_instance() assert instance_a is instance_b def test_singleton_shares_translations(self) -> None: """Test that multiple calls via singleton share the same data.""" instance_a = TranslationService.get_instance() instance_b = TranslationService.get_instance() assert instance_a.translations is instance_b.translations class TestConvenienceFunction: """Test the module-level _() convenience function.""" def test_convenience_function_returns_translation(self) -> None: """Test _() returns correct translation for a given key.""" result = _("header.logo", "de") assert result == "Blog" def test_convenience_function_defaults_to_english(self) -> None: """Test _() uses DEFAULT_LOCALE when no locale is given.""" result = _("nav.posts") assert result == "Posts" assert DEFAULT_LOCALE == "en" def test_convenience_function_returns_key_on_missing(self) -> None: """Test _() returns key when translation does not exist.""" result = _("utterly.fake.key", "fr") assert result == "utterly.fake.key" class TestParseAcceptLanguage: """Test the Accept-Language header parser.""" def test_empty_header_returns_empty_list(self) -> None: """Test empty Accept-Language returns empty list.""" assert _parse_accept_language("") == [] def test_single_locale(self) -> None: """Test a single locale code is returned as a single-element list.""" assert _parse_accept_language("fr") == ["fr"] def test_multiple_locales_with_quality(self) -> None: """Test multiple locales with q-values are returned in order.""" result = _parse_accept_language("fr, en;q=0.9, de;q=0.8") assert result == ["fr", "en", "de"] def test_region_code_strips_to_base_language(self) -> None: """Test fr-FR is normalised to fr.""" assert _parse_accept_language("fr-FR") == ["fr"] def test_complex_header(self) -> None: """Test a realistic Accept-Language header with multiple locales.""" result = _parse_accept_language("ru-RU, ru;q=0.9, en;q=0.5") assert result == ["ru", "ru", "en"] def test_whitespace_around_locales(self) -> None: """Test whitespace around locale codes is handled gracefully.""" result = _parse_accept_language(" en , fr;q=0.5 ") assert result == ["en", "fr"] class TestGetBestLocale: """Test locale detection from cookie and Accept-Language header.""" def test_cookie_takes_priority(self) -> None: """Test cookie value is used when it is a supported locale.""" request = _make_mock_request(cookies={"locale": "de"}, headers={"accept-language": "ru"}) assert _get_best_locale(request) == "de" def test_invalid_cookie_falls_back_to_header(self) -> None: """Test unsupported locale in cookie falls back to Accept-Language.""" request = _make_mock_request(cookies={"locale": "zz"}, headers={"accept-language": "fr"}) assert _get_best_locale(request) == "fr" def test_no_cookie_uses_accept_language(self) -> None: """Test Accept-Language is used when no cookie is present.""" request = _make_mock_request(headers={"accept-language": "de"}) assert _get_best_locale(request) == "de" def test_no_cookie_no_header_returns_default(self) -> None: """Test default locale returned when neither cookie nor header matches.""" request = _make_mock_request() assert _get_best_locale(request) == DEFAULT_LOCALE def test_accept_language_unsupported_returns_default(self) -> None: """Test unsupported language in header falls back to default.""" request = _make_mock_request(headers={"accept-language": "zh"}) assert _get_best_locale(request) == DEFAULT_LOCALE def test_missing_cookie_key_uses_header(self) -> None: """Test absent locale cookie is treated as no preference.""" request = _make_mock_request(cookies={"other": "val"}, headers={"accept-language": "ru"}) assert _get_best_locale(request) == "ru" class TestSetupLocaleManager: """Test the middleware helper that sets locale on request state.""" @pytest.mark.asyncio async def test_sets_locale_on_request_state(self) -> None: """Test setup_locale_manager sets locale when not already present.""" request = _make_mock_request(headers={"accept-language": "fr"}) await setup_locale_manager(request) assert request.state.locale == "fr" @pytest.mark.asyncio async def test_does_not_override_existing_locale(self) -> None: """Test setup_locale_manager does not override an already-set locale.""" request = _make_mock_request(headers={"accept-language": "fr"}) request.state.locale = "de" await setup_locale_manager(request) assert request.state.locale == "de" @pytest.mark.asyncio async def test_default_locale_when_no_match(self) -> None: """Test setup_locale_manager uses default when nothing matches.""" request = _make_mock_request() await setup_locale_manager(request) assert request.state.locale == DEFAULT_LOCALE