refactor: centralize frontend detection with FrontendDetector

Major architecture change to unify frontend detection:

## Problem Solved
- Eliminated code duplication across 3 middleware files
- Fixed incomplete path detection (now detects /api/v1/admin/*)
- Unified on FrontendType enum (deprecates RequestContext)
- Added request.state.frontend_type for all requests

## New Components
- app/core/frontend_detector.py: Centralized FrontendDetector class
- middleware/frontend_type.py: FrontendTypeMiddleware (replaces ContextMiddleware)
- docs/architecture/frontend-detection.md: Complete architecture documentation

## Changes
- main.py: Use FrontendTypeMiddleware instead of ContextMiddleware
- middleware/context.py: Deprecated (kept for backwards compatibility)
- middleware/platform_context.py: Use FrontendDetector.is_admin()
- middleware/vendor_context.py: Use FrontendDetector.is_admin()
- middleware/language.py: Use FrontendType instead of context_value
- app/exceptions/handler.py: Use FrontendType.STOREFRONT
- app/exceptions/error_renderer.py: Use FrontendType
- Customer routes: Cookie path changed from /shop to /storefront

## Documentation
- docs/architecture/frontend-detection.md: New comprehensive docs
- docs/architecture/middleware.md: Updated for new system
- docs/architecture/request-flow.md: Updated for FrontendType
- docs/backend/middleware-reference.md: Updated API reference

## Tests
- tests/unit/core/test_frontend_detector.py: 37 new tests
- tests/unit/middleware/test_frontend_type.py: 11 new tests
- tests/unit/middleware/test_context.py: Updated for compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 16:15:19 +01:00
parent e77535e2cd
commit b769f5a047
17 changed files with 1393 additions and 915 deletions

View File

@@ -0,0 +1,256 @@
# tests/unit/core/test_frontend_detector.py
"""
Unit tests for FrontendDetector.
Tests cover:
- Detection for all frontend types (ADMIN, VENDOR, STOREFRONT, PLATFORM)
- Path-based detection (dev mode)
- Subdomain-based detection (prod mode)
- Custom domain detection
- Legacy /shop/ path support
- Priority order of detection methods
"""
import pytest
from app.core.frontend_detector import FrontendDetector, get_frontend_type
from app.modules.enums import FrontendType
@pytest.mark.unit
class TestFrontendDetectorAdmin:
"""Test suite for admin frontend detection."""
def test_detect_admin_from_subdomain(self):
"""Test admin detection from admin subdomain."""
result = FrontendDetector.detect(host="admin.oms.lu", path="/dashboard")
assert result == FrontendType.ADMIN
def test_detect_admin_from_subdomain_with_port(self):
"""Test admin detection from admin subdomain with port."""
result = FrontendDetector.detect(host="admin.localhost:8000", path="/dashboard")
assert result == FrontendType.ADMIN
def test_detect_admin_from_path(self):
"""Test admin detection from /admin path."""
result = FrontendDetector.detect(host="localhost", path="/admin/vendors")
assert result == FrontendType.ADMIN
def test_detect_admin_from_api_path(self):
"""Test admin detection from /api/v1/admin path."""
result = FrontendDetector.detect(host="localhost", path="/api/v1/admin/users")
assert result == FrontendType.ADMIN
def test_detect_admin_nested_path(self):
"""Test admin detection with nested admin path."""
result = FrontendDetector.detect(host="oms.lu", path="/admin/vendors/123/products")
assert result == FrontendType.ADMIN
@pytest.mark.unit
class TestFrontendDetectorVendor:
"""Test suite for vendor dashboard frontend detection."""
def test_detect_vendor_from_path(self):
"""Test vendor detection from /vendor/ path."""
result = FrontendDetector.detect(host="localhost", path="/vendor/settings")
assert result == FrontendType.VENDOR
def test_detect_vendor_from_api_path(self):
"""Test vendor detection from /api/v1/vendor path."""
result = FrontendDetector.detect(host="localhost", path="/api/v1/vendor/products")
assert result == FrontendType.VENDOR
def test_detect_vendor_nested_path(self):
"""Test vendor detection with nested vendor path."""
result = FrontendDetector.detect(host="oms.lu", path="/vendor/dashboard/analytics")
assert result == FrontendType.VENDOR
def test_vendors_plural_not_vendor_dashboard(self):
"""Test that /vendors/ path is NOT vendor dashboard (it's storefront)."""
result = FrontendDetector.detect(host="localhost", path="/vendors/wizamart/storefront")
assert result == FrontendType.STOREFRONT
@pytest.mark.unit
class TestFrontendDetectorStorefront:
"""Test suite for storefront frontend detection."""
def test_detect_storefront_from_path(self):
"""Test storefront detection from /storefront path."""
result = FrontendDetector.detect(host="localhost", path="/storefront/products")
assert result == FrontendType.STOREFRONT
def test_detect_storefront_from_api_path(self):
"""Test storefront detection from /api/v1/storefront path."""
result = FrontendDetector.detect(host="localhost", path="/api/v1/storefront/cart")
assert result == FrontendType.STOREFRONT
def test_detect_storefront_from_vendors_path(self):
"""Test storefront detection from /vendors/ path (path-based vendor access)."""
result = FrontendDetector.detect(host="localhost", path="/vendors/wizamart/products")
assert result == FrontendType.STOREFRONT
def test_detect_storefront_from_vendor_subdomain(self):
"""Test storefront detection from vendor subdomain."""
result = FrontendDetector.detect(host="wizamart.oms.lu", path="/products")
assert result == FrontendType.STOREFRONT
def test_detect_storefront_from_vendor_context(self):
"""Test storefront detection when vendor context is set."""
result = FrontendDetector.detect(
host="mybakery.lu", path="/about", has_vendor_context=True
)
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
class TestFrontendDetectorPlatform:
"""Test suite for platform marketing frontend detection."""
def test_detect_platform_from_root(self):
"""Test platform detection from root path."""
result = FrontendDetector.detect(host="localhost", path="/")
assert result == FrontendType.PLATFORM
def test_detect_platform_from_marketing_page(self):
"""Test platform detection from marketing page."""
result = FrontendDetector.detect(host="oms.lu", path="/pricing")
assert result == FrontendType.PLATFORM
def test_detect_platform_from_about(self):
"""Test platform detection from about page."""
result = FrontendDetector.detect(host="localhost", path="/about")
assert result == FrontendType.PLATFORM
def test_detect_platform_from_api_path(self):
"""Test platform detection from /api/v1/platform path."""
result = FrontendDetector.detect(host="localhost", path="/api/v1/platform/config")
assert result == FrontendType.PLATFORM
@pytest.mark.unit
class TestFrontendDetectorPriority:
"""Test suite for detection priority order."""
def test_admin_subdomain_priority_over_path(self):
"""Test that admin subdomain takes priority."""
result = FrontendDetector.detect(host="admin.oms.lu", path="/storefront/products")
assert result == FrontendType.ADMIN
def test_admin_path_priority_over_vendor_context(self):
"""Test that admin path takes priority over vendor context."""
result = FrontendDetector.detect(
host="localhost", path="/admin/dashboard", has_vendor_context=True
)
assert result == FrontendType.ADMIN
def test_path_priority_over_subdomain(self):
"""Test that explicit path takes priority for vendor/storefront."""
# /vendor/ path on a vendor subdomain -> VENDOR (path wins)
result = FrontendDetector.detect(host="wizamart.oms.lu", path="/vendor/settings")
assert result == FrontendType.VENDOR
@pytest.mark.unit
class TestFrontendDetectorHelpers:
"""Test suite for helper methods."""
def test_strip_port(self):
"""Test port stripping from host."""
assert FrontendDetector._strip_port("localhost:8000") == "localhost"
assert FrontendDetector._strip_port("oms.lu") == "oms.lu"
assert FrontendDetector._strip_port("admin.localhost:9999") == "admin.localhost"
def test_get_subdomain(self):
"""Test subdomain extraction."""
assert FrontendDetector._get_subdomain("wizamart.oms.lu") == "wizamart"
assert FrontendDetector._get_subdomain("admin.oms.lu") == "admin"
assert FrontendDetector._get_subdomain("oms.lu") is None
assert FrontendDetector._get_subdomain("localhost") is None
assert FrontendDetector._get_subdomain("127.0.0.1") is None
def test_is_admin(self):
"""Test is_admin convenience method."""
assert FrontendDetector.is_admin("admin.oms.lu", "/dashboard") is True
assert FrontendDetector.is_admin("localhost", "/admin/vendors") is True
assert FrontendDetector.is_admin("localhost", "/vendor/settings") is False
def test_is_vendor(self):
"""Test is_vendor convenience method."""
assert FrontendDetector.is_vendor("localhost", "/vendor/settings") is True
assert FrontendDetector.is_vendor("localhost", "/api/v1/vendor/products") is True
assert FrontendDetector.is_vendor("localhost", "/admin/dashboard") is False
def test_is_storefront(self):
"""Test is_storefront convenience method."""
assert FrontendDetector.is_storefront("localhost", "/storefront/products") is True
assert FrontendDetector.is_storefront("wizamart.oms.lu", "/products") is True
assert FrontendDetector.is_storefront("localhost", "/admin/dashboard") is False
def test_is_platform(self):
"""Test is_platform convenience method."""
assert FrontendDetector.is_platform("localhost", "/") is True
assert FrontendDetector.is_platform("oms.lu", "/pricing") is True
assert FrontendDetector.is_platform("localhost", "/admin/dashboard") is False
def test_is_api_request(self):
"""Test is_api_request convenience method."""
assert FrontendDetector.is_api_request("/api/v1/vendors") is True
assert FrontendDetector.is_api_request("/api/v1/admin/users") is True
assert FrontendDetector.is_api_request("/admin/dashboard") is False
@pytest.mark.unit
class TestGetFrontendTypeFunction:
"""Test suite for get_frontend_type convenience function."""
def test_get_frontend_type_admin(self):
"""Test get_frontend_type returns admin."""
result = get_frontend_type("localhost", "/admin/dashboard")
assert result == FrontendType.ADMIN
def test_get_frontend_type_vendor(self):
"""Test get_frontend_type returns vendor."""
result = get_frontend_type("localhost", "/vendor/settings")
assert result == FrontendType.VENDOR
def test_get_frontend_type_storefront(self):
"""Test get_frontend_type returns storefront."""
result = get_frontend_type("localhost", "/storefront/products")
assert result == FrontendType.STOREFRONT
def test_get_frontend_type_platform(self):
"""Test get_frontend_type returns platform."""
result = get_frontend_type("localhost", "/pricing")
assert result == FrontendType.PLATFORM
@pytest.mark.unit
class TestReservedSubdomains:
"""Test suite for reserved subdomain handling."""
def test_www_subdomain_not_storefront(self):
"""Test that www subdomain is not treated as vendor storefront."""
result = FrontendDetector.detect(host="www.oms.lu", path="/")
assert result == FrontendType.PLATFORM
def test_api_subdomain_not_storefront(self):
"""Test that api subdomain is not treated as vendor storefront."""
result = FrontendDetector.detect(host="api.oms.lu", path="/v1/products")
assert result == FrontendType.PLATFORM
def test_portal_subdomain_not_storefront(self):
"""Test that portal subdomain is not treated as vendor storefront."""
result = FrontendDetector.detect(host="portal.oms.lu", path="/")
assert result == FrontendType.PLATFORM