dodil/erp-brick-time-off
DODIL ERP brick: time off
tests/test_permission_matrix.py
"""The permission matrix: every route answers a caller who holds nothing with a refusal (never a 2xx, never a 500), and the machine permissions reach no
person role and no wildcard grant."""
import re
import pytest
from conftest import EMPLOYEE, HR_ADMIN, MANAGER, NOBODY, OWNER, ROLES, api, app, person, refused
MACHINE_ROUTES = (
("POST", "/employees/4410/leaver", {"last_day": "2026-12-31", "source_ref": "hr:left:4410:1"}),
("GET", "/persons/4410/balance", {"on": "2026-12-31"}),
("GET", "/absences", {"company_code": "UK01", "from": "2026-11-01", "to": "2026-11-30"}),
("POST", "/year-end/run", {"leave_year": 2026}),
("POST", "/migration/odoo/load", {"odoo_db": "x", "cutover_date": "2026-10-31", "company_code": "UK01", "years": [2026], "employee_map": {}, "leave_types": {},
"upstream_signed": True, "waiting": "re_enter"}),
("POST", "/migration/odoo/x/reconcile", None),
)
def _routes():
out = []
for path, methods in app.openapi()["paths"].items():
for method in methods:
if method.upper() in ("GET", "POST", "PUT", "DELETE", "PATCH") and not path.startswith(("/process", "/health", "/docs", "/openapi", "/mcp")):
out.append((method.upper(), re.sub(r"\{[^}]+\}", "1", path)))
return sorted(out)
@pytest.mark.parametrize("method,path", _routes())
def test_a_caller_with_no_permission_is_refused_on_every_route(method, path):
status, _ = api(method, path, NOBODY, {} if method != "GET" else None)
assert status in (401, 403, 404, 405, 422), (method, path, status)
def test_the_matrix_finds_every_route_the_brick_serves():
assert len(_routes()) >= 47 # 47 at 0.2.0: the parametrised test above runs once per route, so a new route is probed
@pytest.mark.parametrize("who", [EMPLOYEE, MANAGER, HR_ADMIN, OWNER], ids=["employee", "manager", "hr_admin", "owner"])
def test_machine_routes_are_refused_to_every_person_role(who):
for method, path, body in MACHINE_ROUTES:
refused(api(method, path, who, body), 403, "tof:")
def test_a_wildcard_grant_does_not_reach_a_machine_permission():
everyone = person("wild@uk.example", ["tof:*", "*"])
for method, path, body in MACHINE_ROUTES:
refused(api(method, path, everyone, body), 403, "tof:")
def test_no_person_role_holds_a_machine_permission():
machine = {"tof:balance:accrue", "tof:year_end:run", "tof:reminder:tick", "tof:leaver:intake", "tof:absence:read_for_payroll", "tof:balance:read_for_payroll",
"tof:migration:load", "tof:migration:reconcile"}
for role in ("employee", "manager", "hr_admin", "owner"):
assert not machine & set(ROLES[role]), role
def test_the_controls_on_balances_are_the_owners_and_never_the_hr_user_they_control():
assert "tof:settings:controls" in ROLES["owner"] and "tof:settings:controls" not in ROLES["hr_admin"]
assert "tof:migration:sign" in ROLES["owner"] and "tof:migration:sign" not in ROLES["hr_admin"]