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:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user