test_backup.py 28 KB

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