- Extract store/platform context from Referer header for storefront API requests
(StoreContextMiddleware and PlatformContextMiddleware) so login POST works in
dev mode where API paths lack /platforms/{code}/ prefix
- Set customer token cookie path to "/" for cross-route compatibility
- Fix double storefront in URLs: replace {{ base_url }}storefront/ with {{ base_url }}
across all 24 storefront templates
- Fix auth error redirect to include platform prefix and use store_code
- Update seed script to output correct storefront login URLs
- Add 20 new unit tests covering all fixes; fix 9 pre-existing test failures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
495 lines
18 KiB
Python
495 lines
18 KiB
Python
# tests/unit/middleware/test_language.py
|
|
"""
|
|
Unit tests for LanguageMiddleware, set_language_cookie, and delete_language_cookie.
|
|
|
|
Tests cover:
|
|
- Admin frontend: user pref used, falls back to "en"
|
|
- Store dashboard: calls resolve_store_dashboard_language, no-store fallback
|
|
- Storefront: calls resolve_storefront_language, no-store fallback
|
|
- General: platform frontend, fallback/unknown type, unsupported language
|
|
validation, request.state.language set, request.state.language_info populated,
|
|
cookie language read
|
|
- Helpers: _get_user_language_from_token, _get_customer_language_from_token,
|
|
set_language_cookie (valid/invalid), delete_language_cookie
|
|
- Private methods: user with pref, user without pref, no user, customer with pref
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
import pytest
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response
|
|
|
|
from app.modules.enums import FrontendType
|
|
from middleware.language import (
|
|
LanguageMiddleware,
|
|
delete_language_cookie,
|
|
set_language_cookie,
|
|
)
|
|
|
|
|
|
def _make_request(
|
|
frontend_type=None,
|
|
store=None,
|
|
cookies=None,
|
|
headers=None,
|
|
current_user=None,
|
|
current_customer=None,
|
|
):
|
|
"""Build a mock Request with the given state attributes."""
|
|
request = Mock(spec=Request)
|
|
request.state = Mock(spec=[])
|
|
request.cookies = cookies or {}
|
|
request.headers = headers or {}
|
|
|
|
if frontend_type is not None:
|
|
request.state.frontend_type = frontend_type
|
|
if store is not None:
|
|
request.state.store = store
|
|
if current_user is not None:
|
|
request.state.current_user = current_user
|
|
if current_customer is not None:
|
|
request.state.current_customer = current_customer
|
|
|
|
return request
|
|
|
|
|
|
def _make_call_next():
|
|
"""Return (call_next, response) pair."""
|
|
response = Mock(status_code=200, headers={})
|
|
call_next = AsyncMock(return_value=response)
|
|
return call_next, response
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestLanguageDispatchAdmin
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.unit
|
|
class TestLanguageDispatchAdmin:
|
|
"""Admin frontend language resolution."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_user_preferred_language_used(self):
|
|
"""Admin: user's preferred_language is used when set."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
user = Mock()
|
|
user.preferred_language = "de"
|
|
request = _make_request(
|
|
frontend_type=FrontendType.ADMIN,
|
|
current_user=user,
|
|
)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value=None):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.language == "de"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_falls_back_to_en(self):
|
|
"""Admin: falls back to 'en' when no user preference."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request(frontend_type=FrontendType.ADMIN)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value=None):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.language == "en"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestLanguageDispatchStore
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.unit
|
|
class TestLanguageDispatchStore:
|
|
"""Store dashboard language resolution."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_store_calls_resolve_store_dashboard_language(self):
|
|
"""Store: resolve_store_dashboard_language is called with correct args."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
user = Mock()
|
|
user.preferred_language = "de"
|
|
store = Mock()
|
|
store.dashboard_language = "fr"
|
|
request = _make_request(
|
|
frontend_type=FrontendType.STORE,
|
|
store=store,
|
|
current_user=user,
|
|
)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with (
|
|
patch("middleware.language.parse_accept_language", return_value=None),
|
|
patch(
|
|
"middleware.language.resolve_store_dashboard_language",
|
|
return_value="de",
|
|
) as mock_resolve,
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_resolve.assert_called_once_with(
|
|
user_preferred="de",
|
|
store_dashboard="fr",
|
|
cookie_language=None,
|
|
)
|
|
assert request.state.language == "de"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_store_no_store_passes_none_dashboard(self):
|
|
"""Store: store_dashboard is None when request.state has no store."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request(frontend_type=FrontendType.STORE)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with (
|
|
patch("middleware.language.parse_accept_language", return_value=None),
|
|
patch(
|
|
"middleware.language.resolve_store_dashboard_language",
|
|
return_value="fr",
|
|
) as mock_resolve,
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_resolve.assert_called_once_with(
|
|
user_preferred=None,
|
|
store_dashboard=None,
|
|
cookie_language=None,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestLanguageDispatchStorefront
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.unit
|
|
class TestLanguageDispatchStorefront:
|
|
"""Storefront language resolution."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storefront_calls_resolve_storefront_language(self):
|
|
"""Storefront: resolve_storefront_language called with correct args."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
customer = Mock()
|
|
customer.preferred_language = "lb"
|
|
store = Mock()
|
|
store.storefront_language = "fr"
|
|
store.storefront_languages = ["fr", "de", "lb"]
|
|
request = _make_request(
|
|
frontend_type=FrontendType.STOREFRONT,
|
|
store=store,
|
|
current_customer=customer,
|
|
cookies={"lang": "de"},
|
|
headers={"accept-language": "en-US,en;q=0.9"},
|
|
)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with (
|
|
patch(
|
|
"middleware.language.parse_accept_language", return_value="en"
|
|
),
|
|
patch(
|
|
"middleware.language.resolve_storefront_language",
|
|
return_value="lb",
|
|
) as mock_resolve,
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_resolve.assert_called_once_with(
|
|
customer_preferred="lb",
|
|
session_language="de",
|
|
store_storefront="fr",
|
|
browser_language="en",
|
|
enabled_languages=["fr", "de", "lb"],
|
|
)
|
|
assert request.state.language == "lb"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storefront_no_store_passes_none(self):
|
|
"""Storefront: store fields are None when no store in state."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request(frontend_type=FrontendType.STOREFRONT)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with (
|
|
patch("middleware.language.parse_accept_language", return_value=None),
|
|
patch(
|
|
"middleware.language.resolve_storefront_language",
|
|
return_value="fr",
|
|
) as mock_resolve,
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_resolve.assert_called_once_with(
|
|
customer_preferred=None,
|
|
session_language=None,
|
|
store_storefront=None,
|
|
browser_language=None,
|
|
enabled_languages=None,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestLanguageDispatchGeneral
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.unit
|
|
class TestLanguageDispatchGeneral:
|
|
"""Platform, fallback/unknown, validation, state, and cookie tests."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_platform_uses_cookie_language(self):
|
|
"""Platform: cookie language is used first."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request(
|
|
frontend_type=FrontendType.PLATFORM,
|
|
cookies={"lang": "de"},
|
|
)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value="en"):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.language == "de"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_platform_falls_back_to_browser_language(self):
|
|
"""Platform: browser language used when no cookie."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request(frontend_type=FrontendType.PLATFORM)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value="de"):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.language == "de"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fallback_unknown_frontend_type(self):
|
|
"""Unknown/None frontend type uses cookie → browser → default."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request() # no frontend_type
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value=None):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
# DEFAULT_LANGUAGE is "fr"
|
|
assert request.state.language == "fr"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unsupported_language_falls_back_to_default(self):
|
|
"""Unsupported language code is replaced by DEFAULT_LANGUAGE."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request(
|
|
frontend_type=FrontendType.PLATFORM,
|
|
cookies={"lang": "xx"}, # unsupported
|
|
)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value=None):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.language == "fr"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_state_language_is_set(self):
|
|
"""request.state.language is always set after dispatch."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request(frontend_type=FrontendType.PLATFORM)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value="en"):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert hasattr(request.state, "language")
|
|
assert request.state.language == "en"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_state_language_info_populated(self):
|
|
"""request.state.language_info contains code, cookie, browser, frontend_type."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request(
|
|
frontend_type=FrontendType.PLATFORM,
|
|
cookies={"lang": "en"},
|
|
headers={"accept-language": "de"},
|
|
)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value="de"):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
info = request.state.language_info
|
|
assert info["code"] == "en"
|
|
assert info["cookie"] == "en"
|
|
assert info["browser"] == "de"
|
|
assert info["frontend_type"] == "platform"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cookie_language_read_from_request(self):
|
|
"""The 'lang' cookie is read and considered in resolution."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request(
|
|
frontend_type=FrontendType.PLATFORM,
|
|
cookies={"lang": "lb"},
|
|
)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value=None):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.language == "lb"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestLanguageHelpers
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.unit
|
|
class TestLanguageHelpers:
|
|
"""Tests for set_language_cookie and delete_language_cookie."""
|
|
|
|
def test_set_language_cookie_valid(self):
|
|
"""set_language_cookie sets cookie for supported language."""
|
|
response = Mock(spec=Response)
|
|
result = set_language_cookie(response, "de")
|
|
|
|
response.set_cookie.assert_called_once_with(
|
|
key="lang",
|
|
value="de",
|
|
max_age=60 * 60 * 24 * 365,
|
|
httponly=False,
|
|
samesite="lax",
|
|
path="/",
|
|
)
|
|
assert result is response
|
|
|
|
def test_set_language_cookie_invalid_language(self):
|
|
"""set_language_cookie does NOT call set_cookie for unsupported lang."""
|
|
response = Mock(spec=Response)
|
|
result = set_language_cookie(response, "xx")
|
|
|
|
response.set_cookie.assert_not_called()
|
|
assert result is response
|
|
|
|
def test_delete_language_cookie(self):
|
|
"""delete_language_cookie calls delete_cookie with correct key."""
|
|
response = Mock(spec=Response)
|
|
result = delete_language_cookie(response)
|
|
|
|
response.delete_cookie.assert_called_once_with(key="lang", path="/")
|
|
assert result is response
|
|
|
|
def test_get_user_language_from_token_with_pref(self):
|
|
"""_get_user_language_from_token returns preferred_language."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = Mock(spec=Request)
|
|
request.state = Mock()
|
|
request.state.current_user = Mock()
|
|
request.state.current_user.preferred_language = "de"
|
|
|
|
assert middleware._get_user_language_from_token(request) == "de"
|
|
|
|
def test_get_user_language_from_token_no_user(self):
|
|
"""_get_user_language_from_token returns None with no user."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = Mock(spec=Request)
|
|
request.state = Mock(spec=[]) # no current_user
|
|
|
|
assert middleware._get_user_language_from_token(request) is None
|
|
|
|
def test_get_customer_language_from_token_with_pref(self):
|
|
"""_get_customer_language_from_token returns preferred_language."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = Mock(spec=Request)
|
|
request.state = Mock()
|
|
request.state.current_customer = Mock()
|
|
request.state.current_customer.preferred_language = "lb"
|
|
|
|
assert middleware._get_customer_language_from_token(request) == "lb"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestLanguageDispatchPrivateMethods
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.unit
|
|
class TestLanguageDispatchPrivateMethods:
|
|
"""Integration-style tests exercising private helpers through dispatch."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_user_with_preferred_language(self):
|
|
"""Admin + user with pref → uses that preference."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
user = Mock()
|
|
user.preferred_language = "lb"
|
|
request = _make_request(
|
|
frontend_type=FrontendType.ADMIN,
|
|
current_user=user,
|
|
)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value=None):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.language == "lb"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_user_without_preferred_language(self):
|
|
"""Admin + user without preferred_language → falls back to 'en'."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
user = Mock(spec=[]) # no preferred_language attr
|
|
request = _make_request(
|
|
frontend_type=FrontendType.ADMIN,
|
|
current_user=user,
|
|
)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value=None):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.language == "en"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_no_user_at_all(self):
|
|
"""Admin + no user at all → falls back to 'en'."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
request = _make_request(frontend_type=FrontendType.ADMIN)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with patch("middleware.language.parse_accept_language", return_value=None):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.language == "en"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storefront_customer_with_preferred_language(self):
|
|
"""Storefront + customer with pref → passed to resolve function."""
|
|
middleware = LanguageMiddleware(app=None)
|
|
customer = Mock()
|
|
customer.preferred_language = "de"
|
|
store = Mock()
|
|
store.storefront_language = "fr"
|
|
store.storefront_languages = ["fr", "de"]
|
|
request = _make_request(
|
|
frontend_type=FrontendType.STOREFRONT,
|
|
store=store,
|
|
current_customer=customer,
|
|
)
|
|
call_next, _ = _make_call_next()
|
|
|
|
with (
|
|
patch("middleware.language.parse_accept_language", return_value=None),
|
|
patch(
|
|
"middleware.language.resolve_storefront_language",
|
|
return_value="de",
|
|
) as mock_resolve,
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_resolve.assert_called_once_with(
|
|
customer_preferred="de",
|
|
session_language=None,
|
|
store_storefront="fr",
|
|
browser_language=None,
|
|
enabled_languages=["fr", "de"],
|
|
)
|
|
assert request.state.language == "de"
|