Files
blog.pyaqa.ru/tests/unit/domain/test_roles.py
Sergey Vanyushkin 184b95969c
Some checks failed
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/type Pipeline was successful
feat(auth): implement Keycloak authentication with RBAC and pagination
Major changes:
- Add Keycloak integration via token introspection endpoint
- Implement RBAC system with roles: admin, user, guest
- Add role-based permissions for post operations
- Add pagination support (default limit: 10) to list endpoints
- Add published_only filter with admin-only override for unpublished posts

Security improvements:
- Remove hardcoded default secrets (SECRET_KEY, KEYCLOAK_CLIENT_SECRET)
- Update .env.example with proper security placeholders
- Add comprehensive RBAC unit tests

Infrastructure:
- Add httpx dependency for HTTP client
- Add KeycloakAuthClient with token caching (TTL: 60s)
- Add role-based dependencies (RequireAdmin, RequireUser, etc.)
- Update DI container with Keycloak provider

Endpoints updated:
- GET /posts: filter by published status (admin can see all)
- Add pagination params (limit, offset) to list endpoints
- Enforce RBAC on post operations

Tests:
- Add 16 auth infrastructure tests
- Add 13 RBAC role tests
- Update existing tests for new required settings

Breaking changes:
- SECRET_KEY and KEYCLOAK_CLIENT_SECRET now required (no defaults)
2026-05-02 11:21:45 +03:00

124 lines
4.8 KiB
Python

"""Tests for role-based access control."""
from app.domain.roles import (
ROLE_PERMISSIONS,
Permission,
Role,
get_effective_role,
has_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