feat: unify language handling across the application
- Updated the language configuration to use a single source of truth from `DEFAULT_LANGUAGE` for the bot, API, and Mini App, eliminating auto-detection from user settings. - Refactored the `get_lang` function to always return `DEFAULT_LANGUAGE`, ensuring consistent language usage throughout the application. - Modified the handling of language in various components, including API responses and UI elements, to reflect the new language management approach. - Enhanced documentation and comments to clarify the changes in language handling. - Added unit tests to verify the new language handling behavior and ensure coverage for the updated functionality.
This commit is contained in:
@@ -10,24 +10,27 @@ import duty_teller.config as config
|
||||
|
||||
|
||||
class TestLangFromAcceptLanguage:
|
||||
"""Tests for _lang_from_accept_language."""
|
||||
"""Tests for _lang_from_accept_language: always returns config.DEFAULT_LANGUAGE."""
|
||||
|
||||
def test_none_returns_default(self):
|
||||
def test_always_returns_default_language(self):
|
||||
"""Header is ignored; result is always config.DEFAULT_LANGUAGE."""
|
||||
assert deps._lang_from_accept_language(None) == config.DEFAULT_LANGUAGE
|
||||
|
||||
def test_empty_string_returns_default(self):
|
||||
assert deps._lang_from_accept_language("") == config.DEFAULT_LANGUAGE
|
||||
assert deps._lang_from_accept_language(" ") == config.DEFAULT_LANGUAGE
|
||||
assert deps._lang_from_accept_language("ru-RU,ru;q=0.9") == config.DEFAULT_LANGUAGE
|
||||
assert deps._lang_from_accept_language("en-US") == config.DEFAULT_LANGUAGE
|
||||
assert deps._lang_from_accept_language("zz") == config.DEFAULT_LANGUAGE
|
||||
assert deps._lang_from_accept_language("x") == config.DEFAULT_LANGUAGE
|
||||
|
||||
def test_ru_ru_returns_ru(self):
|
||||
assert deps._lang_from_accept_language("ru-RU,ru;q=0.9") == "ru"
|
||||
def test_returns_ru_when_default_language_is_ru(self):
|
||||
with patch.object(config, "DEFAULT_LANGUAGE", "ru"):
|
||||
assert deps._lang_from_accept_language("en-US") == "ru"
|
||||
assert deps._lang_from_accept_language(None) == "ru"
|
||||
|
||||
def test_en_us_returns_en(self):
|
||||
assert deps._lang_from_accept_language("en-US") == "en"
|
||||
|
||||
def test_invalid_fallback_to_en(self):
|
||||
assert deps._lang_from_accept_language("zz") == "en"
|
||||
assert deps._lang_from_accept_language("x") == "en"
|
||||
def test_returns_en_when_default_language_is_en(self):
|
||||
with patch.object(config, "DEFAULT_LANGUAGE", "en"):
|
||||
assert deps._lang_from_accept_language("ru-RU") == "en"
|
||||
assert deps._lang_from_accept_language(None) == "en"
|
||||
|
||||
|
||||
class TestAuthErrorDetail:
|
||||
|
||||
@@ -23,6 +23,41 @@ def test_health(client):
|
||||
assert r.json() == {"status": "ok"}
|
||||
|
||||
|
||||
def test_health_has_vary_accept_language(client):
|
||||
"""NoCacheStaticMiddleware adds Vary: Accept-Language to all responses."""
|
||||
r = client.get("/health")
|
||||
assert r.status_code == 200
|
||||
assert "accept-language" in r.headers.get("vary", "").lower()
|
||||
|
||||
|
||||
def test_app_static_has_no_store_and_vary(client):
|
||||
"""Static files under /app get Cache-Control: no-store and Vary: Accept-Language."""
|
||||
r = client.get("/app/")
|
||||
if r.status_code != 200:
|
||||
r = client.get("/app")
|
||||
assert r.status_code == 200, "webapp static mount should serve index at /app or /app/"
|
||||
assert r.headers.get("cache-control") == "no-store"
|
||||
assert "accept-language" in r.headers.get("vary", "").lower()
|
||||
|
||||
|
||||
def test_app_js_has_no_store(client):
|
||||
"""JS and all static under /app get Cache-Control: no-store."""
|
||||
r = client.get("/app/js/main.js")
|
||||
assert r.status_code == 200
|
||||
assert r.headers.get("cache-control") == "no-store"
|
||||
|
||||
|
||||
def test_app_config_js_returns_lang_from_default_language(client):
|
||||
"""GET /app/config.js returns JS setting window.__DT_LANG from config.DEFAULT_LANGUAGE."""
|
||||
r = client.get("/app/config.js")
|
||||
assert r.status_code == 200
|
||||
assert r.headers.get("content-type", "").startswith("application/javascript")
|
||||
assert r.headers.get("cache-control") == "no-store"
|
||||
body = r.text
|
||||
assert "window.__DT_LANG" in body
|
||||
assert config.DEFAULT_LANGUAGE in body
|
||||
|
||||
|
||||
def test_duties_invalid_date_format(client):
|
||||
r = client.get("/api/duties", params={"from": "2025-01-01", "to": "invalid"})
|
||||
assert r.status_code == 400
|
||||
|
||||
@@ -1,48 +1,46 @@
|
||||
"""Unit tests for duty_teller.i18n: get_lang, t, fallback to en."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import duty_teller.config as config
|
||||
from duty_teller.i18n import get_lang, t
|
||||
|
||||
|
||||
def test_get_lang_none_returns_en():
|
||||
assert get_lang(None) == "en"
|
||||
def test_get_lang_always_returns_default_language():
|
||||
"""get_lang ignores user and always returns config.DEFAULT_LANGUAGE."""
|
||||
assert get_lang(None) == config.DEFAULT_LANGUAGE
|
||||
user_ru = MagicMock()
|
||||
user_ru.language_code = "ru"
|
||||
assert get_lang(user_ru) == config.DEFAULT_LANGUAGE
|
||||
user_en = MagicMock()
|
||||
user_en.language_code = "en"
|
||||
assert get_lang(user_en) == config.DEFAULT_LANGUAGE
|
||||
user_any = MagicMock(spec=[])
|
||||
assert get_lang(user_any) == config.DEFAULT_LANGUAGE
|
||||
|
||||
|
||||
def test_get_lang_ru_returns_ru():
|
||||
user = MagicMock()
|
||||
user.language_code = "ru"
|
||||
assert get_lang(user) == "ru"
|
||||
def test_get_lang_returns_ru_when_default_language_is_ru():
|
||||
"""When DEFAULT_LANGUAGE is ru, get_lang returns 'ru' regardless of user."""
|
||||
with patch("duty_teller.i18n.core.config") as mock_cfg:
|
||||
mock_cfg.DEFAULT_LANGUAGE = "ru"
|
||||
from duty_teller.i18n.core import get_lang as core_get_lang
|
||||
|
||||
assert core_get_lang(None) == "ru"
|
||||
user = MagicMock()
|
||||
user.language_code = "en"
|
||||
assert core_get_lang(user) == "ru"
|
||||
|
||||
|
||||
def test_get_lang_ru_ru_returns_ru():
|
||||
user = MagicMock()
|
||||
user.language_code = "ru-RU"
|
||||
assert get_lang(user) == "ru"
|
||||
def test_get_lang_returns_en_when_default_language_is_en():
|
||||
"""When DEFAULT_LANGUAGE is en, get_lang returns 'en' regardless of user."""
|
||||
with patch("duty_teller.i18n.core.config") as mock_cfg:
|
||||
mock_cfg.DEFAULT_LANGUAGE = "en"
|
||||
from duty_teller.i18n.core import get_lang as core_get_lang
|
||||
|
||||
|
||||
def test_get_lang_en_returns_en():
|
||||
user = MagicMock()
|
||||
user.language_code = "en"
|
||||
assert get_lang(user) == "en"
|
||||
|
||||
|
||||
def test_get_lang_uk_returns_en():
|
||||
user = MagicMock()
|
||||
user.language_code = "uk"
|
||||
assert get_lang(user) == "en"
|
||||
|
||||
|
||||
def test_get_lang_empty_returns_en():
|
||||
user = MagicMock()
|
||||
user.language_code = ""
|
||||
assert get_lang(user) == "en"
|
||||
|
||||
|
||||
def test_get_lang_missing_attr_returns_en():
|
||||
user = MagicMock(spec=[]) # no language_code
|
||||
assert get_lang(user) == "en"
|
||||
assert core_get_lang(None) == "en"
|
||||
user = MagicMock()
|
||||
user.language_code = "ru"
|
||||
assert core_get_lang(user) == "en"
|
||||
|
||||
|
||||
def test_t_en_start_greeting():
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""Tests for duty_teller.api.telegram_auth.validate_init_data and validate_init_data_with_reason."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import duty_teller.config as config
|
||||
from duty_teller.api.telegram_auth import (
|
||||
validate_init_data,
|
||||
validate_init_data_with_reason,
|
||||
@@ -52,7 +55,7 @@ def test_user_without_username_returns_none_from_validate_init_data():
|
||||
|
||||
|
||||
def test_user_without_username_but_with_id_succeeds_with_reason():
|
||||
"""With validate_init_data_with_reason, valid user.id is enough; username may be None."""
|
||||
"""With validate_init_data_with_reason, valid user.id is enough; lang is DEFAULT_LANGUAGE."""
|
||||
bot_token = "123:ABC"
|
||||
user = {"id": 456, "first_name": "Test", "language_code": "ru"}
|
||||
init_data = make_init_data(user, bot_token)
|
||||
@@ -62,11 +65,11 @@ def test_user_without_username_but_with_id_succeeds_with_reason():
|
||||
assert telegram_user_id == 456
|
||||
assert username is None
|
||||
assert reason == "ok"
|
||||
assert lang == "ru"
|
||||
assert lang == config.DEFAULT_LANGUAGE
|
||||
|
||||
|
||||
def test_user_without_id_returns_no_user_id():
|
||||
"""When user object exists but has no 'id', return no_user_id."""
|
||||
"""When user object exists but has no 'id', return no_user_id; lang is DEFAULT_LANGUAGE."""
|
||||
bot_token = "123:ABC"
|
||||
user = {"first_name": "Test"} # no id
|
||||
init_data = make_init_data(user, bot_token)
|
||||
@@ -76,7 +79,17 @@ def test_user_without_id_returns_no_user_id():
|
||||
assert telegram_user_id is None
|
||||
assert username is None
|
||||
assert reason == "no_user_id"
|
||||
assert lang == "en"
|
||||
assert lang == config.DEFAULT_LANGUAGE
|
||||
|
||||
|
||||
def test_validate_init_data_with_reason_returns_default_language_ignoring_user_lang():
|
||||
"""Returned lang is always config.DEFAULT_LANGUAGE, not user.language_code."""
|
||||
with patch("duty_teller.api.telegram_auth.config.DEFAULT_LANGUAGE", "ru"):
|
||||
user = {"id": 1, "first_name": "U", "language_code": "en"}
|
||||
init_data = make_init_data(user, "123:ABC")
|
||||
_, _, reason, lang = validate_init_data_with_reason(init_data, "123:ABC")
|
||||
assert reason == "ok"
|
||||
assert lang == "ru"
|
||||
|
||||
|
||||
def test_empty_init_data_returns_none():
|
||||
|
||||
Reference in New Issue
Block a user