fix: storefront login 403, cookie path, double-storefront URLs, and auth redirects
- 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:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
# ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user