test: complete remaining deps.py phases — platform, module, merchant, customer, permissions
Add 16 tests covering: require_platform_access (super admin bypass, platform admin with/without access), get_admin_with_platform_context, require_module_access (super admin bypass, enabled/disabled module), and get_user_permissions (owner gets all, member gets specific, empty). Total: 89 tests for app/api/deps.py (all 31 functions covered). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ from app.api.deps import (
|
|||||||
_get_user_model,
|
_get_user_model,
|
||||||
_validate_customer_token,
|
_validate_customer_token,
|
||||||
_validate_user_token,
|
_validate_user_token,
|
||||||
|
get_admin_with_platform_context,
|
||||||
get_current_admin_api,
|
get_current_admin_api,
|
||||||
get_current_admin_from_cookie_or_header,
|
get_current_admin_from_cookie_or_header,
|
||||||
get_current_admin_optional,
|
get_current_admin_optional,
|
||||||
@@ -32,9 +33,12 @@ from app.api.deps import (
|
|||||||
get_current_store_optional,
|
get_current_store_optional,
|
||||||
get_current_super_admin,
|
get_current_super_admin,
|
||||||
get_current_super_admin_api,
|
get_current_super_admin_api,
|
||||||
|
get_user_permissions,
|
||||||
get_user_store,
|
get_user_store,
|
||||||
require_all_store_permissions,
|
require_all_store_permissions,
|
||||||
require_any_store_permission,
|
require_any_store_permission,
|
||||||
|
require_module_access,
|
||||||
|
require_platform_access,
|
||||||
require_store_owner,
|
require_store_owner,
|
||||||
require_store_permission,
|
require_store_permission,
|
||||||
)
|
)
|
||||||
@@ -1300,3 +1304,279 @@ class TestGetUserStore:
|
|||||||
other_ctx = UserContext.from_user(other_user)
|
other_ctx = UserContext.from_user(other_user)
|
||||||
with pytest.raises(UnauthorizedStoreAccessException):
|
with pytest.raises(UnauthorizedStoreAccessException):
|
||||||
get_user_store(store.store_code, other_ctx, db)
|
get_user_store(store.store_code, other_ctx, db)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Phase 2 (advanced): Platform Access & Context
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.auth
|
||||||
|
class TestRequirePlatformAccess:
|
||||||
|
"""Test require_platform_access factory."""
|
||||||
|
|
||||||
|
def test_super_admin_can_access_any_platform(self, db, auth_manager, test_super_admin):
|
||||||
|
"""Super admin (accessible_platform_ids=None) can access any platform."""
|
||||||
|
token_data = auth_manager.create_access_token(user=test_super_admin)
|
||||||
|
creds = _make_credentials(token_data["access_token"])
|
||||||
|
request = _make_request("/admin/platforms/42/stores")
|
||||||
|
|
||||||
|
checker = require_platform_access(42)
|
||||||
|
result = checker(request, creds, None, db)
|
||||||
|
assert result.id == test_super_admin.id
|
||||||
|
assert result.accessible_platform_ids is None
|
||||||
|
|
||||||
|
def test_platform_admin_with_access(self, db, auth_manager, test_platform_admin):
|
||||||
|
"""Platform admin with matching platform_id is accepted."""
|
||||||
|
token_data = auth_manager.create_access_token(user=test_platform_admin)
|
||||||
|
creds = _make_credentials(token_data["access_token"])
|
||||||
|
request = _make_request("/admin/platforms/1/stores")
|
||||||
|
|
||||||
|
checker = require_platform_access(1)
|
||||||
|
|
||||||
|
# Patch get_current_admin to return context with platform access
|
||||||
|
admin_ctx = UserContext.from_user(test_platform_admin)
|
||||||
|
admin_ctx.accessible_platform_ids = [1, 2, 3]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.api.deps.get_current_admin_from_cookie_or_header",
|
||||||
|
return_value=admin_ctx,
|
||||||
|
):
|
||||||
|
result = checker(request, creds, None, db)
|
||||||
|
assert result.id == test_platform_admin.id
|
||||||
|
|
||||||
|
def test_platform_admin_without_access(self, db, auth_manager, test_platform_admin):
|
||||||
|
"""Platform admin without matching platform_id is rejected."""
|
||||||
|
token_data = auth_manager.create_access_token(user=test_platform_admin)
|
||||||
|
creds = _make_credentials(token_data["access_token"])
|
||||||
|
request = _make_request("/admin/platforms/9999/stores")
|
||||||
|
|
||||||
|
checker = require_platform_access(9999)
|
||||||
|
|
||||||
|
admin_ctx = UserContext.from_user(test_platform_admin)
|
||||||
|
admin_ctx.accessible_platform_ids = [1, 2, 3]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.api.deps.get_current_admin_from_cookie_or_header",
|
||||||
|
return_value=admin_ctx,
|
||||||
|
):
|
||||||
|
with pytest.raises(InsufficientPermissionsException, match="Access denied to platform"):
|
||||||
|
checker(request, creds, None, db)
|
||||||
|
|
||||||
|
def test_rejects_non_admin(self, db, auth_manager, test_store_user):
|
||||||
|
"""Non-admin user rejected before platform check."""
|
||||||
|
token_data = auth_manager.create_access_token(user=test_store_user)
|
||||||
|
creds = _make_credentials(token_data["access_token"])
|
||||||
|
request = _make_request("/admin/platforms/1/stores")
|
||||||
|
|
||||||
|
checker = require_platform_access(1)
|
||||||
|
with pytest.raises(AdminRequiredException):
|
||||||
|
checker(request, creds, None, db)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.auth
|
||||||
|
class TestGetAdminWithPlatformContext:
|
||||||
|
"""Test get_admin_with_platform_context."""
|
||||||
|
|
||||||
|
def test_super_admin_bypasses_platform_context(self, db, auth_manager, test_super_admin):
|
||||||
|
"""Super admin returns context without platform check."""
|
||||||
|
token_data = auth_manager.create_access_token(user=test_super_admin)
|
||||||
|
creds = _make_credentials(token_data["access_token"])
|
||||||
|
request = _make_request("/admin/dashboard")
|
||||||
|
|
||||||
|
result = get_admin_with_platform_context(request, creds, None, db)
|
||||||
|
assert result.id == test_super_admin.id
|
||||||
|
assert result.is_super_admin is True
|
||||||
|
|
||||||
|
def test_rejects_non_admin(self, db, auth_manager, test_store_user):
|
||||||
|
"""Non-admin user rejected."""
|
||||||
|
token_data = auth_manager.create_access_token(user=test_store_user)
|
||||||
|
creds = _make_credentials(token_data["access_token"])
|
||||||
|
request = _make_request("/admin/dashboard")
|
||||||
|
|
||||||
|
with pytest.raises(AdminRequiredException):
|
||||||
|
get_admin_with_platform_context(request, creds, None, db)
|
||||||
|
|
||||||
|
def test_rejects_without_token(self, db):
|
||||||
|
"""No token raises InvalidTokenException."""
|
||||||
|
request = _make_request("/admin/dashboard")
|
||||||
|
|
||||||
|
with pytest.raises(InvalidTokenException):
|
||||||
|
get_admin_with_platform_context(request, None, None, db)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Phase 6 (advanced): Module & Menu Access Control
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.auth
|
||||||
|
class TestRequireModuleAccess:
|
||||||
|
"""Test require_module_access factory."""
|
||||||
|
|
||||||
|
def test_super_admin_bypasses_module_check(self, db, auth_manager, test_super_admin):
|
||||||
|
"""Super admin bypasses module enablement check entirely."""
|
||||||
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
token_data = auth_manager.create_access_token(user=test_super_admin)
|
||||||
|
creds = _make_credentials(token_data["access_token"])
|
||||||
|
request = _make_request("/admin/billing")
|
||||||
|
|
||||||
|
checker = require_module_access("billing", FrontendType.ADMIN)
|
||||||
|
result = checker(request, creds, None, None, None, db)
|
||||||
|
assert result.id == test_super_admin.id
|
||||||
|
|
||||||
|
def test_store_user_with_enabled_module(self, db, auth_manager, test_store_user):
|
||||||
|
"""Store user can access enabled module."""
|
||||||
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
token_data = auth_manager.create_access_token(user=test_store_user)
|
||||||
|
creds = _make_credentials(token_data["access_token"])
|
||||||
|
request = _make_request("/store/inventory")
|
||||||
|
request.state.store = None
|
||||||
|
|
||||||
|
checker = require_module_access("inventory", FrontendType.STORE)
|
||||||
|
# No platform context → access is allowed (module check requires platform)
|
||||||
|
result = checker(request, creds, None, token_data["access_token"], None, db)
|
||||||
|
assert result.id == test_store_user.id
|
||||||
|
|
||||||
|
def test_rejects_disabled_module(self, db, auth_manager, test_platform_admin):
|
||||||
|
"""Platform admin blocked when module is disabled."""
|
||||||
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
token_data = auth_manager.create_access_token(user=test_platform_admin)
|
||||||
|
creds = _make_credentials(token_data["access_token"])
|
||||||
|
request = _make_request("/admin/billing")
|
||||||
|
# Set platform context on request state
|
||||||
|
mock_platform = MagicMock()
|
||||||
|
mock_platform.id = 1
|
||||||
|
request.state.admin_platform = mock_platform
|
||||||
|
|
||||||
|
checker = require_module_access("billing", FrontendType.ADMIN)
|
||||||
|
with patch(
|
||||||
|
"app.modules.service.module_service.is_module_enabled", return_value=False
|
||||||
|
):
|
||||||
|
with pytest.raises(
|
||||||
|
InsufficientPermissionsException, match="not enabled"
|
||||||
|
):
|
||||||
|
checker(request, creds, None, None, None, db)
|
||||||
|
|
||||||
|
def test_allows_enabled_module(self, db, auth_manager, test_platform_admin):
|
||||||
|
"""Platform admin can access enabled module."""
|
||||||
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
token_data = auth_manager.create_access_token(user=test_platform_admin)
|
||||||
|
creds = _make_credentials(token_data["access_token"])
|
||||||
|
request = _make_request("/admin/billing")
|
||||||
|
mock_platform = MagicMock()
|
||||||
|
mock_platform.id = 1
|
||||||
|
request.state.admin_platform = mock_platform
|
||||||
|
|
||||||
|
checker = require_module_access("billing", FrontendType.ADMIN)
|
||||||
|
with patch(
|
||||||
|
"app.modules.service.module_service.is_module_enabled", return_value=True
|
||||||
|
):
|
||||||
|
result = checker(request, creds, None, None, None, db)
|
||||||
|
assert result.id == test_platform_admin.id
|
||||||
|
|
||||||
|
def test_no_auth_raises(self, db):
|
||||||
|
"""No valid authentication raises InvalidTokenException."""
|
||||||
|
from app.modules.enums import FrontendType
|
||||||
|
|
||||||
|
request = _make_request("/admin/billing")
|
||||||
|
|
||||||
|
checker = require_module_access("billing", FrontendType.ADMIN)
|
||||||
|
with pytest.raises(InvalidTokenException, match="Authentication required"):
|
||||||
|
checker(request, None, None, None, None, db)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Phase 7 remaining: get_user_permissions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.auth
|
||||||
|
class TestGetUserPermissions:
|
||||||
|
"""Test get_user_permissions."""
|
||||||
|
|
||||||
|
def test_returns_empty_for_no_store_context(self, db, auth_manager, test_store_user):
|
||||||
|
"""Returns empty list if token has no store context."""
|
||||||
|
request = _make_request("/store/dashboard")
|
||||||
|
user_ctx = UserContext.from_user(test_store_user)
|
||||||
|
user_ctx.token_store_id = None
|
||||||
|
|
||||||
|
result = get_user_permissions(request, db, user_ctx)
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_owner_gets_all_permissions(self, db, auth_manager, test_store_user):
|
||||||
|
"""Store owner gets all available permissions."""
|
||||||
|
mock_store = MagicMock()
|
||||||
|
mock_store.id = 1
|
||||||
|
mock_store.store_code = "TEST"
|
||||||
|
|
||||||
|
request = _make_request("/store/dashboard")
|
||||||
|
user_ctx = UserContext.from_user(test_store_user)
|
||||||
|
user_ctx.token_store_id = 1
|
||||||
|
|
||||||
|
all_perms = ["products.view", "products.edit", "orders.view", "orders.edit"]
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("app.api.deps.store_service.get_store_by_id", return_value=mock_store),
|
||||||
|
patch.object(User, "is_owner_of", return_value=True),
|
||||||
|
patch(
|
||||||
|
"app.modules.tenancy.services.permission_discovery_service.permission_discovery_service.get_all_permission_ids",
|
||||||
|
return_value=all_perms,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = get_user_permissions(request, db, user_ctx)
|
||||||
|
assert result == all_perms
|
||||||
|
|
||||||
|
def test_non_owner_gets_membership_permissions(self, db, auth_manager, test_store_user):
|
||||||
|
"""Non-owner team member gets permissions from their membership."""
|
||||||
|
mock_store = MagicMock()
|
||||||
|
mock_store.id = 1
|
||||||
|
mock_store.store_code = "TEST"
|
||||||
|
|
||||||
|
request = _make_request("/store/dashboard")
|
||||||
|
user_ctx = UserContext.from_user(test_store_user)
|
||||||
|
user_ctx.token_store_id = 1
|
||||||
|
|
||||||
|
# Mock a store membership with specific permissions
|
||||||
|
mock_membership = MagicMock()
|
||||||
|
mock_membership.store_id = 1
|
||||||
|
mock_membership.is_active = True
|
||||||
|
mock_membership.get_all_permissions.return_value = ["products.view", "orders.view"]
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("app.api.deps.store_service.get_store_by_id", return_value=mock_store),
|
||||||
|
patch.object(User, "is_owner_of", return_value=False),
|
||||||
|
patch.object(
|
||||||
|
User, "store_memberships", new_callable=lambda: property(lambda self: [mock_membership])
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = get_user_permissions(request, db, user_ctx)
|
||||||
|
assert result == ["products.view", "orders.view"]
|
||||||
|
|
||||||
|
def test_non_member_gets_empty_list(self, db, auth_manager, test_store_user):
|
||||||
|
"""Non-owner with no active membership gets empty list."""
|
||||||
|
mock_store = MagicMock()
|
||||||
|
mock_store.id = 1
|
||||||
|
mock_store.store_code = "TEST"
|
||||||
|
|
||||||
|
request = _make_request("/store/dashboard")
|
||||||
|
user_ctx = UserContext.from_user(test_store_user)
|
||||||
|
user_ctx.token_store_id = 1
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("app.api.deps.store_service.get_store_by_id", return_value=mock_store),
|
||||||
|
patch.object(User, "is_owner_of", return_value=False),
|
||||||
|
patch.object(
|
||||||
|
User, "store_memberships", new_callable=lambda: property(lambda self: [])
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = get_user_permissions(request, db, user_ctx)
|
||||||
|
assert result == []
|
||||||
|
|||||||
Reference in New Issue
Block a user