Files
orion/tests/unit/services/test_module_service.py
Samir Boulahtit d7a0ff8818 refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture:

## Models Migration
- Moved all domain models from models/database/ to their respective modules:
  - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc.
  - cms: MediaFile, VendorTheme
  - messaging: Email, VendorEmailSettings, VendorEmailTemplate
  - core: AdminMenuConfig
- models/database/ now only contains Base and TimestampMixin (infrastructure)

## Schemas Migration
- Moved all domain schemas from models/schema/ to their respective modules:
  - tenancy: company, vendor, admin, team, vendor_domain
  - cms: media, image, vendor_theme
  - messaging: email
- models/schema/ now only contains base.py and auth.py (infrastructure)

## Routes Migration
- Moved admin routes from app/api/v1/admin/ to modules:
  - menu_config.py -> core module
  - modules.py -> tenancy module
  - module_config.py -> tenancy module
- app/api/v1/admin/ now only aggregates auto-discovered module routes

## Menu System
- Implemented module-driven menu system with MenuDiscoveryService
- Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT
- Added MenuItemDefinition and MenuSectionDefinition dataclasses
- Each module now defines its own menu items in definition.py
- MenuService integrates with MenuDiscoveryService for template rendering

## Documentation
- Updated docs/architecture/models-structure.md
- Updated docs/architecture/menu-management.md
- Updated architecture validation rules for new exceptions

## Architecture Validation
- Updated MOD-019 rule to allow base.py in models/schema/
- Created core module exceptions.py and schemas/ directory
- All validation errors resolved (only warnings remain)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:02:56 +01:00

398 lines
15 KiB
Python

# tests/unit/services/test_module_service.py
"""
Unit tests for ModuleService.
Tests cover:
- Module enablement checking
- Module dependency resolution
- Menu item filtering by modules
- Platform module configuration
"""
import pytest
from app.modules import MODULES, ModuleDefinition, module_service
from app.modules.registry import (
get_core_module_codes,
get_menu_item_module,
get_module,
validate_module_dependencies,
)
from app.modules.service import ModuleService
from app.modules.enums import FrontendType
@pytest.mark.unit
@pytest.mark.service
class TestModuleRegistry:
"""Test module registry structure and validation."""
def test_all_modules_defined(self):
"""Test that expected modules are defined."""
expected_modules = {
"core",
"platform-admin",
"billing",
"inventory",
"orders",
"marketplace",
"customers",
"cms",
"analytics",
"messaging",
"dev-tools",
"monitoring",
}
assert set(MODULES.keys()) == expected_modules
def test_core_modules_marked_correctly(self):
"""Test that core modules have is_core=True."""
core_codes = get_core_module_codes()
assert "core" in core_codes
assert "platform-admin" in core_codes
assert len(core_codes) == 2
def test_module_dependencies_valid(self):
"""Test that all module dependencies reference valid modules."""
errors = validate_module_dependencies()
assert errors == [], f"Module dependency validation failed: {errors}"
def test_marketplace_requires_inventory(self):
"""Test that marketplace module depends on inventory."""
marketplace = get_module("marketplace")
assert marketplace is not None
assert "inventory" in marketplace.requires
def test_core_modules_have_no_dependencies(self):
"""Test that core modules don't depend on optional modules."""
for code in get_core_module_codes():
module = get_module(code)
assert module is not None
# Core module dependencies should only be other core modules (or empty)
for req in module.requires:
assert req in get_core_module_codes(), (
f"Core module '{code}' depends on optional module '{req}'"
)
@pytest.mark.unit
@pytest.mark.service
class TestModuleMenuMapping:
"""Test menu item to module mapping."""
def test_dashboard_maps_to_core(self):
"""Test that dashboard menu item maps to core module."""
module_code = get_menu_item_module("dashboard", FrontendType.ADMIN)
assert module_code == "core"
def test_billing_items_map_to_billing(self):
"""Test that billing menu items map to billing module."""
billing_items = ["subscription-tiers", "subscriptions", "billing-history"]
for item in billing_items:
module_code = get_menu_item_module(item, FrontendType.ADMIN)
assert module_code == "billing", f"{item} should map to billing, got {module_code}"
def test_inventory_items_map_to_inventory(self):
"""Test that inventory menu items map to inventory module."""
module_code = get_menu_item_module("inventory", FrontendType.ADMIN)
assert module_code == "inventory"
def test_marketplace_items_map_to_marketplace(self):
"""Test that marketplace menu items map to marketplace module."""
module_code = get_menu_item_module("marketplace-letzshop", FrontendType.ADMIN)
assert module_code == "marketplace"
def test_vendor_billing_maps_to_billing(self):
"""Test that vendor billing menu item maps to billing module."""
module_code = get_menu_item_module("billing", FrontendType.VENDOR)
assert module_code == "billing"
@pytest.mark.unit
@pytest.mark.service
class TestModuleDefinition:
"""Test ModuleDefinition class."""
def test_module_definition_equality(self):
"""Test that modules are equal by code."""
mod1 = ModuleDefinition(code="test", name="Test 1")
mod2 = ModuleDefinition(code="test", name="Test 2")
mod3 = ModuleDefinition(code="other", name="Other")
assert mod1 == mod2 # Same code
assert mod1 != mod3 # Different code
def test_module_has_feature(self):
"""Test has_feature method."""
module = ModuleDefinition(
code="test",
name="Test",
features=["feature_a", "feature_b"],
)
assert module.has_feature("feature_a")
assert module.has_feature("feature_b")
assert not module.has_feature("feature_c")
def test_module_has_menu_item(self):
"""Test has_menu_item method."""
module = ModuleDefinition(
code="test",
name="Test",
menu_items={
FrontendType.ADMIN: ["item-a", "item-b"],
FrontendType.VENDOR: ["item-c"],
},
)
assert module.has_menu_item("item-a")
assert module.has_menu_item("item-c")
assert not module.has_menu_item("item-d")
def test_module_check_dependencies(self):
"""Test check_dependencies method."""
module = ModuleDefinition(
code="test",
name="Test",
requires=["dep1", "dep2"],
)
# All deps enabled
assert module.check_dependencies({"dep1", "dep2", "dep3"}) == []
# Missing dep1
assert module.check_dependencies({"dep2"}) == ["dep1"]
# Missing both
assert set(module.check_dependencies(set())) == {"dep1", "dep2"}
@pytest.mark.unit
@pytest.mark.service
class TestModuleServiceWithPlatform:
"""Test ModuleService with database platform."""
def test_get_platform_modules_all_enabled(self, db, test_platform):
"""Test that all modules are enabled when not configured."""
# Platform has no enabled_modules setting, so all should be enabled
service = ModuleService()
modules = service.get_platform_modules(db, test_platform.id)
# Should return all modules
assert len(modules) == len(MODULES)
def test_get_platform_modules_with_config(self, db, test_platform):
"""Test that only configured modules are enabled."""
# Set enabled_modules in platform settings
test_platform.settings = {"enabled_modules": ["billing", "inventory"]}
db.commit()
service = ModuleService()
module_codes = service.get_enabled_module_codes(db, test_platform.id)
# Should include core modules + configured modules
assert "core" in module_codes
assert "platform-admin" in module_codes
assert "billing" in module_codes
assert "inventory" in module_codes
assert "marketplace" not in module_codes # Not configured
def test_is_module_enabled_core_always_enabled(self, db, test_platform):
"""Test that core modules are always enabled."""
test_platform.settings = {"enabled_modules": []} # Empty list
db.commit()
service = ModuleService()
assert service.is_module_enabled(db, test_platform.id, "core")
assert service.is_module_enabled(db, test_platform.id, "platform-admin")
def test_is_module_enabled_optional_disabled(self, db, test_platform):
"""Test that optional modules can be disabled."""
test_platform.settings = {"enabled_modules": ["inventory"]}
db.commit()
service = ModuleService()
assert service.is_module_enabled(db, test_platform.id, "inventory")
assert not service.is_module_enabled(db, test_platform.id, "billing")
def test_dependency_resolution(self, db, test_platform):
"""Test that enabling marketplace auto-enables inventory."""
# Enable marketplace but not inventory
test_platform.settings = {"enabled_modules": ["marketplace"]}
db.commit()
service = ModuleService()
module_codes = service.get_enabled_module_codes(db, test_platform.id)
# Inventory should be auto-enabled due to marketplace dependency
assert "marketplace" in module_codes
assert "inventory" in module_codes
def test_get_module_menu_items(self, db, test_platform):
"""Test getting menu items for enabled modules."""
test_platform.settings = {"enabled_modules": ["billing"]}
db.commit()
service = ModuleService()
menu_items = service.get_module_menu_items(db, test_platform.id, FrontendType.ADMIN)
# Should include core and billing menu items
assert "dashboard" in menu_items # core
assert "settings" in menu_items # core
assert "subscription-tiers" in menu_items # billing
assert "subscriptions" in menu_items # billing
# Should NOT include disabled module items
assert "marketplace-letzshop" not in menu_items
def test_is_menu_item_module_enabled(self, db, test_platform):
"""Test checking if menu item's module is enabled."""
test_platform.settings = {"enabled_modules": ["billing"]}
db.commit()
service = ModuleService()
# Billing menu item should be enabled
assert service.is_menu_item_module_enabled(
db, test_platform.id, "subscription-tiers", FrontendType.ADMIN
)
# Marketplace menu item should be disabled
assert not service.is_menu_item_module_enabled(
db, test_platform.id, "marketplace-letzshop", FrontendType.ADMIN
)
def test_filter_menu_items_by_modules(self, db, test_platform):
"""Test filtering menu items by enabled modules."""
test_platform.settings = {"enabled_modules": ["billing"]}
db.commit()
service = ModuleService()
# Try to filter a mix of enabled and disabled items
all_items = {"dashboard", "subscription-tiers", "marketplace-letzshop", "inventory"}
filtered = service.filter_menu_items_by_modules(
db, test_platform.id, all_items, FrontendType.ADMIN
)
# Should keep core and billing items, remove marketplace and inventory
assert "dashboard" in filtered
assert "subscription-tiers" in filtered
assert "marketplace-letzshop" not in filtered
assert "inventory" not in filtered
@pytest.mark.unit
@pytest.mark.service
class TestModuleServiceEnableDisable:
"""Test module enable/disable operations."""
def test_enable_module(self, db, test_platform):
"""Test enabling a module."""
test_platform.settings = {"enabled_modules": ["billing"]}
db.commit()
service = ModuleService()
result = service.enable_module(db, test_platform.id, "analytics")
db.commit()
assert result is True
assert service.is_module_enabled(db, test_platform.id, "analytics")
def test_disable_module(self, db, test_platform):
"""Test disabling a module."""
test_platform.settings = {"enabled_modules": ["billing", "analytics"]}
db.commit()
service = ModuleService()
result = service.disable_module(db, test_platform.id, "analytics")
db.commit()
assert result is True
assert not service.is_module_enabled(db, test_platform.id, "analytics")
assert service.is_module_enabled(db, test_platform.id, "billing")
def test_cannot_disable_core_module(self, db, test_platform):
"""Test that core modules cannot be disabled."""
service = ModuleService()
result = service.disable_module(db, test_platform.id, "core")
assert result is False
assert service.is_module_enabled(db, test_platform.id, "core")
def test_disable_module_cascades_to_dependents(self, db, test_platform):
"""Test that disabling a module also disables its dependents."""
# Enable marketplace (which requires inventory)
test_platform.settings = {"enabled_modules": ["marketplace", "inventory"]}
db.commit()
service = ModuleService()
# Disable inventory - should also disable marketplace
result = service.disable_module(db, test_platform.id, "inventory")
db.commit()
assert result is True
assert not service.is_module_enabled(db, test_platform.id, "inventory")
assert not service.is_module_enabled(db, test_platform.id, "marketplace")
def test_set_enabled_modules(self, db, test_platform):
"""Test setting all enabled modules at once."""
service = ModuleService()
result = service.set_enabled_modules(
db, test_platform.id, ["billing", "inventory", "orders"]
)
db.commit()
assert result is True
module_codes = service.get_enabled_module_codes(db, test_platform.id)
# Should have core + specified modules
assert "core" in module_codes
assert "platform-admin" in module_codes
assert "billing" in module_codes
assert "inventory" in module_codes
assert "orders" in module_codes
assert "marketplace" not in module_codes
def test_invalid_module_code_ignored(self, db, test_platform):
"""Test that invalid module codes are ignored."""
service = ModuleService()
result = service.set_enabled_modules(
db, test_platform.id, ["billing", "invalid_module", "inventory"]
)
db.commit()
assert result is True
module_codes = service.get_enabled_module_codes(db, test_platform.id)
assert "billing" in module_codes
assert "inventory" in module_codes
assert "invalid_module" not in module_codes
@pytest.mark.unit
@pytest.mark.service
class TestModuleServiceByCode:
"""Test ModuleService methods that work with platform code."""
def test_get_platform_modules_by_code(self, db, test_platform):
"""Test getting modules by platform code."""
service = ModuleService()
modules = service.get_platform_modules_by_code(db, test_platform.code)
# Should return all modules for platform without config
assert len(modules) == len(MODULES)
def test_is_module_enabled_by_code(self, db, test_platform):
"""Test checking module enablement by platform code."""
test_platform.settings = {"enabled_modules": ["billing"]}
db.commit()
service = ModuleService()
assert service.is_module_enabled_by_code(db, test_platform.code, "billing")
assert service.is_module_enabled_by_code(db, test_platform.code, "core")
assert not service.is_module_enabled_by_code(db, test_platform.code, "marketplace")
def test_nonexistent_platform_code_returns_all(self, db):
"""Test that nonexistent platform code returns all modules."""
service = ModuleService()
modules = service.get_platform_modules_by_code(db, "nonexistent_platform")
# Should return all modules as fallback
assert len(modules) == len(MODULES)