feat: add module definition completeness validation and permissions
Add new validation rules MOD-020 to MOD-023 for module definition completeness and standardize permissions across all modules. Changes: - Add MOD-020: Module definitions must have required attributes - Add MOD-021: Modules with menus should have features - Add MOD-022: Feature modules should have permissions - Add MOD-023: Modules with routers should use get_*_with_routers pattern Module permissions added: - analytics: view, export, manage_dashboards - billing: view_tiers, manage_tiers, view_subscriptions, manage_subscriptions, view_invoices - cart: view, manage - checkout: view_settings, manage_settings - cms: view_pages, manage_pages, view_media, manage_media, manage_themes - loyalty: view_programs, manage_programs, view_rewards, manage_rewards - marketplace: view_integration, manage_integration, sync_products - messaging: view_messages, send_messages, manage_templates - payments: view_gateways, manage_gateways, view_transactions Module improvements: - Complete cart module with features and permissions - Complete checkout module with features and permissions - Add features to catalog module - Add version to cms module - Fix loyalty platform_router attachment - Add path definitions to payments module - Remove empty scheduled_tasks from dev_tools module Documentation: - Update module-system.md with new validation rules - Update architecture-rules.md with MOD-020 to MOD-023 Tests: - Add unit tests for module definition completeness - Add tests for permission structure validation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -551,3 +551,175 @@ module_rules:
|
||||
- "__init__.py"
|
||||
- "base.py"
|
||||
- "auth.py"
|
||||
|
||||
# =========================================================================
|
||||
# Module Definition Completeness Rules
|
||||
# =========================================================================
|
||||
|
||||
- id: "MOD-020"
|
||||
name: "Module definition must have required attributes"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Module definitions should include at minimum:
|
||||
- code: Module identifier
|
||||
- name: Human-readable name
|
||||
- description: What the module does
|
||||
- version: Semantic version
|
||||
- features: List of features (unless infrastructure module)
|
||||
- permissions: Access control definitions (unless internal or storefront-only)
|
||||
|
||||
EXAMPLES (incomplete):
|
||||
module = ModuleDefinition(
|
||||
code="cart",
|
||||
name="Shopping Cart",
|
||||
description="...",
|
||||
version="1.0.0",
|
||||
# Missing features and permissions
|
||||
)
|
||||
|
||||
EXAMPLES (complete):
|
||||
module = ModuleDefinition(
|
||||
code="orders",
|
||||
name="Order Management",
|
||||
description="...",
|
||||
version="1.0.0",
|
||||
features=["order_management", "fulfillment_tracking"],
|
||||
permissions=[
|
||||
PermissionDefinition(id="orders.view", ...),
|
||||
],
|
||||
)
|
||||
|
||||
EXCEPTIONS:
|
||||
- is_internal=True modules may skip permissions
|
||||
- Infrastructure modules (is_core=True with no UI) may skip features
|
||||
- Storefront-only modules (session-based, no admin UI) may have minimal permissions
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Consistency: All modules follow the same definition pattern
|
||||
- RBAC: Permissions enable proper role-based access control
|
||||
- Feature flags: Features enable selective module functionality
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/definition.py"
|
||||
required_attributes:
|
||||
- "code"
|
||||
- "name"
|
||||
- "description"
|
||||
- "version"
|
||||
|
||||
- id: "MOD-021"
|
||||
name: "Modules with menus should have features"
|
||||
severity: "warning"
|
||||
description: |
|
||||
If a module defines menu items or menu sections, it should also
|
||||
define features to describe what functionality it provides.
|
||||
|
||||
Menus indicate the module has UI and user-facing functionality,
|
||||
which should be documented as features.
|
||||
|
||||
WRONG:
|
||||
module = ModuleDefinition(
|
||||
code="billing",
|
||||
menus={
|
||||
FrontendType.ADMIN: [...],
|
||||
},
|
||||
# Missing features list
|
||||
)
|
||||
|
||||
RIGHT:
|
||||
module = ModuleDefinition(
|
||||
code="billing",
|
||||
features=[
|
||||
"subscription_management",
|
||||
"billing_history",
|
||||
"invoice_generation",
|
||||
],
|
||||
menus={
|
||||
FrontendType.ADMIN: [...],
|
||||
},
|
||||
)
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Documentation: Features describe what the module does
|
||||
- Feature flags: Enables/disables specific functionality
|
||||
- Consistency: All UI modules describe their capabilities
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/definition.py"
|
||||
validates:
|
||||
- "menus -> features"
|
||||
|
||||
- id: "MOD-022"
|
||||
name: "Feature modules should have permissions"
|
||||
severity: "info"
|
||||
description: |
|
||||
Modules with features should define permissions unless:
|
||||
- is_internal=True (internal tools like dev_tools)
|
||||
- Storefront-only module (session-based, no authentication)
|
||||
|
||||
Permissions enable role-based access control (RBAC) for module
|
||||
functionality.
|
||||
|
||||
EXCEPTIONS:
|
||||
- is_internal=True modules (internal tooling)
|
||||
- Modules with only storefront features (cart, checkout without admin UI)
|
||||
- Infrastructure modules (contracts, core utilities)
|
||||
|
||||
EXAMPLE:
|
||||
module = ModuleDefinition(
|
||||
code="billing",
|
||||
features=["subscription_management", ...],
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="billing.view_subscriptions",
|
||||
label_key="billing.permissions.view_subscriptions",
|
||||
description_key="billing.permissions.view_subscriptions_desc",
|
||||
category="billing",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- RBAC: Permissions enable proper access control
|
||||
- Security: Restrict who can access module features
|
||||
- Consistency: All feature modules define their access rules
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/definition.py"
|
||||
validates:
|
||||
- "features -> permissions"
|
||||
exceptions:
|
||||
- "is_internal=True"
|
||||
|
||||
- id: "MOD-023"
|
||||
name: "Modules with routers should use get_*_with_routers pattern"
|
||||
severity: "info"
|
||||
description: |
|
||||
Modules that define routers (admin_router, vendor_router, etc.)
|
||||
should follow the lazy import pattern with a dedicated function:
|
||||
|
||||
def get_{module}_module_with_routers() -> ModuleDefinition:
|
||||
|
||||
This pattern:
|
||||
1. Avoids circular imports during module initialization
|
||||
2. Ensures routers are attached at the right time
|
||||
3. Provides a consistent API for router registration
|
||||
|
||||
WRONG:
|
||||
# Direct router assignment at module level
|
||||
module.admin_router = admin_router
|
||||
|
||||
RIGHT:
|
||||
def _get_admin_router():
|
||||
from app.modules.orders.routes.admin import admin_router
|
||||
return admin_router
|
||||
|
||||
def get_orders_module_with_routers() -> ModuleDefinition:
|
||||
orders_module.admin_router = _get_admin_router()
|
||||
return orders_module
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Prevents circular imports
|
||||
- Consistent pattern across all modules
|
||||
- Clear API for module registration
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/definition.py"
|
||||
validates:
|
||||
- "router imports -> get_*_with_routers function"
|
||||
|
||||
@@ -6,7 +6,7 @@ Defines the analytics module including its features, menu items,
|
||||
route configurations, and self-contained module settings.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -37,6 +37,27 @@ analytics_module = ModuleDefinition(
|
||||
"export_reports", # Export to CSV/Excel
|
||||
"usage_metrics", # Usage and performance metrics
|
||||
],
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="analytics.view",
|
||||
label_key="analytics.permissions.view",
|
||||
description_key="analytics.permissions.view_desc",
|
||||
category="analytics",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="analytics.export",
|
||||
label_key="analytics.permissions.export",
|
||||
description_key="analytics.permissions.export_desc",
|
||||
category="analytics",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="analytics.manage_dashboards",
|
||||
label_key="analytics.permissions.manage_dashboards",
|
||||
description_key="analytics.permissions.manage_dashboards_desc",
|
||||
category="analytics",
|
||||
),
|
||||
],
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [
|
||||
# Analytics appears in dashboard for admin
|
||||
|
||||
@@ -6,7 +6,7 @@ Defines the billing module including its features, menu items,
|
||||
route configurations, and scheduled tasks.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, ScheduledTask
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -42,6 +42,39 @@ billing_module = ModuleDefinition(
|
||||
"trial_management", # Manage vendor trial periods
|
||||
"limit_overrides", # Override tier limits per vendor
|
||||
],
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="billing.view_tiers",
|
||||
label_key="billing.permissions.view_tiers",
|
||||
description_key="billing.permissions.view_tiers_desc",
|
||||
category="billing",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="billing.manage_tiers",
|
||||
label_key="billing.permissions.manage_tiers",
|
||||
description_key="billing.permissions.manage_tiers_desc",
|
||||
category="billing",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="billing.view_subscriptions",
|
||||
label_key="billing.permissions.view_subscriptions",
|
||||
description_key="billing.permissions.view_subscriptions_desc",
|
||||
category="billing",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="billing.manage_subscriptions",
|
||||
label_key="billing.permissions.manage_subscriptions",
|
||||
description_key="billing.permissions.manage_subscriptions_desc",
|
||||
category="billing",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="billing.view_invoices",
|
||||
label_key="billing.permissions.view_invoices",
|
||||
description_key="billing.permissions.view_invoices_desc",
|
||||
category="billing",
|
||||
),
|
||||
],
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [
|
||||
"subscription-tiers", # Manage tier definitions
|
||||
|
||||
@@ -6,7 +6,8 @@ This module provides shopping cart functionality for customer storefronts.
|
||||
It is session-based and does not require customer authentication.
|
||||
"""
|
||||
|
||||
from app.modules.base import ModuleDefinition
|
||||
from app.modules.base import ModuleDefinition, PermissionDefinition
|
||||
|
||||
|
||||
module = ModuleDefinition(
|
||||
code="cart",
|
||||
@@ -15,4 +16,29 @@ module = ModuleDefinition(
|
||||
version="1.0.0",
|
||||
is_self_contained=True,
|
||||
requires=["inventory"], # Checks inventory availability
|
||||
features=[
|
||||
"cart_management", # Basic cart CRUD operations
|
||||
"cart_persistence", # Session and database persistence
|
||||
"cart_item_operations", # Add, update, remove items
|
||||
"shipping_calculation", # Calculate shipping for cart
|
||||
"promotion_application", # Apply discounts and promotions
|
||||
],
|
||||
# Note: Cart is primarily session-based storefront functionality.
|
||||
# These permissions are for admin access to cart data/settings.
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="cart.view",
|
||||
label_key="cart.permissions.view",
|
||||
description_key="cart.permissions.view_desc",
|
||||
category="cart",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="cart.manage",
|
||||
label_key="cart.permissions.manage",
|
||||
description_key="cart.permissions.manage_desc",
|
||||
category="cart",
|
||||
),
|
||||
],
|
||||
# Cart is storefront-only - no admin/vendor menus needed
|
||||
menu_items={},
|
||||
)
|
||||
|
||||
@@ -16,6 +16,14 @@ module = ModuleDefinition(
|
||||
version="1.0.0",
|
||||
is_self_contained=True,
|
||||
requires=["inventory"],
|
||||
features=[
|
||||
"product_catalog", # Core product catalog functionality
|
||||
"product_search", # Search and filtering
|
||||
"product_variants", # Product variants management
|
||||
"product_categories", # Category organization
|
||||
"product_attributes", # Custom attributes
|
||||
"product_import_export", # Bulk import/export
|
||||
],
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
# app/modules/checkout/definition.py
|
||||
"""Checkout module definition."""
|
||||
"""
|
||||
Checkout module definition.
|
||||
|
||||
This module handles the checkout flow, converting cart contents into orders.
|
||||
Orchestrates payment processing and order creation.
|
||||
"""
|
||||
|
||||
from app.modules.base import ModuleDefinition, PermissionDefinition
|
||||
|
||||
from app.modules.base import ModuleDefinition
|
||||
|
||||
module = ModuleDefinition(
|
||||
code="checkout",
|
||||
@@ -10,4 +16,29 @@ module = ModuleDefinition(
|
||||
version="1.0.0",
|
||||
is_self_contained=True,
|
||||
requires=["cart", "orders", "payments", "customers"],
|
||||
features=[
|
||||
"checkout_flow", # Multi-step checkout process
|
||||
"order_creation", # Create orders from cart
|
||||
"payment_processing", # Payment integration during checkout
|
||||
"checkout_validation", # Address, inventory, payment validation
|
||||
"guest_checkout", # Allow checkout without account
|
||||
],
|
||||
# Note: Checkout is primarily storefront functionality.
|
||||
# These permissions are for admin access to checkout settings.
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="checkout.view_settings",
|
||||
label_key="checkout.permissions.view_settings",
|
||||
description_key="checkout.permissions.view_settings_desc",
|
||||
category="checkout",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="checkout.manage_settings",
|
||||
label_key="checkout.permissions.manage_settings",
|
||||
description_key="checkout.permissions.manage_settings_desc",
|
||||
category="checkout",
|
||||
),
|
||||
],
|
||||
# Checkout is storefront-only - no admin/vendor menus needed
|
||||
menu_items={},
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ This is a self-contained module with:
|
||||
- Templates: app.modules.cms.templates (namespaced as cms/)
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ cms_module = ModuleDefinition(
|
||||
code="cms",
|
||||
name="Content Management",
|
||||
description="Content pages, media library, and vendor themes.",
|
||||
version="1.0.0",
|
||||
features=[
|
||||
"cms_basic", # Basic page editing
|
||||
"cms_custom_pages", # Custom page creation
|
||||
@@ -43,6 +44,39 @@ cms_module = ModuleDefinition(
|
||||
"cms_seo", # SEO tools
|
||||
"media_library", # Media file management
|
||||
],
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="cms.view_pages",
|
||||
label_key="cms.permissions.view_pages",
|
||||
description_key="cms.permissions.view_pages_desc",
|
||||
category="cms",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="cms.manage_pages",
|
||||
label_key="cms.permissions.manage_pages",
|
||||
description_key="cms.permissions.manage_pages_desc",
|
||||
category="cms",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="cms.view_media",
|
||||
label_key="cms.permissions.view_media",
|
||||
description_key="cms.permissions.view_media_desc",
|
||||
category="cms",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="cms.manage_media",
|
||||
label_key="cms.permissions.manage_media",
|
||||
description_key="cms.permissions.manage_media_desc",
|
||||
category="cms",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="cms.manage_themes",
|
||||
label_key="cms.permissions.manage_themes",
|
||||
description_key="cms.permissions.manage_themes_desc",
|
||||
category="cms",
|
||||
),
|
||||
],
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [
|
||||
"content-pages", # Platform content pages
|
||||
|
||||
@@ -85,20 +85,8 @@ dev_tools_module = ModuleDefinition(
|
||||
schemas_path="app.modules.dev_tools.schemas",
|
||||
exceptions_path="app.modules.dev_tools.exceptions",
|
||||
tasks_path="app.modules.dev_tools.tasks",
|
||||
# =========================================================================
|
||||
# Scheduled Tasks
|
||||
# =========================================================================
|
||||
# Note: Code quality and test tasks are on-demand, not scheduled.
|
||||
# If scheduled scans are desired, they can be added here:
|
||||
# scheduled_tasks=[
|
||||
# ScheduledTask(
|
||||
# name="dev_tools.nightly_code_scan",
|
||||
# task="app.modules.dev_tools.tasks.code_quality.execute_code_quality_scan",
|
||||
# schedule="0 2 * * *", # Daily at 02:00
|
||||
# options={"queue": "long_running"},
|
||||
# ),
|
||||
# ],
|
||||
scheduled_tasks=[],
|
||||
# If scheduled scans are desired, add ScheduledTask entries here.
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Defines the loyalty module including its features, menu items,
|
||||
route configurations, and scheduled tasks.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, ScheduledTask
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ def _get_vendor_router():
|
||||
return vendor_router
|
||||
|
||||
|
||||
def _get_public_router():
|
||||
"""Lazy import of public router to avoid circular imports."""
|
||||
from app.modules.loyalty.routes.api.public import public_router
|
||||
def _get_platform_router():
|
||||
"""Lazy import of platform router to avoid circular imports."""
|
||||
from app.modules.loyalty.routes.api.platform import platform_router
|
||||
|
||||
return public_router
|
||||
return platform_router
|
||||
|
||||
|
||||
# Loyalty module definition
|
||||
@@ -60,6 +60,33 @@ loyalty_module = ModuleDefinition(
|
||||
"loyalty_stats", # Dashboard statistics
|
||||
"loyalty_reports", # Transaction reports
|
||||
],
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="loyalty.view_programs",
|
||||
label_key="loyalty.permissions.view_programs",
|
||||
description_key="loyalty.permissions.view_programs_desc",
|
||||
category="loyalty",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="loyalty.manage_programs",
|
||||
label_key="loyalty.permissions.manage_programs",
|
||||
description_key="loyalty.permissions.manage_programs_desc",
|
||||
category="loyalty",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="loyalty.view_rewards",
|
||||
label_key="loyalty.permissions.view_rewards",
|
||||
description_key="loyalty.permissions.view_rewards_desc",
|
||||
category="loyalty",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="loyalty.manage_rewards",
|
||||
label_key="loyalty.permissions.manage_rewards",
|
||||
description_key="loyalty.permissions.manage_rewards_desc",
|
||||
category="loyalty",
|
||||
),
|
||||
],
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [
|
||||
"loyalty-programs", # View all programs
|
||||
@@ -168,8 +195,7 @@ def get_loyalty_module_with_routers() -> ModuleDefinition:
|
||||
"""
|
||||
loyalty_module.admin_router = _get_admin_router()
|
||||
loyalty_module.vendor_router = _get_vendor_router()
|
||||
# Note: public_router needs to be attached separately in main.py
|
||||
# as it doesn't require authentication
|
||||
loyalty_module.platform_router = _get_platform_router()
|
||||
return loyalty_module
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ dependencies, route configurations, and scheduled tasks.
|
||||
Note: This module requires the inventory module to be enabled.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, ScheduledTask
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -43,6 +43,27 @@ marketplace_module = ModuleDefinition(
|
||||
"order_import", # Import orders from marketplace
|
||||
"marketplace_analytics", # Marketplace performance metrics
|
||||
],
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="marketplace.view_integration",
|
||||
label_key="marketplace.permissions.view_integration",
|
||||
description_key="marketplace.permissions.view_integration_desc",
|
||||
category="marketplace",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="marketplace.manage_integration",
|
||||
label_key="marketplace.permissions.manage_integration",
|
||||
description_key="marketplace.permissions.manage_integration_desc",
|
||||
category="marketplace",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="marketplace.sync_products",
|
||||
label_key="marketplace.permissions.sync_products",
|
||||
description_key="marketplace.permissions.sync_products_desc",
|
||||
category="marketplace",
|
||||
),
|
||||
],
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [
|
||||
"marketplace-letzshop", # Marketplace monitoring
|
||||
|
||||
@@ -6,7 +6,7 @@ Defines the messaging module including its features, menu items,
|
||||
route configurations, and self-contained module settings.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -37,6 +37,27 @@ messaging_module = ModuleDefinition(
|
||||
"message_attachments", # File attachments
|
||||
"admin_notifications", # System admin notifications
|
||||
],
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="messaging.view_messages",
|
||||
label_key="messaging.permissions.view_messages",
|
||||
description_key="messaging.permissions.view_messages_desc",
|
||||
category="messaging",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="messaging.send_messages",
|
||||
label_key="messaging.permissions.send_messages",
|
||||
description_key="messaging.permissions.send_messages_desc",
|
||||
category="messaging",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="messaging.manage_templates",
|
||||
label_key="messaging.permissions.manage_templates",
|
||||
description_key="messaging.permissions.manage_templates_desc",
|
||||
category="messaging",
|
||||
),
|
||||
],
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [
|
||||
"messages", # Admin messages
|
||||
|
||||
@@ -15,7 +15,7 @@ This separation allows:
|
||||
3. Orders without billing (customer payments only)
|
||||
"""
|
||||
|
||||
from app.modules.base import ModuleDefinition
|
||||
from app.modules.base import ModuleDefinition, PermissionDefinition
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -51,6 +51,27 @@ payments_module = ModuleDefinition(
|
||||
"bank_transfer", # Bank transfer support
|
||||
"transaction_history", # Transaction records
|
||||
],
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="payments.view_gateways",
|
||||
label_key="payments.permissions.view_gateways",
|
||||
description_key="payments.permissions.view_gateways_desc",
|
||||
category="payments",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="payments.manage_gateways",
|
||||
label_key="payments.permissions.manage_gateways",
|
||||
description_key="payments.permissions.manage_gateways_desc",
|
||||
category="payments",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="payments.view_transactions",
|
||||
label_key="payments.permissions.view_transactions",
|
||||
description_key="payments.permissions.view_transactions_desc",
|
||||
category="payments",
|
||||
),
|
||||
],
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [
|
||||
"payment-gateways", # Configure payment gateways
|
||||
@@ -61,7 +82,14 @@ payments_module = ModuleDefinition(
|
||||
},
|
||||
is_core=False,
|
||||
is_internal=False,
|
||||
is_self_contained=True, # Enable auto-discovery from routes/api/
|
||||
# =========================================================================
|
||||
# Self-Contained Module Configuration
|
||||
# =========================================================================
|
||||
is_self_contained=True,
|
||||
services_path="app.modules.payments.services",
|
||||
models_path="app.modules.payments.models",
|
||||
schemas_path="app.modules.payments.schemas",
|
||||
exceptions_path="app.modules.payments.exceptions",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -72,33 +72,35 @@ touch app/modules/mymodule/exceptions.py
|
||||
|
||||
## Three-Tier Classification
|
||||
|
||||
### Core Modules (4)
|
||||
### Core Modules (5)
|
||||
|
||||
Core modules are **always enabled** and cannot be disabled. They provide fundamental platform functionality.
|
||||
|
||||
| Module | Description | Key Features |
|
||||
|--------|-------------|--------------|
|
||||
| `core` | Dashboard, settings, profile | Basic platform operation |
|
||||
| `tenancy` | Platform, company, vendor, admin user management | Multi-tenant infrastructure |
|
||||
| `cms` | Content pages, media library, themes | Content management |
|
||||
| `customers` | Customer database, profiles, segmentation | Customer data management |
|
||||
| Module | Description | Key Features | Permissions |
|
||||
|--------|-------------|--------------|-------------|
|
||||
| `contracts` | Cross-module protocols and interfaces | Service protocols, type-safe interfaces | - |
|
||||
| `core` | Dashboard, settings, profile | Basic platform operation | 5 |
|
||||
| `cms` | Content pages, media library, themes | Content management | 5 |
|
||||
| `customers` | Customer database, profiles, segmentation | Customer data management | 4 |
|
||||
| `tenancy` | Platform, company, vendor, admin user management | Multi-tenant infrastructure | 4 |
|
||||
|
||||
### Optional Modules (10)
|
||||
### Optional Modules (11)
|
||||
|
||||
Optional modules can be **enabled or disabled per platform**. They provide additional functionality that may not be needed by all platforms.
|
||||
|
||||
| Module | Dependencies | Description |
|
||||
|--------|--------------|-------------|
|
||||
| `cart` | - | Shopping cart management, session-based carts |
|
||||
| `catalog` | - | Customer-facing product browsing |
|
||||
| `checkout` | `cart`, `orders`, `payments` | Cart-to-order conversion, checkout flow |
|
||||
| `payments` | - | Payment gateway integrations (Stripe, PayPal, etc.) |
|
||||
| `billing` | `payments` | Platform subscriptions, vendor invoices |
|
||||
| `inventory` | - | Stock management, locations |
|
||||
| `orders` | `payments` | Order management, customer checkout |
|
||||
| `marketplace` | `inventory` | Letzshop integration |
|
||||
| `analytics` | - | Reports, dashboards |
|
||||
| `messaging` | - | Messages, notifications |
|
||||
| Module | Dependencies | Description | Permissions |
|
||||
|--------|--------------|-------------|-------------|
|
||||
| `analytics` | - | Reports, dashboards | 3 |
|
||||
| `billing` | `payments` | Platform subscriptions, vendor invoices | 5 |
|
||||
| `cart` | `inventory` | Shopping cart management, session-based carts | 2 |
|
||||
| `catalog` | `inventory` | Customer-facing product browsing | 6 |
|
||||
| `checkout` | `cart`, `orders`, `payments`, `customers` | Cart-to-order conversion, checkout flow | 2 |
|
||||
| `inventory` | - | Stock management, locations | 3 |
|
||||
| `loyalty` | `customers` | Stamp/points loyalty programs, wallet integration | 4 |
|
||||
| `marketplace` | `inventory` | Letzshop integration | 3 |
|
||||
| `messaging` | - | Messages, notifications | 3 |
|
||||
| `orders` | `payments` | Order management, customer checkout | 4 |
|
||||
| `payments` | - | Payment gateway integrations (Stripe, PayPal, etc.) | 3 |
|
||||
|
||||
### Internal Modules (2)
|
||||
|
||||
@@ -167,8 +169,8 @@ Each module must have a `definition.py` with a `ModuleDefinition` instance:
|
||||
|
||||
```python
|
||||
# app/modules/analytics/definition.py
|
||||
from app.modules.base import ModuleDefinition
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
from app.modules.base import ModuleDefinition, PermissionDefinition
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
analytics_module = ModuleDefinition(
|
||||
# Identity
|
||||
@@ -191,6 +193,22 @@ analytics_module = ModuleDefinition(
|
||||
"custom_reports",
|
||||
],
|
||||
|
||||
# Module-driven permissions (RBAC)
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="analytics.view",
|
||||
label_key="analytics.permissions.view",
|
||||
description_key="analytics.permissions.view_desc",
|
||||
category="analytics",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="analytics.export",
|
||||
label_key="analytics.permissions.export",
|
||||
description_key="analytics.permissions.export_desc",
|
||||
category="analytics",
|
||||
),
|
||||
],
|
||||
|
||||
# Menu items per frontend
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [], # Analytics uses dashboard
|
||||
@@ -218,6 +236,7 @@ analytics_module = ModuleDefinition(
|
||||
| `version` | `str` | Semantic version (default: "1.0.0") |
|
||||
| `requires` | `list[str]` | Module codes this depends on |
|
||||
| `features` | `list[str]` | Feature codes for tier gating |
|
||||
| `permissions` | `list[PermissionDefinition]` | RBAC permission definitions |
|
||||
| `menu_items` | `dict` | Menu items per frontend type |
|
||||
| `is_core` | `bool` | Cannot be disabled if True |
|
||||
| `is_internal` | `bool` | Admin-only if True |
|
||||
@@ -929,6 +948,10 @@ The architecture validator (`scripts/validate_architecture.py`) enforces module
|
||||
| MOD-017 | ERROR | Services must be in modules, not `app/services/` |
|
||||
| MOD-018 | ERROR | Tasks must be in modules, not `app/tasks/` |
|
||||
| MOD-019 | ERROR | Schemas must be in modules, not `models/schema/` |
|
||||
| MOD-020 | WARNING | Module definition must have required attributes (code, name, description, version, features) |
|
||||
| MOD-021 | WARNING | Modules with menus should have features defined |
|
||||
| MOD-022 | INFO | Feature modules should have permissions (unless internal or storefront-only) |
|
||||
| MOD-023 | INFO | Modules with routers should use `get_*_with_routers` pattern |
|
||||
|
||||
Run validation:
|
||||
```bash
|
||||
|
||||
@@ -850,6 +850,93 @@ Use mobile-first responsive classes.
|
||||
|
||||
---
|
||||
|
||||
## Module Structure Rules
|
||||
|
||||
Module rules enforce consistent structure and completeness across all modules in `app/modules/`.
|
||||
|
||||
### MOD-020: Module Definition Completeness
|
||||
**Severity:** Warning
|
||||
|
||||
Module definitions should include required attributes: code, name, description, version, and features.
|
||||
|
||||
```python
|
||||
# ✅ Good - Complete definition
|
||||
module = ModuleDefinition(
|
||||
code="billing",
|
||||
name="Billing & Subscriptions",
|
||||
description="Platform subscription management",
|
||||
version="1.0.0",
|
||||
features=["subscription_management", "billing_history"],
|
||||
permissions=[...],
|
||||
)
|
||||
|
||||
# ❌ Bad - Missing features
|
||||
module = ModuleDefinition(
|
||||
code="billing",
|
||||
name="Billing",
|
||||
description="...",
|
||||
version="1.0.0",
|
||||
# Missing features and permissions
|
||||
)
|
||||
```
|
||||
|
||||
### MOD-021: Modules with Menus Should Have Features
|
||||
**Severity:** Warning
|
||||
|
||||
If a module defines menu items or menu sections, it should also define features.
|
||||
|
||||
```python
|
||||
# ❌ Bad - Has menus but no features
|
||||
module = ModuleDefinition(
|
||||
code="billing",
|
||||
menus={FrontendType.ADMIN: [...]},
|
||||
# Missing features!
|
||||
)
|
||||
```
|
||||
|
||||
### MOD-022: Feature Modules Should Have Permissions
|
||||
**Severity:** Info
|
||||
|
||||
Modules with features should define permissions for RBAC, unless:
|
||||
- `is_internal=True` (internal tools)
|
||||
- Storefront-only module (session-based, no admin UI)
|
||||
|
||||
```python
|
||||
# ✅ Good - Features with permissions
|
||||
module = ModuleDefinition(
|
||||
code="billing",
|
||||
features=["subscription_management"],
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="billing.view_subscriptions",
|
||||
label_key="billing.permissions.view_subscriptions",
|
||||
description_key="billing.permissions.view_subscriptions_desc",
|
||||
category="billing",
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### MOD-023: Router Pattern Consistency
|
||||
**Severity:** Info
|
||||
|
||||
Modules with routers should use the `get_*_with_routers()` pattern for lazy imports.
|
||||
|
||||
```python
|
||||
# ✅ Good - Lazy router pattern
|
||||
def _get_admin_router():
|
||||
from app.modules.billing.routes.api.admin import admin_router
|
||||
return admin_router
|
||||
|
||||
def get_billing_module_with_routers() -> ModuleDefinition:
|
||||
billing_module.admin_router = _get_admin_router()
|
||||
return billing_module
|
||||
```
|
||||
|
||||
See [Module System Architecture](../architecture/module-system.md) for complete MOD-001 to MOD-019 rules.
|
||||
|
||||
---
|
||||
|
||||
## Security & Multi-Tenancy Rules
|
||||
|
||||
### Multi-Tenancy Rules
|
||||
@@ -1030,20 +1117,21 @@ All rules are defined in `.architecture-rules.yaml`. To modify rules:
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
| Category | Rules | Errors | Warnings |
|
||||
|----------|-------|--------|----------|
|
||||
| Backend | 20 | 15 | 5 |
|
||||
| Frontend JS | 7 | 6 | 1 |
|
||||
| Frontend Templates | 8 | 4 | 4 |
|
||||
| Frontend Macros | 5 | 2 | 3 |
|
||||
| Frontend Components | 1 | 0 | 1 |
|
||||
| Frontend Styling | 4 | 1 | 3 |
|
||||
| Naming | 5 | 3 | 2 |
|
||||
| Security | 5 | 5 | 0 |
|
||||
| Quality | 3 | 2 | 1 |
|
||||
| **Total** | **58** | **38** | **20** |
|
||||
| Category | Rules | Errors | Warnings | Info |
|
||||
|----------|-------|--------|----------|------|
|
||||
| Backend | 20 | 15 | 5 | 0 |
|
||||
| Module Structure | 23 | 7 | 10 | 6 |
|
||||
| Frontend JS | 7 | 6 | 1 | 0 |
|
||||
| Frontend Templates | 8 | 4 | 4 | 0 |
|
||||
| Frontend Macros | 5 | 2 | 3 | 0 |
|
||||
| Frontend Components | 1 | 0 | 1 | 0 |
|
||||
| Frontend Styling | 4 | 1 | 3 | 0 |
|
||||
| Naming | 5 | 3 | 2 | 0 |
|
||||
| Security | 5 | 5 | 0 | 0 |
|
||||
| Quality | 3 | 2 | 1 | 0 |
|
||||
| **Total** | **81** | **45** | **30** | **6** |
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-12-21
|
||||
**Version:** 2.4
|
||||
**Last Updated:** 2026-02-02
|
||||
**Version:** 2.5
|
||||
|
||||
@@ -4351,6 +4351,117 @@ class ArchitectureValidator:
|
||||
suggestion="Create 'exceptions.py' or 'exceptions/__init__.py'",
|
||||
)
|
||||
|
||||
# MOD-020: Check module definition has required attributes
|
||||
self._validate_module_definition_completeness(module_dir, module_name, definition_file, definition_content)
|
||||
|
||||
def _validate_module_definition_completeness(
|
||||
self,
|
||||
module_dir: Path,
|
||||
module_name: str,
|
||||
definition_file: Path,
|
||||
definition_content: str
|
||||
):
|
||||
"""
|
||||
Validate module definition completeness (MOD-020 to MOD-023).
|
||||
|
||||
Checks:
|
||||
- MOD-020: Required attributes (code, name, description, version, features, permissions)
|
||||
- MOD-021: Modules with menus should have features
|
||||
- MOD-022: Feature modules should have permissions
|
||||
- MOD-023: Modules with routers should use get_*_with_routers pattern
|
||||
"""
|
||||
# Detect module characteristics
|
||||
is_internal = "is_internal=True" in definition_content or "is_internal = True" in definition_content
|
||||
is_core_infrastructure = (
|
||||
("is_core=True" in definition_content or "is_core = True" in definition_content) and
|
||||
("menu_items={}" in definition_content or "menu_items = {}" in definition_content or "menu_items" not in definition_content)
|
||||
)
|
||||
|
||||
has_features = "features=[" in definition_content or "features = [" in definition_content
|
||||
has_permissions = "permissions=[" in definition_content or "permissions = [" in definition_content
|
||||
has_menus = "menus={" in definition_content or "menus = {" in definition_content
|
||||
has_menu_items = (
|
||||
"menu_items={" in definition_content and
|
||||
not re.search(r"menu_items\s*=\s*\{\s*\}", definition_content)
|
||||
)
|
||||
|
||||
# Check for router lazy import pattern
|
||||
has_router_imports = "_get_admin_router" in definition_content or "_get_vendor_router" in definition_content
|
||||
has_get_with_routers = re.search(r"def get_\w+_module_with_routers\s*\(", definition_content)
|
||||
|
||||
# MOD-020: Check required attributes
|
||||
# Basic required attributes
|
||||
required_attrs = ["code=", "name=", "description=", "version="]
|
||||
for attr in required_attrs:
|
||||
if attr not in definition_content:
|
||||
attr_name = attr.replace("=", "")
|
||||
self._add_violation(
|
||||
rule_id="MOD-020",
|
||||
rule_name="Module definition must have required attributes",
|
||||
severity=Severity.WARNING,
|
||||
file_path=definition_file,
|
||||
line_number=1,
|
||||
message=f"Module '{module_name}' missing required attribute '{attr_name}'",
|
||||
context="ModuleDefinition()",
|
||||
suggestion=f"Add '{attr_name}' to ModuleDefinition",
|
||||
)
|
||||
|
||||
# Check features (unless infrastructure module)
|
||||
if not has_features and not is_core_infrastructure:
|
||||
self._add_violation(
|
||||
rule_id="MOD-020",
|
||||
rule_name="Module definition must have required attributes",
|
||||
severity=Severity.WARNING,
|
||||
file_path=definition_file,
|
||||
line_number=1,
|
||||
message=f"Module '{module_name}' missing 'features' list",
|
||||
context="ModuleDefinition() without features",
|
||||
suggestion="Add 'features=[...]' to describe module capabilities",
|
||||
)
|
||||
|
||||
# MOD-021: Modules with menus should have features
|
||||
if (has_menus or has_menu_items) and not has_features:
|
||||
self._add_violation(
|
||||
rule_id="MOD-021",
|
||||
rule_name="Modules with menus should have features",
|
||||
severity=Severity.WARNING,
|
||||
file_path=definition_file,
|
||||
line_number=1,
|
||||
message=f"Module '{module_name}' has menus but no features defined",
|
||||
context="menus={...} without features=[...]",
|
||||
suggestion="Add 'features=[...]' to describe what the module provides",
|
||||
)
|
||||
|
||||
# MOD-022: Feature modules should have permissions
|
||||
if has_features and not has_permissions and not is_internal:
|
||||
# Check if it's a storefront-only module (cart, checkout)
|
||||
is_storefront_only = module_name in ["cart", "checkout"]
|
||||
|
||||
if not is_storefront_only:
|
||||
self._add_violation(
|
||||
rule_id="MOD-022",
|
||||
rule_name="Feature modules should have permissions",
|
||||
severity=Severity.INFO,
|
||||
file_path=definition_file,
|
||||
line_number=1,
|
||||
message=f"Module '{module_name}' has features but no permissions defined",
|
||||
context="features=[...] without permissions=[...]",
|
||||
suggestion="Add 'permissions=[PermissionDefinition(...), ...]' for RBAC",
|
||||
)
|
||||
|
||||
# MOD-023: Modules with routers should use get_*_with_routers pattern
|
||||
if has_router_imports and not has_get_with_routers:
|
||||
self._add_violation(
|
||||
rule_id="MOD-023",
|
||||
rule_name="Modules with routers should use get_*_with_routers pattern",
|
||||
severity=Severity.INFO,
|
||||
file_path=definition_file,
|
||||
line_number=1,
|
||||
message=f"Module '{module_name}' has router imports but no get_*_with_routers() function",
|
||||
context="_get_admin_router() or _get_vendor_router() without wrapper",
|
||||
suggestion=f"Add 'def get_{module_name}_module_with_routers()' function",
|
||||
)
|
||||
|
||||
def _validate_legacy_locations(self, target_path: Path):
|
||||
"""
|
||||
Validate that code is not in legacy locations (MOD-016 to MOD-019).
|
||||
|
||||
2
tests/unit/modules/__init__.py
Normal file
2
tests/unit/modules/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# tests/unit/modules/__init__.py
|
||||
"""Unit tests for module system."""
|
||||
315
tests/unit/modules/test_module_definitions.py
Normal file
315
tests/unit/modules/test_module_definitions.py
Normal file
@@ -0,0 +1,315 @@
|
||||
# tests/unit/modules/test_module_definitions.py
|
||||
"""
|
||||
Unit tests for module definitions.
|
||||
|
||||
Tests cover:
|
||||
- Module definition completeness (MOD-020)
|
||||
- Menu/feature consistency (MOD-021)
|
||||
- Permission coverage (MOD-022)
|
||||
- Router pattern consistency (MOD-023)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.registry import MODULES
|
||||
from app.modules.base import ModuleDefinition, PermissionDefinition
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestModuleDefinitionCompleteness:
|
||||
"""Test MOD-020: Module definitions have required attributes."""
|
||||
|
||||
def test_all_modules_have_code(self):
|
||||
"""Test that all modules have a code defined."""
|
||||
for code, module in MODULES.items():
|
||||
assert module.code, f"Module {code} missing 'code' attribute"
|
||||
assert module.code == code, f"Module code mismatch: {module.code} != {code}"
|
||||
|
||||
def test_all_modules_have_name(self):
|
||||
"""Test that all modules have a name defined."""
|
||||
for code, module in MODULES.items():
|
||||
assert module.name, f"Module {code} missing 'name' attribute"
|
||||
|
||||
def test_all_modules_have_description(self):
|
||||
"""Test that all modules have a description defined."""
|
||||
for code, module in MODULES.items():
|
||||
assert module.description, f"Module {code} missing 'description' attribute"
|
||||
|
||||
def test_all_modules_have_version(self):
|
||||
"""Test that all modules have a version defined."""
|
||||
for code, module in MODULES.items():
|
||||
assert module.version, f"Module {code} missing 'version' attribute"
|
||||
# Version should be semantic versioning format
|
||||
parts = module.version.split(".")
|
||||
assert len(parts) >= 2, f"Module {code} has invalid version format: {module.version}"
|
||||
|
||||
def test_all_modules_have_features(self):
|
||||
"""Test that all modules have features defined."""
|
||||
for code, module in MODULES.items():
|
||||
assert module.features is not None, f"Module {code} missing 'features' attribute"
|
||||
assert isinstance(module.features, list), f"Module {code} features should be a list"
|
||||
# All modules should have at least one feature
|
||||
assert len(module.features) > 0, f"Module {code} has empty features list"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestModulePermissions:
|
||||
"""Test MOD-022: Feature modules should have permissions."""
|
||||
|
||||
# Modules exempt from permission requirements
|
||||
EXEMPT_MODULES = {
|
||||
"contracts", # Infrastructure module
|
||||
"dev-tools", # Internal module (is_internal=True)
|
||||
"monitoring", # Internal module (is_internal=True)
|
||||
}
|
||||
|
||||
def test_feature_modules_have_permissions(self):
|
||||
"""Test that modules with features have permissions (unless exempt)."""
|
||||
for code, module in MODULES.items():
|
||||
if code in self.EXEMPT_MODULES:
|
||||
continue
|
||||
|
||||
# If module has features, it should have permissions
|
||||
if module.features:
|
||||
assert module.permissions is not None, (
|
||||
f"Module {code} has features but no permissions"
|
||||
)
|
||||
assert len(module.permissions) > 0, (
|
||||
f"Module {code} has features but empty permissions list"
|
||||
)
|
||||
|
||||
def test_internal_modules_exempt_from_permissions(self):
|
||||
"""Test that internal modules don't require permissions."""
|
||||
internal_modules = [m for m in MODULES.values() if getattr(m, "is_internal", False)]
|
||||
# Internal modules exist
|
||||
assert len(internal_modules) > 0, "No internal modules found"
|
||||
# They may or may not have permissions (exempt from requirement)
|
||||
|
||||
def test_permission_structure(self):
|
||||
"""Test that permissions have required fields."""
|
||||
for code, module in MODULES.items():
|
||||
if not module.permissions:
|
||||
continue
|
||||
|
||||
for perm in module.permissions:
|
||||
assert isinstance(perm, PermissionDefinition), (
|
||||
f"Module {code} permission is not a PermissionDefinition"
|
||||
)
|
||||
assert perm.id, f"Module {code} has permission without id"
|
||||
assert perm.label_key, f"Module {code} permission {perm.id} missing label_key"
|
||||
assert perm.description_key, (
|
||||
f"Module {code} permission {perm.id} missing description_key"
|
||||
)
|
||||
assert perm.category, f"Module {code} permission {perm.id} missing category"
|
||||
|
||||
def test_permission_ids_follow_convention(self):
|
||||
"""Test that permission IDs follow the module.action convention."""
|
||||
for code, module in MODULES.items():
|
||||
if not module.permissions:
|
||||
continue
|
||||
|
||||
for perm in module.permissions:
|
||||
# Permission ID should contain a dot
|
||||
assert "." in perm.id, (
|
||||
f"Module {code} permission {perm.id} should follow 'module.action' format"
|
||||
)
|
||||
|
||||
def test_permission_counts(self):
|
||||
"""Test that modules have expected permission counts."""
|
||||
# Modules that should have permissions
|
||||
modules_with_permissions = {
|
||||
code: module
|
||||
for code, module in MODULES.items()
|
||||
if module.permissions
|
||||
}
|
||||
|
||||
# At least 15 modules should have permissions
|
||||
assert len(modules_with_permissions) >= 15, (
|
||||
f"Expected at least 15 modules with permissions, got {len(modules_with_permissions)}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestModuleMenuFeatureConsistency:
|
||||
"""Test MOD-021: Modules with menus should have features."""
|
||||
|
||||
def test_modules_with_menus_have_features(self):
|
||||
"""Test that modules with menu definitions have features."""
|
||||
for code, module in MODULES.items():
|
||||
has_menus = getattr(module, "menus", None) and len(module.menus) > 0
|
||||
has_menu_items = (
|
||||
getattr(module, "menu_items", None) and
|
||||
any(items for items in module.menu_items.values())
|
||||
)
|
||||
|
||||
if has_menus or has_menu_items:
|
||||
assert module.features and len(module.features) > 0, (
|
||||
f"Module {code} has menus but no features defined"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestModuleRouterPattern:
|
||||
"""Test MOD-023: Router pattern consistency."""
|
||||
|
||||
def test_modules_with_routers_attribute(self):
|
||||
"""Test that modules can have router attributes attached."""
|
||||
# After calling get_*_with_routers(), modules should have router attrs
|
||||
# This test just verifies the attribute exists (may be None)
|
||||
for code, module in MODULES.items():
|
||||
# These are optional attributes set by get_*_with_routers()
|
||||
# Just verify they can be accessed without error
|
||||
_ = getattr(module, "admin_router", None)
|
||||
_ = getattr(module, "vendor_router", None)
|
||||
_ = getattr(module, "platform_router", None)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSpecificModulePermissions:
|
||||
"""Test specific modules have their expected permissions."""
|
||||
|
||||
def test_cart_module_permissions(self):
|
||||
"""Test cart module has required permissions."""
|
||||
module = MODULES.get("cart")
|
||||
assert module is not None
|
||||
assert module.permissions is not None
|
||||
|
||||
perm_ids = {p.id for p in module.permissions}
|
||||
assert "cart.view" in perm_ids
|
||||
assert "cart.manage" in perm_ids
|
||||
|
||||
def test_checkout_module_permissions(self):
|
||||
"""Test checkout module has required permissions."""
|
||||
module = MODULES.get("checkout")
|
||||
assert module is not None
|
||||
assert module.permissions is not None
|
||||
|
||||
perm_ids = {p.id for p in module.permissions}
|
||||
assert "checkout.view_settings" in perm_ids
|
||||
assert "checkout.manage_settings" in perm_ids
|
||||
|
||||
def test_analytics_module_permissions(self):
|
||||
"""Test analytics module has required permissions."""
|
||||
module = MODULES.get("analytics")
|
||||
assert module is not None
|
||||
assert module.permissions is not None
|
||||
|
||||
perm_ids = {p.id for p in module.permissions}
|
||||
assert "analytics.view" in perm_ids
|
||||
assert "analytics.export" in perm_ids
|
||||
assert "analytics.manage_dashboards" in perm_ids
|
||||
|
||||
def test_billing_module_permissions(self):
|
||||
"""Test billing module has required permissions."""
|
||||
module = MODULES.get("billing")
|
||||
assert module is not None
|
||||
assert module.permissions is not None
|
||||
|
||||
perm_ids = {p.id for p in module.permissions}
|
||||
assert "billing.view_tiers" in perm_ids
|
||||
assert "billing.manage_tiers" in perm_ids
|
||||
assert "billing.view_subscriptions" in perm_ids
|
||||
assert "billing.manage_subscriptions" in perm_ids
|
||||
assert "billing.view_invoices" in perm_ids
|
||||
|
||||
def test_cms_module_permissions(self):
|
||||
"""Test cms module has required permissions."""
|
||||
module = MODULES.get("cms")
|
||||
assert module is not None
|
||||
assert module.permissions is not None
|
||||
|
||||
perm_ids = {p.id for p in module.permissions}
|
||||
assert "cms.view_pages" in perm_ids
|
||||
assert "cms.manage_pages" in perm_ids
|
||||
assert "cms.view_media" in perm_ids
|
||||
assert "cms.manage_media" in perm_ids
|
||||
assert "cms.manage_themes" in perm_ids
|
||||
|
||||
def test_loyalty_module_permissions(self):
|
||||
"""Test loyalty module has required permissions."""
|
||||
module = MODULES.get("loyalty")
|
||||
assert module is not None
|
||||
assert module.permissions is not None
|
||||
|
||||
perm_ids = {p.id for p in module.permissions}
|
||||
assert "loyalty.view_programs" in perm_ids
|
||||
assert "loyalty.manage_programs" in perm_ids
|
||||
assert "loyalty.view_rewards" in perm_ids
|
||||
assert "loyalty.manage_rewards" in perm_ids
|
||||
|
||||
def test_marketplace_module_permissions(self):
|
||||
"""Test marketplace module has required permissions."""
|
||||
module = MODULES.get("marketplace")
|
||||
assert module is not None
|
||||
assert module.permissions is not None
|
||||
|
||||
perm_ids = {p.id for p in module.permissions}
|
||||
assert "marketplace.view_integration" in perm_ids
|
||||
assert "marketplace.manage_integration" in perm_ids
|
||||
assert "marketplace.sync_products" in perm_ids
|
||||
|
||||
def test_messaging_module_permissions(self):
|
||||
"""Test messaging module has required permissions."""
|
||||
module = MODULES.get("messaging")
|
||||
assert module is not None
|
||||
assert module.permissions is not None
|
||||
|
||||
perm_ids = {p.id for p in module.permissions}
|
||||
assert "messaging.view_messages" in perm_ids
|
||||
assert "messaging.send_messages" in perm_ids
|
||||
assert "messaging.manage_templates" in perm_ids
|
||||
|
||||
def test_payments_module_permissions(self):
|
||||
"""Test payments module has required permissions."""
|
||||
module = MODULES.get("payments")
|
||||
assert module is not None
|
||||
assert module.permissions is not None
|
||||
|
||||
perm_ids = {p.id for p in module.permissions}
|
||||
assert "payments.view_gateways" in perm_ids
|
||||
assert "payments.manage_gateways" in perm_ids
|
||||
assert "payments.view_transactions" in perm_ids
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestModuleFeatures:
|
||||
"""Test module feature definitions."""
|
||||
|
||||
def test_cart_module_features(self):
|
||||
"""Test cart module has required features."""
|
||||
module = MODULES.get("cart")
|
||||
assert module is not None
|
||||
|
||||
expected_features = {
|
||||
"cart_management",
|
||||
"cart_persistence",
|
||||
"cart_item_operations",
|
||||
"shipping_calculation",
|
||||
"promotion_application",
|
||||
}
|
||||
assert expected_features.issubset(set(module.features))
|
||||
|
||||
def test_checkout_module_features(self):
|
||||
"""Test checkout module has required features."""
|
||||
module = MODULES.get("checkout")
|
||||
assert module is not None
|
||||
|
||||
expected_features = {
|
||||
"checkout_flow",
|
||||
"order_creation",
|
||||
"payment_processing",
|
||||
"checkout_validation",
|
||||
"guest_checkout",
|
||||
}
|
||||
assert expected_features.issubset(set(module.features))
|
||||
|
||||
def test_catalog_module_features(self):
|
||||
"""Test catalog module has required features."""
|
||||
module = MODULES.get("catalog")
|
||||
assert module is not None
|
||||
|
||||
expected_features = {
|
||||
"product_catalog",
|
||||
"product_search",
|
||||
}
|
||||
assert expected_features.issubset(set(module.features))
|
||||
Reference in New Issue
Block a user