"""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)