refactor: remove legacy /shop and /api/v1/shop dead code

After the storefront migration, no live routes mount under /api/v1/shop/.
Remove all dead code that detected/handled shop API requests: the
is_shop_api_request() method, the shop API dispatch branch in middleware,
the RequestContext.SHOP enum member (renamed to STOREFRONT), legacy path
prefixes in FrontendDetector, and all associated tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 13:16:43 +01:00
parent 874e254c11
commit 9173448645
10 changed files with 76 additions and 333 deletions

View File

@@ -22,7 +22,9 @@ naming_rules:
description: |
Service files should use singular name + _service (vendor_service.py)
pattern:
file_pattern: "app/services/**/*.py"
file_pattern:
- "app/services/**/*.py"
- "app/modules/*/services/**/*.py"
check: "service_naming"
- id: "NAM-003"
@@ -31,14 +33,16 @@ naming_rules:
description: |
Both database and schema model files use singular names (product.py)
pattern:
file_pattern: "models/**/*.py"
file_pattern:
- "models/**/*.py"
- "app/modules/*/models/**/*.py"
check: "singular_naming"
- id: "NAM-004"
name: "Use consistent terminology: vendor not shop"
severity: "warning"
description: |
Use 'vendor' consistently, not 'shop' (except for shop frontend)
Use 'vendor' consistently, not 'shop' (except for storefront)
pattern:
file_pattern: "app/**/*.py"
discouraged_terms:

View File

@@ -20,19 +20,19 @@ MERCHANT ROUTES (/merchants/*):
- Role: store (merchant owners are store-role users who own merchants)
- Validates: User owns the merchant via Merchant.owner_user_id
CUSTOMER/SHOP ROUTES (/shop/account/*):
- Cookie: customer_token (path=/shop) OR Authorization header
CUSTOMER/STOREFRONT ROUTES (/storefront/account/*):
- Cookie: customer_token (path=/storefront) OR Authorization header
- Role: customer only
- Blocks: admins, stores
- Note: Public shop pages (/shop/products, etc.) don't require auth
- Note: Public storefront pages (/storefront/products, etc.) don't require auth
This dual authentication approach supports:
- HTML pages: Use cookies (automatic browser behavior)
- API calls: Use Authorization headers (explicit JavaScript control)
The cookie path restrictions prevent cross-context cookie leakage:
- admin_token is NEVER sent to /store/* or /shop/*
- store_token is NEVER sent to /admin/* or /shop/*
- admin_token is NEVER sent to /store/* or /storefront/*
- store_token is NEVER sent to /admin/* or /storefront/*
- customer_token is NEVER sent to /admin/* or /store/*
"""
@@ -1019,7 +1019,7 @@ def get_merchant_for_current_user_page(
# ============================================================================
# CUSTOMER AUTHENTICATION (SHOP)
# CUSTOMER AUTHENTICATION (STOREFRONT)
# ============================================================================
@@ -1095,7 +1095,7 @@ def _validate_customer_token(token: str, request: Request, db: Session):
raise InvalidTokenException("Customer account is inactive")
# Validate store context matches token
# This prevents using a customer token from store A on store B's shop
# This prevents using a customer token from store A on store B's storefront
request_store = getattr(request.state, "store", None)
if request_store and token_store_id:
if request_store.id != token_store_id:
@@ -1123,8 +1123,8 @@ def get_current_customer_from_cookie_or_header(
"""
Get current customer from customer_token cookie or Authorization header.
Used for shop account HTML pages (/shop/account/*) that need cookie-based auth.
Note: Public shop pages (/shop/products, etc.) don't use this dependency.
Used for storefront account HTML pages (/storefront/account/*) that need cookie-based auth.
Note: Public storefront pages (/storefront/products, etc.) don't use this dependency.
Validates that token store_id matches request store (URL-based detection).
@@ -1164,7 +1164,7 @@ def get_current_customer_api(
"""
Get current customer from Authorization header ONLY.
Used for shop API endpoints that should not accept cookies.
Used for storefront API endpoints that should not accept cookies.
Validates that token store_id matches request store (URL-based detection).
Args:

View File

@@ -46,8 +46,6 @@ class FrontendDetector:
STOREFRONT_PATH_PREFIXES = (
"/storefront",
"/api/v1/storefront",
"/shop", # Legacy support
"/api/v1/shop", # Legacy support
"/stores/", # Path-based store access
)
MERCHANT_PATH_PREFIXES = ("/merchants", "/api/v1/merchants")
@@ -113,7 +111,7 @@ class FrontendDetector:
return FrontendType.PLATFORM
# 3. Store subdomain detection (wizamart.oms.lu)
# If subdomain exists and is not reserved -> it's a store shop
# If subdomain exists and is not reserved -> it's a store storefront
if subdomain and subdomain not in cls.RESERVED_SUBDOMAINS:
logger.debug(
f"[FRONTEND_DETECTOR] Detected STOREFRONT from subdomain: {subdomain}"

View File

@@ -14,7 +14,7 @@ Migration guide:
- RequestContext.API -> Check with FrontendDetector.is_api_request()
- RequestContext.ADMIN -> FrontendType.ADMIN
- RequestContext.STORE_DASHBOARD -> FrontendType.STORE
- RequestContext.SHOP -> FrontendType.STOREFRONT
- RequestContext.STOREFRONT -> FrontendType.STOREFRONT
- RequestContext.FALLBACK -> FrontendType.PLATFORM (or handle API separately)
- get_request_context(request) -> get_frontend_type(request)
@@ -44,14 +44,14 @@ class RequestContext(str, Enum):
- API -> Use FrontendDetector.is_api_request() + FrontendType
- ADMIN -> FrontendType.ADMIN
- STORE_DASHBOARD -> FrontendType.STORE
- SHOP -> FrontendType.STOREFRONT
- STOREFRONT -> FrontendType.STOREFRONT
- FALLBACK -> FrontendType.PLATFORM
"""
API = "api"
ADMIN = "admin"
STORE_DASHBOARD = "store"
SHOP = "shop"
STOREFRONT = "storefront"
FALLBACK = "fallback"
@@ -82,7 +82,7 @@ def get_request_context(request: Request) -> RequestContext:
mapping = {
FrontendType.ADMIN: RequestContext.ADMIN,
FrontendType.STORE: RequestContext.STORE_DASHBOARD,
FrontendType.STOREFRONT: RequestContext.SHOP,
FrontendType.STOREFRONT: RequestContext.STOREFRONT,
FrontendType.PLATFORM: RequestContext.FALLBACK,
}

View File

@@ -239,31 +239,26 @@ class StoreContextManager:
"""Check if request is for API endpoints."""
return FrontendDetector.is_api_request(request.url.path)
@staticmethod
def is_shop_api_request(request: Request) -> bool:
"""Check if request is for shop API endpoints."""
return request.url.path.startswith("/api/v1/shop/")
@staticmethod
def extract_store_from_referer(request: Request) -> dict | None:
"""
Extract store context from Referer header.
Used for shop API requests where store context comes from the page
that made the API call (e.g., JavaScript on /stores/wizamart/shop/products
calling /api/v1/shop/products).
Used for storefront API requests where store context comes from the page
that made the API call (e.g., JavaScript on /stores/wizamart/storefront/products
calling /api/v1/storefront/products).
Extracts store from Referer URL patterns:
- http://localhost:8000/stores/wizamart/shop/... → wizamart
- http://wizamart.platform.com/shop/... → wizamart (subdomain) # noqa
- http://custom-domain.com/shop/... → custom-domain.com # noqa
- http://localhost:8000/stores/wizamart/storefront/... → wizamart
- http://wizamart.platform.com/storefront/... → wizamart (subdomain) # noqa
- http://custom-domain.com/storefront/... → custom-domain.com # noqa
Returns store context dict or None if unable to extract.
"""
referer = request.headers.get("referer") or request.headers.get("origin")
if not referer:
logger.debug("[STORE] No Referer/Origin header for shop API request")
logger.debug("[STORE] No Referer/Origin header for storefront API request")
return None
try:
@@ -287,7 +282,7 @@ class StoreContextManager:
)
# Method 1: Path-based detection from referer path
# /stores/wizamart/shop/products → wizamart
# /stores/wizamart/storefront/products → wizamart
if referer_path.startswith(("/stores/", "/store/")):
prefix = (
"/stores/" if referer_path.startswith("/stores/") else "/store/"
@@ -448,75 +443,10 @@ class StoreContextMiddleware(BaseHTTPMiddleware):
request.state.clean_path = request.url.path
return await call_next(request)
# Handle shop API routes specially - extract store from Referer header
if StoreContextManager.is_shop_api_request(request):
logger.debug(
f"[STORE] Shop API request detected: {request.url.path}",
extra={
"path": request.url.path,
"referer": request.headers.get("referer", ""),
},
)
store_context = StoreContextManager.extract_store_from_referer(request)
if store_context:
db_gen = get_db()
db = next(db_gen)
try:
store = StoreContextManager.get_store_from_context(
db, store_context
)
if store:
request.state.store = store
request.state.store_context = store_context
request.state.clean_path = request.url.path
logger.debug(
"[STORE_CONTEXT] Store detected from Referer for shop API",
extra={
"store_id": store.id,
"store_name": store.name,
"store_subdomain": store.subdomain,
"detection_method": store_context.get(
"detection_method"
),
"api_path": request.url.path,
"referer": store_context.get("referer", ""),
},
)
else:
logger.warning(
"[WARNING] Store context from Referer but store not found",
extra={
"context": store_context,
"detection_method": store_context.get(
"detection_method"
),
"api_path": request.url.path,
},
)
request.state.store = None
request.state.store_context = store_context
request.state.clean_path = request.url.path
finally:
db.close()
else:
logger.warning(
"[STORE] Shop API request without Referer header",
extra={"path": request.url.path},
)
request.state.store = None
request.state.store_context = None
request.state.clean_path = request.url.path
return await call_next(request)
# Skip store detection for other API routes (admin API, store API have store_id in URL)
# Skip store detection for API routes (admin API, store API have store_id in URL)
if StoreContextManager.is_api_request(request):
logger.debug(
f"[STORE] Skipping store detection for non-shop API: {request.url.path}",
f"[STORE] Skipping store detection for non-storefront API: {request.url.path}",
extra={"path": request.url.path, "reason": "api"},
)
request.state.store = None

View File

@@ -26,7 +26,7 @@ from main import app
from tests.integration.middleware.middleware_test_routes import (
admin_router,
api_router,
shop_router,
storefront_router,
store_router,
)
from tests.integration.middleware.middleware_test_routes import (
@@ -39,7 +39,7 @@ if not any(r.path.startswith("/middleware-test") for r in app.routes if hasattr(
app.include_router(api_router)
app.include_router(admin_router)
app.include_router(store_router)
app.include_router(shop_router)
app.include_router(storefront_router)
@pytest.fixture

View File

@@ -10,7 +10,7 @@ IMPORTANT: Routes are organized by prefix to avoid conflicts:
- /api/middleware-test/* - API context testing
- /admin/middleware-test/* - Admin context testing
- /store/middleware-test/* - Store dashboard context testing
- /shop/middleware-test/* - Shop context testing
- /storefront/middleware-test/* - Storefront context testing
"""
from fastapi import APIRouter, Request
@@ -531,15 +531,15 @@ async def test_store_dashboard_theme(request: Request):
# =============================================================================
# Shop Context Test Router
# Storefront Context Test Router
# =============================================================================
shop_router = APIRouter(prefix="/shop/middleware-test")
storefront_router = APIRouter(prefix="/storefront/middleware-test")
@shop_router.get("/context")
async def test_shop_context(request: Request):
"""Test shop context detection."""
@storefront_router.get("/context")
async def test_storefront_context(request: Request):
"""Test storefront context detection."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
theme = getattr(request.state, "theme", None)
@@ -552,9 +552,9 @@ async def test_shop_context(request: Request):
}
@shop_router.get("/custom-domain-context")
async def test_shop_custom_domain_context(request: Request):
"""Test shop context with custom domain."""
@storefront_router.get("/custom-domain-context")
async def test_storefront_custom_domain_context(request: Request):
"""Test storefront context with custom domain."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
return {
@@ -564,9 +564,9 @@ async def test_shop_custom_domain_context(request: Request):
}
@shop_router.get("/theme")
async def test_shop_theme(request: Request):
"""Test theme in shop context."""
@storefront_router.get("/theme")
async def test_storefront_theme(request: Request):
"""Test theme in storefront context."""
context_type = getattr(request.state, "context_type", None)
theme = getattr(request.state, "theme", None)
colors = theme.get("colors", {}) if theme else {}

View File

@@ -7,7 +7,6 @@ Tests cover:
- Path-based detection (dev mode)
- Subdomain-based detection (prod mode)
- Custom domain detection
- Legacy /shop/ path support
- Priority order of detection methods
"""
@@ -103,15 +102,6 @@ class TestFrontendDetectorStorefront:
)
assert result == FrontendType.STOREFRONT
def test_detect_storefront_legacy_shop_path(self):
"""Test storefront detection from legacy /shop path."""
result = FrontendDetector.detect(host="localhost", path="/shop/products")
assert result == FrontendType.STOREFRONT
def test_detect_storefront_legacy_shop_api_path(self):
"""Test storefront detection from legacy /api/v1/shop path."""
result = FrontendDetector.detect(host="localhost", path="/api/v1/shop/cart")
assert result == FrontendType.STOREFRONT
@pytest.mark.unit

View File

@@ -32,7 +32,7 @@ class TestRequestContextEnumBackwardCompatibility:
assert RequestContext.API.value == "api"
assert RequestContext.ADMIN.value == "admin"
assert RequestContext.STORE_DASHBOARD.value == "store"
assert RequestContext.SHOP.value == "shop"
assert RequestContext.STOREFRONT.value == "storefront"
assert RequestContext.FALLBACK.value == "fallback"
def test_request_context_types(self):
@@ -101,7 +101,7 @@ class TestGetRequestContextBackwardCompatibility:
assert context == RequestContext.STORE_DASHBOARD
def test_get_request_context_maps_storefront(self):
"""Test get_request_context maps FrontendType.STOREFRONT to RequestContext.SHOP."""
"""Test get_request_context maps FrontendType.STOREFRONT to RequestContext.STOREFRONT."""
from app.modules.enums import FrontendType
request = Mock(spec=Request)
@@ -113,7 +113,7 @@ class TestGetRequestContextBackwardCompatibility:
warnings.simplefilter("ignore", DeprecationWarning)
context = get_request_context(request)
assert context == RequestContext.SHOP
assert context == RequestContext.STOREFRONT
def test_get_request_context_maps_platform_to_fallback(self):
"""Test get_request_context maps FrontendType.PLATFORM to RequestContext.FALLBACK."""

View File

@@ -102,10 +102,10 @@ class TestStoreContextManager:
"""Test path-based detection with /store/ prefix."""
request = Mock(spec=Request)
request.headers = {"host": "localhost"}
request.url = Mock(path="/store/store1/shop")
request.url = Mock(path="/store/store1/storefront")
# Set platform_clean_path to simulate PlatformContextMiddleware output
request.state = Mock()
request.state.platform_clean_path = "/store/store1/shop"
request.state.platform_clean_path = "/store/store1/storefront"
context = StoreContextManager.detect_store_context(request)
@@ -119,10 +119,10 @@ class TestStoreContextManager:
"""Test path-based detection with /stores/ prefix."""
request = Mock(spec=Request)
request.headers = {"host": "localhost"}
request.url = Mock(path="/stores/store1/shop")
request.url = Mock(path="/stores/store1/storefront")
# Set platform_clean_path to simulate PlatformContextMiddleware output
request.state = Mock()
request.state.platform_clean_path = "/stores/store1/shop"
request.state.platform_clean_path = "/stores/store1/storefront"
context = StoreContextManager.detect_store_context(request)
@@ -310,24 +310,24 @@ class TestStoreContextManager:
def test_extract_clean_path_from_store_path(self):
"""Test extracting clean path from /store/ prefix."""
request = Mock(spec=Request)
request.url = Mock(path="/store/store1/shop/products")
request.url = Mock(path="/store/store1/storefront/products")
store_context = {"detection_method": "path", "path_prefix": "/store/store1"}
clean_path = StoreContextManager.extract_clean_path(request, store_context)
assert clean_path == "/shop/products"
assert clean_path == "/storefront/products"
def test_extract_clean_path_from_stores_path(self):
"""Test extracting clean path from /stores/ prefix."""
request = Mock(spec=Request)
request.url = Mock(path="/stores/store1/shop/products")
request.url = Mock(path="/stores/store1/storefront/products")
store_context = {"detection_method": "path", "path_prefix": "/stores/store1"}
clean_path = StoreContextManager.extract_clean_path(request, store_context)
assert clean_path == "/shop/products"
assert clean_path == "/storefront/products"
def test_extract_clean_path_root(self):
"""Test extracting clean path when result is empty (should return /)."""
@@ -343,22 +343,22 @@ class TestStoreContextManager:
def test_extract_clean_path_no_path_context(self):
"""Test extracting clean path for non-path detection methods."""
request = Mock(spec=Request)
request.url = Mock(path="/shop/products")
request.url = Mock(path="/storefront/products")
store_context = {"detection_method": "subdomain", "subdomain": "store1"}
clean_path = StoreContextManager.extract_clean_path(request, store_context)
assert clean_path == "/shop/products"
assert clean_path == "/storefront/products"
def test_extract_clean_path_no_context(self):
"""Test extracting clean path with no store context."""
request = Mock(spec=Request)
request.url = Mock(path="/shop/products")
request.url = Mock(path="/storefront/products")
clean_path = StoreContextManager.extract_clean_path(request, None)
assert clean_path == "/shop/products"
assert clean_path == "/storefront/products"
# ========================================================================
# Request Type Detection Tests
@@ -392,7 +392,7 @@ class TestStoreContextManager:
"""Test non-admin request."""
request = Mock(spec=Request)
request.headers = {"host": "store1.platform.com"}
request.url = Mock(path="/shop")
request.url = Mock(path="/storefront")
assert StoreContextManager.is_admin_request(request) is False
@@ -406,49 +406,10 @@ class TestStoreContextManager:
def test_is_not_api_request(self):
"""Test non-API request."""
request = Mock(spec=Request)
request.url = Mock(path="/shop/products")
request.url = Mock(path="/storefront/products")
assert StoreContextManager.is_api_request(request) is False
# ========================================================================
# Shop API Request Detection Tests
# ========================================================================
def test_is_shop_api_request(self):
"""Test shop API request detection."""
request = Mock(spec=Request)
request.url = Mock(path="/api/v1/shop/products")
assert StoreContextManager.is_shop_api_request(request) is True
def test_is_shop_api_request_cart(self):
"""Test shop API request detection for cart endpoint."""
request = Mock(spec=Request)
request.url = Mock(path="/api/v1/shop/cart")
assert StoreContextManager.is_shop_api_request(request) is True
def test_is_not_shop_api_request_admin(self):
"""Test non-shop API request (admin API)."""
request = Mock(spec=Request)
request.url = Mock(path="/api/v1/admin/stores")
assert StoreContextManager.is_shop_api_request(request) is False
def test_is_not_shop_api_request_store(self):
"""Test non-shop API request (store API)."""
request = Mock(spec=Request)
request.url = Mock(path="/api/v1/store/products")
assert StoreContextManager.is_shop_api_request(request) is False
def test_is_not_shop_api_request_non_api(self):
"""Test non-shop API request (non-API path)."""
request = Mock(spec=Request)
request.url = Mock(path="/shop/products")
assert StoreContextManager.is_shop_api_request(request) is False
# ========================================================================
# Extract Store From Referer Tests
# ========================================================================
@@ -457,7 +418,7 @@ class TestStoreContextManager:
"""Test extracting store from referer with /stores/ path."""
request = Mock(spec=Request)
request.headers = {
"referer": "http://localhost:8000/stores/wizamart/shop/products"
"referer": "http://localhost:8000/stores/wizamart/storefront/products"
}
context = StoreContextManager.extract_store_from_referer(request)
@@ -472,7 +433,7 @@ class TestStoreContextManager:
"""Test extracting store from referer with /store/ path."""
request = Mock(spec=Request)
request.headers = {
"referer": "http://localhost:8000/store/myshop/shop/products"
"referer": "http://localhost:8000/store/myshop/storefront/products"
}
context = StoreContextManager.extract_store_from_referer(request)
@@ -486,7 +447,7 @@ class TestStoreContextManager:
def test_extract_store_from_referer_subdomain(self):
"""Test extracting store from referer with subdomain."""
request = Mock(spec=Request)
request.headers = {"referer": "http://wizamart.platform.com/shop/products"}
request.headers = {"referer": "http://wizamart.platform.com/storefront/products"}
with patch("middleware.store_context.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
@@ -501,7 +462,7 @@ class TestStoreContextManager:
def test_extract_store_from_referer_custom_domain(self):
"""Test extracting store from referer with custom domain."""
request = Mock(spec=Request)
request.headers = {"referer": "http://my-custom-shop.com/shop/products"}
request.headers = {"referer": "http://my-custom-shop.com/storefront/products"}
with patch("middleware.store_context.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
@@ -525,7 +486,7 @@ class TestStoreContextManager:
def test_extract_store_from_referer_origin_header(self):
"""Test extracting store from origin header when referer is missing."""
request = Mock(spec=Request)
request.headers = {"origin": "http://localhost:8000/stores/testshop/shop"}
request.headers = {"origin": "http://localhost:8000/stores/testshop/storefront"}
context = StoreContextManager.extract_store_from_referer(request)
@@ -548,7 +509,7 @@ class TestStoreContextManager:
def test_extract_store_from_referer_ignores_www_subdomain(self):
"""Test that www subdomain is not extracted from referer."""
request = Mock(spec=Request)
request.headers = {"referer": "http://www.platform.com/shop"}
request.headers = {"referer": "http://www.platform.com/storefront"}
with patch("middleware.store_context.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
@@ -560,7 +521,7 @@ class TestStoreContextManager:
def test_extract_store_from_referer_localhost_not_custom_domain(self):
"""Test that localhost is not treated as custom domain."""
request = Mock(spec=Request)
request.headers = {"referer": "http://localhost:8000/shop"}
request.headers = {"referer": "http://localhost:8000/storefront"}
with patch("middleware.store_context.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
@@ -601,7 +562,7 @@ class TestStoreContextManager:
@pytest.mark.parametrize(
"path",
[
"/shop/products",
"/storefront/products",
"/admin/dashboard",
"/api/stores",
"/about",
@@ -686,7 +647,7 @@ class TestStoreContextMiddleware:
request = Mock(spec=Request)
request.headers = {"host": "store1.platform.com"}
request.url = Mock(path="/shop/products")
request.url = Mock(path="/storefront/products")
request.state = Mock()
call_next = AsyncMock(return_value=Mock())
@@ -714,7 +675,7 @@ class TestStoreContextMiddleware:
patch.object(
StoreContextManager,
"extract_clean_path",
return_value="/shop/products",
return_value="/storefront/products",
),
patch("middleware.store_context.get_db", return_value=iter([mock_db])),
):
@@ -722,7 +683,7 @@ class TestStoreContextMiddleware:
assert request.state.store is mock_store
assert request.state.store_context == store_context
assert request.state.clean_path == "/shop/products"
assert request.state.clean_path == "/storefront/products"
call_next.assert_called_once_with(request)
@pytest.mark.asyncio
@@ -732,7 +693,7 @@ class TestStoreContextMiddleware:
request = Mock(spec=Request)
request.headers = {"host": "nonexistent.platform.com"}
request.url = Mock(path="/shop")
request.url = Mock(path="/storefront")
request.state = Mock()
call_next = AsyncMock(return_value=Mock())
@@ -756,7 +717,7 @@ class TestStoreContextMiddleware:
assert request.state.store is None
assert request.state.store_context == store_context
assert request.state.clean_path == "/shop"
assert request.state.clean_path == "/storefront"
call_next.assert_called_once_with(request)
@pytest.mark.asyncio
@@ -820,146 +781,6 @@ class TestStoreContextMiddleware:
assert request.state.clean_path == path
call_next.assert_called_once_with(request)
# ========================================================================
# Shop API Request Handling Tests
# ========================================================================
@pytest.mark.asyncio
async def test_middleware_shop_api_with_referer_store_found(self):
"""Test middleware handles shop API request with store from Referer."""
middleware = StoreContextMiddleware(app=None)
request = Mock(spec=Request)
request.headers = {
"host": "localhost",
"referer": "http://localhost:8000/stores/wizamart/shop/products",
}
request.url = Mock(path="/api/v1/shop/cart")
request.state = Mock()
call_next = AsyncMock(return_value=Mock())
mock_store = Mock()
mock_store.id = 1
mock_store.name = "Wizamart"
mock_store.subdomain = "wizamart"
store_context = {
"subdomain": "wizamart",
"detection_method": "path",
"path_prefix": "/stores/wizamart",
"full_prefix": "/stores/",
}
mock_db = MagicMock()
with (
patch.object(StoreContextManager, "is_admin_request", return_value=False),
patch.object(
StoreContextManager, "is_static_file_request", return_value=False
),
patch.object(
StoreContextManager, "is_shop_api_request", return_value=True
),
patch.object(
StoreContextManager,
"extract_store_from_referer",
return_value=store_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 == store_context
assert request.state.clean_path == "/api/v1/shop/cart"
call_next.assert_called_once_with(request)
@pytest.mark.asyncio
async def test_middleware_shop_api_with_referer_store_not_found(self):
"""Test middleware handles shop API when store from Referer not in database."""
middleware = StoreContextMiddleware(app=None)
request = Mock(spec=Request)
request.headers = {
"host": "localhost",
"referer": "http://localhost:8000/stores/nonexistent/shop/products",
}
request.url = Mock(path="/api/v1/shop/cart")
request.state = Mock()
call_next = AsyncMock(return_value=Mock())
store_context = {
"subdomain": "nonexistent",
"detection_method": "path",
"path_prefix": "/stores/nonexistent",
"full_prefix": "/stores/",
}
mock_db = MagicMock()
with (
patch.object(StoreContextManager, "is_admin_request", return_value=False),
patch.object(
StoreContextManager, "is_static_file_request", return_value=False
),
patch.object(
StoreContextManager, "is_shop_api_request", return_value=True
),
patch.object(
StoreContextManager,
"extract_store_from_referer",
return_value=store_context,
),
patch.object(
StoreContextManager, "get_store_from_context", return_value=None
),
patch("middleware.store_context.get_db", return_value=iter([mock_db])),
):
await middleware.dispatch(request, call_next)
assert request.state.store is None
assert request.state.store_context == store_context
assert request.state.clean_path == "/api/v1/shop/cart"
call_next.assert_called_once_with(request)
@pytest.mark.asyncio
async def test_middleware_shop_api_without_referer(self):
"""Test middleware handles shop API request without Referer header."""
middleware = StoreContextMiddleware(app=None)
request = Mock(spec=Request)
request.headers = {"host": "localhost"}
request.url = Mock(path="/api/v1/shop/products")
request.state = Mock()
call_next = AsyncMock(return_value=Mock())
with (
patch.object(StoreContextManager, "is_admin_request", return_value=False),
patch.object(
StoreContextManager, "is_static_file_request", return_value=False
),
patch.object(
StoreContextManager, "is_shop_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
assert request.state.clean_path == "/api/v1/shop/products"
call_next.assert_called_once_with(request)
@pytest.mark.unit
@pytest.mark.stores