seitime-frappe/frappe/tests/test_search.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

250 lines
7.6 KiB
Python

# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import re
from functools import partial
import frappe
from frappe.desk.search import get_names_for_mentions, search_link, search_widget
from frappe.tests import IntegrationTestCase
class TestSearch(IntegrationTestCase):
def setUp(self):
if self._testMethodName == "test_link_field_order":
setup_test_link_field_order(self)
self.addCleanup(teardown_test_link_field_order, self)
def test_search_field_sanitizer(self):
results = search_link("DocType", "User", query=None, filters=None, page_length=20, searchfield="name")
self.assertTrue("User" in results[0]["value"])
# raise exception on injection
for searchfield in (
"1=1",
"select * from tabSessions) --",
"name or (select * from tabSessions)",
"*",
";",
"select`sid`from`tabSessions`",
):
self.assertRaises(
frappe.DataError,
search_link,
"DocType",
"User",
query=None,
filters=None,
page_length=20,
searchfield=searchfield,
)
def test_only_enabled_in_mention(self):
email = "test_disabled_user_in_mentions@example.com"
frappe.delete_doc("User", email)
if not frappe.db.exists("User", email):
user = frappe.new_doc("User")
user.update(
{
"email": email,
"first_name": email.split("@", 1)[0],
"enabled": False,
"allowed_in_mentions": True,
}
)
# saved when roles are added
user.add_roles(
"System Manager",
)
names_for_mention = [user.get("id") for user in get_names_for_mentions("")]
self.assertNotIn(email, names_for_mention)
def test_link_field_order(self):
# Making a request to the search_link with the tree doctype
results = search_link(
doctype=self.tree_doctype_name,
txt="all",
query=None,
filters=None,
page_length=20,
searchfield=None,
)
# Check whether the result is sorted or not
self.assertEqual(self.parent_doctype_name, results[0]["value"])
# Check whether searching for parent also list out children
self.assertEqual(len(results), len(self.child_doctypes_names) + 1)
# Search for the word "pay", part of the word "pays" (country) in french.
def test_link_search_in_foreign_language(self):
try:
frappe.local.lang = "fr"
output = search_widget(doctype="DocType", txt="pay", page_length=20)
result = [["found" for x in y if x == "Country"] for y in output]
self.assertTrue(["found"] in result)
finally:
frappe.local.lang = "en"
def test_doctype_search_in_foreign_language(self):
def do_search(txt: str):
return search_link(
doctype="DocType",
txt=txt,
query="frappe.core.report.permitted_documents_for_user.permitted_documents_for_user.query_doctypes",
filters={"user": "Administrator"},
page_length=20,
searchfield=None,
)
try:
frappe.local.lang = "en"
results = do_search("user")
self.assertIn("User", [x["value"] for x in results])
frappe.local.lang = "fr"
results = do_search("utilisateur")
self.assertIn("User", [x["value"] for x in results])
frappe.local.lang = "de"
results = do_search("nutzer")
self.assertIn("User", [x["value"] for x in results])
finally:
frappe.local.lang = "en"
def test_validate_and_sanitize_search_inputs(self):
# should raise error if searchfield is injectable
self.assertRaises(
frappe.DataError,
get_data,
*("User", "Random", "select * from tabSessions) --", "1", "10", dict()),
)
# page_len and start should be converted to int
self.assertListEqual(
get_data("User", "Random", "email", "name or (select * from tabSessions)", "10", dict()),
["User", "Random", "email", 0, 10, {}],
)
self.assertListEqual(
get_data("User", "Random", "email", page_len="2", start="10", filters=dict()),
["User", "Random", "email", 10, 2, {}],
)
# DocType can be passed as None which should be accepted
self.assertListEqual(
get_data(None, "Random", "email", "2", "10", dict()), [None, "Random", "email", 2, 10, {}]
)
# return empty string if passed doctype is invalid
self.assertListEqual(get_data("Random DocType", "Random", "email", "2", "10", dict()), [])
# should not fail if function is called via frappe.call with extra arguments
args = ("Random DocType", "Random", "email", "2", "10", dict())
kwargs = {"as_dict": False}
self.assertListEqual(frappe.call("frappe.tests.test_search.get_data", *args, **kwargs), [])
# should not fail if query has @ symbol in it
results = search_link("User", "user@random", searchfield="name")
self.assertListEqual(results, [])
def test_reference_doctype(self):
"""search query methods should get reference_doctype if they want"""
results = search_link(
doctype="User",
txt="",
filters=None,
page_length=20,
reference_doctype="ToDo",
query="frappe.tests.test_search.query_with_reference_doctype",
)
self.assertListEqual(results, [])
def test_search_relevance(self):
frappe.db.set_value("Language", {"name": ("like", "e%")}, "enabled", 1)
search = partial(search_link, doctype="Language", filters=None, page_length=10)
for row in search(txt="e"):
self.assertTrue(row["value"].startswith("e"))
for row in search(txt="es"):
self.assertIn("es", row["value"])
# Assume that "es" is used at least 10 times, it should now be first
frappe.db.set_value("Language", "es", "idx", 10)
self.assertEqual("es", search(txt="es")[0]["value"])
def test_search_with_paren(self):
search = partial(search_link, doctype="Language", filters=None, page_length=10)
result = search(txt="(txt)")
self.assertEqual(result, [])
@frappe.validate_and_sanitize_search_inputs
def get_data(doctype, txt, searchfield, start, page_len, filters):
return [doctype, txt, searchfield, start, page_len, filters]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def query_with_reference_doctype(doctype, txt, searchfield, start, page_len, filters, reference_doctype=None):
return []
def setup_test_link_field_order(TestCase):
TestCase.tree_doctype_name = "Test Tree Order"
TestCase.child_doctype_list = []
TestCase.child_doctypes_names = ["USA", "India", "Russia", "China"]
TestCase.parent_doctype_name = "All Territories"
# Create Tree doctype
if not frappe.db.exists("DocType", TestCase.tree_doctype_name):
TestCase.tree_doc = frappe.get_doc(
{
"doctype": "DocType",
"name": TestCase.tree_doctype_name,
"module": "Custom",
"custom": 1,
"is_tree": 1,
"autoname": "field:random",
"fields": [{"fieldname": "random", "label": "Random", "fieldtype": "Data"}],
}
).insert()
TestCase.tree_doc.search_fields = "parent_test_tree_order"
TestCase.tree_doc.save()
else:
TestCase.tree_doc = frappe.get_doc("DocType", TestCase.tree_doctype_name)
# Create root for the tree doctype
if not frappe.db.exists(TestCase.tree_doctype_name, {"random": TestCase.parent_doctype_name}):
frappe.get_doc(
{"doctype": TestCase.tree_doctype_name, "random": TestCase.parent_doctype_name, "is_group": 1}
).insert(ignore_if_duplicate=True)
# Create children for the root
for child_name in TestCase.child_doctypes_names:
temp = frappe.get_doc(
{
"doctype": TestCase.tree_doctype_name,
"random": child_name,
"parent_test_tree_order": TestCase.parent_doctype_name,
}
).insert(ignore_if_duplicate=True)
TestCase.child_doctype_list.append(temp)
def teardown_test_link_field_order(TestCase):
# Deleting all the created doctype
for child_doctype in TestCase.child_doctype_list:
child_doctype.delete()
frappe.delete_doc(
TestCase.tree_doctype_name,
TestCase.parent_doctype_name,
ignore_permissions=True,
force=True,
for_reload=True,
)
TestCase.tree_doc.delete()