Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
120 lines
3.7 KiB
Python
120 lines
3.7 KiB
Python
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# See license.txt
|
|
|
|
import frappe
|
|
from frappe.permissions import add_permission, update_permission_property
|
|
from frappe.tests import IntegrationTestCase
|
|
|
|
from crm.api.activities import get_activities
|
|
|
|
RESTRICTED_FIELD = "permlevel_secret"
|
|
RESTRICTED_VALUE = "salary is 100000"
|
|
|
|
|
|
class TestActivityPermlevel(IntegrationTestCase):
|
|
"""A permlevel-restricted field is hidden on the form layout, so its values
|
|
must not leak through the activity timeline either (frappe/crm#805).
|
|
|
|
rep@permlevel.test -- Sales User, owns the lead, no permlevel 1 access
|
|
mgr@permlevel.test -- Sales Manager, reads permlevel 1
|
|
"""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
make_user("rep@permlevel.test", roles=["Sales User"])
|
|
make_user("mgr@permlevel.test", roles=["Sales Manager"])
|
|
|
|
if not frappe.db.exists("Custom Field", {"dt": "CRM Lead", "fieldname": RESTRICTED_FIELD}):
|
|
frappe.get_doc(
|
|
{
|
|
"doctype": "Custom Field",
|
|
"dt": "CRM Lead",
|
|
"fieldname": RESTRICTED_FIELD,
|
|
"label": "Permlevel Secret",
|
|
"fieldtype": "Data",
|
|
"permlevel": 1,
|
|
}
|
|
).insert(ignore_permissions=True)
|
|
|
|
# only Sales Manager gets permlevel 1 on CRM Lead
|
|
add_permission("CRM Lead", "Sales Manager", 1)
|
|
update_permission_property("CRM Lead", "Sales Manager", 1, "write", 1)
|
|
frappe.clear_cache(doctype="CRM Lead")
|
|
|
|
def setUp(self):
|
|
frappe.db.savepoint("test_activity_permlevel")
|
|
|
|
def tearDown(self):
|
|
frappe.set_user("Administrator")
|
|
frappe.db.rollback(save_point="test_activity_permlevel")
|
|
|
|
def make_lead_with_restricted_change(self):
|
|
lead = frappe.get_doc(
|
|
{"doctype": "CRM Lead", "lead_owner": "rep@permlevel.test", "first_name": "Permlevel"}
|
|
)
|
|
lead.flags.ignore_mandatory = True
|
|
lead.insert(ignore_permissions=True)
|
|
|
|
lead.set(RESTRICTED_FIELD, RESTRICTED_VALUE)
|
|
# the timeline is built from versions, which tests skip unless asked for
|
|
lead.save(ignore_permissions=True, ignore_version=False)
|
|
return lead
|
|
|
|
def activities_as(self, user, name):
|
|
frappe.set_user(user)
|
|
try:
|
|
activities, *_ = get_activities(name)
|
|
finally:
|
|
frappe.set_user("Administrator")
|
|
return activities
|
|
|
|
def test_restricted_field_change_is_hidden_from_activity(self):
|
|
lead = self.make_lead_with_restricted_change()
|
|
activities = self.activities_as("rep@permlevel.test", lead.name)
|
|
|
|
self.assertNotIn(RESTRICTED_FIELD, changed_fields(activities))
|
|
self.assertNotIn(RESTRICTED_VALUE, frappe.as_json(activities))
|
|
|
|
def test_permitted_user_still_sees_the_change(self):
|
|
lead = self.make_lead_with_restricted_change()
|
|
activities = self.activities_as("mgr@permlevel.test", lead.name)
|
|
|
|
self.assertIn(RESTRICTED_FIELD, changed_fields(activities))
|
|
|
|
def test_unrestricted_fields_are_untouched(self):
|
|
lead = self.make_lead_with_restricted_change()
|
|
lead.status = "Qualified"
|
|
lead.save(ignore_permissions=True, ignore_version=False)
|
|
|
|
activities = self.activities_as("rep@permlevel.test", lead.name)
|
|
self.assertIn("status", changed_fields(activities))
|
|
self.assertNotIn(RESTRICTED_FIELD, changed_fields(activities))
|
|
|
|
|
|
def changed_fields(activities):
|
|
"""Fieldnames reported by the timeline, including grouped versions."""
|
|
fields = set()
|
|
for activity in activities:
|
|
for entry in [activity, *(activity.get("other_versions") or [])]:
|
|
data = entry.get("data")
|
|
if isinstance(data, dict) and data.get("field"):
|
|
fields.add(data["field"])
|
|
return fields
|
|
|
|
|
|
def make_user(email, roles=None):
|
|
if frappe.db.exists("User", email):
|
|
return frappe.get_doc("User", email)
|
|
user = frappe.get_doc(
|
|
{
|
|
"doctype": "User",
|
|
"email": email,
|
|
"first_name": email.split("@")[0],
|
|
"send_welcome_email": 0,
|
|
}
|
|
).insert(ignore_permissions=True)
|
|
for role in roles or []:
|
|
user.add_roles(role)
|
|
return user
|