From e3702efd446a9116cd32922f947755d0f8166aca Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:39:10 +0530 Subject: [PATCH] refactor: move whitelist_for_tests to common utils and apply to all test endpoints - Moved whitelist_for_tests decorator from ui_test_helpers to frappe.tests.utils - Enhanced decorator to accept kwargs for passing to frappe.whitelist() - Applied decorator consistently to test endpoints in: - test_api.py - test_api_v2.py - test_caching.py - test_search.py - Updated all imports to use direct import from frappe.tests.utils --- frappe/tests/test_api.py | 5 ++- frappe/tests/test_api_v2.py | 4 +- frappe/tests/test_caching.py | 5 ++- frappe/tests/test_search.py | 3 +- frappe/tests/ui_test_helpers.py | 70 +++++++++++++++------------------ frappe/tests/utils/__init__.py | 30 ++++++++++++++ 6 files changed, 71 insertions(+), 46 deletions(-) diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index fea624e67d..967379086b 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -16,6 +16,7 @@ from werkzeug.test import TestResponse import frappe from frappe.installer import update_site_config from frappe.tests import IntegrationTestCase +from frappe.tests.utils import whitelist_for_tests from frappe.utils import cint, get_test_client, get_url try: @@ -522,7 +523,7 @@ def generate_admin_keys(): frappe.db.commit() -@frappe.whitelist() +@whitelist_for_tests() def test(*, fail=False, handled=True, message="Failed"): if fail: if handled: @@ -533,6 +534,6 @@ def test(*, fail=False, handled=True, message="Failed"): frappe.msgprint(message) -@frappe.whitelist(allow_guest=True) +@whitelist_for_tests(allow_guest=True) def test_array(data): return data diff --git a/frappe/tests/test_api_v2.py b/frappe/tests/test_api_v2.py index de1831729c..d763dd0566 100644 --- a/frappe/tests/test_api_v2.py +++ b/frappe/tests/test_api_v2.py @@ -6,7 +6,7 @@ import requests import frappe from frappe.installer import update_site_config from frappe.tests.test_api import FrappeAPITestCase, suppress_stdout -from frappe.tests.utils import toggle_test_mode +from frappe.tests.utils import toggle_test_mode, whitelist_for_tests authorization_token = None @@ -317,7 +317,7 @@ def generate_admin_keys(): frappe.db.commit() -@frappe.whitelist() +@whitelist_for_tests() def test(*, fail=False, handled=True, message="Failed"): if fail: if handled: diff --git a/frappe/tests/test_caching.py b/frappe/tests/test_caching.py index 172f1689d4..4111ca8dfb 100644 --- a/frappe/tests/test_caching.py +++ b/frappe/tests/test_caching.py @@ -5,6 +5,7 @@ import frappe from frappe.core.doctype.doctype.test_doctype import new_doctype from frappe.tests import IntegrationTestCase from frappe.tests.test_api import FrappeAPITestCase +from frappe.tests.utils import whitelist_for_tests from frappe.utils.caching import redis_cache, request_cache, site_cache CACHE_TTL = 4 @@ -21,14 +22,14 @@ def request_specific_api(a: list | tuple | dict | int, b: int) -> int: return a**b * todays_value -@frappe.whitelist(allow_guest=True) +@whitelist_for_tests(allow_guest=True) @site_cache def ping() -> str: register_with_external_service(frappe.local.site) return "pong" -@frappe.whitelist(allow_guest=True) +@whitelist_for_tests(allow_guest=True) @site_cache(ttl=CACHE_TTL) def ping_with_ttl() -> str: register_with_external_service(frappe.local.site) diff --git a/frappe/tests/test_search.py b/frappe/tests/test_search.py index fefebccc0e..7ed7d3bf46 100644 --- a/frappe/tests/test_search.py +++ b/frappe/tests/test_search.py @@ -7,6 +7,7 @@ from functools import partial import frappe from frappe.desk.search import get_names_for_mentions, search_link, search_widget from frappe.tests import IntegrationTestCase +from frappe.tests.utils import whitelist_for_tests class TestSearch(IntegrationTestCase): @@ -186,7 +187,7 @@ def get_data(doctype, txt, searchfield, start, page_len, filters): return [doctype, txt, searchfield, start, page_len, filters] -@frappe.whitelist() +@whitelist_for_tests() @frappe.validate_and_sanitize_search_inputs def query_with_reference_doctype(doctype, txt, searchfield, start, page_len, filters, reference_doctype=None): return [] diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 9ab6da7484..ece918d0f1 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -4,21 +4,13 @@ import frappe from frappe import _ from frappe.permissions import AUTOMATIC_ROLES from frappe.tests.test_helpers import create_test_blog_category +from frappe.tests.utils import whitelist_for_tests from frappe.utils import add_to_date, now UI_TEST_USER = "frappe@example.com" -def whitelist_for_tests(fn): - if frappe.request and not (frappe._dev_server and (frappe.conf.allow_tests or os.environ.get("CI"))): - frappe.throw( # nosemgrep: frappe-missing-translate-function-python - 'Cannot run UI tests. Use a development server with "bench start" and ensure that the "allow_tests" site config is enabled.' - ) - - return frappe.whitelist()(fn) - - -@whitelist_for_tests +@whitelist_for_tests() def create_if_not_exists(doc): """Create records if they dont exist. Will check for uniqueness by checking if a record exists with these field value pairs @@ -48,7 +40,7 @@ def create_if_not_exists(doc): return names -@whitelist_for_tests +@whitelist_for_tests() def create_todo_records(): frappe.db.truncate("ToDo") @@ -84,7 +76,7 @@ def create_todo_records(): ).insert() -@whitelist_for_tests +@whitelist_for_tests() def prepare_webform_test(): for note in frappe.get_all("Note", pluck="name"): frappe.delete_doc("Note", note, force=True) @@ -92,14 +84,14 @@ def prepare_webform_test(): frappe.delete_doc_if_exists("Web Form", "note") -@whitelist_for_tests +@whitelist_for_tests() def create_doctype_for_attachment(): create_test_blog_category() doc = frappe.get_doc("Test Blog Category", "_Test Blog Category 2") return doc -@whitelist_for_tests +@whitelist_for_tests() def create_communication_record(): doc = frappe.get_doc( { @@ -113,7 +105,7 @@ def create_communication_record(): return doc -@whitelist_for_tests +@whitelist_for_tests() def setup_workflow(): from frappe.workflow.doctype.workflow.test_workflow import create_todo_workflow @@ -122,7 +114,7 @@ def setup_workflow(): frappe.clear_cache() -@whitelist_for_tests +@whitelist_for_tests() def create_contact_phone_nos_records(): if frappe.get_all("Contact", {"first_name": "Test Contact"}): return @@ -134,7 +126,7 @@ def create_contact_phone_nos_records(): doc.insert() -@whitelist_for_tests +@whitelist_for_tests() def create_doctype(name, fields): fields = frappe.parse_json(fields) if frappe.db.exists("DocType", name): @@ -152,7 +144,7 @@ def create_doctype(name, fields): ).insert() -@whitelist_for_tests +@whitelist_for_tests() def create_child_doctype(name, fields): fields = frappe.parse_json(fields) if frappe.db.exists("DocType", name): @@ -170,7 +162,7 @@ def create_child_doctype(name, fields): ).insert() -@whitelist_for_tests +@whitelist_for_tests() def create_contact_records(): if frappe.get_all("Contact", {"first_name": "Test Form Contact 1"}): return @@ -180,7 +172,7 @@ def create_contact_records(): insert_contact("Test Form Contact 3", "12345") -@whitelist_for_tests +@whitelist_for_tests() def create_multiple_todo_records(): if frappe.get_all("ToDo", {"description": "Multiple ToDo 1"}): return @@ -196,7 +188,7 @@ def insert_contact(first_name, phone_number): doc.insert() -@whitelist_for_tests +@whitelist_for_tests() def create_form_tour(): if frappe.db.exists("Form Tour", {"name": "Test Form Tour"}): return @@ -246,7 +238,7 @@ def create_form_tour(): tour.insert() -@whitelist_for_tests +@whitelist_for_tests() def create_data_for_discussions(): web_page = create_web_page("Test page for discussions", "test-page-discussions", False) create_topic_and_reply(web_page) @@ -302,7 +294,7 @@ def create_topic_and_reply(web_page): reply.save() -@whitelist_for_tests +@whitelist_for_tests() def update_webform_to_multistep(): if not frappe.db.exists("Web Form", "update-profile-duplicate"): doc = frappe.get_doc("Web Form", "edit-profile") @@ -314,7 +306,7 @@ def update_webform_to_multistep(): _doc.save() -@whitelist_for_tests +@whitelist_for_tests() def update_child_table(name): doc = frappe.get_doc("DocType", name) if len(doc.fields) == 1: @@ -332,7 +324,7 @@ def update_child_table(name): doc.save() -@whitelist_for_tests +@whitelist_for_tests() def insert_doctype_with_child_table_record(name): if frappe.get_all(name, {"title": "Test Grid Search"}): return @@ -378,7 +370,7 @@ def insert_doctype_with_child_table_record(name): doc.insert() -@whitelist_for_tests +@whitelist_for_tests() def insert_translations(): translation = [ { @@ -411,7 +403,7 @@ def insert_translations(): frappe.get_doc(doc).insert(ignore_if_duplicate=True) -@whitelist_for_tests +@whitelist_for_tests() def create_test_user(username=None): name = username or UI_TEST_USER @@ -439,7 +431,7 @@ def create_test_user(username=None): frappe.db.set_single_value("Workspace Settings", "workspace_setup_completed", 1) -@whitelist_for_tests +@whitelist_for_tests() def setup_tree_doctype(): frappe.delete_doc_if_exists("DocType", "Custom Tree", force=True) @@ -463,7 +455,7 @@ def setup_tree_doctype(): frappe.get_doc({"doctype": "Custom Tree", "tree": "All Trees"}).insert() -@whitelist_for_tests +@whitelist_for_tests() def setup_image_doctype(): frappe.delete_doc_if_exists("DocType", "Custom Image", force=True) @@ -482,7 +474,7 @@ def setup_image_doctype(): ).insert() -@whitelist_for_tests +@whitelist_for_tests() def setup_inbox(): frappe.db.delete("User Email") doc = frappe.new_doc("Email Account") @@ -494,7 +486,7 @@ def setup_inbox(): user.save() -@whitelist_for_tests +@whitelist_for_tests() def setup_default_view(view, force_reroute=None): frappe.delete_doc_if_exists("Property Setter", "Event-main-default_view") frappe.delete_doc_if_exists("Property Setter", "Event-main-force_re_route_to_default_view") @@ -525,7 +517,7 @@ def setup_default_view(view, force_reroute=None): ).insert() -@whitelist_for_tests +@whitelist_for_tests() def create_kanban(): if not frappe.db.exists("Custom Field", "Note-kanban"): frappe.get_doc( @@ -567,12 +559,12 @@ def create_kanban(): ).insert() -@whitelist_for_tests +@whitelist_for_tests() def create_todo(description): return frappe.get_doc({"doctype": "ToDo", "description": description}).insert() -@whitelist_for_tests +@whitelist_for_tests() def create_todo_with_attachment_limit(description): from frappe.custom.doctype.property_setter.property_setter import make_property_setter @@ -581,7 +573,7 @@ def create_todo_with_attachment_limit(description): return frappe.get_doc({"doctype": "ToDo", "description": description}).insert() -@whitelist_for_tests +@whitelist_for_tests() def create_admin_kanban(): if not frappe.db.exists("Kanban Board", "Admin Kanban"): frappe.get_doc( @@ -610,7 +602,7 @@ def create_admin_kanban(): ).insert() -@whitelist_for_tests +@whitelist_for_tests() def add_remove_role(action, user, role): user_doc = frappe.get_doc("User", user) if action == "remove": @@ -619,7 +611,7 @@ def add_remove_role(action, user, role): user_doc.add_roles(role) -@whitelist_for_tests +@whitelist_for_tests() def publish_realtime( event=None, message=None, @@ -640,7 +632,7 @@ def publish_realtime( ) -@whitelist_for_tests +@whitelist_for_tests() def publish_progress(duration=3, title=None, doctype=None, docname=None): # This should consider session user and only show it to current user. frappe.enqueue(slow_task, duration=duration, title=title, doctype=doctype, docname=docname) @@ -656,7 +648,7 @@ def slow_task(duration, title, doctype, docname): time.sleep(int(duration) / steps) -@whitelist_for_tests +@whitelist_for_tests() def empty_my_workspaces(): my_workspaces = frappe.get_doc("Workspace Sidebar", "My Workspaces") my_workspaces.items = [] diff --git a/frappe/tests/utils/__init__.py b/frappe/tests/utils/__init__.py index 58f80f6ecb..8e559405c8 100644 --- a/frappe/tests/utils/__init__.py +++ b/frappe/tests/utils/__init__.py @@ -1,4 +1,6 @@ import logging +import os +from functools import wraps import frappe @@ -7,6 +9,34 @@ logger = logging.Logger(__file__) from .generators import * +def whitelist_for_tests(**whitelist_kwargs): + """Decorator to whitelist test endpoints. + + Only allows access when running in test mode or running a development server with testing enabled. + Supports all parameters that @frappe.whitelist() accepts. + + Usage: + @whitelist_for_tests(allow_guest=True) + def my_guest_test_endpoint(): + ... + """ + + def decorator(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + if not ( + frappe.in_test or (frappe._dev_server and (frappe.conf.allow_tests or os.environ.get("CI"))) + ): + frappe.throw( # nosemgrep: frappe-missing-translate-function-python + 'Test endpoints are only available when running in test mode or running a development server ("bench start") with the "allow_tests" site config enabled' + ) + return fn(*args, **kwargs) + + return frappe.whitelist(**whitelist_kwargs)(wrapper) + + return decorator + + def check_orpahned_doctypes(): """Check that all doctypes in DB actually exist after patch test""" from frappe.model.base_document import get_controller