fix: storefront login 403, cookie path, double-storefront URLs, and auth redirects
Some checks failed
CI / ruff (push) Successful in 9s
CI / pytest (push) Failing after 46m52s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 30s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

- 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>
This commit is contained in:
2026-02-24 12:29:52 +01:00
parent 32e4aa6564
commit f47c680cb8
38 changed files with 759 additions and 165 deletions

View File

@@ -29,7 +29,9 @@ class TestFrontendTypeMiddleware:
request = Mock(spec=Request)
request.url = Mock(path="/admin/dashboard")
request.headers = {"host": "localhost"}
request.state = Mock(clean_path="/admin/dashboard", store=None)
request.state = Mock(spec=[])
request.state.platform_clean_path = "/admin/dashboard"
request.state.store = None
call_next = AsyncMock(return_value=Mock())
@@ -47,7 +49,9 @@ class TestFrontendTypeMiddleware:
request = Mock(spec=Request)
request.url = Mock(path="/store/settings")
request.headers = {"host": "localhost"}
request.state = Mock(clean_path="/store/settings", store=None)
request.state = Mock(spec=[])
request.state.platform_clean_path = "/store/settings"
request.state.store = None
call_next = AsyncMock(return_value=Mock())
@@ -64,7 +68,9 @@ class TestFrontendTypeMiddleware:
request = Mock(spec=Request)
request.url = Mock(path="/storefront/products")
request.headers = {"host": "localhost"}
request.state = Mock(clean_path="/storefront/products", store=None)
request.state = Mock(spec=[])
request.state.platform_clean_path = "/storefront/products"
request.state.store = None
call_next = AsyncMock(return_value=Mock())
@@ -83,7 +89,9 @@ class TestFrontendTypeMiddleware:
request.headers = {"host": "orion.omsflow.lu"}
mock_store = Mock()
mock_store.name = "Test Store"
request.state = Mock(clean_path="/products", store=mock_store)
request.state = Mock(spec=[])
request.state.platform_clean_path = "/products"
request.state.store = mock_store
call_next = AsyncMock(return_value=Mock())
@@ -100,7 +108,9 @@ class TestFrontendTypeMiddleware:
request = Mock(spec=Request)
request.url = Mock(path="/pricing")
request.headers = {"host": "localhost"}
request.state = Mock(clean_path="/pricing", store=None)
request.state = Mock(spec=[])
request.state.platform_clean_path = "/pricing"
request.state.store = None
call_next = AsyncMock(return_value=Mock())
@@ -127,21 +137,23 @@ class TestFrontendTypeMiddleware:
assert response is expected_response
@pytest.mark.asyncio
async def test_middleware_uses_clean_path_when_available(self):
"""Test middleware uses clean_path when available."""
async def test_middleware_uses_platform_clean_path_when_available(self):
"""Test middleware uses platform_clean_path when available."""
middleware = FrontendTypeMiddleware(app=None)
request = Mock(spec=Request)
request.url = Mock(path="/stores/orion/store/settings")
request.url = Mock(path="/platforms/main/store/settings")
request.headers = {"host": "localhost"}
# clean_path shows the rewritten path
request.state = Mock(clean_path="/store/settings", store=None)
# platform_clean_path shows the path with platform prefix stripped
request.state = Mock(spec=[])
request.state.platform_clean_path = "/store/settings"
request.state.store = None
call_next = AsyncMock(return_value=Mock())
await middleware.dispatch(request, call_next)
# Should detect as STORE based on clean_path
# Should detect as STORE based on platform_clean_path
assert request.state.frontend_type == FrontendType.STORE
@pytest.mark.asyncio

View File

@@ -132,6 +132,7 @@ class TestLanguageDispatchStore:
mock_resolve.assert_called_once_with(
user_preferred="de",
store_dashboard="fr",
cookie_language=None,
)
assert request.state.language == "de"
@@ -154,6 +155,7 @@ class TestLanguageDispatchStore:
mock_resolve.assert_called_once_with(
user_preferred=None,
store_dashboard=None,
cookie_language=None,
)
@@ -356,6 +358,7 @@ class TestLanguageHelpers:
max_age=60 * 60 * 24 * 365,
httponly=False,
samesite="lax",
path="/",
)
assert result is response
@@ -372,7 +375,7 @@ class TestLanguageHelpers:
response = Mock(spec=Response)
result = delete_language_cookie(response)
response.delete_cookie.assert_called_once_with(key="lang")
response.delete_cookie.assert_called_once_with(key="lang", path="/")
assert result is response
def test_get_user_language_from_token_with_pref(self):

View File

@@ -995,3 +995,88 @@ class TestURLRoutingSummary:
assert context["detection_method"] == "domain"
assert context["domain"] == "omsflow.lu"
# clean_path not set for domain detection - uses original path
@pytest.mark.unit
@pytest.mark.middleware
class TestExtractPlatformFromReferer:
"""Tests for Referer-based platform detection for storefront API requests."""
def test_extract_platform_from_referer_with_platforms_prefix(self):
"""Extract platform code from Referer with /platforms/{code}/ path."""
middleware = PlatformContextMiddleware(app=None)
result = middleware._extract_platform_from_referer(
"http://localhost:8000/platforms/loyalty/storefront/FASHIONHUB/account/login"
)
assert result is not None
assert result["path_prefix"] == "loyalty"
assert result["detection_method"] == "path"
def test_extract_platform_from_referer_no_platforms_prefix(self):
"""Referer without /platforms/ returns None."""
middleware = PlatformContextMiddleware(app=None)
result = middleware._extract_platform_from_referer(
"http://localhost:8000/storefront/FASHIONHUB/products"
)
assert result is None
def test_extract_platform_from_referer_empty_string(self):
"""Empty referer returns None."""
middleware = PlatformContextMiddleware(app=None)
result = middleware._extract_platform_from_referer("")
assert result is None
def test_extract_platform_from_referer_oms_platform(self):
"""Extract OMS platform from Referer."""
middleware = PlatformContextMiddleware(app=None)
result = middleware._extract_platform_from_referer(
"http://localhost:8000/platforms/oms/store/WIZATECH/dashboard"
)
assert result is not None
assert result["path_prefix"] == "oms"
@pytest.mark.unit
@pytest.mark.middleware
class TestPlatformContextMiddlewareReferer:
"""Test PlatformContextMiddleware __call__ with Referer-based platform detection."""
@pytest.mark.asyncio
async def test_middleware_storefront_api_uses_referer_platform(self):
"""Test storefront API requests on localhost extract platform from Referer."""
mock_app = AsyncMock()
middleware = PlatformContextMiddleware(app=mock_app)
mock_platform = Mock()
mock_platform.id = 3
mock_platform.code = "loyalty"
mock_platform.name = "Loyalty Platform"
scope = {
"type": "http",
"path": "/api/v1/storefront/auth/login",
"headers": [
(b"host", b"localhost:8000"),
(b"referer", b"http://localhost:8000/platforms/loyalty/storefront/FASHIONHUB/account/login"),
],
}
receive = AsyncMock()
send = AsyncMock()
mock_db = MagicMock()
with patch(
"middleware.platform_context.get_db", return_value=iter([mock_db])
), patch.object(
PlatformContextManager,
"get_platform_from_context",
return_value=mock_platform,
):
await middleware(scope, receive, send)
# Platform should be detected from Referer
assert scope["state"]["platform"] is mock_platform
assert scope["state"]["platform_context"]["path_prefix"] == "loyalty"
# API path should NOT be rewritten to the Referer's path
assert scope["path"] == "/api/v1/storefront/auth/login"

View File

@@ -733,6 +733,115 @@ class TestStoreContextMiddleware:
assert request.state.clean_path == "/random/path"
call_next.assert_called_once_with(request)
# ========================================================================
# Storefront API Referer Tests
# ========================================================================
@pytest.mark.asyncio
async def test_middleware_storefront_api_uses_referer(self):
"""Test storefront API requests get store context from Referer header."""
middleware = StoreContextMiddleware(app=None)
request = Mock(spec=Request)
request.headers = {
"host": "localhost",
"referer": "http://localhost:8000/platforms/loyalty/storefront/FASHIONHUB/account/login",
}
request.url = Mock(path="/api/v1/storefront/auth/login")
request.state = Mock()
call_next = AsyncMock(return_value=Mock())
mock_store = Mock()
mock_store.id = 4
mock_store.name = "Fashion Hub"
mock_store.subdomain = "fashionhub"
referer_context = {
"subdomain": "FASHIONHUB",
"detection_method": "path",
"path_prefix": "/storefront/FASHIONHUB",
"full_prefix": "/storefront/",
"host": "localhost",
"referer": "http://localhost:8000/platforms/loyalty/storefront/FASHIONHUB/account/login",
}
mock_db = MagicMock()
with (
patch.object(
StoreContextManager,
"is_api_request",
return_value=True,
),
patch.object(
StoreContextManager,
"extract_store_from_referer",
return_value=referer_context,
),
patch.object(
StoreContextManager,
"get_store_from_context",
return_value=mock_store,
),
patch("middleware.store_context.get_db", return_value=iter([mock_db])),
):
await middleware.dispatch(request, call_next)
assert request.state.store is mock_store
assert request.state.store_context is referer_context
call_next.assert_called_once_with(request)
@pytest.mark.asyncio
async def test_middleware_storefront_api_no_referer(self):
"""Test storefront API request without Referer header sets store to None."""
middleware = StoreContextMiddleware(app=None)
request = Mock(spec=Request)
request.headers = {"host": "localhost"}
request.url = Mock(path="/api/v1/storefront/auth/login")
request.state = Mock()
call_next = AsyncMock(return_value=Mock())
with (
patch.object(
StoreContextManager,
"is_api_request",
return_value=True,
),
patch.object(
StoreContextManager,
"extract_store_from_referer",
return_value=None,
),
):
await middleware.dispatch(request, call_next)
assert request.state.store is None
assert request.state.store_context is None
call_next.assert_called_once_with(request)
@pytest.mark.asyncio
async def test_middleware_non_storefront_api_still_skips(self):
"""Test non-storefront API requests still skip store detection."""
middleware = StoreContextMiddleware(app=None)
request = Mock(spec=Request)
request.headers = {"host": "localhost"}
request.url = Mock(path="/api/v1/admin/stores")
request.state = Mock()
call_next = AsyncMock(return_value=Mock())
with patch.object(
StoreContextManager, "is_api_request", return_value=True
):
await middleware.dispatch(request, call_next)
assert request.state.store is None
call_next.assert_called_once_with(request)
# ========================================================================
# System Path Skipping Tests
# ========================================================================