test_backup.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. #!/usr/bin/python3
  2. #
  3. # The Qubes OS Project, https://www.qubes-os.org/
  4. #
  5. # Copyright (C) 2016 Marta Marczykowska-Górecka
  6. # <marmarta@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License along
  19. # with this program; if not, write to the Free Software Foundation, Inc.,
  20. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  21. #
  22. import logging.handlers
  23. import unittest
  24. import unittest.mock
  25. from PyQt5 import QtTest, QtCore, QtWidgets
  26. from qubesadmin import Qubes, events, utils, exc
  27. from qubesmanager import backup
  28. from qubesmanager.tests import init_qtapp
  29. import asyncio
  30. class BackupTest(unittest.TestCase):
  31. def setUp(self):
  32. super(BackupTest, self).setUp()
  33. self.qtapp, self.loop = init_qtapp()
  34. # mock up nonexistence of saved backup settings
  35. self.patcher_open = unittest.mock.patch('builtins.open')
  36. self.mock_open = self.patcher_open.start()
  37. self.mock_open.side_effect = FileNotFoundError()
  38. self.addCleanup(self.patcher_open.stop)
  39. # mock up the Backup Thread to avoid accidentally changing system state
  40. self.patcher_thread = unittest.mock.patch(
  41. 'qubesmanager.backup.BackupThread')
  42. self.mock_thread = self.patcher_thread.start()
  43. self.addCleanup(self.patcher_thread.stop)
  44. self.qapp = Qubes()
  45. self.dispatcher = events.EventsDispatcher(self.qapp)
  46. self.dialog = backup.BackupVMsWindow(
  47. self.qtapp, self.qapp, self.dispatcher)
  48. self.dialog.show()
  49. def tearDown(self):
  50. self.dialog.close()
  51. self.qtapp.processEvents()
  52. yield from asyncio.sleep(1)
  53. super(BackupTest, self).tearDown()
  54. def test_00_window_loads(self):
  55. self.assertTrue(self.dialog.select_vms_widget is not None)
  56. def test_01_vms_load_correctly(self):
  57. all_vms = len([vm for vm in self.qapp.domains
  58. if not vm.features.get('internal', False)])
  59. selected_vms = self.dialog.select_vms_widget.selected_list.count()
  60. available_vms = self.dialog.select_vms_widget.available_list.count()
  61. self.assertEqual(all_vms, available_vms + selected_vms)
  62. def test_02_correct_defaults(self):
  63. # backup is compressed
  64. self.assertTrue(self.dialog.compress_checkbox.isChecked(),
  65. "Compress backup should be checked by default")
  66. # correct VMs are selected
  67. include_in_backups_no = len(
  68. [vm for vm in self.qapp.domains
  69. if not vm.features.get('internal', False)
  70. and getattr(vm, 'include_in_backups', True)])
  71. selected_no = self.dialog.select_vms_widget.selected_list.count()
  72. self.assertEqual(include_in_backups_no, selected_no,
  73. "Incorrect VMs selected by default")
  74. # passphrase is empty
  75. self.assertEqual(self.dialog.passphrase_line_edit.text(), "",
  76. "Passphrase should be empty")
  77. # save defaults
  78. self.assertTrue(self.dialog.save_profile_checkbox.isChecked(),
  79. "By default, profile should be saved")
  80. def test_03_select_vms_widget(self):
  81. number_of_all_vms = len([vm for vm in self.qapp.domains
  82. if not vm.features.get('internal', False)])
  83. # select all
  84. self.dialog.select_vms_widget.add_all_button.click()
  85. self.assertEqual(number_of_all_vms,
  86. self.dialog.select_vms_widget.selected_list.count(),
  87. "Add All VMs does not work")
  88. # remove all
  89. self.dialog.select_vms_widget.remove_all_button.click()
  90. self.assertEqual(number_of_all_vms,
  91. self.dialog.select_vms_widget.available_list.count(),
  92. "Remove All VMs does not work")
  93. self._select_vm("work")
  94. self.assertEqual(self.dialog.select_vms_widget.selected_list.count(),
  95. 1, "Select a single VM does not work")
  96. def test_04_open_directory(self):
  97. self.dialog.next()
  98. self.assertTrue(self.dialog.currentPage()
  99. is self.dialog.select_dir_page)
  100. with unittest.mock.patch('qubesmanager.backup_utils.'
  101. 'select_path_button_clicked') as mock_func:
  102. self.dialog.select_path_button.click()
  103. mock_func.assert_called_once_with(unittest.mock.ANY)
  104. def test_05_running_vms_listed(self):
  105. self.dialog.next()
  106. self.assertTrue(self.dialog.currentPage()
  107. is self.dialog.select_dir_page)
  108. running_vms = [vm.name for vm in self.qapp.domains if vm.is_running()]
  109. listed_vms = []
  110. for i in range(self.dialog.appvm_combobox.count()):
  111. listed_vms.append(self.dialog.appvm_combobox.itemText(i))
  112. self.assertListEqual(sorted(running_vms), sorted(listed_vms),
  113. "Incorrect list of running vms")
  114. def test_06_passphrase_verification(self):
  115. self.dialog.next()
  116. self.assertTrue(self.dialog.currentPage()
  117. is self.dialog.select_dir_page)
  118. # required to check if next button is correctly enabled
  119. self.dialog.dir_line_edit.setText("/home")
  120. next_button = self.dialog.button(self.dialog.NextButton)
  121. # check if next remains inactive for various incorrect
  122. # passphrase/incorrect combinations
  123. self.dialog.passphrase_line_edit.setText("pass")
  124. self.dialog.passphrase_line_edit_verify.setText("fail")
  125. self.assertFalse(next_button.isEnabled(),
  126. "Mismatched passphrase/verification accepted")
  127. self.dialog.passphrase_line_edit.setText("pass")
  128. self.dialog.passphrase_line_edit_verify.setText("")
  129. self.assertFalse(next_button.isEnabled(), "Empty verification accepted")
  130. self.dialog.passphrase_line_edit.setText("")
  131. self.dialog.passphrase_line_edit_verify.setText("fail")
  132. self.assertFalse(next_button.isEnabled(), "Empty passphrase accepted")
  133. self.dialog.passphrase_line_edit.setText("")
  134. self.dialog.passphrase_line_edit_verify.setText("")
  135. self.assertFalse(next_button.isEnabled(),
  136. "Empty passphrase and verification accepted")
  137. # check if next is active for a correct passphrase/verify
  138. # combination
  139. self.dialog.passphrase_line_edit.setText("pass")
  140. self.dialog.passphrase_line_edit_verify.setText("pass")
  141. self.assertTrue(next_button.isEnabled(),
  142. "Matching passphrase/verification not accepted")
  143. def test_07_disk_space_correct(self):
  144. for i in range(self.dialog.select_vms_widget.available_list.count()):
  145. item = self.dialog.select_vms_widget.available_list.item(i)
  146. if item.vm.name == "dom0" or item.vm.get_disk_utilization() > 0:
  147. self.assertGreater(
  148. item.size, 0,
  149. "{} size incorrectly reported as 0".format(item.vm.name))
  150. def test_08_total_size_correct(self):
  151. # select nothing
  152. self.dialog.select_vms_widget.remove_all_button.click()
  153. self.assertEqual(self.dialog.total_size_label.text(), "0",
  154. "Total size of 0 vms incorrectly reported as 0")
  155. current_size = 0
  156. # select a single VM
  157. self._select_vm("sys-net")
  158. current_size += self.qapp.domains["sys-net"].get_disk_utilization()
  159. self.assertEqual(self.dialog.total_size_label.text(),
  160. utils.size_to_human(current_size),
  161. "Size incorrectly listed for a single VM")
  162. # add two more
  163. self._select_vm("sys-firewall")
  164. self._select_vm("work")
  165. current_size += self.qapp.domains["sys-firewall"].get_disk_utilization()
  166. current_size += self.qapp.domains["work"].get_disk_utilization()
  167. self.assertEqual(self.dialog.total_size_label.text(),
  168. utils.size_to_human(current_size),
  169. "Size incorrectly listed for several VMs")
  170. # remove one
  171. self._deselect_vm("sys-net")
  172. current_size -= self.qapp.domains["sys-net"].get_disk_utilization()
  173. self.assertEqual(self.dialog.total_size_label.text(),
  174. utils.size_to_human(current_size),
  175. "Size incorrectly listed for several VMs")
  176. @unittest.mock.patch('qubesmanager.backup_utils.write_backup_profile')
  177. @unittest.mock.patch('qubesadmin.Qubes.qubesd_call',
  178. return_value=b'backup output')
  179. def test_10_first_backup(self, mock_qubesd, mock_write_profile):
  180. self.assertTrue(self.dialog.currentPage()
  181. is self.dialog.select_vms_page)
  182. self.dialog.select_vms_widget.remove_all_button.click()
  183. self._select_vm("work")
  184. self._click_next()
  185. self.assertTrue(self.dialog.currentPage()
  186. is self.dialog.select_dir_page)
  187. # setup backup
  188. self._select_location("dom0")
  189. self.dialog.dir_line_edit.setText("/home")
  190. self.dialog.passphrase_line_edit.setText("pass")
  191. self.dialog.passphrase_line_edit_verify.setText("pass")
  192. self.dialog.save_profile_checkbox.setChecked(True)
  193. self.dialog.turn_off_checkbox.setChecked(False)
  194. self.dialog.compress_checkbox.setChecked(False)
  195. expected_settings = {'destination_vm': "dom0",
  196. 'destination_path': "/home",
  197. 'include': ["work"],
  198. 'passphrase_text': "pass",
  199. 'compression': False}
  200. with unittest.mock.patch.object(self.dialog.textEdit, 'setText')\
  201. as mock_set_text:
  202. self._click_next()
  203. # make sure the confirmation is not empty
  204. self.assertTrue(self.dialog.currentPage()
  205. is self.dialog.confirm_page)
  206. mock_write_profile.assert_called_with(expected_settings, True)
  207. mock_qubesd.assert_called_with('dom0', 'admin.backup.Info',
  208. unittest.mock.ANY)
  209. mock_set_text.assert_called_once_with("backup output")
  210. # make sure the backup is executed
  211. self._click_next()
  212. self.mock_thread.assert_called_once_with(self.qapp.domains["dom0"])
  213. self.mock_thread().start.assert_called_once_with()
  214. @unittest.mock.patch('qubesmanager.backup_utils.write_backup_profile')
  215. @unittest.mock.patch('qubesadmin.Qubes.qubesd_call',
  216. return_value=b'backup output')
  217. def test_11_second_backup(self, mock_qubesd, mock_write_profile):
  218. self.assertTrue(self.dialog.currentPage()
  219. is self.dialog.select_vms_page)
  220. self.dialog.select_vms_widget.remove_all_button.click()
  221. self._select_vm("work")
  222. self._select_vm("sys-net")
  223. self._select_vm("dom0")
  224. self._click_next()
  225. self.assertTrue(self.dialog.currentPage()
  226. is self.dialog.select_dir_page)
  227. # setup backup
  228. self._select_location("sys-net")
  229. self.dialog.dir_line_edit.setText("/home")
  230. self.dialog.passphrase_line_edit.setText("longerPassPhrase")
  231. self.dialog.passphrase_line_edit_verify.setText("longerPassPhrase")
  232. self.dialog.save_profile_checkbox.setChecked(False)
  233. self.dialog.turn_off_checkbox.setChecked(False)
  234. self.dialog.compress_checkbox.setChecked(True)
  235. expected_settings = {'destination_vm': "sys-net",
  236. 'destination_path': "/home",
  237. 'include': ["dom0", "sys-net", "work"],
  238. 'passphrase_text': "longerPassPhrase",
  239. 'compression': True}
  240. with unittest.mock.patch.object(self.dialog.textEdit, 'setText')\
  241. as mock_set_text:
  242. self._click_next()
  243. # make sure the confirmation is not empty
  244. self.assertTrue(self.dialog.currentPage()
  245. is self.dialog.confirm_page)
  246. mock_write_profile.assert_called_with(expected_settings, True)
  247. mock_qubesd.assert_called_with('dom0', 'admin.backup.Info',
  248. unittest.mock.ANY)
  249. mock_set_text.assert_called_once_with("backup output")
  250. # make sure the backup is executed
  251. self._click_next()
  252. self.mock_thread.assert_called_once_with(self.qapp.domains["sys-net"])
  253. self.mock_thread().start.assert_called_once_with()
  254. @unittest.mock.patch('qubesmanager.backup_utils.load_backup_profile')
  255. def test_20_loading_settings(self, mock_load):
  256. mock_load.return_value = {
  257. 'destination_vm': "sys-net",
  258. 'destination_path': "/home",
  259. 'include': ["dom0", "sys-net", "work"],
  260. 'passphrase_text': "longerPassPhrase",
  261. 'compression': True
  262. }
  263. self.dialog.hide()
  264. self.dialog.deleteLater()
  265. self.qtapp.processEvents()
  266. self.dialog = backup.BackupVMsWindow(
  267. self.qtapp, self.qapp, self.dispatcher)
  268. self.dialog.show()
  269. # check if settings were loaded
  270. self.assertEqual(self.dialog.appvm_combobox.currentText(), "sys-net",
  271. "Destination VM not loaded")
  272. self.assertEqual(self.dialog.dir_line_edit.text(), "/home",
  273. "Destination path not loaded")
  274. self.assertEqual(self.dialog.passphrase_line_edit.text(),
  275. "longerPassPhrase", "Passphrase not loaded")
  276. self.assertEqual(self.dialog.passphrase_line_edit_verify.text(),
  277. "longerPassPhrase", "Passphrase verify not loaded")
  278. self.assertTrue(self.dialog.compress_checkbox.isChecked())
  279. # check that 'include' vms were not pre-selected
  280. include_in_backups_no = len(
  281. [vm for vm in self.qapp.domains
  282. if not vm.features.get('internal', False)
  283. and getattr(vm, 'include_in_backups', True)])
  284. selected_no = self.dialog.select_vms_widget.selected_list.count()
  285. self.assertEqual(include_in_backups_no, selected_no,
  286. "Incorrect VM list selected")
  287. # check no errors were detected
  288. self.assertFalse(self.dialog.unrecognized_config_label.isVisible())
  289. @unittest.mock.patch('qubesmanager.backup_utils.load_backup_profile')
  290. def test_21_loading_settings_error(self, mock_load):
  291. mock_load.return_value = {
  292. 'destination_vm': "incorrect_vm",
  293. }
  294. self.dialog.hide()
  295. self.dialog.deleteLater()
  296. self.qtapp.processEvents()
  297. self.dialog = backup.BackupVMsWindow(
  298. self.qtapp, self.qapp, self.dispatcher)
  299. self.dialog.show()
  300. # check errors were detected
  301. self.assertTrue(self.dialog.unrecognized_config_label.isVisible())
  302. @unittest.mock.patch('qubesmanager.backup_utils.load_backup_profile')
  303. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.information')
  304. def test_22_loading_settings_exc(self, mock_info, mock_load):
  305. mock_load.side_effect = exc.QubesException('Error')
  306. self.dialog.hide()
  307. self.dialog.deleteLater()
  308. self.qtapp.processEvents()
  309. self.dialog = backup.BackupVMsWindow(
  310. self.qtapp, self.qapp, self.dispatcher)
  311. self.dialog.show()
  312. # check error was reported
  313. self.assertEqual(mock_info.call_count, 1, "Warning not shown")
  314. @unittest.mock.patch('qubesmanager.backup_utils.write_backup_profile')
  315. @unittest.mock.patch('qubesadmin.Qubes.qubesd_call',
  316. return_value=b'backup output')
  317. def test_23_cancel_confirm(self, *_args):
  318. self._click_next()
  319. self.assertTrue(self.dialog.currentPage()
  320. is self.dialog.select_dir_page)
  321. self._select_location("dom0")
  322. self.dialog.dir_line_edit.setText("/home")
  323. self.dialog.passphrase_line_edit.setText("pass")
  324. self.dialog.passphrase_line_edit_verify.setText("pass")
  325. self._click_next()
  326. # attempt to cancel
  327. with unittest.mock.patch('os.remove') as mock_remove:
  328. self._click_cancel()
  329. mock_remove.assert_called_once_with(
  330. '/etc/qubes/backup/qubes-manager-backup-tmp.conf')
  331. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
  332. @unittest.mock.patch('qubesmanager.backup_utils.write_backup_profile')
  333. @unittest.mock.patch('qubesadmin.Qubes.qubesd_call',
  334. return_value=b'backup output')
  335. def test_24_cancel_in_progress(self, mock_call, *_args):
  336. self._click_next()
  337. self.assertTrue(self.dialog.currentPage()
  338. is self.dialog.select_dir_page)
  339. self._select_location("dom0")
  340. self.dialog.dir_line_edit.setText("/home")
  341. self.dialog.passphrase_line_edit.setText("pass")
  342. self.dialog.passphrase_line_edit_verify.setText("pass")
  343. self._click_next()
  344. self._click_next()
  345. # attempt to cancel
  346. with unittest.mock.patch('os.remove') as mock_remove:
  347. self._click_cancel()
  348. mock_call.assert_called_with('dom0', 'admin.backup.Cancel',
  349. 'qubes-manager-backup-tmp')
  350. mock_remove.assert_called_once_with(
  351. '/etc/qubes/backup/qubes-manager-backup-tmp.conf')
  352. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
  353. @unittest.mock.patch('os.system')
  354. @unittest.mock.patch('os.remove')
  355. @unittest.mock.patch('qubesmanager.backup_utils.write_backup_profile')
  356. @unittest.mock.patch('qubesadmin.Qubes.qubesd_call',
  357. return_value=b'backup output')
  358. def test_25_successful_backup(self, _a, _b, mock_remove,
  359. mock_system, mock_warning):
  360. self._click_next()
  361. self.assertTrue(self.dialog.currentPage()
  362. is self.dialog.select_dir_page)
  363. self._select_location("dom0")
  364. self.dialog.dir_line_edit.setText("/home")
  365. self.dialog.passphrase_line_edit.setText("pass")
  366. self.dialog.passphrase_line_edit_verify.setText("pass")
  367. self.dialog.turn_off_checkbox.setChecked(False)
  368. self._click_next()
  369. self._click_next()
  370. # assume backup went correctly
  371. self.mock_thread().msg = None
  372. self.mock_thread().finished.connect.assert_called_once_with(
  373. self.dialog.backup_finished)
  374. self.dialog.backup_finished()
  375. self.assertFalse(self.dialog.button(
  376. self.dialog.CancelButton).isEnabled())
  377. self.assertTrue(self.dialog.button(
  378. self.dialog.FinishButton).isEnabled())
  379. mock_remove.assert_called_once_with(
  380. '/etc/qubes/backup/qubes-manager-backup-tmp.conf')
  381. self.assertEqual(mock_system.call_count, 0,
  382. "System turned off unnecessarily")
  383. self.assertEqual(mock_warning.call_count, 0,
  384. "Backup succeeded but received warning")
  385. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
  386. @unittest.mock.patch('os.system')
  387. @unittest.mock.patch('os.remove')
  388. @unittest.mock.patch('qubesmanager.backup_utils.write_backup_profile')
  389. @unittest.mock.patch('qubesadmin.Qubes.qubesd_call',
  390. return_value=b'backup output')
  391. def test_26_success_backup_poweroff(
  392. self, _a, _b, mock_remove, mock_system, mock_warning):
  393. self._click_next()
  394. self.assertTrue(self.dialog.currentPage()
  395. is self.dialog.select_dir_page)
  396. self._select_location("dom0")
  397. self.dialog.dir_line_edit.setText("/home")
  398. self.dialog.passphrase_line_edit.setText("pass")
  399. self.dialog.passphrase_line_edit_verify.setText("pass")
  400. self.dialog.turn_off_checkbox.setChecked(True)
  401. self._click_next()
  402. self._click_next()
  403. # assume backup went correctly
  404. self.mock_thread().msg = None
  405. self.mock_thread().finished.connect.assert_called_once_with(
  406. self.dialog.backup_finished)
  407. self.dialog.backup_finished()
  408. self.assertFalse(self.dialog.button(
  409. self.dialog.CancelButton).isEnabled())
  410. self.assertTrue(self.dialog.button(
  411. self.dialog.FinishButton).isEnabled())
  412. mock_remove.assert_called_once_with(
  413. '/etc/qubes/backup/qubes-manager-backup-tmp.conf')
  414. mock_system.assert_called_once_with('systemctl poweroff')
  415. self.assertEqual(mock_warning.call_count, 0,
  416. "Backup succeeded but received warning")
  417. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
  418. @unittest.mock.patch('os.system')
  419. @unittest.mock.patch('os.remove')
  420. @unittest.mock.patch('qubesmanager.backup_utils.write_backup_profile')
  421. @unittest.mock.patch('qubesadmin.Qubes.qubesd_call',
  422. return_value=b'backup output')
  423. def test_27_failed_backup(
  424. self, _a, _b, mock_remove, mock_system, mock_warn):
  425. self._click_next()
  426. self.assertTrue(self.dialog.currentPage()
  427. is self.dialog.select_dir_page)
  428. self._select_location("dom0")
  429. self.dialog.dir_line_edit.setText("/home")
  430. self.dialog.passphrase_line_edit.setText("pass")
  431. self.dialog.passphrase_line_edit_verify.setText("pass")
  432. self.dialog.turn_off_checkbox.setChecked(True)
  433. self._click_next()
  434. self._click_next()
  435. # assume backup went wrong
  436. self.mock_thread().msg = "Error"
  437. self.mock_thread().finished.connect.assert_called_once_with(
  438. self.dialog.backup_finished)
  439. self.dialog.backup_finished()
  440. self.assertFalse(self.dialog.button(
  441. self.dialog.CancelButton).isEnabled())
  442. self.assertTrue(self.dialog.button(
  443. self.dialog.FinishButton).isEnabled())
  444. mock_remove.assert_called_once_with(
  445. '/etc/qubes/backup/qubes-manager-backup-tmp.conf')
  446. self.assertEqual(mock_system.call_count, 0,
  447. "Attempted shutdown at failed backup")
  448. self.assertEqual(mock_warn.call_count, 1)
  449. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
  450. @unittest.mock.patch('os.system')
  451. @unittest.mock.patch('os.remove')
  452. @unittest.mock.patch('qubesmanager.backup_utils.write_backup_profile')
  453. @unittest.mock.patch('qubesadmin.Qubes.qubesd_call',
  454. return_value=b'backup output')
  455. def test_28_progress(
  456. self, _a, _b, _mock_remove, _mock_system, _mock_warn):
  457. self._click_next()
  458. self.assertTrue(self.dialog.currentPage()
  459. is self.dialog.select_dir_page)
  460. self._select_location("dom0")
  461. self.dialog.dir_line_edit.setText("/home")
  462. self.dialog.passphrase_line_edit.setText("pass")
  463. self.dialog.passphrase_line_edit_verify.setText("pass")
  464. self.dialog.turn_off_checkbox.setChecked(True)
  465. self._click_next()
  466. self._click_next()
  467. # see if backup is correctly in progress
  468. self.assertTrue(self.dialog.button(
  469. self.dialog.CancelButton).isEnabled())
  470. self.assertFalse(self.dialog.button(
  471. self.dialog.FinishButton).isEnabled())
  472. self.assertEqual(self.dialog.progress_bar.value(), 0,
  473. "Progress bar does not start at 0")
  474. # this is not a perfect method, but it is something
  475. self.dialog.on_backup_progress(None, None, progress='23.3123')
  476. self.assertEqual(self.dialog.progress_bar.value(), 23,
  477. "Progress bar does not update correctly")
  478. self.dialog.on_backup_progress(None, None, progress='87.89')
  479. self.assertEqual(self.dialog.progress_bar.value(), 87,
  480. "Progress bar does not update correctly")
  481. def _select_location(self, vm_name):
  482. widget = self.dialog.appvm_combobox
  483. widget.setCurrentIndex(0)
  484. while not widget.currentText() == vm_name:
  485. if widget.currentIndex() == widget.count():
  486. self.skipTest("target VM not found")
  487. widget.setCurrentIndex(widget.currentIndex() + 1)
  488. def _click_next(self):
  489. next_widget = self.dialog.button(QtWidgets.QWizard.NextButton)
  490. QtTest.QTest.mouseClick(next_widget, QtCore.Qt.LeftButton)
  491. def _click_cancel(self):
  492. cancel_widget = self.dialog.button(QtWidgets.QWizard.CancelButton)
  493. QtTest.QTest.mouseClick(cancel_widget, QtCore.Qt.LeftButton)
  494. def _select_vm(self, name_starts_with):
  495. for i in range(self.dialog.select_vms_widget.available_list.count()):
  496. item = self.dialog.select_vms_widget.available_list.item(i)
  497. if item.text().startswith(name_starts_with):
  498. item.setSelected(True)
  499. self.dialog.select_vms_widget.add_selected_button.click()
  500. return
  501. def _deselect_vm(self, name_starts_with):
  502. for i in range(self.dialog.select_vms_widget.selected_list.count()):
  503. item = self.dialog.select_vms_widget.selected_list.item(i)
  504. if item.text().startswith(name_starts_with):
  505. item.setSelected(True)
  506. self.dialog.select_vms_widget.remove_selected_button.click()
  507. return
  508. class BackupThreadTest(unittest.TestCase):
  509. def test_01_backup_thread_vm_on(self):
  510. vm = unittest.mock.Mock(spec=['is_running', 'app'],
  511. **{'is_running.return_value': True})
  512. vm.app = unittest.mock.Mock()
  513. thread = backup.BackupThread(vm)
  514. thread.run()
  515. vm.app.qubesd_call.assert_called_with(
  516. 'dom0', 'admin.backup.Execute', 'qubes-manager-backup-tmp')
  517. def test_02_backup_thread_vm_off(self):
  518. vm = unittest.mock.Mock(spec=['is_running', 'app', 'start'],
  519. **{'is_running.return_value': False})
  520. vm.app = unittest.mock.Mock()
  521. thread = backup.BackupThread(vm)
  522. thread.run()
  523. vm.app.qubesd_call.assert_called_with(
  524. 'dom0', 'admin.backup.Execute', 'qubes-manager-backup-tmp')
  525. vm.start.assert_called_once_with()
  526. def test_03_backup_thread_error(self):
  527. vm = unittest.mock.Mock(spec=['is_running', 'app'],
  528. **{'is_running.return_value': True})
  529. vm.app = unittest.mock.Mock()
  530. vm.app.qubesd_call.side_effect = exc.QubesException('Error')
  531. thread = backup.BackupThread(vm)
  532. thread.run()
  533. self.assertIsNotNone(thread.msg)
  534. if __name__ == "__main__":
  535. ha_syslog = logging.handlers.SysLogHandler('/dev/log')
  536. ha_syslog.setFormatter(
  537. logging.Formatter('%(name)s[%(process)d]: %(message)s'))
  538. logging.root.addHandler(ha_syslog)
  539. unittest.main()