1681 lines
55 KiB
Python
1681 lines
55 KiB
Python
# 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_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)
|
|
|
|
|
|
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 = """
|
|
<p>You have a new message from: <b>John</b></p>
|
|
<p>Hey, wassup!</p>
|
|
<div class="more-info">
|
|
<a href="http://test.com">Test link 1</a>
|
|
<a href="/about">Test link 2</a>
|
|
<a href="login">Test link 3</a>
|
|
<img src="/assets/frappe/test.jpg">
|
|
</div>
|
|
<div style="background-image: url('/assets/frappe/bg.jpg')">
|
|
Please mail us at <a href="mailto:test@example.com">email</a>
|
|
</div>
|
|
"""
|
|
|
|
html = scrub_urls(html)
|
|
url = get_url()
|
|
|
|
self.assertTrue('<a href="http://test.com">Test link 1</a>' in html)
|
|
self.assertTrue(f'<a href="{url}/about">Test link 2</a>' in html)
|
|
self.assertTrue(f'<a href="{url}/login">Test link 3</a>' in html)
|
|
self.assertTrue(f'<img src="{url}/assets/frappe/test.jpg">' in html)
|
|
self.assertTrue(f"style=\"background-image: url('{url}/assets/frappe/bg.jpg') !important\"" in html)
|
|
self.assertTrue('<a href="mailto:test@example.com">email</a>' 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 = """<script>a=b</script><h1>Hello</h1><p>Para</p>"""
|
|
clean = clean_email_html(sample)
|
|
self.assertFalse("<script>" in clean)
|
|
self.assertTrue("<h1>Hello</h1>" in clean)
|
|
|
|
sample = """<style>body { font-family: Arial }</style><h1>Hello</h1><p>Para</p>"""
|
|
clean = clean_email_html(sample)
|
|
self.assertFalse("<style>" in clean)
|
|
self.assertTrue("<h1>Hello</h1>" in clean)
|
|
|
|
sample = """<h1>Hello</h1><p>Para</p><a href="http://test.com">text</a>"""
|
|
clean = clean_email_html(sample)
|
|
self.assertTrue("<h1>Hello</h1>" in clean)
|
|
self.assertTrue('<a href="http://test.com" rel="noopener noreferrer">text</a>' in clean)
|
|
|
|
def test_sanitize_html(self):
|
|
from frappe.utils.html_utils import sanitize_html
|
|
|
|
clean = sanitize_html("<ol data-list='ordered' unknown_attr='xyz'></ol>")
|
|
self.assertIn("ordered", clean)
|
|
self.assertNotIn("xyz", clean)
|
|
|
|
|
|
class TestValidationUtils(IntegrationTestCase):
|
|
def test_valid_url(self):
|
|
# Edge cases
|
|
self.assertFalse(validate_url(""))
|
|
self.assertFalse(validate_url(None))
|
|
|
|
# Valid URLs
|
|
self.assertTrue(validate_url("https://google.com"))
|
|
self.assertTrue(validate_url("http://frappe.io", throw=True))
|
|
|
|
# Invalid URLs without throw
|
|
self.assertFalse(validate_url("google.io"))
|
|
self.assertFalse(validate_url("google.io"))
|
|
|
|
# Invalid URL with throw
|
|
self.assertRaises(frappe.ValidationError, validate_url, "frappe", throw=True)
|
|
|
|
# Scheme validation
|
|
self.assertFalse(validate_url("https://google.com", valid_schemes="http"))
|
|
self.assertTrue(validate_url("ftp://frappe.cloud", valid_schemes=["https", "ftp"]))
|
|
self.assertFalse(validate_url("bolo://frappe.io", valid_schemes=("http", "https", "ftp", "ftps")))
|
|
self.assertRaises(
|
|
frappe.ValidationError,
|
|
validate_url,
|
|
"gopher://frappe.io",
|
|
valid_schemes="https",
|
|
throw=True,
|
|
)
|
|
|
|
def test_valid_email(self):
|
|
# Edge cases
|
|
self.assertFalse(validate_email_address(""))
|
|
self.assertFalse(validate_email_address(None))
|
|
|
|
# Valid addresses
|
|
self.assertTrue(validate_email_address("someone@frappe.com"))
|
|
self.assertTrue(validate_email_address("someone@frappe.com, anyone@frappe.io"))
|
|
self.assertTrue(validate_email_address("test%201@frappe.com"))
|
|
|
|
# Invalid address
|
|
self.assertFalse(validate_email_address("someone"))
|
|
self.assertFalse(validate_email_address("someone@----.com"))
|
|
self.assertFalse(validate_email_address("test 1@frappe.com"))
|
|
self.assertFalse(validate_email_address("test@example.com test2@example.com,undisclosed-recipient"))
|
|
|
|
# Invalid with throw
|
|
self.assertRaises(
|
|
frappe.InvalidEmailAddressError,
|
|
validate_email_address,
|
|
"someone.com",
|
|
throw=True,
|
|
)
|
|
|
|
self.assertEqual(validate_email_address("Some%20One@frappe.com"), "Some%20One@frappe.com")
|
|
self.assertEqual(
|
|
validate_email_address("erp+Job%20Applicant=JA00004@frappe.com"),
|
|
"erp+Job%20Applicant=JA00004@frappe.com",
|
|
)
|
|
|
|
# RFC 5322 format - Display name with comma (main bug fix)
|
|
self.assertEqual(
|
|
validate_email_address('"Lastname, Firstname" <test@example.com>'), "test@example.com"
|
|
)
|
|
self.assertEqual(validate_email_address('"Doe, John" <john.doe@example.com>'), "john.doe@example.com")
|
|
|
|
# RFC 5322 format - Display name without comma
|
|
self.assertEqual(validate_email_address("Test User <test@example.com>"), "test@example.com")
|
|
|
|
# RFC 5322 format - Multiple emails
|
|
self.assertEqual(
|
|
validate_email_address('"Last, First" <test1@example.com>, "Another, Name" <test2@example.com>'),
|
|
"test1@example.com, test2@example.com",
|
|
)
|
|
|
|
# RFC 5322 format - Mixed with plain emails
|
|
self.assertEqual(
|
|
validate_email_address("Test User <test@example.com>, plain@example.com"),
|
|
"test@example.com, plain@example.com",
|
|
)
|
|
|
|
# Emails with newlines
|
|
self.assertEqual(
|
|
validate_email_address("test1@example.com\ntest2@example.com"),
|
|
"test1@example.com, test2@example.com",
|
|
)
|
|
|
|
# Undisclosed recipients should be filtered
|
|
self.assertEqual(validate_email_address("undisclosed-recipients:;"), "")
|
|
self.assertEqual(
|
|
validate_email_address("test@example.com, undisclosed-recipients:;"), "test@example.com"
|
|
)
|
|
|
|
def test_valid_phone(self):
|
|
valid_phones = ["+91 1234567890", ""]
|
|
|
|
for phone in valid_phones:
|
|
validate_phone_number_with_country_code(phone, "field")
|
|
self.assertRaises(
|
|
frappe.InvalidPhoneNumberError,
|
|
validate_phone_number_with_country_code,
|
|
"+420 1234567890",
|
|
"field",
|
|
)
|
|
|
|
def test_validate_name(self):
|
|
valid_names = ["", "abc", "asd a13", "asd-asd"]
|
|
for name in valid_names:
|
|
validate_name(name, True)
|
|
|
|
invalid_names = ["asd$wat", "asasd/ads"]
|
|
for name in invalid_names:
|
|
self.assertRaises(frappe.InvalidNameError, validate_name, name, True)
|
|
|
|
def test_validate_iban(self):
|
|
valid_ibans = [
|
|
"GB82 WEST 1234 5698 7654 32",
|
|
"DE91 1000 0000 0123 4567 89",
|
|
"FR76 3000 6000 0112 3456 7890 189",
|
|
]
|
|
|
|
invalid_ibans = [
|
|
# wrong checksum (3rd place)
|
|
"GB72 WEST 1234 5698 7654 32",
|
|
"DE81 1000 0000 0123 4567 89",
|
|
"FR66 3000 6000 0112 3456 7890 189",
|
|
]
|
|
|
|
for iban in valid_ibans:
|
|
self.assertTrue(is_valid_iban(iban))
|
|
|
|
for not_iban in invalid_ibans:
|
|
self.assertFalse(is_valid_iban(not_iban))
|
|
|
|
|
|
class TestImage(IntegrationTestCase):
|
|
def test_strip_exif_data(self):
|
|
original_image = Image.open(frappe.get_app_path("frappe", "tests", "data", "exif_sample_image.jpg"))
|
|
original_image_content = open(
|
|
frappe.get_app_path("frappe", "tests", "data", "exif_sample_image.jpg"),
|
|
mode="rb",
|
|
).read()
|
|
|
|
new_image_content = strip_exif_data(original_image_content, "image/jpeg")
|
|
new_image = Image.open(io.BytesIO(new_image_content))
|
|
|
|
self.assertEqual(new_image._getexif(), None)
|
|
self.assertNotEqual(original_image._getexif(), new_image._getexif())
|
|
|
|
def test_optimize_image(self):
|
|
image_file_path = frappe.get_app_path("frappe", "tests", "data", "sample_image_for_optimization.jpg")
|
|
content_type = guess_type(image_file_path)[0]
|
|
original_content = open(image_file_path, mode="rb").read()
|
|
|
|
optimized_content = optimize_image(original_content, content_type, max_width=500, max_height=500)
|
|
optimized_image = Image.open(io.BytesIO(optimized_content))
|
|
width, height = optimized_image.size
|
|
|
|
self.assertLessEqual(width, 500)
|
|
self.assertLessEqual(height, 500)
|
|
self.assertLess(len(optimized_content), len(original_content))
|
|
|
|
|
|
class TestPythonExpressions(IntegrationTestCase):
|
|
def test_validation_for_good_python_expression(self):
|
|
valid_expressions = [
|
|
"foo == bar",
|
|
"foo == 42",
|
|
"password != 'hunter2'",
|
|
"complex != comparison and more_complex == condition",
|
|
"escaped_values == 'str with newline\\n'",
|
|
"check_box_field",
|
|
]
|
|
for expr in valid_expressions:
|
|
try:
|
|
validate_python_code(expr)
|
|
except Exception as e:
|
|
self.fail(f"Invalid error thrown for valid expression: {expr}: {e!s}")
|
|
|
|
def test_validation_for_bad_python_expression(self):
|
|
invalid_expressions = [
|
|
"these_are && js_conditions",
|
|
"more || js_conditions",
|
|
"curly_quotes_bad == “const”",
|
|
"oops = forgot_equals",
|
|
]
|
|
for expr in invalid_expressions:
|
|
self.assertRaises(frappe.ValidationError, validate_python_code, expr)
|
|
|
|
|
|
class TestDiffUtils(IntegrationTestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.doc = frappe.get_doc(doctype="Client Script", dt="Client Script", name="test_client_script")
|
|
cls.doc.insert()
|
|
cls.doc.script = "2;"
|
|
cls.doc.save(ignore_version=False)
|
|
cls.doc.script = "42;"
|
|
cls.doc.save(ignore_version=False)
|
|
|
|
cls.versions = version_query(
|
|
doctype="Version",
|
|
txt="",
|
|
searchfield="name",
|
|
start=0,
|
|
page_len=20,
|
|
filters={"ref_doctype": cls.doc.doctype, "docname": cls.doc.name},
|
|
)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.doc.delete()
|
|
|
|
def test_version_query(self):
|
|
self.assertGreaterEqual(len(self.versions), 2)
|
|
|
|
def test_get_field_value_from_version(self):
|
|
latest_version = self.versions[0][0]
|
|
self.assertEqual("42;", _get_value_from_version(latest_version, fieldname="script")[0])
|
|
old_version = self.versions[1][0]
|
|
self.assertEqual("2;", _get_value_from_version(old_version, fieldname="script")[0])
|
|
|
|
def test_get_version_diff(self):
|
|
old_version = self.versions[1][0]
|
|
latest_version = self.versions[0][0]
|
|
|
|
diff = get_version_diff(old_version, latest_version)
|
|
self.assertIn("-2;", diff)
|
|
self.assertIn("+42;", diff)
|
|
|
|
|
|
class TestDateUtils(IntegrationTestCase):
|
|
def test_first_day_of_week(self):
|
|
# Monday as start of the week
|
|
with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"):
|
|
self.assertEqual(
|
|
frappe.utils.get_first_day_of_week("2020-12-25"),
|
|
frappe.utils.getdate("2020-12-21"),
|
|
)
|
|
self.assertEqual(
|
|
frappe.utils.get_first_day_of_week("2020-12-20"),
|
|
frappe.utils.getdate("2020-12-14"),
|
|
)
|
|
|
|
# Sunday as start of the week
|
|
self.assertEqual(
|
|
frappe.utils.get_first_day_of_week("2020-12-25"),
|
|
frappe.utils.getdate("2020-12-20"),
|
|
)
|
|
self.assertEqual(
|
|
frappe.utils.get_first_day_of_week("2020-12-21"),
|
|
frappe.utils.getdate("2020-12-20"),
|
|
)
|
|
|
|
def test_last_day_of_week(self):
|
|
self.assertEqual(
|
|
frappe.utils.get_last_day_of_week("2020-12-24"),
|
|
frappe.utils.getdate("2020-12-26"),
|
|
)
|
|
self.assertEqual(
|
|
frappe.utils.get_last_day_of_week("2020-12-28"),
|
|
frappe.utils.getdate("2021-01-02"),
|
|
)
|
|
|
|
def test_is_last_day_of_the_month(self):
|
|
self.assertEqual(frappe.utils.is_last_day_of_the_month("2020-12-24"), False)
|
|
self.assertEqual(frappe.utils.is_last_day_of_the_month("2020-12-31"), True)
|
|
|
|
def test_get_time(self):
|
|
datetime_input = now_datetime()
|
|
timedelta_input = get_timedelta()
|
|
time_input = nowtime()
|
|
|
|
self.assertIsInstance(get_time(datetime_input), time)
|
|
self.assertIsInstance(get_time(timedelta_input), time)
|
|
self.assertIsInstance(get_time(time_input), time)
|
|
self.assertIsInstance(get_time("100:2:12"), time)
|
|
self.assertIsInstance(get_time(str(datetime_input)), time)
|
|
self.assertIsInstance(get_time(str(timedelta_input)), time)
|
|
self.assertIsInstance(get_time(str(time_input)), time)
|
|
|
|
def test_get_timedelta(self):
|
|
datetime_input = now_datetime()
|
|
timedelta_input = get_timedelta()
|
|
time_input = nowtime()
|
|
|
|
self.assertIsInstance(get_timedelta(), timedelta)
|
|
self.assertIsInstance(get_timedelta("100:2:12"), timedelta)
|
|
self.assertIsInstance(get_timedelta("17:21:00"), timedelta)
|
|
self.assertIsInstance(get_timedelta("2012-01-19 17:21:00"), timedelta)
|
|
self.assertIsInstance(get_timedelta(str(datetime_input)), timedelta)
|
|
self.assertIsInstance(get_timedelta(str(timedelta_input)), timedelta)
|
|
self.assertIsInstance(get_timedelta(str(time_input)), timedelta)
|
|
self.assertIsInstance(get_timedelta(get_timedelta("100:2:12")), timedelta)
|
|
|
|
def test_to_timedelta(self):
|
|
self.assertEqual(to_timedelta("00:00:01"), timedelta(seconds=1))
|
|
self.assertEqual(to_timedelta("10:00:01"), timedelta(seconds=1, hours=10))
|
|
self.assertEqual(to_timedelta(time(hour=2)), timedelta(hours=2))
|
|
|
|
def test_add_date_utils(self):
|
|
self.assertEqual(add_years(datetime(2020, 1, 1), 1), datetime(2021, 1, 1))
|
|
|
|
def test_duration_to_sec(self):
|
|
self.assertEqual(duration_to_seconds("3h 34m 45s"), 12885)
|
|
self.assertEqual(duration_to_seconds("1h"), 3600)
|
|
self.assertEqual(duration_to_seconds("110m"), 110 * 60)
|
|
self.assertEqual(duration_to_seconds("110m"), 110 * 60)
|
|
|
|
def test_format_duration(self):
|
|
# Basic positive durations
|
|
self.assertEqual(format_duration(0), "")
|
|
self.assertEqual(format_duration(45.7), "45s")
|
|
self.assertEqual(format_duration(90.9), "1m 30s")
|
|
self.assertEqual(format_duration(3600), "1h")
|
|
self.assertEqual(format_duration("12885"), "3h 34m 45s")
|
|
self.assertEqual(format_duration(86400), "1d")
|
|
self.assertEqual(format_duration(86401), "1d 1s")
|
|
|
|
# Negative durations
|
|
self.assertEqual(format_duration(-45.3), "-45s")
|
|
self.assertEqual(format_duration(-12885), "-3h 34m 45s")
|
|
|
|
# hide_days parameter
|
|
self.assertEqual(format_duration(86400, hide_days=True), "24h")
|
|
self.assertEqual(format_duration(90061, hide_days=True), "25h 1m 1s")
|
|
|
|
def test_get_timespan_date_range(self):
|
|
supported_timespans = [
|
|
"last week",
|
|
"last month",
|
|
"last quarter",
|
|
"last 6 months",
|
|
"last year",
|
|
"yesterday",
|
|
"today",
|
|
"tomorrow",
|
|
"this week",
|
|
"this month",
|
|
"this quarter",
|
|
"this year",
|
|
"next week",
|
|
"next month",
|
|
"next quarter",
|
|
"next 6 months",
|
|
"next year",
|
|
]
|
|
|
|
for ts in supported_timespans:
|
|
res = get_timespan_date_range(ts)
|
|
self.assertEqual(len(res), 2)
|
|
|
|
# Manual type checking eh?
|
|
self.assertIsInstance(res[0], date)
|
|
self.assertIsInstance(res[1], date)
|
|
|
|
def test_timesmap_utils(self):
|
|
self.assertEqual(get_year_ending(date(2021, 1, 1)), date(2021, 12, 31))
|
|
self.assertEqual(get_year_ending(date(2021, 1, 31)), date(2021, 12, 31))
|
|
|
|
@given(st.datetimes())
|
|
def test_get_datetime(self, original):
|
|
if is_invalid_date_string(str(original)):
|
|
return
|
|
parsed = get_datetime(str(original))
|
|
self.assertEqual(parsed, original)
|
|
|
|
@given(st.datetimes(timezones=st.timezones()))
|
|
def test_get_datetime_tz_aware(self, original):
|
|
if is_invalid_date_string(str(original)):
|
|
return
|
|
parsed = get_datetime(str(original))
|
|
self.assertEqual(parsed, original)
|
|
|
|
def test_pretty_date(self):
|
|
from frappe import _
|
|
|
|
now = get_datetime()
|
|
|
|
test_cases = {
|
|
now: _("1 second ago"),
|
|
add_to_date(now, minutes=-1): _("1 minute ago"),
|
|
add_to_date(now, minutes=-3): _("3 minutes ago"),
|
|
add_to_date(now, hours=-1): _("1 hour ago"),
|
|
add_to_date(now, hours=-2): _("2 hours ago"),
|
|
add_to_date(now, days=-1): _("1 day ago"),
|
|
add_to_date(now, days=-5): _("5 days ago"),
|
|
add_to_date(now, days=-8): _("1 week ago"),
|
|
add_to_date(now, days=-14): _("2 weeks ago"),
|
|
add_to_date(now, days=-32): _("1 month ago"),
|
|
add_to_date(now, days=-32 * 2): _("2 months ago"),
|
|
add_to_date(now, years=-1, days=-5): _("1 year ago"),
|
|
add_to_date(now, years=-2, days=-10): _("2 years ago"),
|
|
}
|
|
|
|
for dt, exp_message in test_cases.items():
|
|
self.assertEqual(pretty_date(dt), exp_message)
|
|
|
|
self.assertEqual(pretty_date(add_to_date(now, days=-5), mini=True), "5d")
|
|
|
|
def test_date_from_timegrain(self):
|
|
start_date = getdate("2021-01-01")
|
|
|
|
daily = get_dates_from_timegrain(start_date, add_to_date(start_date, days=6), "Daily")
|
|
self.assertEqual(len(daily), 7)
|
|
for idx, d in enumerate(daily):
|
|
self.assertEqual(d, add_to_date(start_date, days=idx))
|
|
|
|
start = get_first_day_of_week(start_date)
|
|
end = add_to_date(add_to_date(start, weeks=52), days=-1)
|
|
weekly = get_dates_from_timegrain(start, end, "Weekly")
|
|
self.assertEqual(len(weekly), 52)
|
|
for idx, d in enumerate(weekly, start=1):
|
|
self.assertEqual(d, add_to_date(start, days=7 * idx - 1))
|
|
|
|
quarterly = get_dates_from_timegrain(start_date, add_to_date(start_date, months=5), "Quarterly")
|
|
self.assertEqual(len(quarterly), 2)
|
|
for idx, d in enumerate(quarterly, start=1):
|
|
self.assertEqual(d, add_to_date(start_date, months=idx * 3, days=-1))
|
|
|
|
yearly = get_dates_from_timegrain(start_date, add_to_date(start_date, years=2), "Yearly")
|
|
self.assertEqual(len(yearly), 3)
|
|
for idx, d in enumerate(yearly, start=1):
|
|
self.assertEqual(d, add_to_date(start_date, years=idx, days=-1))
|
|
|
|
|
|
class TestResponse(IntegrationTestCase):
|
|
def test_json_handler(self):
|
|
class TEST(Enum):
|
|
ABC = "!@)@)!"
|
|
BCE = "ENJD"
|
|
|
|
GOOD_OBJECT = {
|
|
"time_types": [
|
|
date(year=2020, month=12, day=2),
|
|
datetime(
|
|
year=2020,
|
|
month=12,
|
|
day=2,
|
|
hour=23,
|
|
minute=23,
|
|
second=23,
|
|
microsecond=23,
|
|
tzinfo=UTC,
|
|
),
|
|
time(hour=23, minute=23, second=23, microsecond=23, tzinfo=UTC),
|
|
timedelta(days=10, hours=12, minutes=120, seconds=10),
|
|
],
|
|
"float": [
|
|
Decimal("29.21"),
|
|
],
|
|
"doc": [
|
|
frappe.get_doc("System Settings"),
|
|
],
|
|
"iter": [
|
|
{1, 2, 3},
|
|
(1, 2, 3),
|
|
"abcdef",
|
|
],
|
|
"string": "abcdef",
|
|
}
|
|
|
|
BAD_OBJECT = {"Enum": TEST}
|
|
|
|
processed_object = json.loads(json.dumps(GOOD_OBJECT, default=json_handler))
|
|
|
|
self.assertTrue(all([isinstance(x, str) for x in processed_object["time_types"]]))
|
|
self.assertTrue(all([isinstance(x, float) for x in processed_object["float"]]))
|
|
self.assertTrue(all([isinstance(x, list | str) for x in processed_object["iter"]]))
|
|
self.assertIsInstance(processed_object["string"], str)
|
|
with self.assertRaises(TypeError):
|
|
json.dumps(BAD_OBJECT, default=json_handler)
|
|
|
|
|
|
class TestTimeDeltaUtils(IntegrationTestCase):
|
|
def test_format_timedelta(self):
|
|
self.assertEqual(format_timedelta(timedelta(seconds=0)), "0:00:00")
|
|
self.assertEqual(format_timedelta(timedelta(hours=10)), "10:00:00")
|
|
self.assertEqual(format_timedelta(timedelta(hours=100)), "100:00:00")
|
|
self.assertEqual(format_timedelta(timedelta(seconds=100, microseconds=129)), "0:01:40.000129")
|
|
self.assertEqual(
|
|
format_timedelta(timedelta(seconds=100, microseconds=12212199129)),
|
|
"3:25:12.199129",
|
|
)
|
|
|
|
def test_parse_timedelta(self):
|
|
self.assertEqual(parse_timedelta("0:0:0"), timedelta(seconds=0))
|
|
self.assertEqual(parse_timedelta("10:0:0"), timedelta(hours=10))
|
|
self.assertEqual(
|
|
parse_timedelta("7 days, 0:32:18.192221"),
|
|
timedelta(days=7, seconds=1938, microseconds=192221),
|
|
)
|
|
self.assertEqual(parse_timedelta("7 days, 0:32:18"), timedelta(days=7, seconds=1938))
|
|
|
|
|
|
class TestXlsxUtils(IntegrationTestCase):
|
|
def test_unescape(self):
|
|
from frappe.utils.xlsxutils import handle_html
|
|
|
|
val = handle_html("<p>html data ></p>")
|
|
self.assertIn("html data >", val)
|
|
self.assertEqual("abc", handle_html("abc"))
|
|
|
|
|
|
class TestLinkTitle(IntegrationTestCase):
|
|
def test_link_title_doctypes_in_boot_info(self):
|
|
"""
|
|
Test that doctypes are added to link_title_map in boot_info
|
|
"""
|
|
custom_doctype = frappe.get_doc(
|
|
{
|
|
"doctype": "DocType",
|
|
"module": "Core",
|
|
"custom": 1,
|
|
"fields": [
|
|
{
|
|
"label": "Test Field",
|
|
"fieldname": "test_title_field",
|
|
"fieldtype": "Data",
|
|
}
|
|
],
|
|
"show_title_field_in_link": 1,
|
|
"title_field": "test_title_field",
|
|
"permissions": [{"role": "System Manager", "read": 1}],
|
|
"name": "Test Custom Doctype for Link Title",
|
|
}
|
|
)
|
|
custom_doctype.insert()
|
|
|
|
prop_setter = frappe.get_doc(
|
|
{
|
|
"doctype": "Property Setter",
|
|
"doc_type": "User",
|
|
"property": "show_title_field_in_link",
|
|
"property_type": "Check",
|
|
"doctype_or_field": "DocType",
|
|
"value": "1",
|
|
}
|
|
).insert()
|
|
|
|
from frappe.boot import get_link_title_doctypes
|
|
|
|
link_title_doctypes = get_link_title_doctypes()
|
|
self.assertTrue("User" in link_title_doctypes)
|
|
self.assertTrue("Test Custom Doctype for Link Title" in link_title_doctypes)
|
|
|
|
prop_setter.delete()
|
|
custom_doctype.delete()
|
|
|
|
def test_link_titles_on_getdoc(self):
|
|
"""
|
|
Test that link titles are added to the doctype on getdoc
|
|
"""
|
|
prop_setter = frappe.get_doc(
|
|
{
|
|
"doctype": "Property Setter",
|
|
"doc_type": "User",
|
|
"property": "show_title_field_in_link",
|
|
"property_type": "Check",
|
|
"doctype_or_field": "DocType",
|
|
"value": "1",
|
|
}
|
|
).insert()
|
|
|
|
user = frappe.get_doc(
|
|
{
|
|
"doctype": "User",
|
|
"user_type": "Website User",
|
|
"email": "test_user_for_link_title@example.com",
|
|
"send_welcome_email": 0,
|
|
"first_name": "Test User for Link Title",
|
|
}
|
|
).insert(ignore_permissions=True)
|
|
|
|
todo = frappe.get_doc(
|
|
{
|
|
"doctype": "ToDo",
|
|
"description": "test-link-title-on-getdoc",
|
|
"allocated_to": user.name,
|
|
}
|
|
).insert()
|
|
|
|
from frappe.desk.form.load import getdoc
|
|
|
|
getdoc("ToDo", todo.name)
|
|
link_titles = frappe.local.response["_link_titles"]
|
|
|
|
self.assertTrue(f"{user.doctype}::{user.name}" in link_titles)
|
|
self.assertEqual(link_titles[f"{user.doctype}::{user.name}"], user.full_name)
|
|
|
|
todo.delete()
|
|
user.delete()
|
|
prop_setter.delete()
|
|
|
|
|
|
class TestAppParser(MockedRequestTestCase):
|
|
def test_app_name_parser(self):
|
|
self.responses.add(
|
|
"HEAD",
|
|
"https://api.github.com/repos/frappe/healthcare",
|
|
status=200,
|
|
json={},
|
|
)
|
|
bench_path = get_bench_path()
|
|
frappe_app = os.path.join(bench_path, "apps", "frappe")
|
|
self.assertEqual("frappe", parse_app_name(frappe_app))
|
|
self.assertEqual("healthcare", parse_app_name("healthcare"))
|
|
self.assertEqual("healthcare", parse_app_name("https://github.com/frappe/healthcare.git"))
|
|
self.assertEqual("healthcare", parse_app_name("git@github.com:frappe/healthcare.git"))
|
|
self.assertEqual("healthcare", parse_app_name("frappe/healthcare@develop"))
|
|
|
|
|
|
class TestIntrospectionMagic(IntegrationTestCase):
|
|
"""Test utils that inspect live objects"""
|
|
|
|
def test_get_newargs(self):
|
|
# `kwargs` is just convention any **varname should work.
|
|
def f(a, b=2, **args):
|
|
pass
|
|
|
|
safe_kwargs = {"company": "Wind Power", "b": 1}
|
|
self.assertEqual(frappe.get_newargs(f, safe_kwargs), safe_kwargs)
|
|
|
|
unsafe_args = dict(safe_kwargs)
|
|
unsafe_args.update({"ignore_permissions": True, "flags": {"ignore_mandatory": True}})
|
|
self.assertEqual(frappe.get_newargs(f, unsafe_args), safe_kwargs)
|
|
|
|
def test_strip_off_kwargs_when_not_supported(self):
|
|
def f(a, b=2):
|
|
pass
|
|
|
|
args = {"company": "Wind Power", "b": 1}
|
|
self.assertEqual(frappe.get_newargs(f, args), {"b": 1})
|
|
|
|
# No args
|
|
self.assertEqual(frappe.get_newargs(lambda: None, args), {})
|
|
|
|
|
|
class TestMakeRandom(IntegrationTestCase):
|
|
def test_get_random(self):
|
|
self.assertIsInstance(get_random("DocType", doc=True), Document)
|
|
self.assertIsInstance(get_random("DocType"), str)
|
|
|
|
def test_can_make(self):
|
|
self.assertIsInstance(can_make("User"), bool)
|
|
|
|
def test_how_many(self):
|
|
self.assertIsInstance(how_many("User"), int)
|
|
|
|
|
|
class TestLazyLoader(IntegrationTestCase):
|
|
def test_lazy_import_module(self):
|
|
from frappe.utils.lazy_loader import lazy_import
|
|
|
|
with Capturing() as output:
|
|
ls = lazy_import("frappe.tests.data.load_sleep")
|
|
self.assertEqual(output, [])
|
|
|
|
with Capturing() as output:
|
|
ls.time
|
|
self.assertEqual(["Module `frappe.tests.data.load_sleep` loaded"], output)
|
|
|
|
|
|
class TestIdenticon(IntegrationTestCase):
|
|
def test_get_gravatar(self):
|
|
# developers@frappe.io has a gravatar linked so str URL will be returned
|
|
toggle_test_mode(False)
|
|
gravatar_url = get_gravatar("developers@frappe.io")
|
|
toggle_test_mode(True)
|
|
self.assertIsInstance(gravatar_url, str)
|
|
self.assertTrue(gravatar_url.startswith("http"))
|
|
|
|
# random email will require Identicon to be generated, which will be a base64 string
|
|
gravatar_url = get_gravatar(f"developers{random_string(6)}@frappe.io")
|
|
self.assertIsInstance(gravatar_url, str)
|
|
self.assertTrue(gravatar_url.startswith("data:image/png;base64,"))
|
|
|
|
def test_generate_identicon(self):
|
|
identicon = Identicon(random_string(6))
|
|
with patch.object(identicon.image, "show") as show:
|
|
identicon.generate()
|
|
show.assert_called_once()
|
|
|
|
identicon_bs64 = identicon.base64()
|
|
self.assertIsInstance(identicon_bs64, str)
|
|
self.assertTrue(identicon_bs64.startswith("data:image/png;base64,"))
|
|
|
|
|
|
class TestContainerUtils(IntegrationTestCase):
|
|
def test_dict_to_str(self):
|
|
self.assertEqual(dict_to_str({"a": "b"}), "a=b")
|
|
|
|
def test_remove_blanks(self):
|
|
a = {"asd": "", "b": None, "c": "d"}
|
|
remove_blanks(a)
|
|
self.assertEqual(len(a), 1)
|
|
self.assertEqual(a["c"], "d")
|
|
|
|
|
|
class TestLocks(IntegrationTestCase):
|
|
def test_locktimeout(self):
|
|
lock_name = "test_lock"
|
|
with filelock(lock_name):
|
|
with self.assertRaises(LockTimeoutError):
|
|
with filelock(lock_name, timeout=1):
|
|
self.fail("Locks not working")
|
|
|
|
def test_global_lock(self):
|
|
lock_name = "test_global"
|
|
with filelock(lock_name, is_global=True):
|
|
with self.assertRaises(LockTimeoutError):
|
|
with filelock(lock_name, timeout=1, is_global=True):
|
|
self.fail("Global locks not working")
|
|
|
|
|
|
class TestMiscUtils(IntegrationTestCase):
|
|
def test_get_file_timestamp(self):
|
|
self.assertIsInstance(get_file_timestamp(__file__), str)
|
|
|
|
def test_execute_in_shell(self):
|
|
_err, out = execute_in_shell("ls")
|
|
self.assertIn("apps", cstr(out))
|
|
|
|
def test_get_all_sites(self):
|
|
self.assertIn(frappe.local.site, get_sites())
|
|
|
|
def test_get_site_info(self):
|
|
info = get_site_info()
|
|
|
|
installed_apps = [app["app_name"] for app in info["installed_apps"]]
|
|
self.assertIn("frappe", installed_apps)
|
|
self.assertGreaterEqual(len(info["users"]), 1)
|
|
|
|
def test_get_url_to_form(self):
|
|
self.assertTrue(get_url_to_form("System Settings").endswith("/desk/system-settings"))
|
|
self.assertTrue(get_url_to_form("User", "Test User").endswith("/desk/user/Test%20User"))
|
|
|
|
def test_safe_json_load(self):
|
|
self.assertEqual(safe_json_loads("{}"), {})
|
|
self.assertEqual(safe_json_loads("{ /}"), "{ /}")
|
|
self.assertEqual(safe_json_loads("12"), 12) # this is a quirk
|
|
|
|
def test_url_expansion(self):
|
|
unchanged_links = [
|
|
"<a href='tel:12345432'>My Phone</a>)",
|
|
"<a href='mailto:hello@example.com'>My Email</a>)",
|
|
"<a href='data:hello@example.com'>Data</a>)",
|
|
]
|
|
for link in unchanged_links:
|
|
self.assertEqual(link, expand_relative_urls(link))
|
|
|
|
site = get_url()
|
|
|
|
transforms = [("<a href='/about'>About</a>)", f"<a href='{site}/about'>About</a>)")]
|
|
for input, output in transforms:
|
|
self.assertEqual(output, expand_relative_urls(input))
|
|
|
|
|
|
class TestTypingValidations(IntegrationTestCase):
|
|
ERR_REGEX = "^Argument '.*' should be of type '.*' but got '.*' instead.$"
|
|
|
|
def test_validate_whitelisted_api(self):
|
|
@validate_argument_types
|
|
def simple(string: str, number: int):
|
|
return
|
|
|
|
@validate_argument_types
|
|
def varkw(string: str, **kwargs):
|
|
return
|
|
|
|
test_cases = [
|
|
(simple, (object(), object()), {}),
|
|
(varkw, (object(),), {"xyz": object()}),
|
|
]
|
|
|
|
for fn, args, kwargs in test_cases:
|
|
with self.assertRaisesRegex(frappe.FrappeTypeError, self.ERR_REGEX):
|
|
fn(*args, **kwargs)
|
|
|
|
def test_validate_whitelisted_doc_method(self):
|
|
report = frappe.get_last_doc("Report")
|
|
|
|
with self.assertRaisesRegex(frappe.FrappeTypeError, self.ERR_REGEX):
|
|
report.toggle_disable(["disable"])
|
|
|
|
current_value = report.disabled
|
|
changed_value = not current_value
|
|
|
|
report.toggle_disable(changed_value)
|
|
report.toggle_disable(current_value)
|
|
|
|
def test_forced_types(self):
|
|
def func(a, b=None, **kwargs):
|
|
pass
|
|
|
|
lax_types = frappe.whitelist(force_types=False)(func)
|
|
lax_types(1) # should run without error
|
|
|
|
forced_types = frappe.whitelist(force_types=True)(func)
|
|
with self.assertRaises(frappe.FrappeTypeError):
|
|
forced_types(1)
|
|
|
|
@frappe.whitelist(force_types=True)
|
|
def func(a: int, b=None, **kwargs):
|
|
pass
|
|
|
|
with self.assertRaises(frappe.FrappeTypeError):
|
|
func(1)
|
|
|
|
@frappe.whitelist(force_types=True)
|
|
def func(a: int, b: int | None = None, **kwargs):
|
|
pass
|
|
|
|
func(1) # should run without error
|
|
|
|
|
|
class TestTBSanitization(IntegrationTestCase):
|
|
def test_traceback_sanitzation(self):
|
|
try:
|
|
password = "42" # noqa: F841
|
|
args = {"password": "42", "pwd": "42", "safe": "safe_value"}
|
|
args = frappe._dict({"password": "42", "pwd": "42", "safe": "safe_value"}) # noqa: F841
|
|
raise Exception
|
|
except Exception:
|
|
traceback = frappe.get_traceback(with_context=True)
|
|
self.assertNotIn("42", traceback)
|
|
self.assertIn("********", traceback)
|
|
self.assertIn("password =", traceback)
|
|
self.assertIn("safe_value", traceback)
|
|
|
|
|
|
class TestRounding(IntegrationTestCase):
|
|
@IntegrationTestCase.change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
|
|
def test_normal_rounding(self):
|
|
self.assertEqual(flt("what"), 0)
|
|
|
|
self.assertEqual(flt("0.5", 0), 1)
|
|
self.assertEqual(flt("0.3"), 0.3)
|
|
|
|
self.assertEqual(flt("1.5", 0), 2)
|
|
|
|
# positive rounding to integers
|
|
self.assertEqual(flt(0.4, 0), 0)
|
|
self.assertEqual(flt(0.5, 0), 1)
|
|
self.assertEqual(flt(1.455, 0), 1)
|
|
self.assertEqual(flt(1.5, 0), 2)
|
|
|
|
# negative rounding to integers
|
|
self.assertEqual(flt(-0.5, 0), -1)
|
|
self.assertEqual(flt(-1.5, 0), -2)
|
|
|
|
# negative precision i.e. round to nearest 10th
|
|
self.assertEqual(flt(123, -1), 120)
|
|
self.assertEqual(flt(125, -1), 130)
|
|
self.assertEqual(flt(134.45, -1), 130)
|
|
self.assertEqual(flt(135, -1), 140)
|
|
|
|
# positive multiple digit rounding
|
|
self.assertEqual(flt(1.25, 1), 1.3)
|
|
self.assertEqual(flt(0.15, 1), 0.2)
|
|
|
|
# negative multiple digit rounding
|
|
self.assertEqual(flt(-1.25, 1), -1.3)
|
|
self.assertEqual(flt(-0.15, 1), -0.2)
|
|
|
|
def test_normal_rounding_as_argument(self):
|
|
rounding_method = "Commercial Rounding"
|
|
|
|
self.assertEqual(flt("0.5", 0, rounding_method=rounding_method), 1)
|
|
self.assertEqual(flt("0.3", rounding_method=rounding_method), 0.3)
|
|
|
|
self.assertEqual(flt("1.5", 0, rounding_method=rounding_method), 2)
|
|
|
|
# positive rounding to integers
|
|
self.assertEqual(flt(0.4, 0, rounding_method=rounding_method), 0)
|
|
self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 1)
|
|
self.assertEqual(flt(1.455, 0, rounding_method=rounding_method), 1)
|
|
self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2)
|
|
|
|
# negative rounding to integers
|
|
self.assertEqual(flt(-0.5, 0, rounding_method=rounding_method), -1)
|
|
self.assertEqual(flt(-1.5, 0, rounding_method=rounding_method), -2)
|
|
|
|
# negative precision i.e. round to nearest 10th
|
|
self.assertEqual(flt(123, -1, rounding_method=rounding_method), 120)
|
|
self.assertEqual(flt(125, -1, rounding_method=rounding_method), 130)
|
|
self.assertEqual(flt(134.45, -1, rounding_method=rounding_method), 130)
|
|
self.assertEqual(flt(135, -1, rounding_method=rounding_method), 140)
|
|
|
|
# positive multiple digit rounding
|
|
self.assertEqual(flt(1.25, 1, rounding_method=rounding_method), 1.3)
|
|
self.assertEqual(flt(0.15, 1, rounding_method=rounding_method), 0.2)
|
|
self.assertEqual(flt(2.675, 2, rounding_method=rounding_method), 2.68)
|
|
|
|
# negative multiple digit rounding
|
|
self.assertEqual(flt(-1.25, 1, rounding_method=rounding_method), -1.3)
|
|
self.assertEqual(flt(-0.15, 1, rounding_method=rounding_method), -0.2)
|
|
|
|
# Nearest number and not even (the default behaviour)
|
|
self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 1)
|
|
self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2)
|
|
self.assertEqual(flt(2.5, 0, rounding_method=rounding_method), 3)
|
|
self.assertEqual(flt(3.5, 0, rounding_method=rounding_method), 4)
|
|
|
|
self.assertEqual(flt(0.05, 1, rounding_method=rounding_method), 0.1)
|
|
self.assertEqual(flt(1.15, 1, rounding_method=rounding_method), 1.2)
|
|
self.assertEqual(flt(2.25, 1, rounding_method=rounding_method), 2.3)
|
|
self.assertEqual(flt(3.35, 1, rounding_method=rounding_method), 3.4)
|
|
|
|
@IntegrationTestCase.change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
|
|
@given(
|
|
st.decimals(min_value=-1e8, max_value=1e8),
|
|
st.integers(min_value=-2, max_value=4),
|
|
)
|
|
def test_normal_rounding_property(self, number, precision):
|
|
with localcontext() as ctx:
|
|
ctx.rounding = ROUND_HALF_UP
|
|
self.assertEqual(Decimal(str(flt(float(number), precision))), round(number, precision))
|
|
|
|
def test_bankers_rounding(self):
|
|
rounding_method = "Banker's Rounding"
|
|
|
|
self.assertEqual(rounded(0, 0, rounding_method=rounding_method), 0)
|
|
self.assertEqual(rounded(5.551115123125783e-17, 2, rounding_method=rounding_method), 0.0)
|
|
|
|
self.assertEqual(flt("0.5", 0, rounding_method=rounding_method), 0)
|
|
self.assertEqual(flt("0.3", rounding_method=rounding_method), 0.3)
|
|
|
|
self.assertEqual(flt("1.5", 0, rounding_method=rounding_method), 2)
|
|
|
|
# positive rounding to integers
|
|
self.assertEqual(flt(0.4, 0, rounding_method=rounding_method), 0)
|
|
self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 0)
|
|
self.assertEqual(flt(1.455, 0, rounding_method=rounding_method), 1)
|
|
self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2)
|
|
|
|
# negative rounding to integers
|
|
self.assertEqual(flt(-0.5, 0, rounding_method=rounding_method), 0)
|
|
self.assertEqual(flt(-1.5, 0, rounding_method=rounding_method), -2)
|
|
|
|
# negative precision i.e. round to nearest 10th
|
|
self.assertEqual(flt(123, -1, rounding_method=rounding_method), 120)
|
|
self.assertEqual(flt(125, -1, rounding_method=rounding_method), 120)
|
|
self.assertEqual(flt(134.45, -1, rounding_method=rounding_method), 130)
|
|
self.assertEqual(flt(135, -1, rounding_method=rounding_method), 140)
|
|
|
|
# positive multiple digit rounding
|
|
self.assertEqual(flt(1.25, 1, rounding_method=rounding_method), 1.2)
|
|
self.assertEqual(flt(0.15, 1, rounding_method=rounding_method), 0.2)
|
|
self.assertEqual(flt(2.675, 2, rounding_method=rounding_method), 2.68)
|
|
self.assertEqual(flt(-2.675, 2, rounding_method=rounding_method), -2.68)
|
|
|
|
# negative multiple digit rounding
|
|
self.assertEqual(flt(-1.25, 1, rounding_method=rounding_method), -1.2)
|
|
self.assertEqual(flt(-0.15, 1, rounding_method=rounding_method), -0.2)
|
|
|
|
# Nearest number and not even (the default behaviour)
|
|
self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 0)
|
|
self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2)
|
|
self.assertEqual(flt(2.5, 0, rounding_method=rounding_method), 2)
|
|
self.assertEqual(flt(3.5, 0, rounding_method=rounding_method), 4)
|
|
|
|
self.assertEqual(flt(0.05, 1, rounding_method=rounding_method), 0.0)
|
|
self.assertEqual(flt(1.15, 1, rounding_method=rounding_method), 1.2)
|
|
self.assertEqual(flt(2.25, 1, rounding_method=rounding_method), 2.2)
|
|
self.assertEqual(flt(3.35, 1, rounding_method=rounding_method), 3.4)
|
|
|
|
self.assertEqual(flt(-0.5, 0, rounding_method=rounding_method), 0)
|
|
self.assertEqual(flt(-1.5, 0, rounding_method=rounding_method), -2)
|
|
self.assertEqual(flt(-2.5, 0, rounding_method=rounding_method), -2)
|
|
self.assertEqual(flt(-3.5, 0, rounding_method=rounding_method), -4)
|
|
|
|
self.assertEqual(flt(-0.05, 1, rounding_method=rounding_method), 0.0)
|
|
self.assertEqual(flt(-1.15, 1, rounding_method=rounding_method), -1.2)
|
|
self.assertEqual(flt(-2.25, 1, rounding_method=rounding_method), -2.2)
|
|
self.assertEqual(flt(-3.35, 1, rounding_method=rounding_method), -3.4)
|
|
|
|
@IntegrationTestCase.change_settings("System Settings", {"rounding_method": "Banker's Rounding"})
|
|
@given(
|
|
st.decimals(min_value=-1e8, max_value=1e8),
|
|
st.integers(min_value=-2, max_value=4),
|
|
)
|
|
def test_bankers_rounding_property(self, number, precision):
|
|
self.assertEqual(Decimal(str(flt(float(number), precision))), round(number, precision))
|
|
|
|
def test_default_rounding(self):
|
|
self.assertEqual(frappe.get_system_settings("rounding_method"), "Banker's Rounding")
|
|
|
|
@given(
|
|
st.floats(min_value=-(2**32) - 1, max_value=2**32 + 1),
|
|
st.integers(min_value=-(2**63) - 1, max_value=2**63 + 1),
|
|
)
|
|
def test_cint(self, floating_point, integer):
|
|
self.assertEqual(cint(integer), integer)
|
|
self.assertEqual(cint(str(integer)), integer)
|
|
self.assertEqual(cint(str(floating_point)), int(floating_point))
|
|
|
|
|
|
class TestArgumentTypingValidations(IntegrationTestCase):
|
|
def test_validate_argument_types(self):
|
|
from unittest.mock import AsyncMock, MagicMock, Mock
|
|
|
|
from frappe.core.doctype.doctype.doctype import DocType
|
|
|
|
@validate_argument_types
|
|
def test_simple_types(a: int, b: float, c: bool):
|
|
return a, b, c
|
|
|
|
@validate_argument_types
|
|
def test_sequence(a: str, b: list[dict] | None = None, c: dict[str, int] | None = None):
|
|
return a, b, c
|
|
|
|
@validate_argument_types
|
|
def test_doctypes(a: DocType | dict):
|
|
return a
|
|
|
|
@validate_argument_types
|
|
def test_mocks(a: str):
|
|
return a
|
|
|
|
self.assertEqual(test_simple_types(True, 2.0, True), (1, 2.0, True))
|
|
self.assertEqual(test_simple_types(1, 2, 1), (1, 2.0, True))
|
|
self.assertEqual(test_simple_types(1.0, 2, 1), (1, 2.0, True))
|
|
self.assertEqual(test_simple_types(1, 2, "1"), (1, 2.0, True))
|
|
with self.assertRaises(FrappeTypeError):
|
|
test_simple_types(1, 2, "a")
|
|
with self.assertRaises(FrappeTypeError):
|
|
test_simple_types(1, 2, None)
|
|
|
|
self.assertEqual(test_sequence("a", [{"a": 1}], {"a": 1}), ("a", [{"a": 1}], {"a": 1}))
|
|
self.assertEqual(test_sequence("a", None, None), ("a", None, None))
|
|
self.assertEqual(test_sequence("a", [{"a": 1}], None), ("a", [{"a": 1}], None))
|
|
self.assertEqual(test_sequence("a", None, {"a": 1}), ("a", None, {"a": 1}))
|
|
self.assertEqual(test_sequence("a", [{"a": 1}], {"a": "1.0"}), ("a", [{"a": 1}], {"a": 1}))
|
|
with self.assertRaises(FrappeTypeError):
|
|
test_sequence("a", [{"a": 1}], True)
|
|
|
|
doctype = frappe.get_last_doc("DocType")
|
|
self.assertEqual(test_doctypes(doctype), doctype)
|
|
self.assertEqual(test_doctypes(doctype.as_dict()), doctype.as_dict())
|
|
with self.assertRaises(FrappeTypeError):
|
|
test_doctypes("a")
|
|
|
|
self.assertEqual(test_mocks("Hello World"), "Hello World")
|
|
for obj in (AsyncMock, MagicMock, Mock):
|
|
obj_instance = obj()
|
|
self.assertEqual(test_mocks(obj_instance), obj_instance)
|
|
with self.assertRaises(FrappeTypeError):
|
|
test_mocks(1)
|
|
|
|
|
|
class TestChangeLog(IntegrationTestCase):
|
|
def test_get_remote_url(self):
|
|
self.assertIsInstance(get_source_url("frappe"), str)
|
|
|
|
def test_parse_github_url(self):
|
|
# using erpnext as repo in order to be different from the owner
|
|
owner, repo = parse_github_url("https://github.com/frappe/erpnext.git")
|
|
self.assertEqual(owner, "frappe")
|
|
self.assertEqual(repo, "erpnext")
|
|
|
|
owner, repo = parse_github_url("https://github.com/frappe/erpnext")
|
|
self.assertEqual(owner, "frappe")
|
|
self.assertEqual(repo, "erpnext")
|
|
|
|
owner, repo = parse_github_url("git@github.com:frappe/erpnext.git")
|
|
self.assertEqual(owner, "frappe")
|
|
self.assertEqual(repo, "erpnext")
|
|
|
|
owner, repo = parse_github_url("https://gitlab.com/gitlab-org/gitlab")
|
|
self.assertIsNone(owner)
|
|
self.assertIsNone(repo)
|
|
|
|
self.assertRaises(ValueError, parse_github_url, remote_url=None)
|
|
|
|
|
|
class TestCrypto(IntegrationTestCase):
|
|
def test_hashing(self):
|
|
self.assertEqual(sha256_hash(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
|
self.assertEqual(
|
|
sha256_hash(b"The quick brown fox jumps over the lazy dog"),
|
|
"d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
|
|
)
|
|
|
|
|
|
class TestURLTrackers(IntegrationTestCase):
|
|
def test_add_trackers_to_url(self):
|
|
url = "https://example.com"
|
|
source = "test_source"
|
|
campaign = "test_campaign"
|
|
medium = "test_medium"
|
|
content = "test_content"
|
|
|
|
with patch("frappe.db.get_value") as mock_get_value:
|
|
mock_get_value.side_effect = lambda *args: args[1] # Return unslugged input value
|
|
result = add_trackers_to_url(url, source, campaign, medium, content)
|
|
|
|
expected = "https://example.com?utm_source=test_source&utm_medium=test_medium&utm_campaign=test_campaign&utm_content=test_content"
|
|
self.assertEqual(result, expected)
|
|
|
|
def test_parse_and_map_trackers_from_url(self):
|
|
url = "https://example.com?utm_source=test_source&utm_medium=test_medium&utm_campaign=test_campaign&utm_content=test_content"
|
|
|
|
with patch("frappe.db.get_value") as mock_get_value:
|
|
mock_get_value.return_value = None # Simulate no existing records
|
|
result = parse_and_map_trackers_from_url(url)
|
|
|
|
expected = {
|
|
"utm_source": "test_source",
|
|
"utm_medium": "test_medium",
|
|
"utm_campaign": "test_campaign",
|
|
"utm_content": "test_content",
|
|
}
|
|
self.assertEqual(result, expected)
|
|
|
|
def test_map_trackers(self):
|
|
url_trackers = {
|
|
"utm_source": "test_source",
|
|
"utm_medium": "test_medium",
|
|
"utm_campaign": "test_campaign",
|
|
"utm_content": "test_content",
|
|
}
|
|
|
|
result = map_trackers(url_trackers, create=True)
|
|
|
|
expected = {
|
|
"utm_source": frappe.get_doc("UTM Source", "test_source"),
|
|
"utm_medium": frappe.get_doc("UTM Medium", "test_medium"),
|
|
"utm_campaign": frappe.get_doc("UTM Campaign", "test_campaign"),
|
|
"utm_content": "test_content",
|
|
}
|
|
self.assertDocumentEqual(result["utm_source"], expected["utm_source"])
|
|
self.assertDocumentEqual(result["utm_medium"], expected["utm_medium"])
|
|
self.assertDocumentEqual(result["utm_campaign"], expected["utm_campaign"])
|
|
self.assertEqual(result["utm_content"], expected["utm_content"])
|
|
|
|
|
|
class TestDataUtils(UnitTestCase):
|
|
def setUp(self):
|
|
frappe.local.lang = "en"
|
|
|
|
def tearDown(self):
|
|
frappe.local.lang = "en"
|
|
|
|
def test_comma_and(self):
|
|
self.assertEqual(comma_and(["a", "b", "c"]), "'a', 'b', and 'c'")
|
|
self.assertEqual(comma_and(["a", "b", "c"], add_quotes=False), "a, b, and c")
|
|
|
|
frappe.local.lang = "pt-BR"
|
|
|
|
self.assertEqual(comma_and(["a", "b", "c"]), "'a', 'b' e 'c'")
|
|
self.assertEqual(comma_and(["a", "b", "c"], add_quotes=False), "a, b e c")
|
|
|
|
def test_comma_or(self):
|
|
self.assertEqual(comma_or(["a", "b", "c"]), "'a', 'b', or 'c'")
|
|
self.assertEqual(comma_or(["a", "b", "c"], add_quotes=False), "a, b, or c")
|
|
|
|
frappe.local.lang = "pt-BR"
|
|
|
|
self.assertEqual(comma_or(["a", "b", "c"]), "'a', 'b' ou 'c'")
|
|
self.assertEqual(comma_or(["a", "b", "c"], add_quotes=False), "a, b ou c")
|