Files
nas-tools/dashboard/backend/tests/unit/test_rbac.py
Gan, Jimmy 8431920d26
Run Tests / Backend Tests (pull_request) Has been cancelled
Run Tests / Frontend Tests (pull_request) Has been cancelled
Run Tests / Test Summary (pull_request) Has been cancelled
feat: add comprehensive unit and integration testing infrastructure
- Backend: pytest with unit tests (auth, rbac, config) and integration tests (auth flow, docker, files)
- Frontend: vitest with unit tests (api client) and component tests (login)
- CI: Gitea Actions workflow for automated testing with coverage reports
- Documentation: TESTING.md guide with setup, usage, and best practices
- Coverage goals: 80%+ backend, 70%+ frontend
2026-03-30 11:11:39 +08:00

317 lines
11 KiB
Python

"""
Unit tests for rbac.py - Role-based access control.
"""
import pytest
import json
import os
from rbac import (
load_rbac,
save_rbac,
update_rbac,
resolve_role,
get_pages,
get_sidebar_links,
get_sidebar_order,
DEFAULT_RBAC,
ROLE_GROUP_MAP,
)
class TestLoadRBAC:
"""Test loading RBAC configuration."""
def test_load_rbac_from_file(self, mock_config, temp_rbac_file):
"""Test loading RBAC from existing file."""
rbac = load_rbac()
assert "role_defaults" in rbac
assert "user_overrides" in rbac
assert "admin" in rbac["role_defaults"]
assert "member" in rbac["role_defaults"]
assert "viewer" in rbac["role_defaults"]
def test_load_rbac_missing_file(self, mock_config, temp_volume_root):
"""Test loading RBAC when file doesn't exist returns defaults."""
# Don't create rbac file
rbac = load_rbac()
assert rbac == DEFAULT_RBAC
def test_load_rbac_corrupted_file(self, mock_config, temp_volume_root):
"""Test loading RBAC with corrupted file returns defaults."""
rbac_file = os.path.join(temp_volume_root, "docker/nas-dashboard/rbac.json")
os.makedirs(os.path.dirname(rbac_file), exist_ok=True)
# Write invalid JSON
with open(rbac_file, "w") as f:
f.write("invalid json {{{")
rbac = load_rbac()
assert rbac == DEFAULT_RBAC
class TestSaveRBAC:
"""Test saving RBAC configuration."""
def test_save_rbac(self, mock_config, temp_volume_root):
"""Test saving RBAC configuration."""
rbac_file = os.path.join(temp_volume_root, "docker/nas-dashboard/rbac.json")
test_data = {
"role_defaults": {"admin": {"pages": "*"}},
"user_overrides": {"testuser": {"pages": ["dashboard"]}}
}
save_rbac(test_data)
assert os.path.exists(rbac_file)
with open(rbac_file) as f:
loaded = json.load(f)
assert loaded == test_data
def test_save_rbac_creates_directory(self, mock_config, temp_volume_root):
"""Test that save_rbac creates directory if it doesn't exist."""
rbac_file = os.path.join(temp_volume_root, "docker/nas-dashboard/rbac.json")
# Remove directory
import shutil
if os.path.exists(os.path.dirname(rbac_file)):
shutil.rmtree(os.path.dirname(rbac_file))
test_data = {"role_defaults": {}, "user_overrides": {}}
save_rbac(test_data)
assert os.path.exists(rbac_file)
class TestUpdateRBAC:
"""Test atomic RBAC updates."""
def test_update_rbac_with_mutator(self, mock_config, temp_rbac_file):
"""Test updating RBAC with mutator function."""
def add_user_override(data):
data["user_overrides"]["newuser"] = {"pages": ["dashboard", "docker"]}
return "success"
result = update_rbac(add_user_override)
assert result == "success"
# Verify changes persisted
rbac = load_rbac()
assert "newuser" in rbac["user_overrides"]
assert rbac["user_overrides"]["newuser"]["pages"] == ["dashboard", "docker"]
def test_update_rbac_thread_safe(self, mock_config, temp_rbac_file):
"""Test that update_rbac is thread-safe."""
import threading
results = []
def increment_counter(data):
current = data.get("counter", 0)
data["counter"] = current + 1
return data["counter"]
# Run multiple updates concurrently
threads = []
for _ in range(10):
t = threading.Thread(target=lambda: results.append(update_rbac(increment_counter)))
threads.append(t)
t.start()
for t in threads:
t.join()
# Final counter should be 10
rbac = load_rbac()
assert rbac.get("counter") == 10
class TestResolveRole:
"""Test role resolution from groups."""
def test_resolve_role_admin(self):
"""Test resolving admin role."""
groups = ["dashboard_admin", "other_group"]
role = resolve_role(groups)
assert role == "admin"
def test_resolve_role_member(self):
"""Test resolving member role."""
groups = ["dashboard_member"]
role = resolve_role(groups)
assert role == "member"
def test_resolve_role_viewer(self):
"""Test resolving viewer role."""
groups = ["dashboard_viewer", "unrelated_group"]
role = resolve_role(groups)
assert role == "viewer"
def test_resolve_role_no_match(self):
"""Test resolving role with no matching groups."""
groups = ["other_group", "another_group"]
role = resolve_role(groups)
assert role is None
def test_resolve_role_empty_groups(self):
"""Test resolving role with empty groups list."""
role = resolve_role([])
assert role is None
def test_resolve_role_priority(self):
"""Test that first matching group is used."""
groups = ["dashboard_admin", "dashboard_member"]
role = resolve_role(groups)
assert role == "admin"
class TestGetPages:
"""Test page access resolution."""
def test_get_pages_admin_default(self, mock_config, temp_rbac_file):
"""Test getting pages for admin with default settings."""
pages = get_pages("testuser", "admin")
assert pages == "*"
def test_get_pages_member_default(self, mock_config, temp_rbac_file):
"""Test getting pages for member with default settings."""
pages = get_pages("testuser", "member")
assert isinstance(pages, list)
assert "dashboard" in pages
assert "docker" in pages
def test_get_pages_viewer_default(self, mock_config, temp_rbac_file):
"""Test getting pages for viewer with default settings."""
pages = get_pages("testuser", "viewer")
assert pages == ["dashboard"]
def test_get_pages_with_user_override(self, mock_config, temp_rbac_file):
"""Test getting pages with user-specific override."""
# Add user override
def add_override(data):
data["user_overrides"]["customuser"] = {"pages": ["dashboard", "files"]}
update_rbac(add_override)
pages = get_pages("customuser", "admin")
assert pages == ["dashboard", "files"]
def test_get_pages_unknown_role(self, mock_config, temp_rbac_file):
"""Test getting pages for unknown role returns empty list."""
pages = get_pages("testuser", "unknown_role")
assert pages == []
class TestGetSidebarLinks:
"""Test sidebar link access resolution."""
def test_get_sidebar_links_admin_default(self, mock_config, temp_rbac_file):
"""Test getting sidebar links for admin."""
links = get_sidebar_links("testuser", "admin")
assert links == "*"
def test_get_sidebar_links_member_default(self, mock_config, temp_rbac_file):
"""Test getting sidebar links for member."""
links = get_sidebar_links("testuser", "member")
assert links == "*"
def test_get_sidebar_links_viewer_default(self, mock_config, temp_rbac_file):
"""Test getting sidebar links for viewer."""
links = get_sidebar_links("testuser", "viewer")
assert links == ["dashboard"]
def test_get_sidebar_links_with_user_override(self, mock_config, temp_rbac_file):
"""Test getting sidebar links with user override."""
def add_override(data):
data["user_overrides"]["customuser"] = {
"pages": "*",
"sidebar_links": ["dashboard", "docker", "files"]
}
update_rbac(add_override)
links = get_sidebar_links("customuser", "admin")
assert links == ["dashboard", "docker", "files"]
def test_get_sidebar_links_no_override(self, mock_config, temp_rbac_file):
"""Test that user without sidebar_links override gets role default."""
def add_override(data):
data["user_overrides"]["customuser"] = {"pages": ["dashboard"]}
# No sidebar_links specified
update_rbac(add_override)
links = get_sidebar_links("customuser", "admin")
assert links == "*" # Should fall back to admin default
class TestGetSidebarOrder:
"""Test sidebar order retrieval."""
def test_get_sidebar_order_no_override(self, mock_config, temp_rbac_file):
"""Test getting sidebar order when user has no override."""
order = get_sidebar_order("testuser")
assert order is None
def test_get_sidebar_order_with_override(self, mock_config, temp_rbac_file):
"""Test getting sidebar order with user override."""
custom_order = {
"Main": ["dashboard", "docker"],
"Media": ["navidrome", "immich"]
}
def add_override(data):
data["user_overrides"]["customuser"] = {"sidebar_order": custom_order}
update_rbac(add_override)
order = get_sidebar_order("customuser")
assert order == custom_order
class TestDefaultRBAC:
"""Test default RBAC configuration."""
def test_default_rbac_structure(self):
"""Test that DEFAULT_RBAC has correct structure."""
assert "role_defaults" in DEFAULT_RBAC
assert "user_overrides" in DEFAULT_RBAC
assert "admin" in DEFAULT_RBAC["role_defaults"]
assert "member" in DEFAULT_RBAC["role_defaults"]
assert "viewer" in DEFAULT_RBAC["role_defaults"]
def test_default_rbac_admin_permissions(self):
"""Test admin has full permissions by default."""
admin = DEFAULT_RBAC["role_defaults"]["admin"]
assert admin["pages"] == "*"
assert admin["sidebar_links"] == "*"
def test_default_rbac_member_permissions(self):
"""Test member has limited permissions."""
member = DEFAULT_RBAC["role_defaults"]["member"]
assert isinstance(member["pages"], list)
assert "dashboard" in member["pages"]
assert member["sidebar_links"] == "*"
def test_default_rbac_viewer_permissions(self):
"""Test viewer has minimal permissions."""
viewer = DEFAULT_RBAC["role_defaults"]["viewer"]
assert viewer["pages"] == ["dashboard"]
assert viewer["sidebar_links"] == ["dashboard"]
class TestRoleGroupMap:
"""Test role group mapping."""
def test_role_group_map_completeness(self):
"""Test that ROLE_GROUP_MAP has all expected mappings."""
assert "dashboard_admin" in ROLE_GROUP_MAP
assert "dashboard_member" in ROLE_GROUP_MAP
assert "dashboard_viewer" in ROLE_GROUP_MAP
assert ROLE_GROUP_MAP["dashboard_admin"] == "admin"
assert ROLE_GROUP_MAP["dashboard_member"] == "member"
assert ROLE_GROUP_MAP["dashboard_viewer"] == "viewer"