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:
2026-02-02 18:23:04 +01:00
parent 30a5c75e74
commit 967f08e4ba
50 changed files with 1014 additions and 66 deletions

View File

@@ -0,0 +1,2 @@
# tests/unit/modules/__init__.py
"""Unit tests for module system."""

View 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))