feat: complete dynamic menu system across all frontends
All checks were successful
CI / ruff (push) Successful in 11s
CI / pytest (push) Successful in 44m40s
CI / validate (push) Successful in 22s
CI / dependency-scanning (push) Successful in 28s
CI / docs (push) Successful in 39s
CI / deploy (push) Successful in 49s

- Add "Merchant Frontend" tab to admin menu-config page
- Merchant render endpoint now respects AdminMenuConfig visibility
  via get_merchant_primary_platform_id() platform resolution
- New store menu render endpoint (GET /store/core/menu/render/store)
  with platform-scoped visibility and store_code interpolation
- Store sidebar migrated from hardcoded Jinja2 macros to dynamic
  Alpine.js x-for rendering with loading skeleton and fallback
- Store init-alpine.js: add loadMenuConfig(), expandSectionForCurrentPage()
- Include store page route fixes, login template updates, and tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 02:14:42 +01:00
parent be248222bc
commit 506171503d
14 changed files with 1364 additions and 158 deletions

View File

@@ -0,0 +1,298 @@
# app/modules/marketplace/tests/unit/test_store_page_routes.py
"""
Unit tests for marketplace store page routes.
Tests the onboarding, marketplace, and letzshop page route logic:
- Onboarding page guards (marketplace module check, completion redirect)
- Marketplace page onboarding gate
- Letzshop page onboarding gate
"""
from unittest.mock import MagicMock, patch
import pytest
from fastapi import Request
from fastapi.responses import RedirectResponse
from app.modules.marketplace.routes.pages.store import (
store_letzshop_page,
store_marketplace_page,
store_onboarding_page,
)
from models.schema.auth import UserContext
def _make_user_context(store_id: int = 1, store_code: str = "teststore") -> UserContext:
"""Create a UserContext for testing."""
return UserContext(
id=1,
email="test@test.com",
username="testuser",
role="merchant_owner",
is_active=True,
token_store_id=store_id,
token_store_code=store_code,
)
def _make_request() -> MagicMock:
"""Create a mock Request."""
return MagicMock(spec=Request)
# ============================================================================
# ONBOARDING PAGE
# ============================================================================
@pytest.mark.unit
@pytest.mark.marketplace
@pytest.mark.asyncio
class TestOnboardingPageGuard:
"""Test that onboarding page redirects to dashboard when marketplace is not enabled."""
@patch("app.modules.marketplace.routes.pages.store.module_service")
@patch("app.modules.marketplace.routes.pages.store.get_store_context")
@patch("app.modules.marketplace.routes.pages.store.templates")
async def test_redirects_to_dashboard_when_no_store_platform(
self, mock_templates, mock_ctx, mock_module_service, db
):
"""Redirect to dashboard if store has no StorePlatform record."""
user = _make_user_context(store_id=99999)
response = await store_onboarding_page(
request=_make_request(),
store_code="teststore",
current_user=user,
db=db,
)
assert isinstance(response, RedirectResponse)
assert response.status_code == 302
assert "/store/teststore/dashboard" in response.headers["location"]
@patch("app.modules.marketplace.routes.pages.store.module_service")
@patch("app.modules.marketplace.routes.pages.store.get_store_context")
@patch("app.modules.marketplace.routes.pages.store.templates")
async def test_redirects_to_dashboard_when_marketplace_not_enabled(
self, mock_templates, mock_ctx, mock_module_service, db, test_store
):
"""Redirect to dashboard if marketplace module is not enabled on platform."""
from app.modules.tenancy.models import Platform
from app.modules.tenancy.models.store_platform import StorePlatform
platform = Platform(code="nomarket", name="No Marketplace", is_active=True)
db.add(platform)
db.flush()
sp = StorePlatform(store_id=test_store.id, platform_id=platform.id, is_active=True)
db.add(sp)
db.commit()
mock_module_service.is_module_enabled.return_value = False
user = _make_user_context(store_id=test_store.id)
response = await store_onboarding_page(
request=_make_request(),
store_code="teststore",
current_user=user,
db=db,
)
assert isinstance(response, RedirectResponse)
assert response.status_code == 302
assert "/store/teststore/dashboard" in response.headers["location"]
mock_module_service.is_module_enabled.assert_called_once_with(
db, platform.id, "marketplace"
)
@patch("app.modules.marketplace.routes.pages.store.OnboardingService")
@patch("app.modules.marketplace.routes.pages.store.module_service")
@patch("app.modules.marketplace.routes.pages.store.get_store_context")
@patch("app.modules.marketplace.routes.pages.store.templates")
async def test_redirects_to_dashboard_when_onboarding_completed(
self, mock_templates, mock_ctx, mock_module_service, mock_onboarding_cls, db, test_store
):
"""Redirect to dashboard if onboarding is already completed."""
from app.modules.tenancy.models import Platform
from app.modules.tenancy.models.store_platform import StorePlatform
platform = Platform(code="mktplace", name="Marketplace", is_active=True)
db.add(platform)
db.flush()
sp = StorePlatform(store_id=test_store.id, platform_id=platform.id, is_active=True)
db.add(sp)
db.commit()
mock_module_service.is_module_enabled.return_value = True
mock_svc = MagicMock()
mock_svc.is_completed.return_value = True
mock_onboarding_cls.return_value = mock_svc
user = _make_user_context(store_id=test_store.id)
response = await store_onboarding_page(
request=_make_request(),
store_code="teststore",
current_user=user,
db=db,
)
assert isinstance(response, RedirectResponse)
assert response.status_code == 302
assert "/store/teststore/dashboard" in response.headers["location"]
@patch("app.modules.marketplace.routes.pages.store.OnboardingService")
@patch("app.modules.marketplace.routes.pages.store.module_service")
@patch("app.modules.marketplace.routes.pages.store.get_store_context")
@patch("app.modules.marketplace.routes.pages.store.templates")
async def test_renders_onboarding_when_marketplace_enabled_and_not_completed(
self, mock_templates, mock_ctx, mock_module_service, mock_onboarding_cls, db, test_store
):
"""Render onboarding wizard when marketplace enabled and onboarding not completed."""
from app.modules.tenancy.models import Platform
from app.modules.tenancy.models.store_platform import StorePlatform
platform = Platform(code="mktplace2", name="Marketplace 2", is_active=True)
db.add(platform)
db.flush()
sp = StorePlatform(store_id=test_store.id, platform_id=platform.id, is_active=True)
db.add(sp)
db.commit()
mock_module_service.is_module_enabled.return_value = True
mock_svc = MagicMock()
mock_svc.is_completed.return_value = False
mock_onboarding_cls.return_value = mock_svc
mock_ctx.return_value = {"request": _make_request()}
mock_templates.TemplateResponse.return_value = "rendered"
user = _make_user_context(store_id=test_store.id)
response = await store_onboarding_page(
request=_make_request(),
store_code="teststore",
current_user=user,
db=db,
)
assert response == "rendered"
mock_templates.TemplateResponse.assert_called_once()
template_name = mock_templates.TemplateResponse.call_args[0][0]
assert template_name == "marketplace/store/onboarding.html"
# ============================================================================
# MARKETPLACE PAGE
# ============================================================================
@pytest.mark.unit
@pytest.mark.marketplace
@pytest.mark.asyncio
class TestMarketplacePageOnboardingGate:
"""Test that marketplace page redirects to onboarding when not completed."""
@patch("app.modules.marketplace.routes.pages.store.OnboardingService")
@patch("app.modules.marketplace.routes.pages.store.get_store_context")
@patch("app.modules.marketplace.routes.pages.store.templates")
async def test_redirects_to_onboarding_when_not_completed(
self, mock_templates, mock_ctx, mock_onboarding_cls, db
):
"""Redirect to onboarding if onboarding not completed."""
mock_svc = MagicMock()
mock_svc.is_completed.return_value = False
mock_onboarding_cls.return_value = mock_svc
user = _make_user_context()
response = await store_marketplace_page(
request=_make_request(),
store_code="teststore",
current_user=user,
db=db,
)
assert isinstance(response, RedirectResponse)
assert response.status_code == 302
assert "/store/teststore/onboarding" in response.headers["location"]
@patch("app.modules.marketplace.routes.pages.store.OnboardingService")
@patch("app.modules.marketplace.routes.pages.store.get_store_context")
@patch("app.modules.marketplace.routes.pages.store.templates")
async def test_renders_marketplace_when_onboarding_completed(
self, mock_templates, mock_ctx, mock_onboarding_cls, db
):
"""Render marketplace page if onboarding is completed."""
mock_svc = MagicMock()
mock_svc.is_completed.return_value = True
mock_onboarding_cls.return_value = mock_svc
mock_ctx.return_value = {"request": _make_request()}
mock_templates.TemplateResponse.return_value = "rendered"
user = _make_user_context()
response = await store_marketplace_page(
request=_make_request(),
store_code="teststore",
current_user=user,
db=db,
)
assert response == "rendered"
mock_templates.TemplateResponse.assert_called_once()
template_name = mock_templates.TemplateResponse.call_args[0][0]
assert template_name == "marketplace/store/marketplace.html"
# ============================================================================
# LETZSHOP PAGE
# ============================================================================
@pytest.mark.unit
@pytest.mark.marketplace
@pytest.mark.asyncio
class TestLetzshopPageOnboardingGate:
"""Test that letzshop page redirects to onboarding when not completed."""
@patch("app.modules.marketplace.routes.pages.store.OnboardingService")
@patch("app.modules.marketplace.routes.pages.store.get_store_context")
@patch("app.modules.marketplace.routes.pages.store.templates")
async def test_redirects_to_onboarding_when_not_completed(
self, mock_templates, mock_ctx, mock_onboarding_cls, db
):
"""Redirect to onboarding if onboarding not completed."""
mock_svc = MagicMock()
mock_svc.is_completed.return_value = False
mock_onboarding_cls.return_value = mock_svc
user = _make_user_context()
response = await store_letzshop_page(
request=_make_request(),
store_code="teststore",
current_user=user,
db=db,
)
assert isinstance(response, RedirectResponse)
assert response.status_code == 302
assert "/store/teststore/onboarding" in response.headers["location"]
@patch("app.modules.marketplace.routes.pages.store.OnboardingService")
@patch("app.modules.marketplace.routes.pages.store.get_store_context")
@patch("app.modules.marketplace.routes.pages.store.templates")
async def test_renders_letzshop_when_onboarding_completed(
self, mock_templates, mock_ctx, mock_onboarding_cls, db
):
"""Render letzshop page if onboarding is completed."""
mock_svc = MagicMock()
mock_svc.is_completed.return_value = True
mock_onboarding_cls.return_value = mock_svc
mock_ctx.return_value = {"request": _make_request()}
mock_templates.TemplateResponse.return_value = "rendered"
user = _make_user_context()
response = await store_letzshop_page(
request=_make_request(),
store_code="teststore",
current_user=user,
db=db,
)
assert response == "rendered"
mock_templates.TemplateResponse.assert_called_once()
template_name = mock_templates.TemplateResponse.call_args[0][0]
assert template_name == "marketplace/store/letzshop.html"