Permission Testing Matrix
Problem: Verify that your franchise app’s permission model works correctly across all roles.
Solution: Define an automated permission matrix test case using IntegrationTestCase. This test iterates through roles and DocTypes, verifying create, read, update, delete, submit, and cancel permissions programmatically.
import frappefrom frappe.tests import IntegrationTestCasefrom itertools import product as cartesian_product
from scoopjoy.tests.factory import ( create_test_outlet, create_test_agreement, create_test_user,)
EXTRA_TEST_RECORD_DEPENDENCIES = ["Customer", "Item"]
# === Permission Matrix Definition ===# True = allowed, False = deniedPERMISSION_MATRIX = { # (Role, DocType): {operation: expected_result} ("Franchise Owner", "Franchise Outlet"): { "read": True, "write": True, "create": False, "delete": False, "submit": False, "cancel": False, }, ("Franchise Owner", "Franchise Agreement"): { "read": True, "write": False, "create": False, "delete": False, "submit": False, "cancel": False, }, ("Franchise Owner", "Royalty Invoice"): { "read": True, "write": False, "create": False, "delete": False, "submit": False, "cancel": False, }, ("Franchise Owner", "Sales Invoice"): { "read": True, "write": True, "create": True, "delete": False, "submit": True, "cancel": False, }, ("Franchise Manager", "Franchise Outlet"): { "read": True, "write": True, "create": True, "delete": False, "submit": False, "cancel": False, }, ("Franchise Manager", "Franchise Agreement"): { "read": True, "write": True, "create": True, "delete": False, "submit": True, "cancel": True, }, ("Franchise Manager", "Royalty Invoice"): { "read": True, "write": True, "create": True, "delete": False, "submit": True, "cancel": False, }, ("Franchise Manager", "Sales Invoice"): { "read": True, "write": True, "create": True, "delete": False, "submit": True, "cancel": True, }, ("Franchise Staff", "Franchise Outlet"): { "read": True, "write": False, "create": False, "delete": False, "submit": False, "cancel": False, }, ("Franchise Staff", "Franchise Agreement"): { "read": False, "write": False, "create": False, "delete": False, "submit": False, "cancel": False, }, ("Franchise Staff", "Royalty Invoice"): { "read": False, "write": False, "create": False, "delete": False, "submit": False, "cancel": False, }, ("Franchise Staff", "Sales Invoice"): { "read": True, "write": True, "create": True, "delete": False, "submit": True, "cancel": False, }, ("ScoopJoy Admin", "Franchise Outlet"): { "read": True, "write": True, "create": True, "delete": True, "submit": False, "cancel": False, }, ("ScoopJoy Admin", "Franchise Agreement"): { "read": True, "write": True, "create": True, "delete": True, "submit": True, "cancel": True, }, ("ScoopJoy Admin", "Royalty Invoice"): { "read": True, "write": True, "create": True, "delete": True, "submit": True, "cancel": True, }, ("ScoopJoy Admin", "Sales Invoice"): { "read": True, "write": True, "create": True, "delete": True, "submit": True, "cancel": True, }, ("Accounts User", "Franchise Outlet"): { "read": True, "write": False, "create": False, "delete": False, "submit": False, "cancel": False, }, ("Accounts User", "Franchise Agreement"): { "read": True, "write": False, "create": False, "delete": False, "submit": False, "cancel": False, }, ("Accounts User", "Royalty Invoice"): { "read": True, "write": True, "create": True, "delete": False, "submit": True, "cancel": True, }, ("Accounts User", "Sales Invoice"): { "read": True, "write": True, "create": True, "delete": False, "submit": True, "cancel": True, },}
# Test users for each roleROLE_USERS = { "Franchise Owner": "perm-owner@scoopjoy.com", "Franchise Manager": "perm-manager@scoopjoy.com", "Franchise Staff": "perm-staff@scoopjoy.com", "ScoopJoy Admin": "perm-admin@scoopjoy.com", "Accounts User": "perm-accounts@scoopjoy.com",}
class TestPermissionMatrix(IntegrationTestCase): """Automated test that iterates the full permission matrix."""
@classmethod def setUpClass(cls): super().setUpClass() cls.outlet = create_test_outlet(outlet_name="Permission Matrix Outlet")
# Create test users for each role with user permission for the outlet for role, email in ROLE_USERS.items(): create_test_user(email, roles=[role], franchise_outlet=cls.outlet.name)
def test_full_permission_matrix(self): """Iterate every cell in the permission matrix.""" failures = []
for (role, doctype), operations in PERMISSION_MATRIX.items(): email = ROLE_USERS[role]
for operation, expected in operations.items(): frappe.set_user(email) try: actual = frappe.has_permission(doctype, operation) if actual != expected: failures.append( f" {role} | {doctype} | {operation}: " f"expected={expected}, got={actual}" ) finally: frappe.set_user("Administrator")
if failures: self.fail( f"Permission matrix has {len(failures)} failures:\n" + "\n".join(failures) )
# --- User Permission (row-level) restrictions ---
def test_user_permission_restricts_outlet_visibility(self): """Franchise Manager sees only their assigned outlet.""" other_outlet = create_test_outlet(outlet_name="Other Perm Outlet")
frappe.set_user("perm-manager@scoopjoy.com") try: visible = frappe.get_list( "Franchise Outlet", filters={"status": "Active"}, pluck="name", ) self.assertIn(self.outlet.name, visible) self.assertNotIn(other_outlet.name, visible) finally: frappe.set_user("Administrator")
# --- Field-level permissions ---
def test_field_level_cost_data_hidden_from_staff(self): """Franchise Staff cannot see cost-related fields.""" frappe.set_user("perm-staff@scoopjoy.com") try: meta = frappe.get_meta("Franchise Outlet") cost_fields = ["monthly_rent", "security_deposit_amount", "royalty_percentage"] for fieldname in cost_fields: field = meta.get_field(fieldname) if field: permlevel = field.permlevel or 0 has_perm = frappe.has_permission( "Franchise Outlet", "read", permlevel=permlevel ) if permlevel > 0: self.assertFalse( has_perm, f"Staff should not see {fieldname} (permlevel={permlevel})", ) finally: frappe.set_user("Administrator")
# --- Workflow-state-based permissions ---
def test_only_admin_can_approve_outlet(self): """Only ScoopJoy Admin can transition outlet to 'Approved'.""" outlet = create_test_outlet( outlet_name="Approval Perm Outlet", status="Under Review", )
# Manager cannot approve frappe.set_user("perm-manager@scoopjoy.com") try: outlet.reload() outlet.status = "Approved" with self.assertRaises(frappe.PermissionError): outlet.save() finally: frappe.set_user("Administrator")
# Admin can approve frappe.set_user("perm-admin@scoopjoy.com") try: outlet.reload() outlet.status = "Approved" outlet.save() self.assertEqual(outlet.status, "Approved") finally: frappe.set_user("Administrator")
# --- Sharing test ---
def test_sharing_grants_temporary_access(self): """Sharing an outlet grants read access to another user.""" outsider = create_test_user("perm-outsider@scoopjoy.com", roles=["Franchise Staff"])
# Outsider has no access initially frappe.set_user("perm-outsider@scoopjoy.com") try: self.assertFalse( frappe.has_permission("Franchise Outlet", "read", self.outlet.name) ) finally: frappe.set_user("Administrator")
# Share the document frappe.share.add( "Franchise Outlet", self.outlet.name, user="perm-outsider@scoopjoy.com", read=1, write=0, )
# Now outsider can read frappe.set_user("perm-outsider@scoopjoy.com") try: self.assertTrue( frappe.has_permission("Franchise Outlet", "read", self.outlet.name) ) # But cannot write self.assertFalse( frappe.has_permission("Franchise Outlet", "write", self.outlet.name) ) finally: frappe.set_user("Administrator") # Cleanup sharing frappe.share.remove( "Franchise Outlet", self.outlet.name, user="perm-outsider@scoopjoy.com", )