Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

729 lignes
24 KiB

  1. # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. # imports - standard imports
  4. import gzip
  5. import importlib
  6. import json
  7. import os
  8. import shlex
  9. import subprocess
  10. import unittest
  11. from contextlib import contextmanager
  12. from functools import wraps
  13. from glob import glob
  14. from pathlib import Path
  15. from unittest.case import skipIf
  16. from unittest.mock import patch
  17. # imports - third party imports
  18. import click
  19. from click import Command
  20. from click.testing import CliRunner, Result
  21. # imports - module imports
  22. import frappe
  23. import frappe.commands.site
  24. import frappe.commands.utils
  25. import frappe.recorder
  26. from frappe.installer import add_to_installed_apps, remove_app
  27. from frappe.query_builder.utils import db_type_is
  28. from frappe.tests.test_query_builder import run_only_if
  29. from frappe.tests.utils import FrappeTestCase
  30. from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now
  31. from frappe.utils.backups import fetch_latest_backups
  32. from frappe.utils.jinja_globals import bundled_asset
  33. _result: Result | None = None
  34. TEST_SITE = "commands-site-O4PN2QKA.test" # added random string tag to avoid collisions
  35. CLI_CONTEXT = frappe._dict(sites=[TEST_SITE])
  36. def clean(value) -> str:
  37. """Strips and converts bytes to str
  38. Args:
  39. value ([type]): [description]
  40. Returns:
  41. [type]: [description]
  42. """
  43. if isinstance(value, bytes):
  44. value = value.decode()
  45. if isinstance(value, str):
  46. value = value.strip()
  47. return value
  48. def missing_in_backup(doctypes: list, file: os.PathLike) -> list:
  49. """Returns list of missing doctypes in the backup.
  50. Args:
  51. doctypes (list): List of DocTypes to be checked
  52. file (str): Path of the database file
  53. Returns:
  54. doctypes(list): doctypes that are missing in backup
  55. """
  56. predicate = 'COPY public."tab{}"' if frappe.conf.db_type == "postgres" else "CREATE TABLE `tab{}`"
  57. with gzip.open(file, "rb") as f:
  58. content = f.read().decode("utf8").lower()
  59. return [doctype for doctype in doctypes if predicate.format(doctype).lower() not in content]
  60. def exists_in_backup(doctypes: list, file: os.PathLike) -> bool:
  61. """Checks if the list of doctypes exist in the database.sql.gz file supplied
  62. Args:
  63. doctypes (list): List of DocTypes to be checked
  64. file (str): Path of the database file
  65. Returns:
  66. bool: True if all tables exist
  67. """
  68. missing_doctypes = missing_in_backup(doctypes, file)
  69. return len(missing_doctypes) == 0
  70. @contextmanager
  71. def maintain_locals():
  72. pre_site = frappe.local.site
  73. pre_flags = frappe.local.flags.copy()
  74. pre_db = frappe.local.db
  75. try:
  76. yield
  77. finally:
  78. post_site = getattr(frappe.local, "site", None)
  79. if not post_site or post_site != pre_site:
  80. frappe.init(site=pre_site)
  81. frappe.local.db = pre_db
  82. frappe.local.flags.update(pre_flags)
  83. def pass_test_context(f):
  84. @wraps(f)
  85. def decorated_function(*args, **kwargs):
  86. return f(CLI_CONTEXT, *args, **kwargs)
  87. return decorated_function
  88. @contextmanager
  89. def cli(cmd: Command, args: list | None = None):
  90. with maintain_locals():
  91. global _result
  92. patch_ctx = patch("frappe.commands.pass_context", pass_test_context)
  93. _module = cmd.callback.__module__
  94. _cmd = cmd.callback.__qualname__
  95. __module = importlib.import_module(_module)
  96. patch_ctx.start()
  97. importlib.reload(__module)
  98. click_cmd = getattr(__module, _cmd)
  99. try:
  100. _result = CliRunner().invoke(click_cmd, args=args)
  101. _result.command = str(cmd)
  102. yield _result
  103. finally:
  104. patch_ctx.stop()
  105. __module = importlib.import_module(_module)
  106. importlib.reload(__module)
  107. importlib.invalidate_caches()
  108. class BaseTestCommands(FrappeTestCase):
  109. @classmethod
  110. def setUpClass(cls) -> None:
  111. super().setUpClass()
  112. cls.setup_test_site()
  113. @classmethod
  114. def execute(self, command, kwargs=None):
  115. site = {"site": frappe.local.site}
  116. cmd_input = None
  117. if kwargs:
  118. cmd_input = kwargs.get("cmd_input", None)
  119. if cmd_input:
  120. if not isinstance(cmd_input, bytes):
  121. raise Exception(f"The input should be of type bytes, not {type(cmd_input).__name__}")
  122. del kwargs["cmd_input"]
  123. kwargs.update(site)
  124. else:
  125. kwargs = site
  126. self.command = " ".join(command.split()).format(**kwargs)
  127. click.secho(self.command, fg="bright_black")
  128. command = shlex.split(self.command)
  129. self._proc = subprocess.run(command, input=cmd_input, capture_output=True)
  130. self.stdout = clean(self._proc.stdout)
  131. self.stderr = clean(self._proc.stderr)
  132. self.returncode = clean(self._proc.returncode)
  133. @classmethod
  134. def setup_test_site(cls):
  135. cmd_config = {
  136. "test_site": TEST_SITE,
  137. "admin_password": frappe.conf.admin_password,
  138. "root_login": frappe.conf.root_login,
  139. "root_password": frappe.conf.root_password,
  140. "db_type": frappe.conf.db_type,
  141. }
  142. if not os.path.exists(os.path.join(TEST_SITE, "site_config.json")):
  143. cls.execute(
  144. "bench new-site {test_site} --admin-password {admin_password} --db-type" " {db_type}",
  145. cmd_config,
  146. )
  147. def _formatMessage(self, msg, standardMsg):
  148. output = super()._formatMessage(msg, standardMsg)
  149. if not hasattr(self, "command") and _result:
  150. command = _result.command
  151. stdout = _result.stdout_bytes.decode() if _result.stdout_bytes else None
  152. stderr = _result.stderr_bytes.decode() if _result.stderr_bytes else None
  153. returncode = _result.exit_code
  154. else:
  155. command = self.command
  156. stdout = self.stdout
  157. stderr = self.stderr
  158. returncode = self.returncode
  159. cmd_execution_summary = "\n".join(
  160. [
  161. "-" * 70,
  162. "Last Command Execution Summary:",
  163. f"Command: {command}" if command else "",
  164. f"Standard Output: {stdout}" if stdout else "",
  165. f"Standard Error: {stderr}" if stderr else "",
  166. f"Return Code: {returncode}" if returncode else "",
  167. ]
  168. ).strip()
  169. return f"{output}\n\n{cmd_execution_summary}"
  170. class TestCommands(BaseTestCommands):
  171. def test_execute(self):
  172. # test 1: execute a command expecting a numeric output
  173. self.execute("bench --site {site} execute frappe.db.get_database_size")
  174. self.assertEqual(self.returncode, 0)
  175. self.assertIsInstance(float(self.stdout), float)
  176. # test 2: execute a command expecting an errored output as local won't exist
  177. self.execute("bench --site {site} execute frappe.local.site")
  178. self.assertEqual(self.returncode, 1)
  179. self.assertIsNotNone(self.stderr)
  180. # test 3: execute a command with kwargs
  181. # Note:
  182. # terminal command has been escaped to avoid .format string replacement
  183. # The returned value has quotes which have been trimmed for the test
  184. self.execute("""bench --site {site} execute frappe.bold --kwargs '{{"text": "DocType"}}'""")
  185. self.assertEqual(self.returncode, 0)
  186. self.assertEqual(self.stdout[1:-1], frappe.bold(text="DocType"))
  187. @unittest.skip
  188. def test_restore(self):
  189. # step 0: create a site to run the test on
  190. global_config = {
  191. "admin_password": frappe.conf.admin_password,
  192. "root_login": frappe.conf.root_login,
  193. "root_password": frappe.conf.root_password,
  194. "db_type": frappe.conf.db_type,
  195. }
  196. site_data = {"test_site": TEST_SITE, **global_config}
  197. for key, value in global_config.items():
  198. if value:
  199. self.execute(f"bench set-config {key} {value} -g")
  200. # test 1: bench restore from full backup
  201. self.execute("bench --site {test_site} backup --ignore-backup-conf", site_data)
  202. self.execute(
  203. "bench --site {test_site} execute frappe.utils.backups.fetch_latest_backups",
  204. site_data,
  205. )
  206. site_data.update({"database": json.loads(self.stdout)["database"]})
  207. self.execute("bench --site {test_site} restore {database}", site_data)
  208. # test 2: restore from partial backup
  209. self.execute("bench --site {test_site} backup --exclude 'ToDo'", site_data)
  210. site_data.update({"kw": "\"{'partial':True}\""})
  211. self.execute(
  212. "bench --site {test_site} execute" " frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
  213. site_data,
  214. )
  215. site_data.update({"database": json.loads(self.stdout)["database"]})
  216. self.execute("bench --site {test_site} restore {database}", site_data)
  217. self.assertEqual(self.returncode, 1)
  218. def test_partial_restore(self):
  219. _now = now()
  220. for num in range(10):
  221. frappe.get_doc(
  222. {
  223. "doctype": "ToDo",
  224. "date": add_to_date(_now, days=num),
  225. "description": frappe.mock("paragraph"),
  226. }
  227. ).insert()
  228. frappe.db.commit()
  229. todo_count = frappe.db.count("ToDo")
  230. # check if todos exist, create a partial backup and see if the state is the same after restore
  231. self.assertIsNot(todo_count, 0)
  232. self.execute("bench --site {site} backup --only 'ToDo'")
  233. db_path = fetch_latest_backups(partial=True)["database"]
  234. self.assertTrue("partial" in db_path)
  235. frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabToDo`")
  236. frappe.db.commit()
  237. self.execute("bench --site {site} partial-restore {path}", {"path": db_path})
  238. self.assertEqual(self.returncode, 0)
  239. self.assertEqual(frappe.db.count("ToDo"), todo_count)
  240. def test_recorder(self):
  241. frappe.recorder.stop()
  242. self.execute("bench --site {site} start-recording")
  243. frappe.local.cache = {}
  244. self.assertEqual(frappe.recorder.status(), True)
  245. self.execute("bench --site {site} stop-recording")
  246. frappe.local.cache = {}
  247. self.assertEqual(frappe.recorder.status(), False)
  248. def test_remove_from_installed_apps(self):
  249. app = "test_remove_app"
  250. add_to_installed_apps(app)
  251. # check: confirm that add_to_installed_apps added the app in the default
  252. self.execute("bench --site {site} list-apps")
  253. self.assertIn(app, self.stdout)
  254. # test 1: remove app from installed_apps global default
  255. self.execute("bench --site {site} remove-from-installed-apps {app}", {"app": app})
  256. self.assertEqual(self.returncode, 0)
  257. self.execute("bench --site {site} list-apps")
  258. self.assertNotIn(app, self.stdout)
  259. def test_list_apps(self):
  260. # test 1: sanity check for command
  261. self.execute("bench --site all list-apps")
  262. self.assertIsNotNone(self.returncode)
  263. self.assertIsInstance(self.stdout or self.stderr, str)
  264. # test 2: bare functionality for single site
  265. self.execute("bench --site {site} list-apps")
  266. self.assertEqual(self.returncode, 0)
  267. list_apps = {_x.split()[0] for _x in self.stdout.split("\n")}
  268. doctype = frappe.get_single("Installed Applications").installed_applications
  269. if doctype:
  270. installed_apps = {x.app_name for x in doctype}
  271. else:
  272. installed_apps = set(frappe.get_installed_apps())
  273. self.assertSetEqual(list_apps, installed_apps)
  274. # test 3: parse json format
  275. self.execute("bench --site {site} list-apps --format json")
  276. self.assertEqual(self.returncode, 0)
  277. self.assertIsInstance(json.loads(self.stdout), dict)
  278. self.execute("bench --site {site} list-apps -f json")
  279. self.assertEqual(self.returncode, 0)
  280. self.assertIsInstance(json.loads(self.stdout), dict)
  281. def test_show_config(self):
  282. # test 1: sanity check for command
  283. self.execute("bench --site all show-config")
  284. self.assertEqual(self.returncode, 0)
  285. # test 2: test keys in table text
  286. self.execute(
  287. "bench --site {site} set-config test_key '{second_order}' --parse",
  288. {"second_order": json.dumps({"test_key": "test_value"})},
  289. )
  290. self.execute("bench --site {site} show-config")
  291. self.assertEqual(self.returncode, 0)
  292. self.assertIn("test_key.test_key", self.stdout.split())
  293. self.assertIn("test_value", self.stdout.split())
  294. # test 3: parse json format
  295. self.execute("bench --site all show-config --format json")
  296. self.assertEqual(self.returncode, 0)
  297. self.assertIsInstance(json.loads(self.stdout), dict)
  298. self.execute("bench --site {site} show-config --format json")
  299. self.assertIsInstance(json.loads(self.stdout), dict)
  300. self.execute("bench --site {site} show-config -f json")
  301. self.assertIsInstance(json.loads(self.stdout), dict)
  302. def test_get_bench_relative_path(self):
  303. bench_path = get_bench_path()
  304. test1_path = os.path.join(bench_path, "test1.txt")
  305. test2_path = os.path.join(bench_path, "sites", "test2.txt")
  306. with open(test1_path, "w+") as test1:
  307. test1.write("asdf")
  308. with open(test2_path, "w+") as test2:
  309. test2.write("asdf")
  310. self.assertTrue("test1.txt" in get_bench_relative_path("test1.txt"))
  311. self.assertTrue("sites/test2.txt" in get_bench_relative_path("test2.txt"))
  312. with self.assertRaises(SystemExit):
  313. get_bench_relative_path("test3.txt")
  314. os.remove(test1_path)
  315. os.remove(test2_path)
  316. def test_frappe_site_env(self):
  317. os.putenv("FRAPPE_SITE", frappe.local.site)
  318. self.execute("bench execute frappe.ping")
  319. self.assertEqual(self.returncode, 0)
  320. self.assertIn("pong", self.stdout)
  321. def test_version(self):
  322. self.execute("bench version")
  323. self.assertEqual(self.returncode, 0)
  324. for output in ["legacy", "plain", "table", "json"]:
  325. self.execute(f"bench version -f {output}")
  326. self.assertEqual(self.returncode, 0)
  327. self.execute("bench version -f invalid")
  328. self.assertEqual(self.returncode, 2)
  329. def test_set_password(self):
  330. from frappe.utils.password import check_password
  331. self.execute("bench --site {site} set-password Administrator test1")
  332. self.assertEqual(self.returncode, 0)
  333. self.assertEqual(check_password("Administrator", "test1"), "Administrator")
  334. # to release the lock taken by check_password
  335. frappe.db.commit()
  336. self.execute("bench --site {site} set-admin-password test2")
  337. self.assertEqual(self.returncode, 0)
  338. self.assertEqual(check_password("Administrator", "test2"), "Administrator")
  339. @skipIf(
  340. not (
  341. frappe.conf.root_password and frappe.conf.admin_password and frappe.conf.db_type == "mariadb"
  342. ),
  343. "DB Root password and Admin password not set in config",
  344. )
  345. def test_bench_drop_site_should_archive_site(self):
  346. # TODO: Make this test postgres compatible
  347. site = TEST_SITE
  348. self.execute(
  349. f"bench new-site {site} --force --verbose "
  350. f"--admin-password {frappe.conf.admin_password} "
  351. f"--mariadb-root-password {frappe.conf.root_password} "
  352. f"--db-type {frappe.conf.db_type or 'mariadb'} "
  353. )
  354. self.assertEqual(self.returncode, 0)
  355. self.execute(f"bench drop-site {site} --force --root-password {frappe.conf.root_password}")
  356. self.assertEqual(self.returncode, 0)
  357. bench_path = get_bench_path()
  358. site_directory = os.path.join(bench_path, f"sites/{site}")
  359. self.assertFalse(os.path.exists(site_directory))
  360. archive_directory = os.path.join(bench_path, f"archived/sites/{site}")
  361. self.assertTrue(os.path.exists(archive_directory))
  362. @skipIf(
  363. not (
  364. frappe.conf.root_password and frappe.conf.admin_password and frappe.conf.db_type == "mariadb"
  365. ),
  366. "DB Root password and Admin password not set in config",
  367. )
  368. def test_force_install_app(self):
  369. if not os.path.exists(os.path.join(get_bench_path(), f"sites/{TEST_SITE}")):
  370. self.execute(
  371. f"bench new-site {TEST_SITE} --verbose "
  372. f"--admin-password {frappe.conf.admin_password} "
  373. f"--mariadb-root-password {frappe.conf.root_password} "
  374. f"--db-type {frappe.conf.db_type or 'mariadb'} "
  375. )
  376. app_name = "frappe"
  377. # set admin password in site_config as when frappe force installs, we don't have the conf
  378. self.execute(f"bench --site {TEST_SITE} set-config admin_password {frappe.conf.admin_password}")
  379. # try installing the frappe_docs app again on test site
  380. self.execute(f"bench --site {TEST_SITE} install-app {app_name}")
  381. self.assertIn(f"{app_name} already installed", self.stdout)
  382. self.assertEqual(self.returncode, 0)
  383. # force install frappe_docs app on the test site
  384. self.execute(f"bench --site {TEST_SITE} install-app {app_name} --force")
  385. self.assertIn(f"Installing {app_name}", self.stdout)
  386. self.assertEqual(self.returncode, 0)
  387. class TestBackups(BaseTestCommands):
  388. backup_map = {
  389. "includes": {
  390. "includes": [
  391. "ToDo",
  392. "Note",
  393. ]
  394. },
  395. "excludes": {"excludes": ["Activity Log", "Access Log", "Error Log"]},
  396. }
  397. home = os.path.expanduser("~")
  398. site_backup_path = frappe.utils.get_site_path("private", "backups")
  399. def setUp(self):
  400. self.files_to_trash = []
  401. def tearDown(self):
  402. if self._testMethodName == "test_backup":
  403. for file in self.files_to_trash:
  404. os.remove(file)
  405. try:
  406. os.rmdir(os.path.dirname(file))
  407. except OSError:
  408. pass
  409. def test_backup_no_options(self):
  410. """Take a backup without any options"""
  411. before_backup = fetch_latest_backups(partial=True)
  412. self.execute("bench --site {site} backup")
  413. after_backup = fetch_latest_backups(partial=True)
  414. self.assertEqual(self.returncode, 0)
  415. self.assertIn("successfully completed", self.stdout)
  416. self.assertNotEqual(before_backup["database"], after_backup["database"])
  417. def test_backup_with_files(self):
  418. """Take a backup with files (--with-files)"""
  419. before_backup = fetch_latest_backups()
  420. self.execute("bench --site {site} backup --with-files")
  421. after_backup = fetch_latest_backups()
  422. self.assertEqual(self.returncode, 0)
  423. self.assertIn("successfully completed", self.stdout)
  424. self.assertIn("with files", self.stdout)
  425. self.assertNotEqual(before_backup, after_backup)
  426. self.assertIsNotNone(after_backup["public"])
  427. self.assertIsNotNone(after_backup["private"])
  428. @run_only_if(db_type_is.MARIADB)
  429. def test_clear_log_table(self):
  430. d = frappe.get_doc(doctype="Error Log", title="Something").insert()
  431. d.db_set("modified", "2010-01-01", update_modified=False)
  432. frappe.db.commit()
  433. tables_before = frappe.db.get_tables(cached=False)
  434. self.execute("bench --site {site} clear-log-table --days=30 --doctype='Error Log'")
  435. self.assertEqual(self.returncode, 0)
  436. frappe.db.commit()
  437. self.assertFalse(frappe.db.exists("Error Log", d.name))
  438. tables_after = frappe.db.get_tables(cached=False)
  439. self.assertEqual(set(tables_before), set(tables_after))
  440. def test_backup_with_custom_path(self):
  441. """Backup to a custom path (--backup-path)"""
  442. backup_path = os.path.join(self.home, "backups")
  443. self.execute(
  444. "bench --site {site} backup --backup-path {backup_path}", {"backup_path": backup_path}
  445. )
  446. self.assertEqual(self.returncode, 0)
  447. self.assertTrue(os.path.exists(backup_path))
  448. self.assertGreaterEqual(len(os.listdir(backup_path)), 2)
  449. def test_backup_with_different_file_paths(self):
  450. """Backup with different file paths (--backup-path-db, --backup-path-files, --backup-path-private-files, --backup-path-conf)"""
  451. kwargs = {
  452. key: os.path.join(self.home, key, value)
  453. for key, value in {
  454. "db_path": "database.sql.gz",
  455. "files_path": "public.tar",
  456. "private_path": "private.tar",
  457. "conf_path": "config.json",
  458. }.items()
  459. }
  460. self.execute(
  461. """bench
  462. --site {site} backup --with-files
  463. --backup-path-db {db_path}
  464. --backup-path-files {files_path}
  465. --backup-path-private-files {private_path}
  466. --backup-path-conf {conf_path}""",
  467. kwargs,
  468. )
  469. self.assertEqual(self.returncode, 0)
  470. for path in kwargs.values():
  471. self.assertTrue(os.path.exists(path))
  472. def test_backup_compress_files(self):
  473. """Take a compressed backup (--compress)"""
  474. self.execute("bench --site {site} backup --with-files --compress")
  475. self.assertEqual(self.returncode, 0)
  476. compressed_files = glob(f"{self.site_backup_path}/*.tgz")
  477. self.assertGreater(len(compressed_files), 0)
  478. def test_backup_verbose(self):
  479. """Take a verbose backup (--verbose)"""
  480. self.execute("bench --site {site} backup --verbose")
  481. self.assertEqual(self.returncode, 0)
  482. def test_backup_only_specific_doctypes(self):
  483. """Take a backup with (include) backup options set in the site config `frappe.conf.backup.includes`"""
  484. self.execute(
  485. "bench --site {site} set-config backup '{includes}' --parse",
  486. {"includes": json.dumps(self.backup_map["includes"])},
  487. )
  488. self.execute("bench --site {site} backup --verbose")
  489. self.assertEqual(self.returncode, 0)
  490. database = fetch_latest_backups(partial=True)["database"]
  491. self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database))
  492. def test_backup_excluding_specific_doctypes(self):
  493. """Take a backup with (exclude) backup options set (`frappe.conf.backup.excludes`, `--exclude`)"""
  494. # test 1: take a backup with frappe.conf.backup.excludes
  495. self.execute(
  496. "bench --site {site} set-config backup '{excludes}' --parse",
  497. {"excludes": json.dumps(self.backup_map["excludes"])},
  498. )
  499. self.execute("bench --site {site} backup --verbose")
  500. self.assertEqual(self.returncode, 0)
  501. database = fetch_latest_backups(partial=True)["database"]
  502. self.assertFalse(exists_in_backup(self.backup_map["excludes"]["excludes"], database))
  503. self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database))
  504. # test 2: take a backup with --exclude
  505. self.execute(
  506. "bench --site {site} backup --exclude '{exclude}'",
  507. {"exclude": ",".join(self.backup_map["excludes"]["excludes"])},
  508. )
  509. self.assertEqual(self.returncode, 0)
  510. database = fetch_latest_backups(partial=True)["database"]
  511. self.assertFalse(exists_in_backup(self.backup_map["excludes"]["excludes"], database))
  512. def test_selective_backup_priority_resolution(self):
  513. """Take a backup with conflicting backup options set (`frappe.conf.excludes`, `--include`)"""
  514. self.execute(
  515. "bench --site {site} backup --include '{include}'",
  516. {"include": ",".join(self.backup_map["includes"]["includes"])},
  517. )
  518. self.assertEqual(self.returncode, 0)
  519. database = fetch_latest_backups(partial=True)["database"]
  520. self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database))
  521. def test_dont_backup_conf(self):
  522. """Take a backup ignoring frappe.conf.backup settings (with --ignore-backup-conf option)"""
  523. self.execute("bench --site {site} backup --ignore-backup-conf")
  524. self.assertEqual(self.returncode, 0)
  525. database = fetch_latest_backups()["database"]
  526. self.assertEqual([], missing_in_backup(self.backup_map["excludes"]["excludes"], database))
  527. class TestRemoveApp(FrappeTestCase):
  528. def test_delete_modules(self):
  529. from frappe.installer import (
  530. _delete_doctypes,
  531. _delete_modules,
  532. _get_module_linked_doctype_field_map,
  533. )
  534. test_module = frappe.new_doc("Module Def")
  535. test_module.update({"module_name": "RemoveThis", "app_name": "frappe"})
  536. test_module.save()
  537. module_def_linked_doctype = frappe.get_doc(
  538. {
  539. "doctype": "DocType",
  540. "name": "Doctype linked with module def",
  541. "module": "RemoveThis",
  542. "custom": 1,
  543. "fields": [
  544. {"label": "Modulen't", "fieldname": "notmodule", "fieldtype": "Link", "options": "Module Def"}
  545. ],
  546. }
  547. ).insert()
  548. doctype_to_link_field_map = _get_module_linked_doctype_field_map()
  549. self.assertIn("Report", doctype_to_link_field_map)
  550. self.assertIn(module_def_linked_doctype.name, doctype_to_link_field_map)
  551. self.assertEqual(doctype_to_link_field_map[module_def_linked_doctype.name], "notmodule")
  552. self.assertNotIn("DocType", doctype_to_link_field_map)
  553. doctypes_to_delete = _delete_modules([test_module.module_name], dry_run=False)
  554. self.assertEqual(len(doctypes_to_delete), 1)
  555. _delete_doctypes(doctypes_to_delete, dry_run=False)
  556. self.assertFalse(frappe.db.exists("Module Def", test_module.module_name))
  557. self.assertFalse(frappe.db.exists("DocType", module_def_linked_doctype.name))
  558. def test_dry_run(self):
  559. """Check if dry run in not destructive."""
  560. # nothing to assert, if this fails rest of the test suite will crumble.
  561. remove_app("frappe", dry_run=True, yes=True, no_backup=True)
  562. class TestSiteMigration(BaseTestCommands):
  563. def test_migrate_cli(self):
  564. with cli(frappe.commands.site.migrate) as result:
  565. self.assertTrue(TEST_SITE in result.stdout)
  566. self.assertEqual(result.exit_code, 0)
  567. self.assertEqual(result.exception, None)
  568. class TestAddNewUser(BaseTestCommands):
  569. def test_create_user(self):
  570. self.execute(
  571. "bench --site {site} add-user test@gmail.com --first-name test --last-name test --password 123 --user-type 'System User' --add-role 'Accounts User' --add-role 'Sales User'"
  572. )
  573. frappe.db.rollback()
  574. self.assertEqual(self.returncode, 0)
  575. user = frappe.get_doc("User", "test@gmail.com")
  576. roles = {r.role for r in user.roles}
  577. self.assertEqual({"Accounts User", "Sales User"}, roles)
  578. class TestBenchBuild(BaseTestCommands):
  579. def test_build_assets_size_check(self):
  580. with cli(frappe.commands.utils.build, "--force --production") as result:
  581. self.assertEqual(result.exit_code, 0)
  582. self.assertEqual(result.exception, None)
  583. CURRENT_SIZE = 3.5 # MB
  584. JS_ASSET_THRESHOLD = 0.1
  585. hooks = frappe.get_hooks()
  586. default_bundle = hooks["app_include_js"]
  587. default_bundle_size = 0.0
  588. for chunk in default_bundle:
  589. abs_path = Path.cwd() / frappe.local.sites_path / bundled_asset(chunk)[1:]
  590. default_bundle_size += abs_path.stat().st_size
  591. self.assertLessEqual(
  592. default_bundle_size / (1024 * 1024),
  593. CURRENT_SIZE * (1 + JS_ASSET_THRESHOLD),
  594. f"Default JS bundle size increased by {JS_ASSET_THRESHOLD:.2%} or more",
  595. )