188 lines
7.0 KiB
Python
188 lines
7.0 KiB
Python
"""Tests for role-based access control."""
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from app.domain.exceptions import ForbiddenException
|
|
from app.domain.roles import (
|
|
ROLE_PERMISSIONS,
|
|
Permission,
|
|
Role,
|
|
get_effective_role,
|
|
has_permission,
|
|
require_permission,
|
|
)
|
|
|
|
|
|
class TestRole:
|
|
"""Test Role enum."""
|
|
|
|
def test_role_values(self) -> None:
|
|
"""Test role enum values."""
|
|
assert Role.ADMIN.value == "admin"
|
|
assert Role.USER.value == "user"
|
|
assert Role.GUEST.value == "guest"
|
|
|
|
def test_role_comparison(self) -> None:
|
|
"""Test role comparison."""
|
|
assert Role.ADMIN == Role.ADMIN
|
|
# USER and ADMIN are different enum values with different string values
|
|
assert Role.USER.value != Role.ADMIN.value # type: ignore[comparison-overlap]
|
|
|
|
|
|
class TestPermissions:
|
|
"""Test permission definitions."""
|
|
|
|
def test_permission_values(self) -> None:
|
|
"""Test permission constants."""
|
|
assert Permission.POST_CREATE == "post:create"
|
|
assert Permission.POST_READ == "post:read"
|
|
assert Permission.POST_READ_UNPUBLISHED == "post:read_unpublished"
|
|
assert Permission.POST_UPDATE == "post:update"
|
|
assert Permission.POST_DELETE == "post:delete"
|
|
assert Permission.POST_PUBLISH == "post:publish"
|
|
|
|
|
|
class TestRolePermissions:
|
|
"""Test role-based permission mapping."""
|
|
|
|
def test_admin_has_all_permissions(self) -> None:
|
|
"""Test admin has all permissions."""
|
|
admin_perms = ROLE_PERMISSIONS[Role.ADMIN]
|
|
assert Permission.POST_CREATE in admin_perms
|
|
assert Permission.POST_READ in admin_perms
|
|
assert Permission.POST_READ_UNPUBLISHED in admin_perms
|
|
assert Permission.POST_UPDATE in admin_perms
|
|
assert Permission.POST_DELETE in admin_perms
|
|
assert Permission.POST_PUBLISH in admin_perms
|
|
|
|
def test_user_permissions(self) -> None:
|
|
"""Test user permissions."""
|
|
user_perms = ROLE_PERMISSIONS[Role.USER]
|
|
assert Permission.POST_CREATE in user_perms
|
|
assert Permission.POST_READ in user_perms
|
|
assert Permission.POST_UPDATE in user_perms
|
|
assert Permission.POST_DELETE in user_perms
|
|
assert Permission.POST_PUBLISH in user_perms
|
|
# User cannot read unpublished
|
|
assert Permission.POST_READ_UNPUBLISHED not in user_perms
|
|
|
|
def test_guest_permissions(self) -> None:
|
|
"""Test guest permissions."""
|
|
guest_perms = ROLE_PERMISSIONS[Role.GUEST]
|
|
assert Permission.POST_READ in guest_perms
|
|
# Guest has very limited permissions
|
|
assert Permission.POST_CREATE not in guest_perms
|
|
assert Permission.POST_UPDATE not in guest_perms
|
|
assert Permission.POST_DELETE not in guest_perms
|
|
assert Permission.POST_READ_UNPUBLISHED not in guest_perms
|
|
|
|
|
|
class TestHasPermission:
|
|
"""Test has_permission function."""
|
|
|
|
def test_admin_has_all_permissions_check(self) -> None:
|
|
"""Test admin permission checks."""
|
|
assert has_permission(Role.ADMIN, Permission.POST_CREATE) is True
|
|
assert has_permission(Role.ADMIN, Permission.POST_READ_UNPUBLISHED) is True
|
|
assert has_permission(Role.ADMIN, "unknown:permission") is False
|
|
|
|
def test_user_limited_permissions(self) -> None:
|
|
"""Test user limited permissions."""
|
|
assert has_permission(Role.USER, Permission.POST_CREATE) is True
|
|
assert has_permission(Role.USER, Permission.POST_READ_UNPUBLISHED) is False
|
|
assert has_permission(Role.USER, Permission.POST_READ) is True
|
|
|
|
def test_guest_read_only(self) -> None:
|
|
"""Test guest read-only access."""
|
|
assert has_permission(Role.GUEST, Permission.POST_READ) is True
|
|
assert has_permission(Role.GUEST, Permission.POST_CREATE) is False
|
|
assert has_permission(Role.GUEST, Permission.POST_UPDATE) is False
|
|
|
|
|
|
class TestGetEffectiveRole:
|
|
"""Test get_effective_role function."""
|
|
|
|
def test_admin_from_roles_list(self) -> None:
|
|
"""Test admin role detection."""
|
|
assert get_effective_role(["admin"]) == Role.ADMIN
|
|
assert get_effective_role(["user", "admin"]) == Role.ADMIN
|
|
assert get_effective_role(["admin", "user"]) == Role.ADMIN
|
|
|
|
def test_user_from_roles_list(self) -> None:
|
|
"""Test user role detection."""
|
|
assert get_effective_role(["user"]) == Role.USER
|
|
assert get_effective_role(["user", "moderator"]) == Role.USER
|
|
|
|
def test_guest_from_roles_list(self) -> None:
|
|
"""Test guest role detection."""
|
|
assert get_effective_role([]) == Role.GUEST
|
|
assert get_effective_role(["unknown"]) == Role.GUEST
|
|
assert get_effective_role(["guest"]) == Role.GUEST
|
|
|
|
def test_role_priority(self) -> None:
|
|
"""Test that admin > user > guest."""
|
|
# Admin takes precedence
|
|
assert get_effective_role(["user", "admin", "guest"]) == Role.ADMIN
|
|
# User takes precedence over guest
|
|
assert get_effective_role(["guest", "user"]) == Role.USER
|
|
|
|
|
|
class TestRequirePermission:
|
|
"""Test require_permission decorator."""
|
|
|
|
async def test_admin_passes_permission_check(self) -> None:
|
|
"""Test admin with permission succeeds."""
|
|
|
|
@require_permission(Permission.POST_CREATE)
|
|
async def dummy(*, token_info: Any = None) -> str:
|
|
return "ok"
|
|
|
|
token = type("TokenInfo", (), {"roles": ["admin"]})()
|
|
result = await dummy(token_info=token)
|
|
assert result == "ok"
|
|
|
|
async def test_user_passes_permission_check(self) -> None:
|
|
"""Test user with permission succeeds."""
|
|
|
|
@require_permission(Permission.POST_READ)
|
|
async def dummy(*, token_info: Any = None) -> str:
|
|
return "ok"
|
|
|
|
token = type("TokenInfo", (), {"roles": ["user"]})()
|
|
result = await dummy(token_info=token)
|
|
assert result == "ok"
|
|
|
|
async def test_no_token_raises(self) -> None:
|
|
"""Test missing token raises ForbiddenException."""
|
|
|
|
@require_permission(Permission.POST_CREATE)
|
|
async def dummy(*, token_info: Any = None) -> str:
|
|
return "ok"
|
|
|
|
with pytest.raises(ForbiddenException, match="Authentication required"):
|
|
await dummy()
|
|
|
|
async def test_guest_without_permission_raises(self) -> None:
|
|
"""Test guest lacking permission raises ForbiddenException."""
|
|
|
|
@require_permission(Permission.POST_CREATE)
|
|
async def dummy(*, token_info: Any = None) -> str:
|
|
return "ok"
|
|
|
|
token = type("TokenInfo", (), {"roles": []})()
|
|
with pytest.raises(ForbiddenException, match="Permission 'post:create' required"):
|
|
await dummy(token_info=token)
|
|
|
|
async def test_user_forbidden_unpublished(self) -> None:
|
|
"""Test user cannot read unpublished via decorator."""
|
|
|
|
@require_permission(Permission.POST_READ_UNPUBLISHED)
|
|
async def dummy(*, token_info: Any = None) -> str:
|
|
return "ok"
|
|
|
|
token = type("TokenInfo", (), {"roles": ["user"]})()
|
|
with pytest.raises(ForbiddenException, match="Permission 'post:read_unpublished' required"):
|
|
await dummy(token_info=token)
|