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

View File

@@ -20,19 +20,19 @@ MERCHANT ROUTES (/merchants/*):
- Role: store (merchant owners are store-role users who own merchants) - Role: store (merchant owners are store-role users who own merchants)
- Validates: User owns the merchant via Merchant.owner_user_id - Validates: User owns the merchant via Merchant.owner_user_id
CUSTOMER/SHOP ROUTES (/shop/account/*): CUSTOMER/STOREFRONT ROUTES (/storefront/account/*):
- Cookie: customer_token (path=/shop) OR Authorization header - Cookie: customer_token (path=/storefront) OR Authorization header
- Role: customer only - Role: customer only
- Blocks: admins, stores - 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: This dual authentication approach supports:
- HTML pages: Use cookies (automatic browser behavior) - HTML pages: Use cookies (automatic browser behavior)
- API calls: Use Authorization headers (explicit JavaScript control) - API calls: Use Authorization headers (explicit JavaScript control)
The cookie path restrictions prevent cross-context cookie leakage: The cookie path restrictions prevent cross-context cookie leakage:
- admin_token is NEVER sent to /store/* or /shop/* - admin_token is NEVER sent to /store/* or /storefront/*
- store_token is NEVER sent to /admin/* or /shop/* - store_token is NEVER sent to /admin/* or /storefront/*
- customer_token is NEVER sent to /admin/* or /store/* - 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") raise InvalidTokenException("Customer account is inactive")
# Validate store context matches token # 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) request_store = getattr(request.state, "store", None)
if request_store and token_store_id: if request_store and token_store_id:
if request_store.id != 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. Get current customer from customer_token cookie or Authorization header.
Used for shop account HTML pages (/shop/account/*) that need cookie-based auth. Used for storefront account HTML pages (/storefront/account/*) that need cookie-based auth.
Note: Public shop pages (/shop/products, etc.) don't use this dependency. Note: Public storefront pages (/storefront/products, etc.) don't use this dependency.
Validates that token store_id matches request store (URL-based detection). 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. 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). Validates that token store_id matches request store (URL-based detection).
Args: Args:

View File

@@ -46,8 +46,6 @@ class FrontendDetector:
STOREFRONT_PATH_PREFIXES = ( STOREFRONT_PATH_PREFIXES = (
"/storefront", "/storefront",
"/api/v1/storefront", "/api/v1/storefront",
"/shop", # Legacy support
"/api/v1/shop", # Legacy support
"/stores/", # Path-based store access "/stores/", # Path-based store access
) )
MERCHANT_PATH_PREFIXES = ("/merchants", "/api/v1/merchants") MERCHANT_PATH_PREFIXES = ("/merchants", "/api/v1/merchants")
@@ -113,7 +111,7 @@ class FrontendDetector:
return FrontendType.PLATFORM return FrontendType.PLATFORM
# 3. Store subdomain detection (wizamart.oms.lu) # 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: if subdomain and subdomain not in cls.RESERVED_SUBDOMAINS:
logger.debug( logger.debug(
f"[FRONTEND_DETECTOR] Detected STOREFRONT from subdomain: {subdomain}" 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.API -> Check with FrontendDetector.is_api_request()
- RequestContext.ADMIN -> FrontendType.ADMIN - RequestContext.ADMIN -> FrontendType.ADMIN
- RequestContext.STORE_DASHBOARD -> FrontendType.STORE - RequestContext.STORE_DASHBOARD -> FrontendType.STORE
- RequestContext.SHOP -> FrontendType.STOREFRONT - RequestContext.STOREFRONT -> FrontendType.STOREFRONT
- RequestContext.FALLBACK -> FrontendType.PLATFORM (or handle API separately) - RequestContext.FALLBACK -> FrontendType.PLATFORM (or handle API separately)
- get_request_context(request) -> get_frontend_type(request) - get_request_context(request) -> get_frontend_type(request)
@@ -44,14 +44,14 @@ class RequestContext(str, Enum):
- API -> Use FrontendDetector.is_api_request() + FrontendType - API -> Use FrontendDetector.is_api_request() + FrontendType
- ADMIN -> FrontendType.ADMIN - ADMIN -> FrontendType.ADMIN
- STORE_DASHBOARD -> FrontendType.STORE - STORE_DASHBOARD -> FrontendType.STORE
- SHOP -> FrontendType.STOREFRONT - STOREFRONT -> FrontendType.STOREFRONT
- FALLBACK -> FrontendType.PLATFORM - FALLBACK -> FrontendType.PLATFORM
""" """
API = "api" API = "api"
ADMIN = "admin" ADMIN = "admin"
STORE_DASHBOARD = "store" STORE_DASHBOARD = "store"
SHOP = "shop" STOREFRONT = "storefront"
FALLBACK = "fallback" FALLBACK = "fallback"
@@ -82,7 +82,7 @@ def get_request_context(request: Request) -> RequestContext:
mapping = { mapping = {
FrontendType.ADMIN: RequestContext.ADMIN, FrontendType.ADMIN: RequestContext.ADMIN,
FrontendType.STORE: RequestContext.STORE_DASHBOARD, FrontendType.STORE: RequestContext.STORE_DASHBOARD,
FrontendType.STOREFRONT: RequestContext.SHOP, FrontendType.STOREFRONT: RequestContext.STOREFRONT,
FrontendType.PLATFORM: RequestContext.FALLBACK, FrontendType.PLATFORM: RequestContext.FALLBACK,
} }

View File

@@ -239,31 +239,26 @@ class StoreContextManager:
"""Check if request is for API endpoints.""" """Check if request is for API endpoints."""
return FrontendDetector.is_api_request(request.url.path) 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 @staticmethod
def extract_store_from_referer(request: Request) -> dict | None: def extract_store_from_referer(request: Request) -> dict | None:
""" """
Extract store context from Referer header. Extract store context from Referer header.
Used for shop API requests where store context comes from the page Used for storefront API requests where store context comes from the page
that made the API call (e.g., JavaScript on /stores/wizamart/shop/products that made the API call (e.g., JavaScript on /stores/wizamart/storefront/products
calling /api/v1/shop/products). calling /api/v1/storefront/products).
Extracts store from Referer URL patterns: Extracts store from Referer URL patterns:
- http://localhost:8000/stores/wizamart/shop/... → wizamart - http://localhost:8000/stores/wizamart/storefront/... → wizamart
- http://wizamart.platform.com/shop/... → wizamart (subdomain) # noqa - http://wizamart.platform.com/storefront/... → wizamart (subdomain) # noqa
- http://custom-domain.com/shop/... → custom-domain.com # noqa - http://custom-domain.com/storefront/... → custom-domain.com # noqa
Returns store context dict or None if unable to extract. Returns store context dict or None if unable to extract.
""" """
referer = request.headers.get("referer") or request.headers.get("origin") referer = request.headers.get("referer") or request.headers.get("origin")
if not referer: 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 return None
try: try:
@@ -287,7 +282,7 @@ class StoreContextManager:
) )
# Method 1: Path-based detection from referer path # Method 1: Path-based detection from referer path
# /stores/wizamart/shop/products → wizamart # /stores/wizamart/storefront/products → wizamart
if referer_path.startswith(("/stores/", "/store/")): if referer_path.startswith(("/stores/", "/store/")):
prefix = ( prefix = (
"/stores/" if referer_path.startswith("/stores/") else "/store/" "/stores/" if referer_path.startswith("/stores/") else "/store/"
@@ -448,75 +443,10 @@ class StoreContextMiddleware(BaseHTTPMiddleware):
request.state.clean_path = request.url.path request.state.clean_path = request.url.path
return await call_next(request) return await call_next(request)
# Handle shop API routes specially - extract store from Referer header # Skip store detection for API routes (admin API, store API have store_id in URL)
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)
if StoreContextManager.is_api_request(request): if StoreContextManager.is_api_request(request):
logger.debug( 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"}, extra={"path": request.url.path, "reason": "api"},
) )
request.state.store = None request.state.store = None

View File

@@ -26,7 +26,7 @@ from main import app
from tests.integration.middleware.middleware_test_routes import ( from tests.integration.middleware.middleware_test_routes import (
admin_router, admin_router,
api_router, api_router,
shop_router, storefront_router,
store_router, store_router,
) )
from tests.integration.middleware.middleware_test_routes import ( 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(api_router)
app.include_router(admin_router) app.include_router(admin_router)
app.include_router(store_router) app.include_router(store_router)
app.include_router(shop_router) app.include_router(storefront_router)
@pytest.fixture @pytest.fixture

View File

@@ -10,7 +10,7 @@ IMPORTANT: Routes are organized by prefix to avoid conflicts:
- /api/middleware-test/* - API context testing - /api/middleware-test/* - API context testing
- /admin/middleware-test/* - Admin context testing - /admin/middleware-test/* - Admin context testing
- /store/middleware-test/* - Store dashboard 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 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") @storefront_router.get("/context")
async def test_shop_context(request: Request): async def test_storefront_context(request: Request):
"""Test shop context detection.""" """Test storefront context detection."""
context_type = getattr(request.state, "context_type", None) context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None) store = getattr(request.state, "store", None)
theme = getattr(request.state, "theme", None) theme = getattr(request.state, "theme", None)
@@ -552,9 +552,9 @@ async def test_shop_context(request: Request):
} }
@shop_router.get("/custom-domain-context") @storefront_router.get("/custom-domain-context")
async def test_shop_custom_domain_context(request: Request): async def test_storefront_custom_domain_context(request: Request):
"""Test shop context with custom domain.""" """Test storefront context with custom domain."""
context_type = getattr(request.state, "context_type", None) context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None) store = getattr(request.state, "store", None)
return { return {
@@ -564,9 +564,9 @@ async def test_shop_custom_domain_context(request: Request):
} }
@shop_router.get("/theme") @storefront_router.get("/theme")
async def test_shop_theme(request: Request): async def test_storefront_theme(request: Request):
"""Test theme in shop context.""" """Test theme in storefront context."""
context_type = getattr(request.state, "context_type", None) context_type = getattr(request.state, "context_type", None)
theme = getattr(request.state, "theme", None) theme = getattr(request.state, "theme", None)
colors = theme.get("colors", {}) if theme else {} colors = theme.get("colors", {}) if theme else {}

View File

@@ -7,7 +7,6 @@ Tests cover:
- Path-based detection (dev mode) - Path-based detection (dev mode)
- Subdomain-based detection (prod mode) - Subdomain-based detection (prod mode)
- Custom domain detection - Custom domain detection
- Legacy /shop/ path support
- Priority order of detection methods - Priority order of detection methods
""" """
@@ -103,15 +102,6 @@ class TestFrontendDetectorStorefront:
) )
assert result == FrontendType.STOREFRONT 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 @pytest.mark.unit

View File

@@ -32,7 +32,7 @@ class TestRequestContextEnumBackwardCompatibility:
assert RequestContext.API.value == "api" assert RequestContext.API.value == "api"
assert RequestContext.ADMIN.value == "admin" assert RequestContext.ADMIN.value == "admin"
assert RequestContext.STORE_DASHBOARD.value == "store" assert RequestContext.STORE_DASHBOARD.value == "store"
assert RequestContext.SHOP.value == "shop" assert RequestContext.STOREFRONT.value == "storefront"
assert RequestContext.FALLBACK.value == "fallback" assert RequestContext.FALLBACK.value == "fallback"
def test_request_context_types(self): def test_request_context_types(self):
@@ -101,7 +101,7 @@ class TestGetRequestContextBackwardCompatibility:
assert context == RequestContext.STORE_DASHBOARD assert context == RequestContext.STORE_DASHBOARD
def test_get_request_context_maps_storefront(self): 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 from app.modules.enums import FrontendType
request = Mock(spec=Request) request = Mock(spec=Request)
@@ -113,7 +113,7 @@ class TestGetRequestContextBackwardCompatibility:
warnings.simplefilter("ignore", DeprecationWarning) warnings.simplefilter("ignore", DeprecationWarning)
context = get_request_context(request) context = get_request_context(request)
assert context == RequestContext.SHOP assert context == RequestContext.STOREFRONT
def test_get_request_context_maps_platform_to_fallback(self): def test_get_request_context_maps_platform_to_fallback(self):
"""Test get_request_context maps FrontendType.PLATFORM to RequestContext.FALLBACK.""" """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.""" """Test path-based detection with /store/ prefix."""
request = Mock(spec=Request) request = Mock(spec=Request)
request.headers = {"host": "localhost"} 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 # Set platform_clean_path to simulate PlatformContextMiddleware output
request.state = Mock() 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) context = StoreContextManager.detect_store_context(request)
@@ -119,10 +119,10 @@ class TestStoreContextManager:
"""Test path-based detection with /stores/ prefix.""" """Test path-based detection with /stores/ prefix."""
request = Mock(spec=Request) request = Mock(spec=Request)
request.headers = {"host": "localhost"} 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 # Set platform_clean_path to simulate PlatformContextMiddleware output
request.state = Mock() 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) context = StoreContextManager.detect_store_context(request)
@@ -310,24 +310,24 @@ class TestStoreContextManager:
def test_extract_clean_path_from_store_path(self): def test_extract_clean_path_from_store_path(self):
"""Test extracting clean path from /store/ prefix.""" """Test extracting clean path from /store/ prefix."""
request = Mock(spec=Request) 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"} store_context = {"detection_method": "path", "path_prefix": "/store/store1"}
clean_path = StoreContextManager.extract_clean_path(request, store_context) 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): def test_extract_clean_path_from_stores_path(self):
"""Test extracting clean path from /stores/ prefix.""" """Test extracting clean path from /stores/ prefix."""
request = Mock(spec=Request) 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"} store_context = {"detection_method": "path", "path_prefix": "/stores/store1"}
clean_path = StoreContextManager.extract_clean_path(request, store_context) 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): def test_extract_clean_path_root(self):
"""Test extracting clean path when result is empty (should return /).""" """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): def test_extract_clean_path_no_path_context(self):
"""Test extracting clean path for non-path detection methods.""" """Test extracting clean path for non-path detection methods."""
request = Mock(spec=Request) request = Mock(spec=Request)
request.url = Mock(path="/shop/products") request.url = Mock(path="/storefront/products")
store_context = {"detection_method": "subdomain", "subdomain": "store1"} store_context = {"detection_method": "subdomain", "subdomain": "store1"}
clean_path = StoreContextManager.extract_clean_path(request, store_context) 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): def test_extract_clean_path_no_context(self):
"""Test extracting clean path with no store context.""" """Test extracting clean path with no store context."""
request = Mock(spec=Request) request = Mock(spec=Request)
request.url = Mock(path="/shop/products") request.url = Mock(path="/storefront/products")
clean_path = StoreContextManager.extract_clean_path(request, None) clean_path = StoreContextManager.extract_clean_path(request, None)
assert clean_path == "/shop/products" assert clean_path == "/storefront/products"
# ======================================================================== # ========================================================================
# Request Type Detection Tests # Request Type Detection Tests
@@ -392,7 +392,7 @@ class TestStoreContextManager:
"""Test non-admin request.""" """Test non-admin request."""
request = Mock(spec=Request) request = Mock(spec=Request)
request.headers = {"host": "store1.platform.com"} request.headers = {"host": "store1.platform.com"}
request.url = Mock(path="/shop") request.url = Mock(path="/storefront")
assert StoreContextManager.is_admin_request(request) is False assert StoreContextManager.is_admin_request(request) is False
@@ -406,49 +406,10 @@ class TestStoreContextManager:
def test_is_not_api_request(self): def test_is_not_api_request(self):
"""Test non-API request.""" """Test non-API request."""
request = Mock(spec=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 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 # Extract Store From Referer Tests
# ======================================================================== # ========================================================================
@@ -457,7 +418,7 @@ class TestStoreContextManager:
"""Test extracting store from referer with /stores/ path.""" """Test extracting store from referer with /stores/ path."""
request = Mock(spec=Request) request = Mock(spec=Request)
request.headers = { 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) context = StoreContextManager.extract_store_from_referer(request)
@@ -472,7 +433,7 @@ class TestStoreContextManager:
"""Test extracting store from referer with /store/ path.""" """Test extracting store from referer with /store/ path."""
request = Mock(spec=Request) request = Mock(spec=Request)
request.headers = { 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) context = StoreContextManager.extract_store_from_referer(request)
@@ -486,7 +447,7 @@ class TestStoreContextManager:
def test_extract_store_from_referer_subdomain(self): def test_extract_store_from_referer_subdomain(self):
"""Test extracting store from referer with subdomain.""" """Test extracting store from referer with subdomain."""
request = Mock(spec=Request) 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: with patch("middleware.store_context.settings") as mock_settings:
mock_settings.platform_domain = "platform.com" mock_settings.platform_domain = "platform.com"
@@ -501,7 +462,7 @@ class TestStoreContextManager:
def test_extract_store_from_referer_custom_domain(self): def test_extract_store_from_referer_custom_domain(self):
"""Test extracting store from referer with custom domain.""" """Test extracting store from referer with custom domain."""
request = Mock(spec=Request) 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: with patch("middleware.store_context.settings") as mock_settings:
mock_settings.platform_domain = "platform.com" mock_settings.platform_domain = "platform.com"
@@ -525,7 +486,7 @@ class TestStoreContextManager:
def test_extract_store_from_referer_origin_header(self): def test_extract_store_from_referer_origin_header(self):
"""Test extracting store from origin header when referer is missing.""" """Test extracting store from origin header when referer is missing."""
request = Mock(spec=Request) 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) context = StoreContextManager.extract_store_from_referer(request)
@@ -548,7 +509,7 @@ class TestStoreContextManager:
def test_extract_store_from_referer_ignores_www_subdomain(self): def test_extract_store_from_referer_ignores_www_subdomain(self):
"""Test that www subdomain is not extracted from referer.""" """Test that www subdomain is not extracted from referer."""
request = Mock(spec=Request) 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: with patch("middleware.store_context.settings") as mock_settings:
mock_settings.platform_domain = "platform.com" mock_settings.platform_domain = "platform.com"
@@ -560,7 +521,7 @@ class TestStoreContextManager:
def test_extract_store_from_referer_localhost_not_custom_domain(self): def test_extract_store_from_referer_localhost_not_custom_domain(self):
"""Test that localhost is not treated as custom domain.""" """Test that localhost is not treated as custom domain."""
request = Mock(spec=Request) 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: with patch("middleware.store_context.settings") as mock_settings:
mock_settings.platform_domain = "platform.com" mock_settings.platform_domain = "platform.com"
@@ -601,7 +562,7 @@ class TestStoreContextManager:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
"/shop/products", "/storefront/products",
"/admin/dashboard", "/admin/dashboard",
"/api/stores", "/api/stores",
"/about", "/about",
@@ -686,7 +647,7 @@ class TestStoreContextMiddleware:
request = Mock(spec=Request) request = Mock(spec=Request)
request.headers = {"host": "store1.platform.com"} request.headers = {"host": "store1.platform.com"}
request.url = Mock(path="/shop/products") request.url = Mock(path="/storefront/products")
request.state = Mock() request.state = Mock()
call_next = AsyncMock(return_value=Mock()) call_next = AsyncMock(return_value=Mock())
@@ -714,7 +675,7 @@ class TestStoreContextMiddleware:
patch.object( patch.object(
StoreContextManager, StoreContextManager,
"extract_clean_path", "extract_clean_path",
return_value="/shop/products", return_value="/storefront/products",
), ),
patch("middleware.store_context.get_db", return_value=iter([mock_db])), 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 is mock_store
assert request.state.store_context == store_context 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) call_next.assert_called_once_with(request)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -732,7 +693,7 @@ class TestStoreContextMiddleware:
request = Mock(spec=Request) request = Mock(spec=Request)
request.headers = {"host": "nonexistent.platform.com"} request.headers = {"host": "nonexistent.platform.com"}
request.url = Mock(path="/shop") request.url = Mock(path="/storefront")
request.state = Mock() request.state = Mock()
call_next = AsyncMock(return_value=Mock()) call_next = AsyncMock(return_value=Mock())
@@ -756,7 +717,7 @@ class TestStoreContextMiddleware:
assert request.state.store is None assert request.state.store is None
assert request.state.store_context == store_context 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) call_next.assert_called_once_with(request)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -820,146 +781,6 @@ class TestStoreContextMiddleware:
assert request.state.clean_path == path assert request.state.clean_path == path
call_next.assert_called_once_with(request) 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.unit
@pytest.mark.stores @pytest.mark.stores