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,388 @@
# app/modules/marketplace/tests/integration/test_store_page_routes.py
"""
Integration tests for marketplace store page routes.
Tests the onboarding, marketplace, and letzshop page routes at:
GET /store/{store_code}/onboarding
GET /store/{store_code}/marketplace
GET /store/{store_code}/letzshop
Verifies:
- Onboarding page redirects to dashboard on non-marketplace platforms
- Onboarding page redirects to dashboard when already completed
- Onboarding page renders when marketplace enabled and not completed
- Marketplace/Letzshop pages redirect to onboarding when not completed
- Marketplace/Letzshop pages render when onboarding is completed
"""
import uuid
from datetime import UTC, datetime
import pytest
from app.api.deps import get_current_store_from_cookie_or_header
from app.modules.marketplace.models import OnboardingStatus, StoreOnboarding
from app.modules.tenancy.models import Merchant, Platform, Store, User
from app.modules.tenancy.models.platform_module import PlatformModule
from app.modules.tenancy.models.store_platform import StorePlatform
from main import app
from models.schema.auth import UserContext
# ============================================================================
# Fixtures
# ============================================================================
@pytest.fixture
def mp_admin(db):
"""Create an admin user for enabling modules."""
from middleware.auth import AuthManager
auth = AuthManager()
user = User(
email=f"mpadmin_{uuid.uuid4().hex[:8]}@test.com",
username=f"mpadmin_{uuid.uuid4().hex[:8]}",
hashed_password=auth.hash_password("pass123"),
role="super_admin",
is_active=True,
)
db.add(user)
db.commit()
db.refresh(user)
return user
@pytest.fixture
def mp_owner(db):
"""Create a store owner user."""
from middleware.auth import AuthManager
auth = AuthManager()
user = User(
email=f"mpowner_{uuid.uuid4().hex[:8]}@test.com",
username=f"mpowner_{uuid.uuid4().hex[:8]}",
hashed_password=auth.hash_password("pass123"),
role="merchant_owner",
is_active=True,
)
db.add(user)
db.commit()
db.refresh(user)
return user
@pytest.fixture
def mp_platform_no_marketplace(db):
"""Create a platform without marketplace module enabled."""
platform = Platform(
code=f"mpnm_{uuid.uuid4().hex[:8]}",
name="No Marketplace Platform",
is_active=True,
)
db.add(platform)
db.commit()
db.refresh(platform)
return platform
@pytest.fixture
def mp_platform_with_marketplace(db, mp_admin):
"""Create a platform with marketplace module enabled."""
platform = Platform(
code=f"mpmk_{uuid.uuid4().hex[:8]}",
name="Marketplace Platform",
is_active=True,
)
db.add(platform)
db.flush()
pm = PlatformModule(
platform_id=platform.id,
module_code="marketplace",
is_enabled=True,
enabled_at=datetime.now(UTC),
enabled_by_user_id=mp_admin.id,
config={},
)
db.add(pm)
db.commit()
db.refresh(platform)
return platform
@pytest.fixture
def mp_merchant(db, mp_owner):
"""Create a merchant."""
merchant = Merchant(
name="MP Test Merchant",
owner_user_id=mp_owner.id,
contact_email=mp_owner.email,
is_active=True,
is_verified=True,
)
db.add(merchant)
db.commit()
db.refresh(merchant)
return merchant
@pytest.fixture
def mp_store(db, mp_merchant):
"""Create a store."""
uid = uuid.uuid4().hex[:8]
store = Store(
merchant_id=mp_merchant.id,
store_code=f"MPSTORE_{uid.upper()}",
subdomain=f"mpstore{uid.lower()}",
name="MP Test Store",
is_active=True,
is_verified=True,
)
db.add(store)
db.commit()
db.refresh(store)
return store
@pytest.fixture
def mp_store_no_marketplace(db, mp_store, mp_platform_no_marketplace):
"""Link store to a platform without marketplace."""
sp = StorePlatform(
store_id=mp_store.id,
platform_id=mp_platform_no_marketplace.id,
is_active=True,
)
db.add(sp)
db.commit()
db.refresh(sp)
return sp
@pytest.fixture
def mp_store_with_marketplace(db, mp_store, mp_platform_with_marketplace):
"""Link store to a platform with marketplace enabled."""
sp = StorePlatform(
store_id=mp_store.id,
platform_id=mp_platform_with_marketplace.id,
is_active=True,
)
db.add(sp)
db.commit()
db.refresh(sp)
return sp
@pytest.fixture
def mp_onboarding_not_completed(db, mp_store):
"""Create an incomplete onboarding record for the store."""
onboarding = StoreOnboarding(
store_id=mp_store.id,
status=OnboardingStatus.IN_PROGRESS.value,
)
db.add(onboarding)
db.commit()
db.refresh(onboarding)
return onboarding
@pytest.fixture
def mp_onboarding_completed(db, mp_store):
"""Create a completed onboarding record for the store."""
onboarding = StoreOnboarding(
store_id=mp_store.id,
status=OnboardingStatus.COMPLETED.value,
step_merchant_profile_completed=True,
step_letzshop_api_completed=True,
step_product_import_completed=True,
step_order_sync_completed=True,
)
db.add(onboarding)
db.commit()
db.refresh(onboarding)
return onboarding
@pytest.fixture
def mp_auth(mp_owner, mp_store):
"""Override auth dependency for store cookie/header auth."""
user_context = UserContext(
id=mp_owner.id,
email=mp_owner.email,
username=mp_owner.username,
role="merchant_owner",
is_active=True,
token_store_id=mp_store.id,
token_store_code=mp_store.store_code,
)
def _override():
return user_context
app.dependency_overrides[get_current_store_from_cookie_or_header] = _override
yield {"Authorization": "Bearer fake-token"}
app.dependency_overrides.pop(get_current_store_from_cookie_or_header, None)
# ============================================================================
# Onboarding page tests
# ============================================================================
@pytest.mark.integration
@pytest.mark.marketplace
class TestOnboardingPageRoutes:
"""Tests for GET /store/{store_code}/onboarding."""
def test_redirects_to_dashboard_on_non_marketplace_platform(
self, client, db, mp_auth, mp_store, mp_store_no_marketplace
):
"""Onboarding page redirects to dashboard on platform without marketplace."""
response = client.get(
f"/store/{mp_store.subdomain}/onboarding",
headers=mp_auth,
follow_redirects=False,
)
assert response.status_code == 302
assert f"/store/{mp_store.subdomain}/dashboard" in response.headers["location"]
def test_redirects_to_dashboard_when_onboarding_completed(
self, client, db, mp_auth, mp_store, mp_store_with_marketplace,
mp_onboarding_completed,
):
"""Onboarding page redirects to dashboard when already completed."""
response = client.get(
f"/store/{mp_store.subdomain}/onboarding",
headers=mp_auth,
follow_redirects=False,
)
assert response.status_code == 302
assert f"/store/{mp_store.subdomain}/dashboard" in response.headers["location"]
def test_renders_onboarding_when_not_completed(
self, client, db, mp_auth, mp_store, mp_store_with_marketplace,
mp_onboarding_not_completed,
):
"""Onboarding page renders wizard when marketplace enabled and not completed."""
response = client.get(
f"/store/{mp_store.subdomain}/onboarding",
headers=mp_auth,
follow_redirects=False,
)
assert response.status_code == 200
def test_renders_onboarding_when_no_onboarding_record(
self, client, db, mp_auth, mp_store, mp_store_with_marketplace,
):
"""Onboarding page renders wizard when marketplace enabled and no record exists."""
# No mp_onboarding_* fixture — is_completed() returns False for missing record
response = client.get(
f"/store/{mp_store.subdomain}/onboarding",
headers=mp_auth,
follow_redirects=False,
)
assert response.status_code == 200
def test_requires_auth(self, client):
"""Returns 401 without auth."""
app.dependency_overrides.pop(get_current_store_from_cookie_or_header, None)
response = client.get(
"/store/anystore/onboarding",
follow_redirects=False,
)
assert response.status_code == 401
# ============================================================================
# Marketplace page tests
# ============================================================================
@pytest.mark.integration
@pytest.mark.marketplace
class TestMarketplacePageRoutes:
"""Tests for GET /store/{store_code}/marketplace."""
def test_redirects_to_onboarding_when_not_completed(
self, client, db, mp_auth, mp_store, mp_store_with_marketplace,
mp_onboarding_not_completed,
):
"""Marketplace page redirects to onboarding when not completed."""
response = client.get(
f"/store/{mp_store.subdomain}/marketplace",
headers=mp_auth,
follow_redirects=False,
)
assert response.status_code == 302
assert f"/store/{mp_store.subdomain}/onboarding" in response.headers["location"]
def test_renders_when_onboarding_completed(
self, client, db, mp_auth, mp_store, mp_store_with_marketplace,
mp_onboarding_completed,
):
"""Marketplace page renders when onboarding is completed."""
response = client.get(
f"/store/{mp_store.subdomain}/marketplace",
headers=mp_auth,
follow_redirects=False,
)
assert response.status_code == 200
def test_redirects_when_no_onboarding_record(
self, client, db, mp_auth, mp_store, mp_store_with_marketplace,
):
"""Marketplace page redirects when no onboarding record (not completed)."""
response = client.get(
f"/store/{mp_store.subdomain}/marketplace",
headers=mp_auth,
follow_redirects=False,
)
assert response.status_code == 302
assert f"/store/{mp_store.subdomain}/onboarding" in response.headers["location"]
# ============================================================================
# Letzshop page tests
# ============================================================================
@pytest.mark.integration
@pytest.mark.marketplace
class TestLetzshopPageRoutes:
"""Tests for GET /store/{store_code}/letzshop."""
def test_redirects_to_onboarding_when_not_completed(
self, client, db, mp_auth, mp_store, mp_store_with_marketplace,
mp_onboarding_not_completed,
):
"""Letzshop page redirects to onboarding when not completed."""
response = client.get(
f"/store/{mp_store.subdomain}/letzshop",
headers=mp_auth,
follow_redirects=False,
)
assert response.status_code == 302
assert f"/store/{mp_store.subdomain}/onboarding" in response.headers["location"]
def test_renders_when_onboarding_completed(
self, client, db, mp_auth, mp_store, mp_store_with_marketplace,
mp_onboarding_completed,
):
"""Letzshop page renders when onboarding is completed."""
response = client.get(
f"/store/{mp_store.subdomain}/letzshop",
headers=mp_auth,
follow_redirects=False,
)
assert response.status_code == 200
def test_redirects_when_no_onboarding_record(
self, client, db, mp_auth, mp_store, mp_store_with_marketplace,
):
"""Letzshop page redirects when no onboarding record (not completed)."""
response = client.get(
f"/store/{mp_store.subdomain}/letzshop",
headers=mp_auth,
follow_redirects=False,
)
assert response.status_code == 302
assert f"/store/{mp_store.subdomain}/onboarding" in response.headers["location"]