""" Unit tests for rbac.py - Role-based access control. """ import json import os from rbac import ( DEFAULT_RBAC, ROLE_GROUP_MAP, get_pages, get_sidebar_links, get_sidebar_order, load_rbac, resolve_role, save_rbac, update_rbac, ) 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"