- Fix IPv6 host parsing with _strip_port() utility - Remove dangerous StorePlatform→Store.subdomain silent fallback - Close storefront gate bypass when frontend_type is None - Add custom subdomain management UI and API for stores - Add domain health diagnostic tool - Convert db.add() in loops to db.add_all() (24 PERF-006 fixes) - Add tests for all new functionality (18 subdomain service tests) - Add .github templates for validator compliance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
818 lines
30 KiB
Python
818 lines
30 KiB
Python
# tests/unit/middleware/test_storefront_access.py
|
|
"""
|
|
Unit tests for StorefrontAccessMiddleware.
|
|
|
|
Tests cover:
|
|
- Passthrough for non-storefront frontend types (ADMIN, STORE, PLATFORM, MERCHANT)
|
|
- Passthrough for static file requests
|
|
- Blocking when no store is detected (not_found)
|
|
- Blocking when store has no subscription (not_activated)
|
|
- Blocking when subscription is inactive (not_activated)
|
|
- Passthrough when subscription is active (TRIAL, ACTIVE, PAST_DUE, CANCELLED)
|
|
- Multi-platform subscription resolution (platform-specific, fallback)
|
|
- API requests return JSON 403
|
|
- Page requests return HTML 403
|
|
- Language detection for unavailable page
|
|
- request.state.subscription and subscription_tier are set on passthrough
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
from fastapi import Request
|
|
from starlette.responses import JSONResponse
|
|
|
|
from app.modules.enums import FrontendType
|
|
from middleware.storefront_access import (
|
|
MESSAGES,
|
|
StorefrontAccessMiddleware,
|
|
_is_static_request,
|
|
)
|
|
|
|
# =============================================================================
|
|
# Helper: build a mock Request with the right state attributes
|
|
# =============================================================================
|
|
|
|
def _make_request(
|
|
path="/storefront/products",
|
|
frontend_type=FrontendType.STOREFRONT,
|
|
store=None,
|
|
platform=None,
|
|
language="en",
|
|
theme=None,
|
|
):
|
|
"""Create a mock Request with pre-set state attributes."""
|
|
request = Mock(spec=Request)
|
|
request.url = Mock(path=path)
|
|
request.state = Mock()
|
|
request.state.frontend_type = frontend_type
|
|
request.state.store = store
|
|
request.state.platform = platform
|
|
request.state.language = language
|
|
request.state.theme = theme
|
|
return request
|
|
|
|
|
|
def _make_store(store_id=1, subdomain="testshop", merchant_id=10):
|
|
"""Create a mock Store object."""
|
|
store = Mock()
|
|
store.id = store_id
|
|
store.subdomain = subdomain
|
|
store.merchant_id = merchant_id
|
|
store.name = "Test Shop"
|
|
return store
|
|
|
|
|
|
def _make_platform(platform_id=1):
|
|
"""Create a mock Platform object."""
|
|
platform = Mock()
|
|
platform.id = platform_id
|
|
platform.code = "oms"
|
|
return platform
|
|
|
|
|
|
def _make_subscription(is_active=True, tier_code="essential"):
|
|
"""Create a mock MerchantSubscription."""
|
|
tier = Mock()
|
|
tier.code = tier_code
|
|
tier.id = 1
|
|
|
|
sub = Mock()
|
|
sub.is_active = is_active
|
|
sub.tier = tier
|
|
sub.status = "active" if is_active else "expired"
|
|
return sub
|
|
|
|
|
|
# =============================================================================
|
|
# Static request detection
|
|
# =============================================================================
|
|
|
|
@pytest.mark.unit
|
|
class TestIsStaticRequest:
|
|
"""Test suite for _is_static_request helper."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
[
|
|
"/static/css/style.css",
|
|
"/static/js/app.js",
|
|
"/uploads/images/photo.jpg",
|
|
"/health",
|
|
"/docs",
|
|
"/redoc",
|
|
"/openapi.json",
|
|
"/storefront/favicon.ico",
|
|
"/some/path/favicon.ico",
|
|
"/static/storefront/css/tailwind.output.css",
|
|
],
|
|
)
|
|
def test_static_paths_detected(self, path):
|
|
"""Test that static/system paths are correctly detected."""
|
|
assert _is_static_request(path) is True
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
[
|
|
"/storefront/products",
|
|
"/storefront/",
|
|
"/api/v1/storefront/cart",
|
|
"/storefront/category/shoes",
|
|
],
|
|
)
|
|
def test_non_static_paths_not_detected(self, path):
|
|
"""Test that real storefront paths are not flagged as static."""
|
|
assert _is_static_request(path) is False
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
[
|
|
"/storefront/logo.png",
|
|
"/some/path/font.woff2",
|
|
"/assets/icon.svg",
|
|
"/file.map",
|
|
],
|
|
)
|
|
def test_static_extensions_detected(self, path):
|
|
"""Test that paths ending with static extensions are detected."""
|
|
assert _is_static_request(path) is True
|
|
|
|
def test_case_insensitive(self):
|
|
"""Test detection is case-insensitive."""
|
|
assert _is_static_request("/STATIC/CSS/STYLE.CSS") is True
|
|
assert _is_static_request("/Uploads/Image.PNG") is True
|
|
|
|
|
|
# =============================================================================
|
|
# Middleware passthrough tests (non-storefront)
|
|
# =============================================================================
|
|
|
|
@pytest.mark.unit
|
|
class TestStorefrontAccessMiddlewarePassthrough:
|
|
"""Test that non-storefront requests pass through without checks."""
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"frontend_type",
|
|
[
|
|
FrontendType.ADMIN,
|
|
FrontendType.STORE,
|
|
FrontendType.PLATFORM,
|
|
FrontendType.MERCHANT,
|
|
],
|
|
)
|
|
async def test_non_storefront_passes_through(self, frontend_type):
|
|
"""Test non-storefront frontend types are not gated."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(frontend_type=frontend_type)
|
|
call_next = AsyncMock(return_value=Mock())
|
|
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_called_once_with(request)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_frontend_type_non_storefront_passes_through(self):
|
|
"""Test request with no frontend_type on non-storefront path passes through."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/admin/dashboard")
|
|
request.state.frontend_type = None
|
|
call_next = AsyncMock(return_value=Mock())
|
|
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_called_once_with(request)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_frontend_type_storefront_path_blocked(self):
|
|
"""Test request with no frontend_type on storefront path is blocked (fail-closed)."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/storefront/products")
|
|
request.state.frontend_type = None
|
|
request.state.store = None
|
|
call_next = AsyncMock()
|
|
|
|
with patch("app.templates_config.templates") as mock_templates:
|
|
mock_templates.TemplateResponse.return_value = Mock(status_code=403)
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_frontend_type_storefront_api_blocked_json(self):
|
|
"""Test request with no frontend_type on storefront API returns JSON 403."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/api/v1/storefront/cart")
|
|
request.state.frontend_type = None
|
|
call_next = AsyncMock()
|
|
|
|
response = await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_not_called()
|
|
assert isinstance(response, JSONResponse)
|
|
assert response.status_code == 403
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_frontend_type_static_passes_through(self):
|
|
"""Test request with no frontend_type on static path passes through."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/static/css/style.css")
|
|
request.state.frontend_type = None
|
|
call_next = AsyncMock(return_value=Mock())
|
|
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_called_once_with(request)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_static_file_passes_through(self):
|
|
"""Test storefront static file requests pass through."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/static/css/style.css")
|
|
call_next = AsyncMock(return_value=Mock())
|
|
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_called_once_with(request)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_favicon_passes_through(self):
|
|
"""Test favicon requests pass through."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/storefront/favicon.ico")
|
|
call_next = AsyncMock(return_value=Mock())
|
|
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_called_once_with(request)
|
|
|
|
|
|
# =============================================================================
|
|
# Blocking tests (no store / no subscription)
|
|
# =============================================================================
|
|
|
|
@pytest.mark.unit
|
|
class TestStorefrontAccessMiddlewareBlocking:
|
|
"""Test that requests are blocked when store/subscription is missing."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_store_returns_not_found(self):
|
|
"""Test 'not_found' when no store is detected."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(store=None)
|
|
call_next = AsyncMock()
|
|
|
|
with patch.object(middleware, "_render_unavailable") as mock_render:
|
|
mock_render.return_value = Mock()
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_render.assert_called_once_with(request, "not_found")
|
|
|
|
call_next.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_subscription_returns_not_activated(self):
|
|
"""Test 'not_activated' when store exists but no subscription."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store()
|
|
platform = _make_platform()
|
|
request = _make_request(store=store, platform=platform)
|
|
call_next = AsyncMock()
|
|
mock_db = MagicMock()
|
|
|
|
with (
|
|
patch(
|
|
"middleware.storefront_access.get_db",
|
|
return_value=iter([mock_db]),
|
|
),
|
|
patch.object(
|
|
middleware,
|
|
"_get_subscription",
|
|
return_value=None,
|
|
),
|
|
):
|
|
with patch.object(middleware, "_render_unavailable") as mock_render:
|
|
mock_render.return_value = Mock()
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_render.assert_called_once_with(
|
|
request, "not_activated", store
|
|
)
|
|
|
|
call_next.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inactive_subscription_returns_not_activated(self):
|
|
"""Test 'not_activated' when subscription exists but is inactive."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store()
|
|
platform = _make_platform()
|
|
request = _make_request(store=store, platform=platform)
|
|
call_next = AsyncMock()
|
|
mock_db = MagicMock()
|
|
|
|
inactive_sub = _make_subscription(is_active=False)
|
|
|
|
with (
|
|
patch(
|
|
"middleware.storefront_access.get_db",
|
|
return_value=iter([mock_db]),
|
|
),
|
|
patch.object(
|
|
middleware,
|
|
"_get_subscription",
|
|
return_value=inactive_sub,
|
|
),
|
|
):
|
|
with patch.object(middleware, "_render_unavailable") as mock_render:
|
|
mock_render.return_value = Mock()
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_render.assert_called_once_with(
|
|
request, "not_activated", store
|
|
)
|
|
|
|
call_next.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_db_session_closed_on_block(self):
|
|
"""Test database session is closed even when request is blocked."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store()
|
|
request = _make_request(store=store)
|
|
call_next = AsyncMock()
|
|
mock_db = MagicMock()
|
|
|
|
with (
|
|
patch(
|
|
"middleware.storefront_access.get_db",
|
|
return_value=iter([mock_db]),
|
|
),
|
|
patch.object(middleware, "_get_subscription", return_value=None),
|
|
patch.object(
|
|
middleware, "_render_unavailable", return_value=Mock()
|
|
),
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_db.close.assert_called_once()
|
|
|
|
|
|
# =============================================================================
|
|
# Active subscription passthrough
|
|
# =============================================================================
|
|
|
|
@pytest.mark.unit
|
|
class TestStorefrontAccessMiddlewareActiveSubscription:
|
|
"""Test passthrough and state injection for active subscriptions."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_active_subscription_passes_through(self):
|
|
"""Test active subscription lets request through."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store()
|
|
platform = _make_platform()
|
|
request = _make_request(store=store, platform=platform)
|
|
call_next = AsyncMock(return_value=Mock())
|
|
mock_db = MagicMock()
|
|
|
|
active_sub = _make_subscription(is_active=True, tier_code="professional")
|
|
|
|
with (
|
|
patch(
|
|
"middleware.storefront_access.get_db",
|
|
return_value=iter([mock_db]),
|
|
),
|
|
patch.object(
|
|
middleware, "_get_subscription", return_value=active_sub
|
|
),
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_called_once_with(request)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sets_subscription_on_request_state(self):
|
|
"""Test subscription and tier are stored on request.state."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store()
|
|
platform = _make_platform()
|
|
request = _make_request(store=store, platform=platform)
|
|
call_next = AsyncMock(return_value=Mock())
|
|
mock_db = MagicMock()
|
|
|
|
active_sub = _make_subscription(is_active=True, tier_code="essential")
|
|
|
|
with (
|
|
patch(
|
|
"middleware.storefront_access.get_db",
|
|
return_value=iter([mock_db]),
|
|
),
|
|
patch.object(
|
|
middleware, "_get_subscription", return_value=active_sub
|
|
),
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.subscription is active_sub
|
|
assert request.state.subscription_tier is active_sub.tier
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_db_session_closed_on_success(self):
|
|
"""Test database session is closed after successful passthrough."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store()
|
|
request = _make_request(store=store)
|
|
call_next = AsyncMock(return_value=Mock())
|
|
mock_db = MagicMock()
|
|
|
|
active_sub = _make_subscription(is_active=True)
|
|
|
|
with (
|
|
patch(
|
|
"middleware.storefront_access.get_db",
|
|
return_value=iter([mock_db]),
|
|
),
|
|
patch.object(
|
|
middleware, "_get_subscription", return_value=active_sub
|
|
),
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_db.close.assert_called_once()
|
|
|
|
|
|
# =============================================================================
|
|
# Multi-platform subscription resolution
|
|
# =============================================================================
|
|
|
|
@pytest.mark.unit
|
|
class TestGetSubscription:
|
|
"""Test _get_subscription multi-platform resolution logic."""
|
|
|
|
def test_uses_detected_platform(self):
|
|
"""Test subscription is fetched for the detected platform."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store(merchant_id=10)
|
|
platform = _make_platform(platform_id=2)
|
|
request = _make_request(store=store, platform=platform)
|
|
mock_db = MagicMock()
|
|
expected_sub = _make_subscription()
|
|
|
|
with patch(
|
|
"app.modules.billing.services.subscription_service.subscription_service"
|
|
) as mock_svc:
|
|
mock_svc.get_merchant_subscription.return_value = expected_sub
|
|
|
|
result = middleware._get_subscription(mock_db, store, request)
|
|
|
|
mock_svc.get_merchant_subscription.assert_called_once_with(
|
|
mock_db, 10, 2
|
|
)
|
|
assert result is expected_sub
|
|
|
|
def test_no_fallback_when_merchant_subscription_none(self):
|
|
"""Test no fallback when get_merchant_subscription returns None."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store(store_id=5, merchant_id=10)
|
|
platform = _make_platform(platform_id=2)
|
|
request = _make_request(store=store, platform=platform)
|
|
mock_db = MagicMock()
|
|
|
|
with patch(
|
|
"app.modules.billing.services.subscription_service.subscription_service"
|
|
) as mock_svc:
|
|
mock_svc.get_merchant_subscription.return_value = None
|
|
|
|
result = middleware._get_subscription(mock_db, store, request)
|
|
|
|
mock_svc.get_merchant_subscription.assert_called_once_with(
|
|
mock_db, 10, 2
|
|
)
|
|
assert result is None
|
|
|
|
def test_no_platform_returns_none(self):
|
|
"""Test when no platform is detected, returns None (no fallback)."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store(store_id=7)
|
|
request = _make_request(store=store, platform=None)
|
|
mock_db = MagicMock()
|
|
|
|
with patch(
|
|
"app.modules.billing.services.subscription_service.subscription_service"
|
|
) as mock_svc:
|
|
result = middleware._get_subscription(mock_db, store, request)
|
|
|
|
mock_svc.get_merchant_subscription.assert_not_called()
|
|
assert result is None
|
|
|
|
|
|
# =============================================================================
|
|
# Response rendering tests
|
|
# =============================================================================
|
|
|
|
@pytest.mark.unit
|
|
class TestRenderUnavailable:
|
|
"""Test _render_unavailable response generation."""
|
|
|
|
def test_api_request_returns_json_403(self):
|
|
"""Test API requests get JSON 403 response."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/api/v1/storefront/cart")
|
|
|
|
response = middleware._render_unavailable(request, "not_activated")
|
|
|
|
assert isinstance(response, JSONResponse)
|
|
assert response.status_code == 403
|
|
assert response.body is not None
|
|
# Decode JSON body
|
|
import json
|
|
|
|
body = json.loads(response.body)
|
|
assert body["error"] == "storefront_not_available"
|
|
assert body["reason"] == "not_activated"
|
|
|
|
def test_api_not_found_returns_json_403(self):
|
|
"""Test API not_found also gets JSON 403."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/api/v1/storefront/products")
|
|
|
|
response = middleware._render_unavailable(request, "not_found")
|
|
|
|
assert isinstance(response, JSONResponse)
|
|
assert response.status_code == 403
|
|
import json
|
|
|
|
body = json.loads(response.body)
|
|
assert body["reason"] == "not_found"
|
|
|
|
def test_page_request_renders_template(self):
|
|
"""Test page requests render the unavailable template."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/storefront/products", language="en")
|
|
|
|
mock_template_response = Mock(status_code=403)
|
|
|
|
with patch("app.templates_config.templates") as mock_templates:
|
|
mock_templates.TemplateResponse.return_value = mock_template_response
|
|
|
|
middleware._render_unavailable(
|
|
request, "not_activated", store=_make_store()
|
|
)
|
|
|
|
mock_templates.TemplateResponse.assert_called_once()
|
|
call_args = mock_templates.TemplateResponse.call_args
|
|
|
|
assert call_args[0][0] == "storefront/unavailable.html"
|
|
context = call_args[0][1]
|
|
assert context["request"] is request
|
|
assert context["reason"] == "not_activated"
|
|
assert context["title"] == MESSAGES["not_activated"]["en"]["title"]
|
|
assert context["message"] == MESSAGES["not_activated"]["en"]["message"]
|
|
assert context["language"] == "en"
|
|
assert call_args[1]["status_code"] == 403
|
|
|
|
@pytest.mark.parametrize("language", ["en", "fr", "de", "lb"])
|
|
def test_page_request_uses_correct_language(self, language):
|
|
"""Test unavailable page renders in the detected language."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(
|
|
path="/storefront/", language=language
|
|
)
|
|
|
|
with patch("app.templates_config.templates") as mock_templates:
|
|
mock_templates.TemplateResponse.return_value = Mock(status_code=403)
|
|
|
|
middleware._render_unavailable(request, "not_found")
|
|
|
|
context = mock_templates.TemplateResponse.call_args[0][1]
|
|
assert context["language"] == language
|
|
assert context["title"] == MESSAGES["not_found"][language]["title"]
|
|
|
|
def test_unsupported_language_falls_back_to_english(self):
|
|
"""Test unsupported language falls back to English."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(
|
|
path="/storefront/", language="pt"
|
|
)
|
|
|
|
with patch("app.templates_config.templates") as mock_templates:
|
|
mock_templates.TemplateResponse.return_value = Mock(status_code=403)
|
|
|
|
middleware._render_unavailable(request, "not_activated")
|
|
|
|
context = mock_templates.TemplateResponse.call_args[0][1]
|
|
assert context["language"] == "en"
|
|
assert context["title"] == MESSAGES["not_activated"]["en"]["title"]
|
|
|
|
def test_page_request_includes_store_when_provided(self):
|
|
"""Test store object is passed to template when available."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store()
|
|
request = _make_request(path="/storefront/")
|
|
|
|
with patch("app.templates_config.templates") as mock_templates:
|
|
mock_templates.TemplateResponse.return_value = Mock(status_code=403)
|
|
|
|
middleware._render_unavailable(request, "not_activated", store=store)
|
|
|
|
context = mock_templates.TemplateResponse.call_args[0][1]
|
|
assert context["store"] is store
|
|
|
|
def test_page_request_store_none_for_not_found(self):
|
|
"""Test store is None for not_found case."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/storefront/")
|
|
|
|
with patch("app.templates_config.templates") as mock_templates:
|
|
mock_templates.TemplateResponse.return_value = Mock(status_code=403)
|
|
|
|
middleware._render_unavailable(request, "not_found")
|
|
|
|
context = mock_templates.TemplateResponse.call_args[0][1]
|
|
assert context["store"] is None
|
|
|
|
|
|
# =============================================================================
|
|
# Messages dict validation
|
|
# =============================================================================
|
|
|
|
@pytest.mark.unit
|
|
class TestMessages:
|
|
"""Validate the MESSAGES dict structure."""
|
|
|
|
def test_all_reasons_have_all_languages(self):
|
|
"""Test every reason has en, fr, de, lb translations."""
|
|
for reason in ("not_found", "not_activated"):
|
|
assert reason in MESSAGES
|
|
for lang in ("en", "fr", "de", "lb"):
|
|
assert lang in MESSAGES[reason], f"Missing {lang} for {reason}"
|
|
assert "title" in MESSAGES[reason][lang]
|
|
assert "message" in MESSAGES[reason][lang]
|
|
|
|
def test_messages_are_non_empty_strings(self):
|
|
"""Test all message values are non-empty strings."""
|
|
for reason in MESSAGES:
|
|
for lang in MESSAGES[reason]:
|
|
for field in ("title", "message"):
|
|
value = MESSAGES[reason][lang][field]
|
|
assert isinstance(value, str)
|
|
assert len(value) > 0
|
|
|
|
|
|
# =============================================================================
|
|
# Full integration-style dispatch tests
|
|
# =============================================================================
|
|
|
|
@pytest.mark.unit
|
|
class TestStorefrontAccessMiddlewareDispatchIntegration:
|
|
"""End-to-end dispatch tests exercising the full middleware flow."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_flow_active_subscription(self):
|
|
"""Test full dispatch: store + active subscription → passthrough."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store(merchant_id=10)
|
|
platform = _make_platform(platform_id=2)
|
|
request = _make_request(
|
|
path="/storefront/products",
|
|
store=store,
|
|
platform=platform,
|
|
)
|
|
expected_response = Mock()
|
|
call_next = AsyncMock(return_value=expected_response)
|
|
mock_db = MagicMock()
|
|
active_sub = _make_subscription(is_active=True, tier_code="professional")
|
|
|
|
with (
|
|
patch(
|
|
"middleware.storefront_access.get_db",
|
|
return_value=iter([mock_db]),
|
|
),
|
|
patch(
|
|
"app.modules.billing.services.subscription_service.subscription_service"
|
|
) as mock_svc,
|
|
):
|
|
mock_svc.get_merchant_subscription.return_value = active_sub
|
|
|
|
result = await middleware.dispatch(request, call_next)
|
|
|
|
assert result is expected_response
|
|
assert request.state.subscription is active_sub
|
|
assert request.state.subscription_tier is active_sub.tier
|
|
call_next.assert_called_once_with(request)
|
|
mock_db.close.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_flow_no_subscription_page_request(self):
|
|
"""Test full dispatch: store + no subscription + page → HTML 403."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store()
|
|
platform = _make_platform()
|
|
request = _make_request(
|
|
path="/storefront/",
|
|
store=store,
|
|
platform=platform,
|
|
language="fr",
|
|
)
|
|
call_next = AsyncMock()
|
|
mock_db = MagicMock()
|
|
|
|
with (
|
|
patch(
|
|
"middleware.storefront_access.get_db",
|
|
return_value=iter([mock_db]),
|
|
),
|
|
patch(
|
|
"app.modules.billing.services.subscription_service.subscription_service"
|
|
) as mock_svc,
|
|
patch("app.templates_config.templates") as mock_templates,
|
|
):
|
|
mock_svc.get_merchant_subscription.return_value = None
|
|
mock_svc.get_subscription_for_store.return_value = None
|
|
mock_templates.TemplateResponse.return_value = Mock(status_code=403)
|
|
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_not_called()
|
|
mock_templates.TemplateResponse.assert_called_once()
|
|
context = mock_templates.TemplateResponse.call_args[0][1]
|
|
assert context["language"] == "fr"
|
|
assert context["reason"] == "not_activated"
|
|
mock_db.close.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_flow_no_subscription_api_request(self):
|
|
"""Test full dispatch: store + no subscription + API → JSON 403."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store()
|
|
platform = _make_platform()
|
|
request = _make_request(
|
|
path="/api/v1/storefront/cart",
|
|
store=store,
|
|
platform=platform,
|
|
)
|
|
call_next = AsyncMock()
|
|
mock_db = MagicMock()
|
|
|
|
with (
|
|
patch(
|
|
"middleware.storefront_access.get_db",
|
|
return_value=iter([mock_db]),
|
|
),
|
|
patch(
|
|
"app.modules.billing.services.subscription_service.subscription_service"
|
|
) as mock_svc,
|
|
):
|
|
mock_svc.get_merchant_subscription.return_value = None
|
|
mock_svc.get_subscription_for_store.return_value = None
|
|
|
|
result = await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_not_called()
|
|
assert isinstance(result, JSONResponse)
|
|
assert result.status_code == 403
|
|
mock_db.close.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_flow_no_store_detected(self):
|
|
"""Test full dispatch: no store → not_found response."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
request = _make_request(path="/storefront/", store=None)
|
|
call_next = AsyncMock()
|
|
|
|
with patch("app.templates_config.templates") as mock_templates:
|
|
mock_templates.TemplateResponse.return_value = Mock(status_code=403)
|
|
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
call_next.assert_not_called()
|
|
context = mock_templates.TemplateResponse.call_args[0][1]
|
|
assert context["reason"] == "not_found"
|
|
assert context["store"] is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_db_closed_on_exception(self):
|
|
"""Test database session is closed even when _get_subscription raises."""
|
|
middleware = StorefrontAccessMiddleware(app=None)
|
|
store = _make_store()
|
|
request = _make_request(store=store)
|
|
call_next = AsyncMock()
|
|
mock_db = MagicMock()
|
|
|
|
with (
|
|
patch(
|
|
"middleware.storefront_access.get_db",
|
|
return_value=iter([mock_db]),
|
|
),
|
|
patch.object(
|
|
middleware,
|
|
"_get_subscription",
|
|
side_effect=Exception("db error"),
|
|
),
|
|
pytest.raises(Exception, match="db error"),
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
mock_db.close.assert_called_once()
|