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:
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