2026-06-03 06:35:03 +02:00
|
|
|
"""Route-guard role-sets in api/deps + api/routes/v1/_role_dep derive from foundation.
|
2026-05-11 02:15:47 +02:00
|
|
|
|
2026-06-03 06:35:03 +02:00
|
|
|
The HTTP-layer guards in `roboco.api.deps` and `roboco.api.routes.v1._role_dep`
|
2026-05-11 02:15:47 +02:00
|
|
|
historically used hand-written frozensets of role-name strings. Phase 4 Task 11
|
|
|
|
|
moves those literals onto `foundation.identity` so adding/renaming a role only
|
|
|
|
|
edits one file. These tests pin the foundation-derived membership and the
|
|
|
|
|
import contract.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import inspect
|
|
|
|
|
|
|
|
|
|
from roboco.api import deps
|
|
|
|
|
from roboco.api.deps import (
|
|
|
|
|
_DEVELOPER_OR_ABOVE_ROLES,
|
|
|
|
|
_GLOBAL_CELL_ACCESS_ROLES,
|
|
|
|
|
_PM_OR_ABOVE_ROLES,
|
|
|
|
|
)
|
2026-06-03 06:35:03 +02:00
|
|
|
from roboco.api.routes.v1 import _role_dep
|
2026-05-11 02:15:47 +02:00
|
|
|
from roboco.foundation.identity import BOARD_ROLES, DEV_ROLES, PM_ROLES, Role
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_pm_or_above_roles_matches_foundation_composition() -> None:
|
|
|
|
|
"""`require_pm_or_above` admits PMs + non-marketing board + CEO.
|
|
|
|
|
|
|
|
|
|
Composition: PM_ROLES | (BOARD_ROLES - {HEAD_MARKETING}) | {CEO}.
|
|
|
|
|
Head-marketing is intentionally excluded — the role is a marketing
|
|
|
|
|
spokesperson, not a workflow approver.
|
|
|
|
|
"""
|
|
|
|
|
expected = PM_ROLES | (BOARD_ROLES - {Role.HEAD_MARKETING}) | {Role.CEO}
|
|
|
|
|
assert expected == _PM_OR_ABOVE_ROLES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_developer_or_above_roles_matches_foundation_composition() -> None:
|
|
|
|
|
"""`require_developer_or_above` admits developers + PM-or-above.
|
|
|
|
|
|
|
|
|
|
Composition: DEV_ROLES | (PM_OR_ABOVE).
|
|
|
|
|
QA and documenter are intentionally NOT in this set — work-session
|
|
|
|
|
create/commit/PR endpoints are dev-only operations.
|
|
|
|
|
"""
|
|
|
|
|
expected = DEV_ROLES | _PM_OR_ABOVE_ROLES
|
|
|
|
|
assert expected == _DEVELOPER_OR_ABOVE_ROLES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_global_cell_access_roles_matches_foundation_composition() -> None:
|
|
|
|
|
"""`require_cell_access` lets main-PM + non-marketing board + CEO cross cells."""
|
|
|
|
|
expected = (BOARD_ROLES - {Role.HEAD_MARKETING}) | {Role.MAIN_PM, Role.CEO}
|
|
|
|
|
assert expected == _GLOBAL_CELL_ACCESS_ROLES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_deps_module_imports_from_foundation() -> None:
|
|
|
|
|
"""`roboco.api.deps` sources its role-set primitives from foundation."""
|
|
|
|
|
src = inspect.getsource(deps)
|
|
|
|
|
assert "from roboco.foundation.identity import" in src
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_role_dep_module_imports_from_foundation() -> None:
|
2026-06-03 06:35:03 +02:00
|
|
|
"""`roboco.api.routes.v1._role_dep` sources its Role enum from foundation."""
|
2026-05-11 02:15:47 +02:00
|
|
|
src = inspect.getsource(_role_dep)
|
|
|
|
|
assert "from roboco.foundation.identity import" in src
|
|
|
|
|
|
|
|
|
|
|
2026-06-03 06:35:03 +02:00
|
|
|
def test_v1_role_dep_sets_match_foundation_roles() -> None:
|
|
|
|
|
"""v1 single-role guards use foundation Role values, not raw strings."""
|
2026-05-11 02:15:47 +02:00
|
|
|
# Single-role guards should pass through Role-typed frozensets so
|
|
|
|
|
# renaming a role lives in foundation, not the guard literal.
|
|
|
|
|
# The Depends() objects wrap the closure, so we can't introspect the
|
|
|
|
|
# frozenset directly — but we can confirm the role values resolve.
|
|
|
|
|
assert Role.DEVELOPER == "developer"
|
|
|
|
|
assert Role.QA == "qa"
|
|
|
|
|
assert Role.DOCUMENTER == "documenter"
|
|
|
|
|
assert Role.CELL_PM == "cell_pm"
|
|
|
|
|
assert Role.MAIN_PM == "main_pm"
|
|
|
|
|
assert Role.PRODUCT_OWNER == "product_owner"
|
|
|
|
|
assert Role.HEAD_MARKETING == "head_marketing"
|
|
|
|
|
assert Role.AUDITOR == "auditor"
|