Files
saas_backend/tests/test_access_registry.py
T
2026-08-31 20:39:41 -04:00

171 lines
5.9 KiB
Python

"""The access registry: every permission the product knows about, and its cache.
Two tables feed one list — the platform's own `accesses` and each module's
`module_accesses` — and the result is cached in Redis for an hour. That cache is
what the permission picker reads, so anything stale there is a permission an
administrator cannot grant, or one they can grant that no longer exists.
"""
from __future__ import annotations
import pytest
from app.core.tenant_context import unscoped
from app.services.auth.access_service import AccessService
from .conftest import requires_db
pytestmark = requires_db
def _codes(rows):
return {row.access_code for row in rows}
def test_both_kinds_of_permission_appear(db, access_factory, module_factory,
module_access_factory):
"""A picker showing only one of the two is a picker that cannot express half
the product."""
module = module_factory()
access_factory("platform.thing")
module_access_factory(module, "module.thing")
with unscoped():
rows = AccessService.get_all_accesses(db)
assert {"platform.thing", "module.thing"} <= _codes(rows)
def test_each_permission_says_where_it_came_from(db, access_factory,
module_factory,
module_access_factory):
"""Two modules can both define "report.view". Without the source, the picker
shows two identical rows and no way to tell them apart."""
module = module_factory(module_name="Reporting")
access_factory("platform.thing")
module_access_factory(module, "module.thing")
with unscoped():
by_code = {r.access_code: r for r in AccessService.get_all_accesses(db)}
assert by_code["platform.thing"].module_name == "SaaS (Internal)"
assert by_code["module.thing"].module_name == "Reporting"
def test_a_category_filter_applies_to_both_kinds(db, access_factory,
module_factory,
module_access_factory):
module = module_factory()
access_factory("billing.read", category="Billing")
access_factory("report.read", category="Reports")
module_access_factory(module, "billing.export")
with unscoped():
rows = AccessService.get_all_accesses(db, category="Billing")
codes = _codes(rows)
assert "billing.read" in codes
assert "report.read" not in codes
def test_the_category_list_is_stable(db, access_factory):
"""It was built from a set and returned unordered, so the dropdown reordered
itself between requests for no reason the user could see."""
access_factory("a.read", category="Alpha")
access_factory("b.read", category="Bravo")
access_factory("c.read", category="Charlie")
with unscoped():
first = AccessService.get_access_categories(db)
second = AccessService.get_access_categories(db)
assert first == second
assert first == sorted(first)
def test_the_list_is_served_from_cache_on_the_second_read(db, access_factory,
fake_redis):
access_factory("cached.thing")
with unscoped():
AccessService.get_all_accesses(db)
assert fake_redis.get("saas:access:v2:all:full")
again = AccessService.get_all_accesses(db)
assert "cached.thing" in _codes(again)
def test_a_broken_cache_entry_does_not_break_the_list(db, access_factory,
fake_redis):
"""Falling back to the database is the only safe reaction. Raising would
make a corrupt cache key take the permission picker down."""
access_factory("resilient.thing")
fake_redis.set("saas:access:v2:all:full", "{not json")
with unscoped():
rows = AccessService.get_all_accesses(db)
assert "resilient.thing" in _codes(rows)
def test_a_sync_clears_the_category_caches_too(db, module_factory,
environment_factory, fake_redis,
monkeypatch):
"""It cleared exactly one key — the unfiltered one.
Every category-filtered listing kept serving the old permission set for up
to an hour after a module changed what it declares. The picker showed
permissions that no longer existed, and hid ones that had just appeared, and
the only clue was that the unfiltered view disagreed with the filtered one.
"""
from app.services.auth import module_permission_service
from app.services.auth.module_permission_service import ModulePermissionService
module = module_factory()
environment_factory(module, slug="prod", is_default=True)
class Response:
status_code = 200
def raise_for_status(self):
pass
def json(self):
return {
"permissions": [
{"permission_code": "fresh.thing", "name": "Fresh",
"category": "Reports"}
]
}
monkeypatch.setattr(
module_permission_service.requests, "post",
lambda *a, **kw: Response(),
)
with unscoped():
AccessService.get_all_accesses(db)
AccessService.get_all_accesses(db, category="Reports")
assert fake_redis.get("saas:access:v2:all:full")
assert fake_redis.get("saas:access:v2:all:Reports")
ModulePermissionService.sync_permissions(db, str(module.id))
assert fake_redis.get("saas:access:v2:all:full") is None
assert fake_redis.get("saas:access:v2:all:Reports") is None
assert "fresh.thing" in _codes(
AccessService.get_all_accesses(db, category="Reports")
)
def test_the_list_works_with_no_cache_at_all(db, access_factory):
"""Redis is optional. A missing cache must mean slower, not broken."""
access_factory("uncached.thing")
with unscoped():
rows = AccessService.get_all_accesses(db)
assert "uncached.thing" in _codes(rows)