# # Copyright (C) 2023 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of # the GNU General Public License v.2, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY expressed or implied, including the implied warranties 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. Any Red Hat trademarks that are incorporated in the # source code or documentation are not subject to the GNU General Public # License and may only be used or replicated with the express permission of # Red Hat, Inc. # import unittest import pytest import tempfile import os from meh.ui.text import TextIntf from pyanaconda.ui.webui import CockpitUserInterface, FIREFOX_THEME_DEFAULT from unittest.mock import Mock, patch from pyanaconda.core.constants import PAYLOAD_TYPE_DNF, PAYLOAD_TYPE_LIVE_IMAGE class SimpleWebUITestCase(unittest.TestCase): """Simple test case for Web UI. The goal of this test is to test execution of the UI behaves as desired. """ def setUp(self): self.intf = CockpitUserInterface(None, None, 0) def _prepare_for_live_testing(self, pid_file, backend_file, pid_content="", remote=0): # prepare UI interface class self.intf = CockpitUserInterface(None, None, remote) self.intf._backend_ready_flag_file = backend_file open(backend_file, "wt").close() # Value could be None if pid_file: self.intf._viewer_pid_file = pid_file # wrote pid if requested if pid_content: with open(pid_file, "wt") as f: f.write(pid_content) def test_webui_defaults(self): """Test that webui interface has correct defaults.""" assert isinstance(self.intf.meh_interface, TextIntf) assert self.intf.tty_num == 6 # Not implemented assert self.intf.showYesNoQuestion("Implemented by browser") is False @patch("pyanaconda.ui.webui.CockpitUserInterface._print_message") def test_error_propagation(self, mocked_print_message): """Test that webui prints erorr to the console. This gets then propagated by service to journal. """ self.intf.showError("My Error") mocked_print_message.assert_called_once_with("My Error") mocked_print_message.reset_mock() self.intf.showDetailedError("My detailed error", "Such a detail!") mocked_print_message.assert_called_once_with("My detailed error\n\nSuch a detail!""") @patch("pyanaconda.ui.webui.flags") def test_setup_automated_installation(self, mocked_flags): """Test webui setup call for automated installation.""" mocked_flags.automatedInstall = True mocked_flags.ksprompt = True with pytest.raises(RuntimeError) as cm: self._setup_interface() assert str(cm.value) == "Automated installations are not supported by Web UI." @patch("pyanaconda.ui.webui.flags") def test_setup_non_interactive_installation(self, mocked_flags): """Test webui setup call for automated installation.""" mocked_flags.automatedInstall = True mocked_flags.ksprompt = False with pytest.raises(RuntimeError) as cm: self._setup_interface() assert str(cm.value) == "Non-interactive installations are not supported by Web UI." @patch("pyanaconda.ui.webui.conf") def test_setup_dir_installation(self, mocked_conf): """Test webui setup call for a dir installation.""" mocked_conf.target.is_directory = True mocked_conf.target.is_image = False with pytest.raises(RuntimeError) as cm: self._setup_interface() assert str(cm.value) == "Dir and image installations are not supported by Web UI." @patch("pyanaconda.ui.webui.conf") def test_setup_image_installation(self, mocked_conf): """Test webui setup call for an image installation.""" mocked_conf.target.is_directory = False mocked_conf.target.is_image = True with pytest.raises(RuntimeError) as cm: self._setup_interface() assert str(cm.value) == "Dir and image installations are not supported by Web UI." @patch("pyanaconda.ui.webui.conf") def test_setup_unsupported_system(self, mocked_conf): """Test webui setup call with unsupported system.""" mocked_conf.target.is_directory = False mocked_conf.target.is_image = False mocked_conf.system.supports_web_ui = False with pytest.raises(RuntimeError) as cm: self._setup_interface() assert str(cm.value) == "This installation environment is not supported by Web UI." @patch("pyanaconda.ui.webui.conf") def test_setup_unsupported_payload(self, mocked_conf): """Test webui setup call with unsupported payload.""" mocked_conf.target.is_directory = False mocked_conf.target.is_image = False mocked_conf.system.supports_web_ui = True with pytest.raises(NotImplementedError) as cm: self._setup_interface(payload_type=PAYLOAD_TYPE_DNF) assert str(cm.value) == "Package installations are not supported by Web UI." @patch("pyanaconda.ui.webui.conf") def test_setup(self, mocked_conf): """Test webui setup call.""" mocked_conf.target.is_directory = False mocked_conf.target.is_image = False mocked_conf.system.supports_web_ui = True self._setup_interface() @staticmethod def _setup_interface(payload_type=PAYLOAD_TYPE_LIVE_IMAGE): """Create and set up the Web UI.""" intf = CockpitUserInterface( payload=Mock(type=payload_type), storage=None, remote=False, ) intf.setup( data=None ) def test_backend_ready_file(self): """Test webui correctly create and remove beackend ready flag file.""" with tempfile.TemporaryDirectory() as fd: backend_file = os.path.join(fd, "backend_ready") self._prepare_for_live_testing(None, backend_file, remote=1) def test_flag_file(): assert os.path.exists(backend_file) is True self.intf._run_webui = test_flag_file self.intf.run() assert os.path.exists(backend_file) is False @patch("pyanaconda.ui.webui.startProgram") @patch("pyanaconda.ui.webui.conf") def test_run_not_on_live(self, mocked_conf, mocked_startProgram): """Test webui run call on boot iso.""" # Execution is different for boot.iso then live environment mocked_conf.system.provides_liveuser = False mocked_process = Mock() mocked_process.pid = 12345 mocked_startProgram.return_value = mocked_process with tempfile.TemporaryDirectory() as fd: pid_file = os.path.join(fd, "anaconda.pid") backend_file = os.path.join(fd, "backend_ready") self._prepare_for_live_testing(pid_file, backend_file, remote=1) self.intf.run() mocked_startProgram.assert_called_once_with(["/usr/libexec/webui-desktop", "-t", FIREFOX_THEME_DEFAULT, "-r", "1", "/cockpit/@localhost/anaconda-webui/index.html"], reset_lang=False) # check if backend flag file was removed after finish of run method assert os.path.exists(backend_file) is False with open(pid_file, "rt") as f: assert f.readlines() == ["12345"] mocked_process.wait.assert_called_once() # test with disabled remote mocked_startProgram.reset_mock() mocked_process.reset_mock() mocked_startProgram.return_value = mocked_process with tempfile.TemporaryDirectory() as fd: pid_file = os.path.join(fd, "anaconda.pid") backend_file = os.path.join(fd, "backend_ready") self._prepare_for_live_testing(pid_file, backend_file) self.intf.run() mocked_startProgram.assert_called_once_with(["/usr/libexec/webui-desktop", "-t", FIREFOX_THEME_DEFAULT, "-r", "0", "/cockpit/@localhost/anaconda-webui/index.html"], reset_lang=False) # check if backend flag file was removed after finish of run method assert os.path.exists(backend_file) is False with open(pid_file, "rt") as f: assert f.readlines() == ["12345"] mocked_process.wait.assert_called_once() @patch("pyanaconda.ui.webui.PidWatcher.watch_process") @patch("pyanaconda.ui.webui.create_main_loop") @patch("pyanaconda.ui.webui.conf") def test_run_on_live_success(self, mocked_conf, mocked_create_main_loop, mocked_watch_process): """Test webui run call on live environment.""" # Execution is different for live because we need to start FF early as possible mocked_conf.system.provides_liveuser = True mocked_main_loop = Mock() # Test on Live media mocked_watch_process.reset_mock() mocked_create_main_loop.reset_mock() mocked_create_main_loop.return_value = mocked_main_loop mocked_main_loop.reset_mock() with tempfile.TemporaryDirectory() as fd: pid_file = os.path.join(fd, "anaconda.pid") backend_file = os.path.join(fd, "backend_ready") self._prepare_for_live_testing(pid_file, backend_file, pid_content="11111") self.intf.run() # check if backend flag file was removed after finish of run method assert os.path.exists(backend_file) is False # check that callback is correctly set mocked_watch_process.assert_called_once_with(11111, self.intf._webui_desktop_closed) mocked_create_main_loop.assert_called_once() mocked_main_loop.run.assert_called_once() # test quit callbacks by calling them (simple tests avoiding execution of the main loop) # test webui callback execution - simulates closing the viewer app self.intf._webui_desktop_closed(11111, 0) mocked_main_loop.quit.assert_called_once() # test webui callback bad status - simulates crash of the viewer app mocked_main_loop.reset_mock() self.intf._webui_desktop_closed(11111, 1) mocked_main_loop.quit.assert_called_once() @patch("pyanaconda.ui.webui.PidWatcher.watch_process") @patch("pyanaconda.ui.webui.create_main_loop") @patch("pyanaconda.ui.webui.conf") def test_run_on_live_failure(self, mocked_conf, mocked_create_main_loop, mocked_watch_process): """Test webui run call on live environment.""" mocked_conf.system.provides_liveuser = True mocked_main_loop = Mock() # Test pid file doesn't exists mocked_watch_process.reset_mock() mocked_create_main_loop.reset_mock() mocked_create_main_loop.return_value = mocked_main_loop mocked_main_loop.reset_mock() with tempfile.TemporaryDirectory() as fd: pid_file = os.path.join(fd, "anaconda.pid") backend_file = os.path.join(fd, "backend_ready") self._prepare_for_live_testing(pid_file, backend_file) with pytest.raises(FileNotFoundError): self.intf.run() # check if backend flag file was removed after finish of run method assert os.path.exists(backend_file) is False mocked_watch_process.assert_not_called() mocked_create_main_loop.assert_not_called() # Test empty pid file mocked_watch_process.reset_mock() mocked_create_main_loop.reset_mock() mocked_create_main_loop.return_value = mocked_main_loop mocked_main_loop.reset_mock() with tempfile.TemporaryDirectory() as fd: pid_file = os.path.join(fd, "anaconda.pid") backend_file = os.path.join(fd, "backend_ready") open(pid_file, "wt").close() self._prepare_for_live_testing(pid_file, backend_file) with pytest.raises(ValueError): self.intf.run() # check if backend flag file was removed after finish of run method assert os.path.exists(backend_file) is False mocked_watch_process.assert_not_called() mocked_create_main_loop.assert_not_called() # Test negative pid mocked_watch_process.reset_mock() mocked_create_main_loop.reset_mock() mocked_create_main_loop.return_value = mocked_main_loop mocked_main_loop.reset_mock() with tempfile.TemporaryDirectory() as fd: pid_file = os.path.join(fd, "anaconda.pid") backend_file = os.path.join(fd, "backend_ready") self._prepare_for_live_testing(pid_file, backend_file, pid_content="-20") with pytest.raises(ValueError): self.intf.run() # check if backend flag file was removed after finish of run method assert os.path.exists(backend_file) is False mocked_watch_process.assert_not_called() mocked_create_main_loop.assert_not_called() # Test bad value pid mocked_watch_process.reset_mock() mocked_create_main_loop.reset_mock() mocked_create_main_loop.return_value = mocked_main_loop mocked_main_loop.reset_mock() with tempfile.TemporaryDirectory() as fd: pid_file = os.path.join(fd, "anaconda.pid") backend_file = os.path.join(fd, "backend_ready") self._prepare_for_live_testing(pid_file, backend_file, pid_content="not-a-number") with pytest.raises(ValueError): self.intf.run() # check if backend flag file was removed after finish of run method assert os.path.exists(backend_file) is False mocked_watch_process.assert_not_called() mocked_create_main_loop.assert_not_called()