"""Seed subsystem: roles, admin user, permissions matrix, idempotency.""" from __future__ import annotations import pytest from sqlalchemy.orm import Session from ypms.security.models import Permission from ypms.security.rbac import ADMIN_ROLE, can from ypms.seed.permissions import load_matrix, seed_permissions from ypms.seed.roles import ROLES from ypms.seed.run import seed_bootstrap_admin, seed_roles from ypms.users.models import Role, User def test_seed_roles_creates_all_and_is_idempotent(db: Session) -> None: roles_first = seed_roles(db) assert set(roles_first) == {r["code"] for r in ROLES} roles_second = seed_roles(db) assert {r.id for r in roles_first.values()} == {r.id for r in roles_second.values()} def test_seed_roles_updates_description_drift(db: Session) -> None: seed_roles(db) orig = db.query(Role).filter(Role.code == "OYP_STAFF").one() orig.description = "wrong description" db.flush() seed_roles(db) refreshed = db.query(Role).filter(Role.code == "OYP_STAFF").one() assert refreshed.description != "wrong description" assert refreshed.description is not None assert "Office of Youth Programs" in refreshed.description def test_seed_bootstrap_admin_creates_admin_with_super_role(db: Session) -> None: roles = seed_roles(db) admin = seed_bootstrap_admin(db, roles) assert admin.username == "chris" assert admin.is_active is True db.refresh(admin) assert ADMIN_ROLE in {r.code for r in admin.roles} def test_seed_bootstrap_admin_is_idempotent(db: Session) -> None: roles = seed_roles(db) first = seed_bootstrap_admin(db, roles) second = seed_bootstrap_admin(db, roles) assert first.id == second.id def test_seed_permissions_inserts_rows_from_matrix(db: Session) -> None: roles = seed_roles(db) added = seed_permissions(db, roles) assert added > 0 total = db.query(Permission).count() assert total == added def test_seed_permissions_is_idempotent(db: Session) -> None: roles = seed_roles(db) first = seed_permissions(db, roles) second = seed_permissions(db, roles) assert second == 0 total_after_second_pass = db.query(Permission).count() assert total_after_second_pass == first def test_matrix_grants_reference_only_declared_actions_and_entities() -> None: matrix = load_matrix() actions = set(matrix["actions"]) entities = set(matrix["entities"]) for role_code, per_entity in matrix["grants"].items(): for entity, action_list in per_entity.items(): assert entity in entities, f"{role_code}: entity '{entity}' not declared" for a in action_list: assert a in actions, f"{role_code}: action '{a}' on '{entity}' not declared" def test_matrix_grants_reference_only_declared_roles() -> None: matrix = load_matrix() role_codes_in_seed = {r["code"] for r in ROLES} for role_code in matrix["grants"]: assert role_code in role_codes_in_seed, ( f"grants contains '{role_code}' but seed.roles.ROLES does not" ) # Super-role must NOT appear in grants (it bypasses). assert ADMIN_ROLE not in matrix["grants"] def test_unknown_role_in_matrix_raises(db: Session) -> None: """Guard: if the JSON references a role the seed doesn't know about, seed fails loudly rather than silently dropping grants.""" roles = seed_roles(db) # Simulate a matrix mismatch by passing a subset of roles. partial = {code: r for code, r in roles.items() if code != "OYP_STAFF"} with pytest.raises(ValueError, match="unknown role 'OYP_STAFF'"): seed_permissions(db, partial) def test_end_to_end_rbac_decisions_per_role(db: Session) -> None: """The point of the seed is that can() returns the right answers. Spot-check each role's most important grants and denials. """ roles = seed_roles(db) seed_permissions(db, roles) def user_with(role_code: str) -> User: from ypms.users.models import UserRole u = User(username=f"u_{role_code.lower()}", password_hash="x") db.add(u) db.flush() db.add(UserRole(user_id=u.id, role_id=roles[role_code].id)) db.flush() db.refresh(u) return u # DOES_SYSTEM_ADMIN bypasses everything. admin = user_with(ADMIN_ROLE) assert can(db, admin, "Delete", "FosterIndicator") is True assert can(db, admin, "Create", "PayrollBatch") is True # OYP_STAFF: general grants, but NOT FERPA/CFSA and NOT PayrollBatch. oyp = user_with("OYP_STAFF") assert can(db, oyp, "Create", "Registrant") is True assert can(db, oyp, "Edit", "Host") is True assert can(db, oyp, "View", "EducationRecord") is False assert can(db, oyp, "View", "FosterIndicator") is False assert can(db, oyp, "Create", "PayrollBatch") is False # ELIGIBILITY_EXAMINER: sees FERPA/CFSA; does NOT edit Host or PayrollBatch. exm = user_with("ELIGIBILITY_EXAMINER") assert can(db, exm, "View", "EducationRecord") is True assert can(db, exm, "View", "FosterIndicator") is True assert can(db, exm, "Create", "EligibilityDecision") is True assert can(db, exm, "Edit", "Host") is False assert can(db, exm, "Create", "PayrollBatch") is False # PAYROLL_ADMIN: banking + payroll batch; NOT FERPA/CFSA. pay = user_with("PAYROLL_ADMIN") assert can(db, pay, "Create", "PayrollBatch") is True assert can(db, pay, "Edit", "Timesheet") is True assert can(db, pay, "View", "FosterIndicator") is False assert can(db, pay, "Edit", "Registrant") is False # REPORTING_ANALYST: strictly read-only; nothing sensitive. rep = user_with("REPORTING_ANALYST") assert can(db, rep, "View", "Application") is True assert can(db, rep, "Create", "Application") is False assert can(db, rep, "View", "FosterIndicator") is False assert can(db, rep, "View", "PayrollBatch") is False # External-portal roles never touch program admin surfaces. host = user_with("HOST_USER") assert can(db, host, "Edit", "Host") is True assert can(db, host, "View", "Registrant") is False assert can(db, host, "View", "FosterIndicator") is False parent = user_with("PARENT_USER") assert can(db, parent, "Create", "ConsentRecord") is True assert can(db, parent, "View", "Registrant") is True assert can(db, parent, "Edit", "Host") is False assert can(db, parent, "View", "PayrollBatch") is False youth = user_with("YOUTH_USER") assert can(db, youth, "Create", "Application") is True assert can(db, youth, "View", "JobPosting") is True assert can(db, youth, "View", "PayrollBatch") is False assert can(db, youth, "View", "FosterIndicator") is False