Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
- Add @role_required to 2 missing routes (krs_api PDF download, zopk milestones) - Add role-based menu visibility in admin bar (hide Users, Security, Benefits, Model Comparison, Debug from OFFICE_MANAGER users) - Inject SystemRole into Jinja2 context processor for template role checks - Replace is_admin checkbox with role select dropdown in user creation form - Migrate routes.py and routes_users_api.py from is_admin to SystemRole-based role assignment via set_role() - Add deprecation notice to is_admin database column - Add 23 RBAC unit tests (hierarchy, has_role, set_role, permissions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
8.3 KiB
Python
227 lines
8.3 KiB
Python
"""
|
|
RBAC Unit Tests
|
|
===============
|
|
|
|
Tests for the Role-Based Access Control system:
|
|
- SystemRole hierarchy
|
|
- has_role() method
|
|
- set_role() with is_admin sync
|
|
- Permission helper methods (can_access_admin_panel, can_manage_users, etc.)
|
|
- role_required decorator
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
# Add project root to path
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
|
|
from database import SystemRole, CompanyRole, User
|
|
|
|
|
|
# ============================================================
|
|
# SystemRole Hierarchy Tests
|
|
# ============================================================
|
|
|
|
class TestSystemRoleHierarchy:
|
|
"""Test that SystemRole enum values enforce correct hierarchy."""
|
|
|
|
def test_role_ordering(self):
|
|
assert SystemRole.UNAFFILIATED < SystemRole.MEMBER
|
|
assert SystemRole.MEMBER < SystemRole.EMPLOYEE
|
|
assert SystemRole.EMPLOYEE < SystemRole.MANAGER
|
|
assert SystemRole.MANAGER < SystemRole.OFFICE_MANAGER
|
|
assert SystemRole.OFFICE_MANAGER < SystemRole.ADMIN
|
|
|
|
def test_role_values(self):
|
|
assert SystemRole.UNAFFILIATED == 10
|
|
assert SystemRole.MEMBER == 20
|
|
assert SystemRole.EMPLOYEE == 30
|
|
assert SystemRole.MANAGER == 40
|
|
assert SystemRole.OFFICE_MANAGER == 50
|
|
assert SystemRole.ADMIN == 100
|
|
|
|
def test_from_string_valid(self):
|
|
assert SystemRole.from_string('ADMIN') == SystemRole.ADMIN
|
|
assert SystemRole.from_string('OFFICE_MANAGER') == SystemRole.OFFICE_MANAGER
|
|
assert SystemRole.from_string('MEMBER') == SystemRole.MEMBER
|
|
|
|
def test_from_string_case_insensitive(self):
|
|
assert SystemRole.from_string('admin') == SystemRole.ADMIN
|
|
assert SystemRole.from_string('member') == SystemRole.MEMBER
|
|
|
|
def test_from_string_invalid_defaults_to_unaffiliated(self):
|
|
assert SystemRole.from_string('INVALID') == SystemRole.UNAFFILIATED
|
|
assert SystemRole.from_string('') == SystemRole.UNAFFILIATED
|
|
|
|
def test_enum_by_name(self):
|
|
assert SystemRole['ADMIN'] == SystemRole.ADMIN
|
|
assert SystemRole['MEMBER'] == SystemRole.MEMBER
|
|
with pytest.raises(KeyError):
|
|
SystemRole['INVALID']
|
|
|
|
|
|
# ============================================================
|
|
# User.has_role() Tests
|
|
# ============================================================
|
|
|
|
def _make_user(role_name='MEMBER', is_admin=False):
|
|
"""Create a fake user object with User methods for testing RBAC logic.
|
|
|
|
Cannot use User() directly since SA instrumented attributes require
|
|
a mapped session. Instead, use a plain object with User's methods bound.
|
|
"""
|
|
class FakeUser:
|
|
pass
|
|
|
|
user = FakeUser()
|
|
user.role = role_name
|
|
user.is_admin = is_admin
|
|
user.company_role = 'NONE'
|
|
user.company_id = None
|
|
|
|
# Bind User's property and methods
|
|
user.system_role = User.system_role.fget(user)
|
|
user.has_role = lambda required_role: User.has_role(user, required_role)
|
|
user.can_access_admin_panel = lambda: User.can_access_admin_panel(user)
|
|
user.can_manage_users = lambda: User.can_manage_users(user)
|
|
user.can_moderate_forum = lambda: User.can_moderate_forum(user)
|
|
return user
|
|
|
|
|
|
class TestHasRole:
|
|
"""Test User.has_role() hierarchical check."""
|
|
|
|
def test_admin_has_all_roles(self):
|
|
user = _make_user('ADMIN')
|
|
assert user.has_role(SystemRole.ADMIN)
|
|
assert user.has_role(SystemRole.OFFICE_MANAGER)
|
|
assert user.has_role(SystemRole.MANAGER)
|
|
assert user.has_role(SystemRole.EMPLOYEE)
|
|
assert user.has_role(SystemRole.MEMBER)
|
|
assert user.has_role(SystemRole.UNAFFILIATED)
|
|
|
|
def test_office_manager_cannot_access_admin(self):
|
|
user = _make_user('OFFICE_MANAGER')
|
|
assert not user.has_role(SystemRole.ADMIN)
|
|
assert user.has_role(SystemRole.OFFICE_MANAGER)
|
|
assert user.has_role(SystemRole.MANAGER)
|
|
assert user.has_role(SystemRole.MEMBER)
|
|
|
|
def test_member_minimal_access(self):
|
|
user = _make_user('MEMBER')
|
|
assert not user.has_role(SystemRole.ADMIN)
|
|
assert not user.has_role(SystemRole.OFFICE_MANAGER)
|
|
assert not user.has_role(SystemRole.MANAGER)
|
|
assert not user.has_role(SystemRole.EMPLOYEE)
|
|
assert user.has_role(SystemRole.MEMBER)
|
|
assert user.has_role(SystemRole.UNAFFILIATED)
|
|
|
|
def test_unaffiliated_lowest_access(self):
|
|
user = _make_user('UNAFFILIATED')
|
|
assert not user.has_role(SystemRole.MEMBER)
|
|
assert user.has_role(SystemRole.UNAFFILIATED)
|
|
|
|
def test_none_role_defaults_to_unaffiliated(self):
|
|
user = _make_user(None)
|
|
assert user.has_role(SystemRole.UNAFFILIATED)
|
|
assert not user.has_role(SystemRole.MEMBER)
|
|
|
|
|
|
# ============================================================
|
|
# User.set_role() Tests
|
|
# ============================================================
|
|
|
|
class TestSetRole:
|
|
"""Test User.set_role() with is_admin sync.
|
|
|
|
Uses a simple namespace object to test set_role logic directly,
|
|
since SQLAlchemy instrumented attributes don't work outside a session.
|
|
"""
|
|
|
|
def _make_settable_user(self, role_name='MEMBER', is_admin=False):
|
|
"""Create an object that can receive set_role() assignments."""
|
|
class FakeUser:
|
|
pass
|
|
user = FakeUser()
|
|
user.role = role_name
|
|
user.is_admin = is_admin
|
|
# Bind set_role method from User class
|
|
user.set_role = lambda new_role, sync_is_admin=True: User.set_role(user, new_role, sync_is_admin)
|
|
return user
|
|
|
|
def test_set_admin_syncs_is_admin_true(self):
|
|
user = self._make_settable_user('MEMBER', is_admin=False)
|
|
user.set_role(SystemRole.ADMIN)
|
|
assert user.role == 'ADMIN'
|
|
assert user.is_admin is True
|
|
|
|
def test_set_member_syncs_is_admin_false(self):
|
|
user = self._make_settable_user('ADMIN', is_admin=True)
|
|
user.set_role(SystemRole.MEMBER)
|
|
assert user.role == 'MEMBER'
|
|
assert user.is_admin is False
|
|
|
|
def test_set_office_manager_is_admin_false(self):
|
|
user = self._make_settable_user('ADMIN', is_admin=True)
|
|
user.set_role(SystemRole.OFFICE_MANAGER)
|
|
assert user.role == 'OFFICE_MANAGER'
|
|
assert user.is_admin is False
|
|
|
|
def test_set_role_without_sync(self):
|
|
user = self._make_settable_user('MEMBER', is_admin=False)
|
|
user.set_role(SystemRole.ADMIN, sync_is_admin=False)
|
|
assert user.role == 'ADMIN'
|
|
assert user.is_admin is False # Not synced
|
|
|
|
|
|
# ============================================================
|
|
# Permission Helper Methods Tests
|
|
# ============================================================
|
|
|
|
class TestPermissionHelpers:
|
|
"""Test can_access_admin_panel, can_manage_users, can_moderate_forum."""
|
|
|
|
def test_admin_can_access_admin_panel(self):
|
|
assert _make_user('ADMIN').can_access_admin_panel()
|
|
|
|
def test_office_manager_can_access_admin_panel(self):
|
|
assert _make_user('OFFICE_MANAGER').can_access_admin_panel()
|
|
|
|
def test_manager_cannot_access_admin_panel(self):
|
|
assert not _make_user('MANAGER').can_access_admin_panel()
|
|
|
|
def test_member_cannot_access_admin_panel(self):
|
|
assert not _make_user('MEMBER').can_access_admin_panel()
|
|
|
|
def test_only_admin_can_manage_users(self):
|
|
assert _make_user('ADMIN').can_manage_users()
|
|
assert not _make_user('OFFICE_MANAGER').can_manage_users()
|
|
assert not _make_user('MANAGER').can_manage_users()
|
|
|
|
def test_office_manager_can_moderate_forum(self):
|
|
assert _make_user('ADMIN').can_moderate_forum()
|
|
assert _make_user('OFFICE_MANAGER').can_moderate_forum()
|
|
assert not _make_user('MANAGER').can_moderate_forum()
|
|
|
|
|
|
# ============================================================
|
|
# CompanyRole Tests
|
|
# ============================================================
|
|
|
|
class TestCompanyRole:
|
|
"""Test CompanyRole enum and hierarchy."""
|
|
|
|
def test_role_ordering(self):
|
|
assert CompanyRole.NONE < CompanyRole.VIEWER
|
|
assert CompanyRole.VIEWER < CompanyRole.EMPLOYEE
|
|
assert CompanyRole.EMPLOYEE < CompanyRole.MANAGER
|
|
|
|
def test_from_string(self):
|
|
assert CompanyRole.from_string('MANAGER') == CompanyRole.MANAGER
|
|
assert CompanyRole.from_string('EMPLOYEE') == CompanyRole.EMPLOYEE
|
|
assert CompanyRole.from_string('INVALID') == CompanyRole.NONE
|