seitime-frappe/frappe/tests/test_safe_exec.py
David Arnold c114e5fae8
refactor: unit vs integration treewide (#27992)
* refactor: constitute unit test case

* fix: docs and type hints

* refactor: mark presumed integration test cases explicitly

At time of writing, we now have at least two base test classes:

- frappe.tests.UnitTestCase
- frappe.tests.IntegrationTestCase

They load in their perspective priority queue during execution.

Probably more to come for more efficient queing and scheduling.

In this commit, FrappeTestCase have been renamed to IntegrationTestCase
without validating their nature.

* feat: Move test-related functions from test_runner.py to tests/utils.py

* refactor: add bare UnitTestCase to all doctype tests

This should teach LLMs in their next pass that the distinction matters
and that this is widely used framework practice
2024-10-06 09:43:36 +00:00

129 lines
3.9 KiB
Python

import types
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils.safe_exec import ServerScriptNotEnabled, get_safe_globals, safe_exec
class TestSafeExec(IntegrationTestCase):
@classmethod
def setUpClass(cls) -> None:
cls.enable_safe_exec()
return super().setUpClass()
def test_import_fails(self):
self.assertRaises(ImportError, safe_exec, "import os")
def test_internal_attributes(self):
self.assertRaises(SyntaxError, safe_exec, "().__class__.__call__")
def test_utils(self):
_locals = dict(out=None)
safe_exec("""out = frappe.utils.cint("1")""", None, _locals)
self.assertEqual(_locals["out"], 1)
def test_safe_eval(self):
TEST_CASES = {
"1+1": 2,
'"abc" in "abl"': False,
'"a" in "abl"': True,
'"a" in ("a", "b")': True,
'"a" in {"a", "b"}': True,
'"a" in {"a": 1, "b": 2}': True,
'"a" in ["a" ,"b"]': True,
}
for code, result in TEST_CASES.items():
self.assertEqual(frappe.safe_eval(code), result)
self.assertRaises(AttributeError, frappe.safe_eval, "frappe.utils.os.path", get_safe_globals())
# Doc/dict objects
user = frappe.new_doc("User")
user.user_type = "System User"
user.enabled = 1
self.assertTrue(frappe.safe_eval("user_type == 'System User'", eval_locals=user.as_dict()))
self.assertEqual(
"System User Test", frappe.safe_eval("user_type + ' Test'", eval_locals=user.as_dict())
)
self.assertEqual(1, frappe.safe_eval("int(enabled)", eval_locals=user.as_dict()))
def test_safe_eval_wal(self):
self.assertRaises(SyntaxError, frappe.safe_eval, "(x := (40+2))")
def test_sql(self):
_locals = dict(out=None)
safe_exec(
"""out = frappe.db.sql("select name from tabDocType where name='DocType'")""", None, _locals
)
self.assertEqual(_locals["out"][0][0], "DocType")
self.assertRaises(
frappe.PermissionError, safe_exec, 'frappe.db.sql("update tabToDo set description=NULL")'
)
def test_query_builder(self):
_locals = dict(out=None)
safe_exec(
script="""out = frappe.qb.from_("User").select(frappe.qb.terms.PseudoColumn("Max(name)")).run()""",
_globals=None,
_locals=_locals,
)
self.assertEqual(frappe.db.sql("SELECT Max(name) FROM tabUser"), _locals["out"])
def test_safe_query_builder(self):
self.assertRaises(frappe.PermissionError, safe_exec, """frappe.qb.from_("User").delete().run()""")
def test_call(self):
# call non whitelisted method
self.assertRaises(frappe.PermissionError, safe_exec, """frappe.call("frappe.get_user")""")
# call whitelisted method
safe_exec("""frappe.call("ping")""")
def test_enqueue(self):
# enqueue non whitelisted method
self.assertRaises(
frappe.PermissionError, safe_exec, """frappe.enqueue("frappe.get_user", now=True)"""
)
# enqueue whitelisted method
safe_exec("""frappe.enqueue("ping", now=True)""")
def test_ensure_getattrable_globals(self):
def check_safe(objects):
for obj in objects:
if isinstance(obj, types.ModuleType | types.CodeType | types.TracebackType | types.FrameType):
self.fail(f"{obj} wont work in safe exec.")
elif isinstance(obj, dict):
check_safe(obj.values())
check_safe(get_safe_globals().values())
def test_unsafe_objects(self):
unsafe_global = {"frappe": frappe}
self.assertRaises(SyntaxError, safe_exec, """frappe.msgprint("Hello")""", unsafe_global)
def test_attrdict(self):
# jinja
frappe.render_template("{% set my_dict = _dict() %} {{- my_dict.works -}}")
# RestrictedPython
safe_exec("my_dict = _dict()")
def test_write_wrapper(self):
# Allow modifying _dict instance
safe_exec("_dict().x = 1")
# dont Allow modifying _dict class
self.assertRaises(Exception, safe_exec, "_dict.x = 1")
def test_print(self):
test_str = frappe.generate_hash()
safe_exec(f"print('{test_str}')")
self.assertEqual(frappe.local.debug_log[-1], test_str)
class TestNoSafeExec(IntegrationTestCase):
def test_safe_exec_disabled_by_default(self):
self.assertRaises(ServerScriptNotEnabled, safe_exec, "pass")