# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import io import json import os import sys from datetime import UTC, date, datetime, time, timedelta, timezone from decimal import ROUND_HALF_UP, Decimal, localcontext from enum import Enum from io import StringIO from mimetypes import guess_type from unittest.mock import patch from hypothesis import given from hypothesis import strategies as st from PIL import Image import frappe from frappe.installer import parse_app_name from frappe.model.document import Document from frappe.tests import IntegrationTestCase, MockedRequestTestCase, UnitTestCase from frappe.tests.utils import toggle_test_mode from frappe.utils import ( ceil, dict_to_str, execute_in_shell, floor, flt, format_timedelta, get_bench_path, get_file_timestamp, get_gravatar, get_link_to_report, get_safe_filters, get_site_info, get_sites, get_url, is_valid_iban, money_in_words, parse_and_map_trackers_from_url, parse_timedelta, random_string, remove_blanks, safe_json_loads, scrub_urls, validate_email_address, validate_name, validate_phone_number_with_country_code, validate_url, ) from frappe.utils.change_log import ( get_source_url, parse_github_url, ) from frappe.utils.data import ( add_to_date, add_trackers_to_url, add_years, cast, cint, comma_and, comma_or, compare, cstr, duration_to_seconds, evaluate_filters, expand_relative_urls, format_duration, get_datetime, get_first_day_of_week, get_time, get_timedelta, get_timespan_date_range, get_url_to_form, get_year_ending, getdate, is_invalid_date_string, map_trackers, now_datetime, nowtime, pretty_date, rounded, sha256_hash, to_timedelta, validate_python_code, ) from frappe.utils.dateutils import get_dates_from_timegrain from frappe.utils.diff import _get_value_from_version, get_version_diff, version_query from frappe.utils.identicon import Identicon from frappe.utils.image import optimize_image, strip_exif_data from frappe.utils.make_random import can_make, get_random, how_many from frappe.utils.response import json_handler from frappe.utils.synchronization import LockTimeoutError, filelock from frappe.utils.typing_validations import FrappeTypeError, validate_argument_types class Capturing(list): # ref: https://stackoverflow.com/a/16571630/10309266 def __enter__(self): self._stdout = sys.stdout sys.stdout = self._stringio = StringIO() return self def __exit__(self, *args): self.extend(self._stringio.getvalue().splitlines()) del self._stringio sys.stdout = self._stdout class TestFilters(IntegrationTestCase): def test_simple_dict(self): self.assertTrue(evaluate_filters({"doctype": "User", "status": "Open"}, {"status": "Open"})) self.assertFalse(evaluate_filters({"doctype": "User", "status": "Open"}, {"status": "Closed"})) def test_multiple_dict(self): self.assertTrue( evaluate_filters( {"doctype": "User", "status": "Open", "name": "Test 1"}, {"status": "Open", "name": "Test 1"}, ) ) self.assertFalse( evaluate_filters( {"doctype": "User", "status": "Open", "name": "Test 1"}, {"status": "Closed", "name": "Test 1"}, ) ) def test_list_filters(self): self.assertTrue( evaluate_filters( {"doctype": "User", "status": "Open", "name": "Test 1"}, [{"status": "Open"}, {"name": "Test 1"}], ) ) self.assertFalse( evaluate_filters( {"doctype": "User", "status": "Open", "name": "Test 1"}, [{"status": "Open"}, {"name": "Test 2"}], ) ) def test_list_filters_as_list(self): self.assertTrue( evaluate_filters( {"doctype": "User", "status": "Open", "name": "Test 1"}, [["status", "=", "Open"], ["name", "=", "Test 1"]], ) ) self.assertFalse( evaluate_filters( {"doctype": "User", "status": "Open", "name": "Test 1"}, [["status", "=", "Open"], ["name", "=", "Test 2"]], ) ) def test_lt_gt(self): self.assertTrue( evaluate_filters( {"doctype": "User", "status": "Open", "age": 20}, {"status": "Open", "age": (">", 10)}, ) ) self.assertFalse( evaluate_filters( {"doctype": "User", "status": "Open", "age": 20}, {"status": "Open", "age": (">", 30)}, ) ) def test_date_time(self): # date fields self.assertTrue( evaluate_filters( {"doctype": "User", "birth_date": "2023-02-28"}, [("User", "birth_date", ">", "01-04-2022")], ) ) self.assertFalse( evaluate_filters( {"doctype": "User", "birth_date": "2023-02-28"}, [("User", "birth_date", "<", "28-02-2023")], ) ) # datetime fields self.assertTrue( evaluate_filters( {"doctype": "User", "last_active": "2023-02-28 15:14:56"}, [("User", "last_active", ">", "01-04-2022 00:00:00")], ) ) self.assertFalse( evaluate_filters( {"doctype": "User", "last_active": "2023-02-28 15:14:56"}, [("User", "last_active", "<", "28-02-2023 00:00:00")], ) ) def test_filter_evaluation(self): doc = { "doctype": "User", "username": "test_abc", "prefix": "startswith", "suffix": "endswith", "empty": None, "number": 0, } test_cases = [ ([["username", "like", "test"]], True), ([["username", "like", "user1"]], False), ([["username", "not like", "test"]], False), ([["username", "not like", "user1"]], True), ([["prefix", "like", "start%"]], True), ([["prefix", "not like", "end%"]], True), ([["suffix", "like", "%with"]], True), ([["suffix", "not like", "%end"]], True), ([["suffix", "is", "set"]], True), ([["suffix", "is", "not set"]], False), ([["empty", "is", "set"]], False), ([["empty", "is", "not set"]], True), ([["number", "is", "set"]], True), ] for filter, expected_result in test_cases: self.assertEqual(evaluate_filters(doc, filter), expected_result, msg=f"{filter}") def test_timespan(self): doc = { "doctype": "User", "last_password_reset_date": getdate(), } self.assertTrue(evaluate_filters(doc, [("last_password_reset_date", "Timespan", "today")])) self.assertFalse(evaluate_filters(doc, [("last_password_reset_date", "Timespan", "last year")])) doc = { "doctype": "User", "last_password_reset_date": None, } self.assertFalse(evaluate_filters(doc, [("last_password_reset_date", "Timespan", "today")])) def test_is_operator(self): """Test 'is' operator for checking if values are set or not set.""" # Test "is set" with different fieldtypes and values self.assertTrue(compare("1", "is", "set", "Int")) self.assertTrue(compare(1, "is", "set", "Int")) self.assertTrue(compare(0, "is", "set", "Int")) # 0 is considered "set" self.assertTrue(compare("hello", "is", "set", "Data")) self.assertTrue(compare(0.0, "is", "set", "Float")) # Test "is set" with unset values - None should always be "not set" regardless of fieldtype self.assertFalse(compare(None, "is", "set", "Int")) self.assertFalse(compare(None, "is", "set", "Float")) self.assertFalse(compare(None, "is", "set", "Check")) self.assertFalse(compare(None, "is", "set", "Data")) self.assertFalse(compare("", "is", "set")) self.assertFalse(compare("", "is", "set", "Data")) self.assertFalse(compare(None, "is", "set")) # Test "is not set" with set values self.assertFalse(compare("1", "is", "not set", "Int")) self.assertFalse(compare(1, "is", "not set", "Int")) self.assertFalse(compare(0, "is", "not set", "Int")) self.assertFalse(compare("hello", "is", "not set", "Data")) self.assertFalse(compare(0.0, "is", "not set", "Float")) # Test "is not set" with unset values - None should always be "not set" regardless of fieldtype self.assertTrue(compare(None, "is", "not set", "Int")) self.assertTrue(compare(None, "is", "not set", "Float")) self.assertTrue(compare(None, "is", "not set", "Check")) self.assertTrue(compare(None, "is", "not set", "Data")) self.assertTrue(compare("", "is", "not set")) self.assertTrue(compare("", "is", "not set", "Data")) self.assertTrue(compare(None, "is", "not set")) def test_in_operators(self): """Test 'in' and 'not in' operators with and without fieldtype casting.""" test_list = ["a", "b", "c"] # Test "in" operator without fieldtype self.assertTrue(compare("a", "in", test_list)) self.assertFalse(compare("", "in", test_list)) self.assertFalse(compare("d", "in", test_list)) self.assertFalse(compare(None, "in", test_list)) # Test "not in" operator without fieldtype self.assertFalse(compare("a", "not in", test_list)) self.assertTrue(compare("", "not in", test_list)) self.assertTrue(compare("d", "not in", test_list)) self.assertTrue(compare(None, "not in", test_list)) # Test "in" operator with fieldtype casting - only first value should be cast string_list = ["1", "2", "3"] self.assertTrue(compare(1, "in", string_list, "Data")) self.assertTrue(compare("2", "in", string_list, "Data")) self.assertFalse(compare(4, "in", string_list, "Data")) # Test type mismatch: Int fieldtype with string list (val2 is NOT cast) mixed_list = ["1", "2", "3"] self.assertFalse(compare("1", "in", mixed_list, "Int")) self.assertFalse(compare(1, "in", mixed_list, "Int")) # Test with matching types: Int fieldtype with int list int_list = [1, 2, 3] self.assertTrue(compare("1", "in", int_list, "Int")) self.assertTrue(compare(2, "in", int_list, "Int")) self.assertFalse(compare("4", "in", int_list, "Int")) # Test "not in" operator with fieldtype casting self.assertFalse(compare(1, "not in", string_list, "Data")) self.assertFalse(compare("2", "not in", string_list, "Data")) self.assertTrue(compare(4, "not in", string_list, "Data")) # Test "not in" with type mismatch self.assertTrue(compare("1", "not in", mixed_list, "Int")) self.assertFalse(compare("1", "not in", int_list, "Int")) # Test with Float fieldtype float_list = [1.5, 2.5, 3.5] self.assertTrue(compare("1.5", "in", float_list, "Float")) self.assertFalse(compare("4.5", "in", float_list, "Float")) # Test None with "in"/"not in" operators - None should not be cast self.assertFalse(compare(None, "in", [""], "Data")) self.assertFalse(compare(None, "in", [0], "Int")) self.assertFalse(compare(None, "in", [0.0], "Float")) self.assertFalse(compare(None, "in", ["", "test"], "Data")) self.assertTrue(compare(None, "in", [None, "test"], "Data")) # Test "not in" with None self.assertTrue(compare(None, "not in", [""], "Data")) self.assertTrue(compare(None, "not in", [0], "Int")) self.assertTrue(compare(None, "not in", [0.0], "Float")) self.assertTrue(compare(None, "not in", ["", "test"], "Data")) self.assertFalse(compare(None, "not in", [None, "test"], "Data")) def test_is_operator_case_insensitive(self): """Test that 'is' operator patterns are case insensitive.""" self.assertTrue(compare("value", "is", "SET")) self.assertTrue(compare("value", "is", "Set")) self.assertTrue(compare("value", "is", "set")) self.assertTrue(compare(None, "is", "NOT SET")) self.assertTrue(compare(None, "is", "Not Set")) self.assertTrue(compare(None, "is", "not set")) def test_get_link_to_report_with_between_filter(self): filters = { "creation": [["between", ["2024-01-01", "2024-12-31"]]], } link = get_link_to_report(name="ToDo", filters=filters) self.assertIn('creation=["between",["2024-01-01","2024-12-31"]]', link) def test_get_safe_filters_preserves_scientific_notation_docnames(self): self.assertEqual(get_safe_filters("3E002"), "3E002") self.assertEqual(get_safe_filters("1E5"), "1E5") self.assertEqual(get_safe_filters("2e10"), "2e10") self.assertEqual(get_safe_filters("1.5"), "1.5") self.assertEqual(get_safe_filters("Infinity"), "Infinity") self.assertEqual(get_safe_filters("NaN"), "NaN") def test_get_safe_filters_still_parses_json(self): self.assertEqual(get_safe_filters('{"name": "ABC"}'), {"name": "ABC"}) self.assertEqual(get_safe_filters('[["name", "=", "ABC"]]'), [["name", "=", "ABC"]]) def test_get_safe_filters_passes_through_non_strings(self): self.assertEqual(get_safe_filters({"name": "ABC"}), {"name": "ABC"}) self.assertEqual(get_safe_filters([["name", "=", "ABC"]]), [["name", "=", "ABC"]]) self.assertIsNone(get_safe_filters(None)) class TestMoney(IntegrationTestCase): def test_money_in_words(self): test_cases = { "BHD": [ (5000, "BHD Five Thousand only."), (5000.0, "BHD Five Thousand only."), (0.1, "One Hundred Fils only."), (0, "BHD Zero only."), ("Fail", ""), ], "NGN": [ (5000, "NGN Five Thousand only."), (5000.0, "NGN Five Thousand only."), (0.1, "Ten Kobo only."), (0, "NGN Zero only."), ("Fail", ""), ], "MRO": [ (5000, "MRO Five Thousand only."), (5000.0, "MRO Five Thousand only."), (1.4, "MRO One and Two Khoums only."), (0.2, "One Khoums only."), (0, "MRO Zero only."), ("Fail", ""), ], } for currency, cases in test_cases.items(): for money, expected_words in cases: words = money_in_words(money, currency) self.assertEqual( words, expected_words, f"{words} is not the same as {expected_words}", ) def test_money_in_words_without_fraction(self): # VND doesn't have fractions words = money_in_words("42.01", "VND") self.assertEqual(words, "VND Forty Two only.") class TestDataManipulation(IntegrationTestCase): def test_scrub_urls(self): html = """

You have a new message from: John

Hey, wassup!

Test link 1 Test link 2 Test link 3
Please mail us at email
""" html = scrub_urls(html) url = get_url() self.assertTrue('Test link 1' in html) self.assertTrue(f'Test link 2' in html) self.assertTrue(f'Test link 3' in html) self.assertTrue(f'' in html) self.assertTrue(f"style=\"background-image: url('{url}/assets/frappe/bg.jpg') !important\"" in html) self.assertTrue('email' in html) class TestFieldCasting(IntegrationTestCase): def test_str_types(self): STR_TYPES = ( "Data", "Text", "Small Text", "Long Text", "Text Editor", "Select", "Link", "Dynamic Link", ) for fieldtype in STR_TYPES: self.assertIsInstance(cast(fieldtype, value=None), str) self.assertIsInstance(cast(fieldtype, value="12-12-2021"), str) self.assertIsInstance(cast(fieldtype, value=""), str) self.assertIsInstance(cast(fieldtype, value=[]), str) self.assertIsInstance(cast(fieldtype, value=set()), str) def test_float_types(self): FLOAT_TYPES = ("Currency", "Float", "Percent") for fieldtype in FLOAT_TYPES: self.assertIsInstance(cast(fieldtype, value=None), float) self.assertIsInstance(cast(fieldtype, value=1.12), float) self.assertIsInstance(cast(fieldtype, value=112), float) def test_int_types(self): INT_TYPES = ("Int", "Check") for fieldtype in INT_TYPES: self.assertIsInstance(cast(fieldtype, value=None), int) self.assertIsInstance(cast(fieldtype, value=1.12), int) self.assertIsInstance(cast(fieldtype, value=112), int) def test_datetime_types(self): self.assertIsInstance(cast("Datetime", value=None), datetime) self.assertIsInstance(cast("Datetime", value="12-2-22"), datetime) def test_date_types(self): self.assertIsInstance(cast("Date", value=None), date) self.assertIsInstance(cast("Date", value="12-12-2021"), date) def test_time_types(self): self.assertIsInstance(cast("Time", value=None), timedelta) self.assertIsInstance(cast("Time", value="12:03:34"), timedelta) class TestMathUtils(IntegrationTestCase): def test_floor(self): from decimal import Decimal self.assertEqual(floor(2), 2) self.assertEqual(floor(12.32904), 12) self.assertEqual(floor(22.7330), 22) self.assertEqual(floor("24.7"), 24) self.assertEqual(floor("26.7"), 26) self.assertEqual(floor(Decimal("29.45")), 29) def test_ceil(self): from decimal import Decimal self.assertEqual(ceil(2), 2) self.assertEqual(ceil(12.32904), 13) self.assertEqual(ceil(22.7330), 23) self.assertEqual(ceil("24.7"), 25) self.assertEqual(ceil("26.7"), 27) self.assertEqual(ceil(Decimal("29.45")), 30) class TestHTMLUtils(IntegrationTestCase): def test_clean_email_html(self): from frappe.utils.html_utils import clean_email_html sample = """

Hello

Para

""" clean = clean_email_html(sample) self.assertFalse("") message = frappe.get_message_log()[-1] self.assertNotIn("script", message.message) frappe.msgprint("") message = frappe.get_message_log()[-1] self.assertIn("