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:
253
tests/integration/api/v1/modules/test_module_access.py
Normal file
253
tests/integration/api/v1/modules/test_module_access.py
Normal 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
|
||||
Reference in New Issue
Block a user