Skip to content

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.

apps/scoopjoy/scoopjoy/tests/test_permission_matrix.py
import frappe
from frappe.tests import IntegrationTestCase
from 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 = denied
PERMISSION_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 role
ROLE_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",
)