Files
orion/tests/integration/middleware/middleware_test_routes.py
Samir Boulahtit 0984ff7d17 feat(tenancy): add merchant-level domain with store override
Merchants can now register domains (e.g., myloyaltyprogram.lu) that all
their stores inherit. Individual stores can override with their own custom
domain. Resolution priority: StoreDomain > MerchantDomain > subdomain.

- Add MerchantDomain model, schema, service, and admin API endpoints
- Add merchant domain fallback in platform and store context middleware
- Add Merchant.primary_domain and Store.effective_domain properties
- Add Alembic migration for merchant_domains table
- Update loyalty user journey docs with subscription & domain setup flow
- Add unit tests (50 passing) and integration tests (15 passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 22:04:49 +01:00

578 lines
20 KiB
Python

# tests/integration/middleware/middleware_test_routes.py
"""
Test routes for middleware integration tests.
These routes are registered at module load time and used by middleware tests
to verify that store context, theme, and other middleware features work correctly.
IMPORTANT: Routes are organized by prefix to avoid conflicts:
- /middleware-test/* - General middleware testing
- /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
"""
from fastapi import APIRouter, Request
# Main test router for general middleware tests
router = APIRouter(prefix="/middleware-test")
# =============================================================================
# Store Context Detection Routes
# =============================================================================
@router.get("/subdomain-detection")
async def test_subdomain_detection(request: Request):
"""Test store detection via subdomain routing."""
store = getattr(request.state, "store", None)
return {
"store_detected": store is not None,
"store_id": store.id if store else None,
"store_code": store.store_code if store else None,
"store_name": store.name if store else None,
"detection_method": "subdomain",
}
@router.get("/subdomain-port")
async def test_subdomain_port(request: Request):
"""Test store detection via subdomain with port number."""
store = getattr(request.state, "store", None)
return {
"store_detected": store is not None,
"store_code": store.store_code if store else None,
}
@router.get("/nonexistent-subdomain")
async def test_nonexistent_subdomain(request: Request):
"""Test nonexistent subdomain handling."""
store = getattr(request.state, "store", None)
return {
"store_detected": store is not None,
"store": None, # Don't serialize store object
}
@router.get("/custom-domain")
async def test_custom_domain(request: Request):
"""Test store detection via custom domain."""
store = getattr(request.state, "store", None)
return {
"store_detected": store is not None,
"store_id": store.id if store else None,
"store_code": store.store_code if store else None,
"detection_method": "custom_domain",
}
@router.get("/custom-domain-www")
async def test_custom_domain_www(request: Request):
"""Test store detection via custom domain with www prefix."""
store = getattr(request.state, "store", None)
return {
"store_detected": store is not None,
"store_code": store.store_code if store else None,
}
@router.get("/merchant-domain-detection")
async def test_merchant_domain_detection(request: Request):
"""Test store detection via merchant domain routing."""
store = getattr(request.state, "store", None)
store_context = getattr(request.state, "store_context", None)
return {
"store_detected": store is not None,
"store_id": store.id if store else None,
"store_code": store.store_code if store else None,
"store_name": store.name if store else None,
"merchant_domain": store_context.get("merchant_domain") if store_context else None,
"merchant_id": store_context.get("merchant_id") if store_context else None,
}
@router.get("/inactive-store-detection")
async def test_inactive_store_detection(request: Request):
"""Test inactive store detection."""
store = getattr(request.state, "store", None)
return {"store_detected": store is not None}
@router.get("/platform-domain")
async def test_platform_domain(request: Request):
"""Test platform domain without subdomain."""
store = getattr(request.state, "store", None)
return {"store_detected": store is not None}
@router.get("/store-id-injection")
async def test_store_id_injection(request: Request):
"""Test store_id injection into request.state."""
store = getattr(request.state, "store", None)
store_id = store.id if store else None
return {
"has_store_id": store_id is not None,
"store_id": store_id,
"store_id_type": type(store_id).__name__ if store_id is not None else None,
}
@router.get("/store-object-injection")
async def test_store_object_injection(request: Request):
"""Test full store object injection into request.state."""
store = getattr(request.state, "store", None)
return {
"has_store": store is not None,
"store_attributes": (
{
"id": store.id,
"name": store.name,
"code": store.store_code,
"subdomain": store.subdomain,
"is_active": store.is_active,
}
if store
else None
),
}
# =============================================================================
# Theme Loading Routes
# Note: Theme structure is: {"colors": {"primary": ..., "secondary": ...}, "branding": {"logo": ...}, ...}
# =============================================================================
@router.get("/theme-loading")
async def test_theme_loading(request: Request):
"""Test theme loading - full theme data."""
theme = getattr(request.state, "theme", None)
# Flatten theme for easier testing
if theme:
colors = theme.get("colors", {})
branding = theme.get("branding", {})
return {
"has_theme": True,
"theme_data": theme,
"primary_color": colors.get("primary"),
"secondary_color": colors.get("secondary"),
"logo_url": branding.get("logo"),
"favicon_url": branding.get("favicon"),
"custom_css": theme.get("custom_css"),
}
return {"has_theme": False, "theme_data": None}
@router.get("/theme-default")
async def test_theme_default(request: Request):
"""Test default theme for store without custom theme."""
theme = getattr(request.state, "theme", None)
if theme:
colors = theme.get("colors", {})
return {
"has_theme": True,
"theme_data": theme,
"primary_color": colors.get("primary"),
"secondary_color": colors.get("secondary"),
}
return {"has_theme": False, "theme_data": None}
@router.get("/theme-no-store")
async def test_theme_no_store(request: Request):
"""Test theme when no store is detected."""
store = getattr(request.state, "store", None)
theme = getattr(request.state, "theme", None)
return {
"has_theme": theme is not None,
"has_store": store is not None,
}
@router.get("/theme-fields")
async def test_theme_fields(request: Request):
"""Test theme contains all expected fields."""
theme = getattr(request.state, "theme", {}) or {}
colors = theme.get("colors", {})
branding = theme.get("branding", {})
return {
"primary_color": colors.get("primary"),
"secondary_color": colors.get("secondary"),
"logo_url": branding.get("logo"),
"favicon_url": branding.get("favicon"),
"custom_css": theme.get("custom_css"),
}
@router.get("/theme-structure")
async def test_theme_structure(request: Request):
"""Test theme structure."""
theme = getattr(request.state, "theme", {}) or {}
colors = theme.get("colors", {})
branding = theme.get("branding", {})
return {
"has_primary_color": "primary" in colors,
"has_secondary_color": "secondary" in colors,
"has_logo_url": "logo" in branding,
"has_favicon_url": "favicon" in branding,
"has_custom_css": "custom_css" in theme,
}
@router.get("/theme-partial")
async def test_theme_partial(request: Request):
"""Test partial theme handling."""
theme = getattr(request.state, "theme", {}) or {}
colors = theme.get("colors", {})
branding = theme.get("branding", {})
return {
"has_theme": bool(theme),
"primary_color": colors.get("primary", "default"),
"logo_url": branding.get("logo", "default"),
}
@router.get("/theme-consistency")
async def test_theme_consistency(request: Request):
"""Test theme consistency across requests."""
theme = getattr(request.state, "theme", {}) or {}
colors = theme.get("colors", {})
return {"theme_primary": colors.get("primary")}
@router.get("/theme-mutable")
async def test_theme_mutable(request: Request):
"""Test theme dict can be read."""
theme = getattr(request.state, "theme", {}) or {}
colors = theme.get("colors", {})
primary = colors.get("primary")
return {"can_read": primary is not None, "value": primary}
@router.get("/theme-store-dependency")
async def test_theme_store_dependency(request: Request):
"""Test theme depends on store middleware."""
store = getattr(request.state, "store", None)
theme = getattr(request.state, "theme", None)
return {
"has_store": store is not None,
"store_id": store.id if store else None,
"has_theme": theme is not None,
}
# =============================================================================
# Context Detection Routes
# =============================================================================
@router.get("/context-detection")
async def test_context_detection(request: Request):
"""Test context detection."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
return {
"context": context_type.value if context_type else None,
"context_enum": context_type.name if context_type else None,
"store_detected": store is not None,
"clean_path": getattr(request.state, "clean_path", None),
}
# =============================================================================
# Middleware Order Routes
# =============================================================================
@router.get("/middleware-order")
async def test_middleware_order(request: Request):
"""Test middleware execution order."""
store = getattr(request.state, "store", None)
context_type = getattr(request.state, "context_type", None)
theme = getattr(request.state, "theme", None)
return {
"store_detected": store is not None,
"context": context_type.value if context_type else None,
"theme_loaded": theme is not None,
"clean_path": getattr(request.state, "clean_path", None),
"has_clean_path": hasattr(request.state, "clean_path"),
"has_context_type": context_type is not None,
}
@router.get("/execution-order")
async def test_execution_order(request: Request):
"""Test middleware execution order - detailed."""
store = getattr(request.state, "store", None)
context_type = getattr(request.state, "context_type", None)
theme = getattr(request.state, "theme", None)
colors = theme.get("colors", {}) if theme else {}
return {
"has_store": store is not None,
"has_clean_path": hasattr(request.state, "clean_path"),
"has_context_type": context_type is not None,
"has_theme": theme is not None,
"theme_primary_color": colors.get("primary"),
}
# =============================================================================
# API Context Test Router
# =============================================================================
api_router = APIRouter(prefix="/api/middleware-test")
@api_router.get("/context")
async def test_api_context(request: Request):
"""Test API context detection."""
context_type = getattr(request.state, "context_type", None)
return {
"context_type": context_type.value if context_type else None,
"context_enum": context_type.name if context_type else None,
}
@api_router.get("/nested-context")
async def test_nested_api_context(request: Request):
"""Test nested API path context."""
context_type = getattr(request.state, "context_type", None)
return {"context_type": context_type.value if context_type else None}
@api_router.get("/store-priority")
async def test_api_store_priority(request: Request):
"""Test API context priority over store."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
return {
"context_type": context_type.value if context_type else None,
"has_store": store is not None,
}
@api_router.get("/fallback-context")
async def test_fallback_context(request: Request):
"""Test fallback context."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
return {
"context_type": context_type.value if context_type else None,
"context_enum": context_type.name if context_type else None,
"has_store": store is not None,
}
@api_router.get("/clean-path-context")
async def test_clean_path_context(request: Request):
"""Test clean path context detection."""
context_type = getattr(request.state, "context_type", None)
return {
"context_type": context_type.value if context_type else None,
"clean_path": getattr(request.state, "clean_path", None),
"original_path": request.url.path,
}
@api_router.get("/enum")
async def test_api_enum(request: Request):
"""Test context enum instance."""
from middleware.context import RequestContext
context = getattr(request.state, "context_type", None)
return {
"is_enum": isinstance(context, RequestContext) if context else False,
"enum_name": context.name if context else None,
"enum_value": context.value if context else None,
}
@api_router.get("/theme")
async def test_api_theme(request: Request):
"""Test theme in API context."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
theme = getattr(request.state, "theme", None)
return {
"context_type": context_type.value if context_type else None,
"has_store": store is not None,
"has_theme": theme is not None,
}
@api_router.get("/missing-store")
async def test_missing_store(request: Request):
"""Test missing store handling."""
store = getattr(request.state, "store", None)
context_type = getattr(request.state, "context_type", None)
return {
"has_store": store is not None,
"store": None, # Don't serialize
"context_type": context_type.value if context_type else None,
}
@api_router.get("/inactive-store")
async def test_inactive_store(request: Request):
"""Test inactive store handling."""
store = getattr(request.state, "store", None)
return {
"has_store": store is not None,
"store": None, # Don't serialize
}
@api_router.get("/admin-subdomain-context")
async def test_admin_subdomain_context(request: Request):
"""Test admin subdomain context."""
context_type = getattr(request.state, "context_type", None)
return {"context_type": context_type.value if context_type else None}
# =============================================================================
# Admin Context Test Router
# =============================================================================
admin_router = APIRouter(prefix="/admin/middleware-test")
@admin_router.get("/context")
async def test_admin_context(request: Request):
"""Test admin context detection."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
theme = getattr(request.state, "theme", None)
return {
"context_type": context_type.value if context_type else None,
"context_enum": context_type.name if context_type else None,
"has_store": store is not None,
"has_theme": theme is not None,
}
@admin_router.get("/nested-context")
async def test_admin_nested_context(request: Request):
"""Test nested admin path context."""
context_type = getattr(request.state, "context_type", None)
return {"context_type": context_type.value if context_type else None}
@admin_router.get("/store-priority")
async def test_admin_store_priority(request: Request):
"""Test admin context priority over store."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
return {
"context_type": context_type.value if context_type else None,
"has_store": store is not None,
}
@admin_router.get("/no-theme")
async def test_admin_no_theme(request: Request):
"""Test admin context has no theme."""
context_type = getattr(request.state, "context_type", None)
theme = getattr(request.state, "theme", None)
return {
"context_type": context_type.value if context_type else None,
"has_theme": theme is not None,
}
# =============================================================================
# Store Dashboard Context Test Router
# =============================================================================
store_router = APIRouter(prefix="/store/middleware-test")
@store_router.get("/context")
async def test_store_dashboard_context(request: Request):
"""Test store dashboard context detection."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
return {
"context_type": context_type.value if context_type else None,
"context_enum": context_type.name if context_type else None,
"has_store": store is not None,
"store_id": store.id if store else None,
"store_code": store.store_code if store else None,
}
@store_router.get("/nested-context")
async def test_store_nested_context(request: Request):
"""Test nested store dashboard path context."""
context_type = getattr(request.state, "context_type", None)
return {"context_type": context_type.value if context_type else None}
@store_router.get("/priority")
async def test_store_priority(request: Request):
"""Test store dashboard context priority."""
context_type = getattr(request.state, "context_type", None)
return {"context_type": context_type.value if context_type else None}
@store_router.get("/theme")
async def test_store_dashboard_theme(request: Request):
"""Test theme in store dashboard context."""
context_type = getattr(request.state, "context_type", None)
theme = getattr(request.state, "theme", None)
colors = theme.get("colors", {}) if theme else {}
return {
"context_type": context_type.value if context_type else None,
"has_theme": theme is not None,
"theme_secondary": colors.get("secondary"),
}
# =============================================================================
# Shop Context Test Router
# =============================================================================
shop_router = APIRouter(prefix="/shop/middleware-test")
@shop_router.get("/context")
async def test_shop_context(request: Request):
"""Test shop context detection."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
theme = getattr(request.state, "theme", None)
return {
"context_type": context_type.value if context_type else None,
"context_enum": context_type.name if context_type else None,
"has_store": store is not None,
"store_id": store.id if store else None,
"has_theme": theme is not None,
}
@shop_router.get("/custom-domain-context")
async def test_shop_custom_domain_context(request: Request):
"""Test shop context with custom domain."""
context_type = getattr(request.state, "context_type", None)
store = getattr(request.state, "store", None)
return {
"context_type": context_type.value if context_type else None,
"store_code": store.store_code if store else None,
"store_id": store.id if store else None,
}
@shop_router.get("/theme")
async def test_shop_theme(request: Request):
"""Test theme in shop context."""
context_type = getattr(request.state, "context_type", None)
theme = getattr(request.state, "theme", None)
colors = theme.get("colors", {}) if theme else {}
return {
"context_type": context_type.value if context_type else None,
"has_theme": theme is not None,
"theme_primary": colors.get("primary"),
}