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>
254 lines
8.8 KiB
Python
254 lines
8.8 KiB
Python
# 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
|