feat: complete modular platform architecture (Phases 1-5)

Phase 1 - Vendor Router Integration:
- Wire up vendor module routers in app/api/v1/vendor/__init__.py
- Use lazy imports via __getattr__ to avoid circular dependencies

Phase 2 - Extract Remaining Modules:
- Create 6 new module directories: customers, cms, analytics, messaging,
  dev_tools, monitoring
- Each module has definition.py and route wrappers
- Update registry to import from extracted modules

Phase 3 - Database Table Migration:
- Add PlatformModule junction table for auditable module tracking
- Add migration zc2m3n4o5p6q7_add_platform_modules_table.py
- Add modules relationship to Platform model
- Update ModuleService with JSON-to-junction-table migration

Phase 4 - Module-Specific Configuration UI:
- Add /api/v1/admin/module-config/* endpoints
- Add module-config.html template and JS

Phase 5 - Integration Tests:
- Add tests/fixtures/module_fixtures.py
- Add tests/integration/api/v1/admin/test_modules.py
- Add tests/integration/api/v1/modules/test_module_access.py

Architecture fixes:
- Fix JS-003 errors: use ...data() directly in Alpine components
- Fix JS-005 warnings: add init() guards to prevent duplicate init
- Fix API-001 errors: add MenuActionResponse Pydantic model
- Add FE-008 noqa for dynamic number input in template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 18:19:00 +01:00
parent f29f1113cd
commit c419090531
55 changed files with 4059 additions and 206 deletions

View File

@@ -0,0 +1,328 @@
# tests/integration/api/v1/admin/test_modules.py
"""
Integration tests for admin module management endpoints.
Tests the /api/v1/admin/modules/* and /api/v1/admin/module-config/* endpoints.
All endpoints require super admin access.
"""
import pytest
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.admin
@pytest.mark.modules
class TestAdminModulesAPI:
"""Tests for admin module management endpoints."""
# ========================================================================
# List Modules Tests
# ========================================================================
def test_list_all_modules(self, client, super_admin_headers):
"""Test super admin listing all modules."""
response = client.get("/api/v1/admin/modules", headers=super_admin_headers)
assert response.status_code == 200
data = response.json()
assert "modules" in data
assert "total" in data
assert data["total"] >= 10 # At least 10 modules defined
# Check expected modules exist
module_codes = [m["code"] for m in data["modules"]]
assert "core" in module_codes
assert "billing" in module_codes
assert "inventory" in module_codes
def test_list_modules_requires_super_admin(self, client, admin_headers):
"""Test that listing modules requires super admin."""
response = client.get("/api/v1/admin/modules", headers=admin_headers)
# Should require super admin
assert response.status_code == 403
def test_list_modules_unauthenticated(self, client):
"""Test that listing modules requires authentication."""
response = client.get("/api/v1/admin/modules")
assert response.status_code == 401
# ========================================================================
# Get Platform Modules Tests
# ========================================================================
def test_get_platform_modules(self, client, super_admin_headers, test_platform):
"""Test getting modules for a specific platform."""
response = client.get(
f"/api/v1/admin/modules/platforms/{test_platform.id}",
headers=super_admin_headers,
)
assert response.status_code == 200
data = response.json()
assert data["platform_id"] == test_platform.id
assert data["platform_code"] == test_platform.code
assert "modules" in data
assert "enabled" in data
assert "disabled" in data
def test_get_platform_modules_not_found(self, client, super_admin_headers):
"""Test getting modules for non-existent platform."""
response = client.get(
"/api/v1/admin/modules/platforms/99999",
headers=super_admin_headers,
)
assert response.status_code == 404
# ========================================================================
# Enable/Disable Module Tests
# ========================================================================
def test_enable_module(self, client, super_admin_headers, test_platform, db):
"""Test enabling a module for a platform."""
# First disable the module via settings
test_platform.settings = {"enabled_modules": ["core", "platform-admin"]}
db.commit()
response = client.post(
f"/api/v1/admin/modules/platforms/{test_platform.id}/enable",
headers=super_admin_headers,
json={"module_code": "billing"},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "billing" in data["message"].lower() or "enabled" in data["message"].lower()
def test_disable_module(self, client, super_admin_headers, test_platform, db):
"""Test disabling a module for a platform."""
# Ensure module is enabled
test_platform.settings = {"enabled_modules": ["billing", "inventory"]}
db.commit()
response = client.post(
f"/api/v1/admin/modules/platforms/{test_platform.id}/disable",
headers=super_admin_headers,
json={"module_code": "billing"},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
def test_cannot_disable_core_module(self, client, super_admin_headers, test_platform):
"""Test that core modules cannot be disabled."""
response = client.post(
f"/api/v1/admin/modules/platforms/{test_platform.id}/disable",
headers=super_admin_headers,
json={"module_code": "core"},
)
assert response.status_code == 400
data = response.json()
assert "core" in data.get("message", "").lower() or "cannot" in data.get("message", "").lower()
def test_enable_invalid_module(self, client, super_admin_headers, test_platform):
"""Test enabling a non-existent module."""
response = client.post(
f"/api/v1/admin/modules/platforms/{test_platform.id}/enable",
headers=super_admin_headers,
json={"module_code": "invalid_module"},
)
assert response.status_code == 400
# ========================================================================
# Bulk Operations Tests
# ========================================================================
def test_update_platform_modules(self, client, super_admin_headers, test_platform):
"""Test updating all enabled modules at once."""
response = client.put(
f"/api/v1/admin/modules/platforms/{test_platform.id}",
headers=super_admin_headers,
json={"module_codes": ["billing", "inventory", "orders"]},
)
assert response.status_code == 200
data = response.json()
assert data["platform_id"] == test_platform.id
# Check that specified modules are enabled
enabled_codes = [m["code"] for m in data["modules"] if m["is_enabled"]]
assert "billing" in enabled_codes
assert "inventory" in enabled_codes
assert "orders" in enabled_codes
# Core modules should always be enabled
assert "core" in enabled_codes
def test_enable_all_modules(self, client, super_admin_headers, test_platform):
"""Test enabling all modules for a platform."""
response = client.post(
f"/api/v1/admin/modules/platforms/{test_platform.id}/enable-all",
headers=super_admin_headers,
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["enabled_count"] >= 10
def test_disable_optional_modules(self, client, super_admin_headers, test_platform):
"""Test disabling all optional modules."""
response = client.post(
f"/api/v1/admin/modules/platforms/{test_platform.id}/disable-optional",
headers=super_admin_headers,
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "core" in data["core_modules"]
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.admin
@pytest.mark.modules
class TestAdminModuleConfigAPI:
"""Tests for admin module configuration endpoints."""
# ========================================================================
# Get Module Config Tests
# ========================================================================
def test_get_module_config(self, client, super_admin_headers, test_platform):
"""Test getting module configuration."""
response = client.get(
f"/api/v1/admin/module-config/platforms/{test_platform.id}/modules/billing/config",
headers=super_admin_headers,
)
assert response.status_code == 200
data = response.json()
assert data["module_code"] == "billing"
assert "config" in data
assert "schema_info" in data
assert "defaults" in data
def test_get_module_config_has_defaults(self, client, super_admin_headers, test_platform):
"""Test that module config includes default values."""
response = client.get(
f"/api/v1/admin/module-config/platforms/{test_platform.id}/modules/billing/config",
headers=super_admin_headers,
)
assert response.status_code == 200
data = response.json()
# Should have default billing config
assert "stripe_mode" in data["config"]
assert "default_trial_days" in data["config"]
def test_get_module_config_invalid_module(self, client, super_admin_headers, test_platform):
"""Test getting config for invalid module."""
response = client.get(
f"/api/v1/admin/module-config/platforms/{test_platform.id}/modules/invalid_module/config",
headers=super_admin_headers,
)
assert response.status_code == 400
# ========================================================================
# Update Module Config Tests
# ========================================================================
def test_update_module_config(self, client, super_admin_headers, test_platform):
"""Test updating module configuration."""
response = client.put(
f"/api/v1/admin/module-config/platforms/{test_platform.id}/modules/billing/config",
headers=super_admin_headers,
json={
"config": {
"stripe_mode": "live",
"default_trial_days": 7,
}
},
)
assert response.status_code == 200
data = response.json()
assert data["config"]["stripe_mode"] == "live"
assert data["config"]["default_trial_days"] == 7
def test_update_module_config_persists(self, client, super_admin_headers, test_platform):
"""Test that config updates persist across requests."""
# Update config
client.put(
f"/api/v1/admin/module-config/platforms/{test_platform.id}/modules/inventory/config",
headers=super_admin_headers,
json={
"config": {
"low_stock_threshold": 25,
}
},
)
# Fetch again
response = client.get(
f"/api/v1/admin/module-config/platforms/{test_platform.id}/modules/inventory/config",
headers=super_admin_headers,
)
assert response.status_code == 200
data = response.json()
assert data["config"]["low_stock_threshold"] == 25
# ========================================================================
# Reset Config Tests
# ========================================================================
def test_reset_module_config(self, client, super_admin_headers, test_platform):
"""Test resetting module config to defaults."""
# First set custom config
client.put(
f"/api/v1/admin/module-config/platforms/{test_platform.id}/modules/billing/config",
headers=super_admin_headers,
json={
"config": {
"stripe_mode": "live",
"default_trial_days": 1,
}
},
)
# Reset to defaults
response = client.post(
f"/api/v1/admin/module-config/platforms/{test_platform.id}/modules/billing/reset",
headers=super_admin_headers,
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
# Config should be reset to defaults
assert data["config"]["stripe_mode"] == "test"
assert data["config"]["default_trial_days"] == 14
# ========================================================================
# Get Defaults Tests
# ========================================================================
def test_get_config_defaults(self, client, super_admin_headers):
"""Test getting default config for a module."""
response = client.get(
"/api/v1/admin/module-config/defaults/billing",
headers=super_admin_headers,
)
assert response.status_code == 200
data = response.json()
assert data["module_code"] == "billing"
assert "defaults" in data
assert "schema_info" in data
assert data["defaults"]["stripe_mode"] == "test"

View File

@@ -0,0 +1,2 @@
# tests/integration/api/v1/modules/__init__.py
"""Integration tests for module access control."""

View File

@@ -0,0 +1,253 @@
# tests/integration/api/v1/modules/test_module_access.py
"""
Integration tests for module-based access control.
Tests verify that:
- Disabled modules return 403 Forbidden
- Enabled modules allow access
- Core modules are always accessible
- Module dependencies are enforced
"""
import pytest
from models.database.platform import Platform
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.modules
class TestModuleAccessControl:
"""Tests for module-based access control on API endpoints."""
# ========================================================================
# Billing Module Access Tests
# ========================================================================
def test_billing_accessible_when_enabled(
self, client, auth_headers, test_vendor, db
):
"""Test billing endpoints accessible when module enabled."""
# Ensure billing module is enabled (default - no config means all enabled)
response = client.get(
"/api/v1/vendor/billing/subscription",
headers=auth_headers,
)
# Should succeed (200) or have other error, but NOT 403 for module
assert response.status_code != 403 or "module" not in response.json().get("message", "").lower()
def test_billing_forbidden_when_disabled(
self, client, auth_headers, test_vendor, db, test_platform
):
"""Test billing endpoints return 403 when module disabled."""
# Disable billing module
test_platform.settings = {"enabled_modules": ["core", "platform-admin", "inventory"]}
db.commit()
response = client.get(
"/api/v1/vendor/billing/subscription",
headers=auth_headers,
)
# Should return 403 with module disabled message
assert response.status_code == 403
data = response.json()
assert "module" in data.get("message", "").lower() or data.get("error_code") == "MODULE_DISABLED"
# ========================================================================
# Inventory Module Access Tests
# ========================================================================
def test_inventory_accessible_when_enabled(
self, client, auth_headers, test_inventory
):
"""Test inventory endpoints accessible when module enabled."""
response = client.get(
"/api/v1/vendor/inventory",
headers=auth_headers,
)
# Should succeed
assert response.status_code == 200
def test_inventory_forbidden_when_disabled(
self, client, auth_headers, db, test_platform
):
"""Test inventory endpoints return 403 when module disabled."""
# Disable inventory module
test_platform.settings = {"enabled_modules": ["core", "platform-admin", "billing"]}
db.commit()
response = client.get(
"/api/v1/vendor/inventory",
headers=auth_headers,
)
# Should return 403
assert response.status_code == 403
data = response.json()
assert "module" in data.get("message", "").lower() or data.get("error_code") == "MODULE_DISABLED"
# ========================================================================
# Orders Module Access Tests
# ========================================================================
def test_orders_accessible_when_enabled(
self, client, auth_headers, test_order
):
"""Test orders endpoints accessible when module enabled."""
response = client.get(
"/api/v1/vendor/orders",
headers=auth_headers,
)
# Should succeed
assert response.status_code == 200
def test_orders_forbidden_when_disabled(
self, client, auth_headers, db, test_platform
):
"""Test orders endpoints return 403 when module disabled."""
# Disable orders module
test_platform.settings = {"enabled_modules": ["core", "platform-admin"]}
db.commit()
response = client.get(
"/api/v1/vendor/orders",
headers=auth_headers,
)
# Should return 403
assert response.status_code == 403
# ========================================================================
# Marketplace Module Access Tests
# ========================================================================
def test_marketplace_accessible_when_enabled(
self, client, auth_headers
):
"""Test marketplace endpoints accessible when module enabled."""
response = client.get(
"/api/v1/vendor/marketplace/settings",
headers=auth_headers,
)
# Should not return 403 for module disabled
# (might be 404 if no settings exist, or 200)
assert response.status_code != 403 or "module" not in response.json().get("message", "").lower()
def test_marketplace_forbidden_when_disabled(
self, client, auth_headers, db, test_platform
):
"""Test marketplace endpoints return 403 when module disabled."""
# Disable marketplace module but keep inventory (its dependency)
test_platform.settings = {"enabled_modules": ["core", "platform-admin", "inventory"]}
db.commit()
response = client.get(
"/api/v1/vendor/marketplace/settings",
headers=auth_headers,
)
# Should return 403
assert response.status_code == 403
# ========================================================================
# Core Module Tests
# ========================================================================
def test_core_always_accessible(
self, client, auth_headers, db, test_platform
):
"""Test core endpoints always accessible even with empty modules."""
# Set empty module list (but core is always added)
test_platform.settings = {"enabled_modules": []}
db.commit()
# Dashboard is a core endpoint
response = client.get(
"/api/v1/vendor/dashboard",
headers=auth_headers,
)
# Should NOT return 403 for module disabled
assert response.status_code != 403 or "module" not in response.json().get("message", "").lower()
# ========================================================================
# Admin Module Access Tests
# ========================================================================
def test_admin_inventory_accessible_when_enabled(
self, client, admin_headers, test_inventory
):
"""Test admin inventory endpoints accessible when module enabled."""
response = client.get(
"/api/v1/admin/inventory",
headers=admin_headers,
)
# Should succeed
assert response.status_code == 200
def test_admin_inventory_forbidden_when_disabled(
self, client, admin_headers, db, test_platform
):
"""Test admin inventory endpoints return 403 when module disabled."""
# Disable inventory module
test_platform.settings = {"enabled_modules": ["core", "platform-admin"]}
db.commit()
response = client.get(
"/api/v1/admin/inventory",
headers=admin_headers,
)
# Should return 403
assert response.status_code == 403
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.modules
class TestModuleDependencyAccess:
"""Tests for module dependency enforcement in access control."""
def test_marketplace_requires_inventory(
self, client, auth_headers, db, test_platform
):
"""Test marketplace requires inventory to be enabled."""
# Enable marketplace but disable inventory
test_platform.settings = {"enabled_modules": ["marketplace"]}
db.commit()
# Due to dependency resolution, inventory should be auto-enabled
response = client.get(
"/api/v1/vendor/inventory",
headers=auth_headers,
)
# Should be accessible because marketplace depends on inventory
# The module service should auto-enable inventory
assert response.status_code != 403 or "module" not in response.json().get("message", "").lower()
def test_disabling_dependency_disables_dependent(
self, client, auth_headers, db, test_platform
):
"""Test that disabling a dependency also affects dependent modules."""
# First enable both
test_platform.settings = {"enabled_modules": ["inventory", "marketplace"]}
db.commit()
# Now disable inventory - marketplace should also be affected
test_platform.settings = {"enabled_modules": []} # Only core remains
db.commit()
# Marketplace should be disabled
response = client.get(
"/api/v1/vendor/marketplace/settings",
headers=auth_headers,
)
assert response.status_code == 403