diff --git a/README.md b/README.md index 8071c89..dafd105 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,15 @@ This will keep translated strings, but will add new ones. ### Updating translations Commit updated `.ts` files into `i18n` directory. + + +Tests +---------------------- + +Located in the tests/ directory. + +To run qube manager and backup tests: + python3 test_name.py -v + +To run global settings tests: + sudo systemctl stop qubesd; sudo -E python3 test_global_settings.py -v ; sudo systemctl start qubesd diff --git a/qubesmanager/tests/__init__.py b/qubesmanager/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qubesmanager/tests/test_backup_01.py b/qubesmanager/tests/test_backup_01.py new file mode 100644 index 0000000..3470061 --- /dev/null +++ b/qubesmanager/tests/test_backup_01.py @@ -0,0 +1,95 @@ +#!/usr/bin/python3 +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2016 Marta Marczykowska-Górecka +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +import logging.handlers +import sys +import unittest +import unittest.mock + +from PyQt4 import QtGui, QtTest, QtCore +from qubesadmin import Qubes +import qubesmanager.backup as backup_gui + + +class BackupTest(unittest.TestCase): + def setUp(self): + super(BackupTest, self).setUp() + + # mock up nonexistence of saved backup settings + self.patcher = unittest.mock.patch('builtins.open') + self.mock_open = self.patcher.start() + self.mock_open.side_effect = FileNotFoundError() + self.addCleanup(self.patcher.stop) + + self.qapp = Qubes() + self.qtapp = QtGui.QApplication(sys.argv) + self.dialog = backup_gui.BackupVMsWindow(self.qtapp, self.qapp) + + def tearDown(self): + del self.dialog + del self.qtapp + del self.qapp + super(BackupTest, self).tearDown() + + def test_00_window_loads(self): + self.assertTrue(self.dialog.select_vms_widget is not None) + + def test_01_vms_load_correctly(self): + all_vms = len([vm for vm in self.qapp.domains + if not vm.features.get('internal', False)]) + + selected_vms = self.dialog.select_vms_widget.selected_list.count() + available_vms = self.dialog.select_vms_widget.available_list.count() + + self.assertEqual(all_vms, available_vms + selected_vms) + + def test_02_correct_defaults(self): + # backup is compressed + self.assertTrue(self.dialog.compress_checkbox.isChecked(), + "Compress backup should be checked by default") + + # correct VMs are selected + include_in_backups_no = len([vm for vm in self.qapp.domains + if not vm.features.get('internal', False) + and getattr(vm, 'include_in_backups', True)]) + selected_no = self.dialog.select_vms_widget.selected_list.count() + self.assertEqual(include_in_backups_no, selected_no, + "Incorrect VMs selected by default") + + # passphrase is empty + self.assertEqual(self.dialog.passphrase_line_edit.text(), "", + "Passphrase should be empty") + + # save defaults + self.assertTrue(self.dialog.save_profile_checkbox.isChecked(), + "By default, profile should be saved") + + # Check if target vms are selected + # Check if no default file loads correctly - another file?? + # TODO: make a separate backup testing file to test various backup defaults + + +if __name__ == "__main__": + ha_syslog = logging.handlers.SysLogHandler('/dev/log') + ha_syslog.setFormatter( + logging.Formatter('%(name)s[%(process)d]: %(message)s')) + logging.root.addHandler(ha_syslog) + unittest.main() diff --git a/qubesmanager/tests/test_global_settings.py b/qubesmanager/tests/test_global_settings.py new file mode 100644 index 0000000..dbccd5d --- /dev/null +++ b/qubesmanager/tests/test_global_settings.py @@ -0,0 +1,332 @@ +#!/usr/bin/python3 +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2016 Marta Marczykowska-Górecka +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +import logging.handlers +import sys +import unittest +import unittest.mock + +from PyQt4 import QtGui, QtTest, QtCore +from qubesadmin import Qubes +from qubes.tests import SystemTestCase +import qubesmanager.global_settings as global_settings +import concurrent.futures + +# sudo systemctl stop qubesd; sudo -E python3 test_backup.py -v ; sudo systemctl start qubesd + +def wrap_in_loop(func): + def wrapped(self): + self.loop.run_until_complete( + self.loop.run_in_executor(self.executor, + func, self)) + return wrapped + + +class GlobalSettingsTest(SystemTestCase): + def setUp(self): + super(GlobalSettingsTest, self).setUp() + + self.qtapp = QtGui.QApplication(sys.argv) + self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) + self.setUpInExecutor() + + @wrap_in_loop + def setUpInExecutor(self): + self.qapp = Qubes() + self.dialog = global_settings.GlobalSettingsWindow( + self.qtapp, self.qapp) + + def tearDown(self): + self.tearDownInExecutor() + super(GlobalSettingsTest, self).tearDown() + + @wrap_in_loop + def tearDownInExecutor(self): + del self.dialog + del self.qtapp + + @wrap_in_loop + def test_00_settings_started(self): + # non-empty drop-downs + self.assertNotEqual( + self.dialog.default_kernel_combo.currentText(), "") + self.assertNotEqual( + self.dialog.default_netvm_combo.currentText(), "") + self.assertNotEqual( + self.dialog.default_template_combo.currentText(), + "") + self.assertNotEqual( + self.dialog.clock_vm_combo.currentText(), "") + self.assertNotEqual( + self.dialog.update_vm_combo.currentText(), "") + + @wrap_in_loop + def test_01_load_correct_defs(self): + # correctly selected default template + selected_default_template = \ + self.dialog.default_template_combo.currentText() + self.assertTrue( + selected_default_template.startswith( + self.app.default_template.name)) + + # correctly selected default NetVM + selected_default_netvm = \ + self.dialog.default_netvm_combo.currentText() + self.assertTrue(selected_default_netvm.startswith( + self.app.default_netvm.name)) + + # correctly selected default kernel + selected_default_kernel = \ + self.dialog.default_kernel_combo.currentText() + self.assertTrue(selected_default_kernel.startswith( + self.app.default_kernel)) + + # correct ClockVM + selected_clockvm = \ + self.dialog.clock_vm_combo.currentText() + correct_clockvm = self.app.clockvm.name if self.app.clockvm \ + else "(none)" + self.assertTrue(selected_clockvm.startswith(correct_clockvm)) + + # correct updateVM + selected_updatevm = \ + self.dialog.update_vm_combo.currentText() + correct_updatevm = \ + self.app.updatevm.name if self.app.updatevm else "(none)" + self.assertTrue(selected_updatevm.startswith(correct_updatevm)) + + # update vm status + self.assertEqual(self.app.check_updates_vm, + self.dialog.updates_vm.isChecked()) + + @wrap_in_loop + def test_02_dom0_updates_load(self): + # check dom0 updates + try: + dom0_updates = self.app.check_updates_dom0 + except AttributeError: + self.skipTest("check_updates_dom0 property not implemented") + return + + self.assertEqual(dom0_updates, self.dialog.updates_dom0.isChecked()) + + def __set_noncurrent(self, widget): + if widget.count() < 2: + self.skipTest("not enough choices for " + widget.objectName()) + + widget.setCurrentIndex(0) + while widget.currentText().endswith("(current)") \ + or widget.currentText().startswith("(none)"): + widget.setCurrentIndex(widget.currentIndex() + 1) + + return widget.currentText() + + def __set_none(self, widget): + widget.setCurrentIndex(0) + while not widget.currentText().startswith("(none)"): + if widget.currentIndex() == widget.count(): + self.skipTest("none not available for " + widget.objectName()) + widget.setCurrentIndex(widget.currentIndex() + 1) + + def __click_ok(self): + okwidget = self.dialog.buttonBox.button( + self.dialog.buttonBox.Ok) + + QtTest.QTest.mouseClick(okwidget, + QtCore.Qt.LeftButton) + + @wrap_in_loop + def test_10_set_update_vm(self): + new_updatevm_name = self.__set_noncurrent(self.dialog.update_vm_combo) + self.__click_ok() + + self.assertEqual(self.app.updatevm.name, new_updatevm_name) + + @wrap_in_loop + def test_11_set_update_vm_to_none(self): + self.__set_none(self.dialog.update_vm_combo) + self.__click_ok() + + self.assertIsNone(self.app.updatevm) + + @wrap_in_loop + def test_12_set_update_vm_to_none2(self): + self.app.updatevm = None + self.dialog = global_settings.GlobalSettingsWindow( + self.qtapp, self.qapp) + + self.assertEqual(self.dialog.update_vm_combo.currentText(), + "(none) (current)") + + @wrap_in_loop + def test_20_set_clock_vm(self): + new_clockvm_name = self.__set_noncurrent(self.dialog.clock_vm_combo) + self.__click_ok() + + self.assertEqual(self.app.clockvm.name, new_clockvm_name) + + @wrap_in_loop + def test_21_set_clock_vm_to_none(self): + self.__set_none(self.dialog.clock_vm_combo) + self.__click_ok() + + self.assertIsNone(self.app.clockvm) + + @wrap_in_loop + def test_22_set_clock_vm_to_none2(self): + self.app.clockvm = None + self.dialog = global_settings.GlobalSettingsWindow( + self.qtapp, self.qapp) + + self.assertEqual(self.dialog.clock_vm_combo.currentText(), + "(none) (current)") + + @wrap_in_loop + def test_30_set_default_netvm(self): + new_netvm_name = self.__set_noncurrent(self.dialog.default_netvm_combo) + self.__click_ok() + + self.assertEqual(self.app.default_netvm.name, new_netvm_name) + + @wrap_in_loop + def test_31_set_default_netvm_to_none(self): + self.__set_none(self.dialog.default_netvm_combo) + self.__click_ok() + + self.assertIsNone(self.app.default_netvm) + + @wrap_in_loop + def test_32_set_default_netvm_to_none2(self): + self.app.default_netvm = None + self.dialog = global_settings.GlobalSettingsWindow( + self.qtapp, self.qapp) + + self.assertEqual(self.dialog.default_netvm_combo.currentText(), + "(none) (current)") + + @wrap_in_loop + def test_40_set_default_template(self): + new_def_template_name = self.__set_noncurrent( + self.dialog.default_template_combo) + self.__click_ok() + + self.assertEqual(self.app.default_template.name, new_def_template_name) + + @wrap_in_loop + def test_50_set_default_kernel(self): + new_def_kernel_name = self.__set_noncurrent( + self.dialog.default_kernel_combo) + self.__click_ok() + + self.assertEqual(self.app.default_kernel, new_def_kernel_name) + + @wrap_in_loop + def test_51_set_default_kernel_to_none(self): + self.__set_none(self.dialog.default_kernel_combo) + self.__click_ok() + + self.assertEqual(self.app.default_kernel, '') + + @wrap_in_loop + def test_52_set_default_kernel_to_none2(self): + self.app.default_kernel = None + self.dialog = global_settings.GlobalSettingsWindow( + self.qtapp, self.qapp) + + self.assertEqual(self.dialog.default_kernel_combo.currentText(), + "(none) (current)") + + @wrap_in_loop + def test_60_set_dom0_updates_true(self): + self.dialog.updates_dom0.setChecked(True) + self.__click_ok() + + if not hasattr(self.app, 'check_updates_dom0'): + self.skipTest("check_updates_dom0 property not implemented") + + self.assertTrue(self.app.check_updates_dom0) + + @wrap_in_loop + def test_61_set_dom0_updates_false(self): + self.dialog.updates_dom0.setChecked(False) + self.__click_ok() + + if not hasattr(self.app, 'check_updates_dom0'): + self.skipTest("check_updates_dom0 property not implemented") + + self.assertFalse(self.app.check_updates_dom0) + + @wrap_in_loop + def test_70_set_vm_updates_true(self): + self.dialog.updates_vm.setChecked(True) + self.__click_ok() + + self.assertTrue(self.app.check_updates_vm) + + @wrap_in_loop + def test_71_set_vm_updates_false(self): + self.dialog.updates_vm.setChecked(False) + self.__click_ok() + + self.assertFalse(self.app.check_updates_vm) + + @wrap_in_loop + def test_72_set_all_vms_true(self): + + with unittest.mock.patch("PyQt4.QtGui.QMessageBox.question", + return_value=QtGui.QMessageBox.Yes) as msgbox: + + QtTest.QTest.mouseClick(self.dialog.enable_updates_all, + QtCore.Qt.LeftButton) + + msgbox.assert_called_once_with( + self.dialog, + "Change state of all qubes", + "Are you sure you want to set all qubes to check for updates?", + unittest.mock.ANY) + + for vm in self.app.domains: + self.assertTrue(vm.features['check-updates']) + + @wrap_in_loop + def test_73_set_all_vms_false(self): + with unittest.mock.patch("PyQt4.QtGui.QMessageBox.question", + return_value=QtGui.QMessageBox.Yes) as msgbox: + QtTest.QTest.mouseClick(self.dialog.disable_updates_all, + QtCore.Qt.LeftButton) + + msgbox.assert_called_once_with( + self.dialog, + "Change state of all qubes", + "Are you sure you want to set all qubes to not check " + "for updates?", + unittest.mock.ANY) + + for vm in self.app.domains: + self.assertFalse(vm.features['check-updates']) + + +if __name__ == "__main__": + ha_syslog = logging.handlers.SysLogHandler('/dev/log') + ha_syslog.setFormatter( + logging.Formatter('%(name)s[%(process)d]: %(message)s')) + logging.root.addHandler(ha_syslog) + unittest.main() diff --git a/qubesmanager/tests/test_qube_manager.py b/qubesmanager/tests/test_qube_manager.py new file mode 100644 index 0000000..46cd16c --- /dev/null +++ b/qubesmanager/tests/test_qube_manager.py @@ -0,0 +1,117 @@ +#!/usr/bin/python3 +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2016 Marta Marczykowska-Górecka +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +import logging.handlers +import sys +import unittest +import unittest.mock +import gc + +from PyQt4 import QtGui, QtTest, QtCore +from qubesadmin import Qubes +import qubesmanager.qube_manager as qube_manager + + +class QubeManagerTest(unittest.TestCase): + def setUp(self): + super(QubeManagerTest, self).setUp() + + # # todo: mockup no settings file + # self.patcher = unittest.mock.patch('builtins.open') + # self.mock_open = self.patcher.start() + # self.mock_open.side_effect = FileNotFoundError() + # self.addCleanup(self.patcher.stop) + + self.qapp = Qubes() + self.qtapp = QtGui.QApplication(sys.argv) + self.dialog = qube_manager.VmManagerWindow(self.qtapp, self.qapp) + + def tearDown(self): + del self.dialog + del self.qtapp + del self.qapp + super(QubeManagerTest, self).tearDown() + gc.collect() + + # 0 - Check if the window was displayed and populated correctly + + def test_00_window_loads(self): + self.assertTrue(self.dialog.table is not None) + + @unittest.expectedFailure + def test_01_table_populates_correctly(self): + vms_in_table = [] + for row in range(self.dialog.table.rowCount()): + item = self.dialog.table.item(row, + self.dialog.columns_indices["Name"]) + self.assertIsNotNone(item) + vms_in_table.append(item.text()) + + actual_vms = [vm.name for vm in self.qapp.domains] + + self.assertEqual(len(vms_in_table), len(actual_vms), + "Incorrect number of VMs loaded") + self.assertListEqual(sorted(vms_in_table), sorted(actual_vms), + "Incorrect VMs loaded") +# todos: + # did settings load correctly + # did settings save corectly + + @unittest.mock.patch('qubesmanager.settings.VMSettingsWindow') + def test_20_vm_open_settings(self, mock_window): + selected_vm = self._select_non_admin_vm() + self.assertIsNotNone(selected_vm, "No valid non-admin VM found") + widget = self.dialog.toolbar.widgetForAction( + self.dialog.action_settings) + QtTest.QTest.mouseClick(widget, + QtCore.Qt.LeftButton) + mock_window.assert_called_once_with(selected_vm, self.qtapp, "basic") + + @unittest.mock.patch('qubesmanager.settings.VMSettingsWindow') + def test_21_vm_firewall_settings(self, mock_window): + selected_vm = self._select_non_admin_vm() + self.assertIsNotNone(selected_vm, "No valid non-admin VM found") + widget = self.dialog.toolbar.widgetForAction( + self.dialog.action_editfwrules) + QtTest.QTest.mouseClick(widget, + QtCore.Qt.LeftButton) + mock_window.assert_called_once_with(selected_vm, self.qtapp, "firewall") + + +# test whether pause/start/resume works + @unittest.mock.patch('qubesmanager.qubesadmin.vm.QubesVM.pause') + @unittest.mock.patch('qubesmanager.qubesadmin.vm.QubesVM.is_running') + @unittest.mock.patch('qubesmanager.qubesadmin.vm.QubesVM.get_power_state') + def _select_non_admin_vm(self): + for row in range(self.dialog.table.rowCount()): + template = self.dialog.table.item( + row, self.dialog.columns_indices["Template"]) + if template.text() != 'AdminVM': + self.dialog.table.setCurrentItem(template) + return template.vm + return None + +if __name__ == "__main__": + ha_syslog = logging.handlers.SysLogHandler('/dev/log') + ha_syslog.setFormatter( + logging.Formatter('%(name)s[%(process)d]: %(message)s')) + logging.root.addHandler(ha_syslog) + unittest.main() diff --git a/rpm_spec/qmgr.spec.in b/rpm_spec/qmgr.spec.in index 2a44416..c2de6bf 100644 --- a/rpm_spec/qmgr.spec.in +++ b/rpm_spec/qmgr.spec.in @@ -113,6 +113,12 @@ rm -rf $RPM_BUILD_ROOT %{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.qm %{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.ts +%{python3_sitelib}/qubesmanager/tests/__pycache__ +%{python3_sitelib}/qubesmanager/tests/__init__.py +%{python3_sitelib}/qubesmanager/tests/test_backup_01.py +%{python3_sitelib}/qubesmanager/tests/test_global_settings.py +%{python3_sitelib}/qubesmanager/tests/test_qube_manager.py + %dir %{python3_sitelib}/qubesmanager-*.egg-info %{python3_sitelib}/qubesmanager-*.egg-info/*