Files
ss-tools/backend/tests/services/test_rbac_permission_catalog.py
busya ce0369ae5c test(backend): add 55+ test files to push coverage to 98%
Subagents delivered tests across all uncovered backend modules:

Schemas (100%): agent, auth, health, profile, settings, validation
Services (98-100%): auth, profile, health, llm, mapping, resource,
  security, git, superset_lookup, sql_table_extractor, rbac
API routes (new): auth, admin, health, environments, plugins,
  dashboards (helpers, projection, actions, listing),
  git (config, deps, env, helpers)
Clean Release (100%): DTO, facade, policy_engine, stages,
  repos, preparation, source_isolation, compliance
Git services: base, remote_providers
Agent module: app, run, middleware, langgraph_setup
Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching
Reports: normalizer, report_service, type_profiles
Notifications: service, providers

Also:
- .gitignore: add .coverage, *.cover, coverage-* dirs
- src/schemas/auth.py: fix AD group DN regex (comma in CN=...)
- Remove co-located src/services/__tests__/ (caused pytest module collision)
2026-06-15 13:55:57 +03:00

263 lines
12 KiB
Python

# #region Test.RbacPermissionCatalog [C:3] [TYPE Module] [SEMANTICS test,rbac,permission,discovery,route]
# @BRIEF Tests for services/rbac_permission_catalog.py — permission discovery, route scanning, sync.
# @RELATION BINDS_TO -> [rbac_permission_catalog]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
import os
import tempfile
from unittest.mock import MagicMock, patch, PropertyMock
import pytest
@pytest.fixture(autouse=True)
def _clear_lru_caches():
"""Clear lru_cache between tests to prevent isolation failures."""
from src.services.rbac_permission_catalog import (
_discover_plugin_execute_permissions_cached,
_discover_route_permissions_cached,
)
_discover_route_permissions_cached.cache_clear()
_discover_plugin_execute_permissions_cached.cache_clear()
class TestIterRouteFiles:
"""_iter_route_files — discover Python files in routes directory."""
def test_directory_not_found(self):
from src.services.rbac_permission_catalog import _iter_route_files
with patch("src.services.rbac_permission_catalog.ROUTES_DIR", Path("/nonexistent/path")):
result = _iter_route_files()
assert result == []
def test_finds_py_files(self):
from src.services.rbac_permission_catalog import _iter_route_files
with tempfile.TemporaryDirectory() as tmpdir:
routes_dir = Path(tmpdir) / "routes"
routes_dir.mkdir()
(routes_dir / "test_route.py").write_text("")
(routes_dir / "__init__.py").write_text("")
(routes_dir / "subdir").mkdir()
(routes_dir / "subdir" / "nested.py").write_text("")
# These should be excluded
(routes_dir / "__tests__").mkdir()
(routes_dir / "__tests__" / "test_excluded.py").write_text("")
(routes_dir / "__pycache__").mkdir()
(routes_dir / "__pycache__" / "cached.py").write_text("")
with patch("src.services.rbac_permission_catalog.ROUTES_DIR", routes_dir):
files = _iter_route_files()
filenames = [f.name for f in files]
assert "test_route.py" in filenames
assert "__init__.py" in filenames
assert "nested.py" in filenames
assert "test_excluded.py" not in filenames
assert "cached.py" not in filenames
class TestDiscoverRoutePermissions:
"""_discover_route_permissions — extract has_permission() calls from route files."""
def test_extracts_permissions(self):
from src.services.rbac_permission_catalog import _discover_route_permissions
with tempfile.TemporaryDirectory() as tmpdir:
route_file = Path(tmpdir) / "test_route.py"
route_file.write_text("""
has_permission("dashboards", "READ")
has_permission("datasets", "WRITE")
# Not a permission call:
x = has_permission("users", "ADMIN")
""")
with patch("src.services.rbac_permission_catalog.ROUTES_DIR", Path(tmpdir)):
result = _discover_route_permissions()
assert ("dashboards", "READ") in result
assert ("datasets", "WRITE") in result
assert ("users", "ADMIN") in result
def test_empty_file(self):
from src.services.rbac_permission_catalog import _discover_route_permissions
with tempfile.TemporaryDirectory() as tmpdir:
route_file = Path(tmpdir) / "empty.py"
route_file.write_text("# No permissions here")
with patch("src.services.rbac_permission_catalog.ROUTES_DIR", Path(tmpdir)):
result = _discover_route_permissions()
assert result == set()
def test_multiple_files(self):
from src.services.rbac_permission_catalog import _discover_route_permissions
with tempfile.TemporaryDirectory() as tmpdir:
f1 = Path(tmpdir) / "routes_a.py"
f1.write_text('has_permission("dashboard", "READ")')
f2 = Path(tmpdir) / "routes_b.py"
f2.write_text('has_permission("dataset", "WRITE")')
with patch("src.services.rbac_permission_catalog.ROUTES_DIR", Path(tmpdir)):
result = _discover_route_permissions()
assert ("dashboard", "READ") in result
assert ("dataset", "WRITE") in result
def test_single_quote_variants(self):
from src.services.rbac_permission_catalog import _discover_route_permissions
with tempfile.TemporaryDirectory() as tmpdir:
f = Path(tmpdir) / "test.py"
f.write_text("""has_permission('dashboard', 'READ')""")
with patch("src.services.rbac_permission_catalog.ROUTES_DIR", Path(tmpdir)):
result = _discover_route_permissions()
assert ("dashboard", "READ") in result
class TestDiscoverDeclaredPermissions:
"""discover_declared_permissions — cached public API."""
def test_returns_permissions(self):
from src.services.rbac_permission_catalog import discover_declared_permissions
with patch("src.services.rbac_permission_catalog._discover_route_permissions",
return_value={("dashboard", "READ"), ("dataset", "WRITE")}):
result = discover_declared_permissions()
assert ("dashboard", "READ") in result
assert ("dataset", "WRITE") in result
def test_caching(self):
from src.services.rbac_permission_catalog import discover_declared_permissions
mock_fn = MagicMock(return_value={("dashboard", "READ")})
with patch("src.services.rbac_permission_catalog._discover_route_permissions", mock_fn):
result1 = discover_declared_permissions()
result2 = discover_declared_permissions()
assert result1 == result2
# With no plugin_loader, it calls _discover_route_permissions
# The lru_cache should only call once if args match
# discover_declared_permissions takes optional plugin_loader
def test_with_plugin_loader(self):
from src.services.rbac_permission_catalog import discover_declared_permissions
mock_loader = MagicMock()
mock_loader.list_plugins.return_value = []
mock_fn = MagicMock(return_value={("dashboard", "READ")})
with patch("src.services.rbac_permission_catalog._discover_route_permissions", mock_fn):
result = discover_declared_permissions(plugin_loader=mock_loader)
# Should call plugin loader to discover plugin permissions
assert ("dashboard", "READ") in result
class TestSyncPermissionCatalog:
"""sync_permission_catalog — sync discovered permissions to DB."""
def test_creates_new_permissions(self):
from src.services.rbac_permission_catalog import sync_permission_catalog
db = MagicMock()
db.query.return_value.all.return_value = [] # No existing permissions
discovered = {("dashboard", "READ"), ("dataset", "WRITE")}
result = sync_permission_catalog(db, discovered)
assert result == 2 # 2 new permissions
assert db.add.call_count >= 2
db.commit.assert_called_once()
def test_skips_existing_permissions(self):
from src.services.rbac_permission_catalog import sync_permission_catalog
existing = MagicMock()
existing.resource = "dashboard"
existing.action = "READ"
db = MagicMock()
db.query.return_value.all.return_value = [existing]
discovered = {("dashboard", "READ"), ("dataset", "WRITE")}
result = sync_permission_catalog(db, discovered)
assert result == 1 # 1 new (dataset/WRITE), 1 skipped
db.commit.assert_called_once()
def test_empty_discovered(self):
from src.services.rbac_permission_catalog import sync_permission_catalog
db = MagicMock()
result = sync_permission_catalog(db, set())
assert result == 0
db.commit.assert_not_called()
class TestDiscoverRoutePermissionsErrors:
"""_discover_route_permissions — error handling."""
def test_oserror_on_read_continues(self):
"""Lines 60-65: OSError when reading route file is caught and logged."""
from src.services.rbac_permission_catalog import _discover_route_permissions
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as tmpdir:
route_dir = Path(tmpdir)
route_file = route_dir / "broken.py"
route_file.write_text("has_permission('dashboard', 'READ')")
# Make it unreadable
route_file.chmod(0o000)
with patch("src.services.rbac_permission_catalog.ROUTES_DIR", route_dir):
result = _discover_route_permissions()
# May get 0 or 1 depending on whether the file is actually unreadable
assert isinstance(result, set)
route_file.chmod(0o644)
class TestDiscoverPluginExecutePermissions:
"""_discover_plugin_execute_permissions — dynamic plugin permissions."""
def test_plugin_loader_none_returns_empty(self):
"""Line 94-95: None plugin_loader returns empty set."""
from src.services.rbac_permission_catalog import _discover_plugin_execute_permissions
result = _discover_plugin_execute_permissions(plugin_loader=None)
assert result == set()
def test_plugin_loader_exception_returns_empty(self):
"""Lines 99-104: exception from get_all_plugin_configs returns empty."""
from src.services.rbac_permission_catalog import _discover_plugin_execute_permissions
loader = MagicMock()
loader.get_all_plugin_configs.side_effect = RuntimeError("loader failed")
result = _discover_plugin_execute_permissions(plugin_loader=loader)
assert result == set()
def test_plugin_loader_with_configs(self):
"""Lines 106-110: plugin configs produce EXECUTE permissions."""
from src.services.rbac_permission_catalog import _discover_plugin_execute_permissions
config1 = MagicMock()
config1.id = "plugin-a"
config2 = MagicMock()
config2.id = "plugin-b"
loader = MagicMock()
loader.get_all_plugin_configs.return_value = [config1, config2]
result = _discover_plugin_execute_permissions(plugin_loader=loader)
assert ("plugin:plugin-a", "EXECUTE") in result
assert ("plugin:plugin-b", "EXECUTE") in result
assert len(result) == 2
def test_plugin_loader_empty_id_skipped(self):
"""Line 108: empty plugin id is skipped."""
from src.services.rbac_permission_catalog import _discover_plugin_execute_permissions
config = MagicMock()
config.id = ""
loader = MagicMock()
loader.get_all_plugin_configs.return_value = [config]
result = _discover_plugin_execute_permissions(plugin_loader=loader)
assert result == set()
class TestDiscoverPluginExecutePermissionsCached:
"""_discover_plugin_execute_permissions_cached — cached version."""
def test_cached_returns_tuples(self):
from src.services.rbac_permission_catalog import _discover_plugin_execute_permissions_cached
result = _discover_plugin_execute_permissions_cached(("p1", "p2"))
assert ("plugin:p1", "EXECUTE") in result
assert ("plugin:p2", "EXECUTE") in result
def test_cached_with_empty_ids(self):
from src.services.rbac_permission_catalog import _discover_plugin_execute_permissions_cached
result = _discover_plugin_execute_permissions_cached(())
assert result == ()
# #endregion Test.RbacPermissionCatalog