* 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
129 lines
3.9 KiB
Python
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")
|