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"]