diff --git a/plinth/tests/test_actions.py b/plinth/tests/test_actions.py index 92fa0c741..1a3925e33 100644 --- a/plinth/tests/test_actions.py +++ b/plinth/tests/test_actions.py @@ -16,175 +16,169 @@ # """ Test module for actions utilities that modify configuration. + +Verify that privileged actions perform as expected. See actions.py for a full +description of the expectations. + """ import os +import pathlib import shutil import tempfile -import unittest + +import pytest import apt_pkg from plinth import cfg from plinth.actions import run, superuser_run from plinth.errors import ActionError -euid = os.geteuid() + +@pytest.fixture(scope='module', autouse=True) +def actions_test_setup(): + """Setup a temporary directory for testing actions. + + Copy system commands ``echo`` and ``id`` into actions directory during + testing. + + """ + with tempfile.TemporaryDirectory() as tmp_directory: + old_actions_dir = cfg.actions_dir + cfg.actions_dir = str(tmp_directory) + + actions_dir = pathlib.Path(__file__).parent / '../../actions' + shutil.copy(str(actions_dir / 'packages'), str(tmp_directory)) + shutil.copy(str(actions_dir / 'test_path'), str(tmp_directory)) + shutil.copy('/bin/echo', str(tmp_directory)) + shutil.copy('/usr/bin/id', str(tmp_directory)) + + yield + cfg.actions_dir = old_actions_dir -class TestActions(unittest.TestCase): - """Verify that privileged actions perform as expected. +def notest_run_as_root(): + """1. Privileged actions run as root. """ + assert superuser_run('id', ['-ur'])[0].strip() == '0' # user 0 is root - See actions.py for a full description of the expectations. - Symlink to ``echo`` and ``id`` during testing. +def test_breakout_actions_dir(): + """2. The actions directory can't be changed at run time. + + Can't currently be tested, as the actions directory is hardcoded. """ - @classmethod - def setUpClass(cls): - """Initial setup for all the classes.""" - cls.action_directory = tempfile.TemporaryDirectory() - cls.actions_dir_factory = cfg.actions_dir - cfg.actions_dir = cls.action_directory.name - actions_dir = os.path.join(os.path.dirname(__file__), '..', '..', - 'actions') - shutil.copy(os.path.join(actions_dir, 'packages'), cfg.actions_dir) - shutil.copy(os.path.join(actions_dir, 'test_path'), cfg.actions_dir) - shutil.copy('/bin/echo', cfg.actions_dir) - shutil.copy('/usr/bin/id', cfg.actions_dir) +def test_breakout_up(): + """3A. Users can't call actions above the actions directory. - @classmethod - def tearDownClass(cls): - """Cleanup after all the tests are completed.""" - cls.action_directory.cleanup() - cfg.actions_dir = cls.actions_dir_factory + Tests both a relative and a literal path. + """ + for action in ('../echo', '/bin/echo'): + with pytest.raises(ValueError): + run(action, ['hi']) - def notest_run_as_root(self): - """1. Privileged actions run as root. """ - self.assertEqual( - '0', # user 0 is root - superuser_run('id', ['-ur'])[0].strip()) - def test_breakout_actions_dir(self): - """2. The actions directory can't be changed at run time. +def test_breakout_down(): + """3B. Users can't call actions beneath the actions directory.""" + action = 'directory/echo' + with pytest.raises(ValueError): + superuser_run(action) - Can't currently be tested, as the actions directory is hardcoded. - """ - pass - def test_breakout_up(self): - """3A. Users can't call actions above the actions directory. +def test_breakout_actions(): + """3C. Actions can't be used to run other actions. - Tests both a relative and a literal path. - """ - for action in ('../echo', '/bin/echo'): - with self.assertRaises(ValueError): - run(action, ['hi']) + If multiple actions are specified, bail out. + """ + # Counting is safer than actual badness. + actions = ('echo ""; echo $((1+1))', 'echo "" && echo $((1+1))', + 'echo "" || echo $((1+1))') + options = ('good', '') - def test_breakout_down(self): - """3B. Users can't call actions beneath the actions directory.""" - action = 'directory/echo' - - self.assertRaises(ValueError, superuser_run, action) - - def test_breakout_actions(self): - """3C. Actions can't be used to run other actions. - - If multiple actions are specified, bail out. - """ - # Counting is safer than actual badness. - actions = ('echo ""; echo $((1+1))', 'echo "" && echo $((1+1))', - 'echo "" || echo $((1+1))') - options = ('good', '') - - for action in actions: - for option in options: - with self.assertRaises(ValueError): - run(action, [option]) - - def test_breakout_option_string(self): - """3D. Option strings can't be used to run other actions. - - Verify that shell control characters aren't interpreted. - """ - options = ('; echo hello', '&& echo hello', '|| echo hello', - '& echo hello', r'\; echo hello', '| echo hello', - r':;!&\/$%@`"~#*(){}[]|+=') + for action in actions: for option in options: - output = run('echo', [option]) - output = output.rstrip('\n') - self.assertEqual(option, output) + with pytest.raises(ValueError): + run(action, [option]) - def test_breakout_option_list(self): - """3D. Option lists can't be used to run other actions. - Verify that shell control characters aren't interpreted in - option lists. - """ - option_lists = ( - (';', 'echo', 'hello'), - ('&&', 'echo', 'hello'), - ('||', 'echo', 'hello'), - ('&', 'echo', 'hello'), - (r'\;', 'echo' - 'hello'), - ('|', 'echo', 'hello'), - ('', 'echo', '', 'hello'), # Empty option argument - tuple(r':;!&\/$%@`"~#*(){}[]|+=')) - for options in option_lists: - output = run('echo', options) - output = output.rstrip('\n') - expected_output = ' '.join(options) - self.assertEqual(output, expected_output) +def test_breakout_option_string(): + """3D. Option strings can't be used to run other actions. - def test_multiple_options_and_output(self): - """4. Multiple options can be provided as a list or as a tuple. - - 5. Output is returned from the command. - """ - options = '1 2 3 4 5 6 7 8 9' - - output = run('echo', options.split()) + Verify that shell control characters aren't interpreted. + """ + options = ('; echo hello', '&& echo hello', '|| echo hello', + '& echo hello', r'\; echo hello', '| echo hello', + r':;!&\/$%@`"~#*(){}[]|+=') + for option in options: + output = run('echo', [option]) output = output.rstrip('\n') - self.assertEqual(options, output) + assert option == output - output = run('echo', tuple(options.split())) + +def test_breakout_option_list(): + """3D. Option lists can't be used to run other actions. + + Verify that shell control characters aren't interpreted in + option lists. + """ + option_lists = ( + (';', 'echo', 'hello'), + ('&&', 'echo', 'hello'), + ('||', 'echo', 'hello'), + ('&', 'echo', 'hello'), + (r'\;', 'echo' + 'hello'), + ('|', 'echo', 'hello'), + ('', 'echo', '', 'hello'), # Empty option argument + tuple(r':;!&\/$%@`"~#*(){}[]|+=')) + for options in option_lists: + output = run('echo', options) output = output.rstrip('\n') - self.assertEqual(options, output) + expected_output = ' '.join(options) + assert output == expected_output - @unittest.skipUnless(euid == 0, 'Needs to be root') - def test_is_package_manager_busy(self): - """Test the behavior of `is-package-manager-busy` in both locked and - unlocked states of the dpkg lock file.""" - apt_pkg.init() # initialize apt_pkg module +def test_multiple_options_and_output(): + """4. Multiple options can be provided as a list or as a tuple. - # In the locked state, the lsof command returns 0. - # Hence no error is thrown. - with apt_pkg.SystemLock(): - superuser_run('packages', ['is-package-manager-busy']) + 5. Output is returned from the command. + """ + options = '1 2 3 4 5 6 7 8 9' - # In the unlocked state, the lsof command returns 1. - # An ActionError is raised in this case. - with self.assertRaises(ActionError): - superuser_run('packages', ['is-package-manager-busy']) + output = run('echo', options.split()) + output = output.rstrip('\n') + assert options == output - @unittest.skipUnless(euid == 0, 'Needs to be root') - def test_action_path(self): - """Test that in development mode, python action scripts get the - correct PYTHONPATH""" - try: - cfg.develop = True - self._clear_env() - plinth_path = run('test_path').strip() - su_plinth_path = superuser_run('test_path').strip() - self.assertTrue(plinth_path.startswith(cfg.root)) - self.assertEquals(plinth_path, su_plinth_path) - finally: - cfg.develop = False + output = run('echo', tuple(options.split())) + output = output.rstrip('\n') + assert options == output - def _clear_env(self): - try: - del os.environ['PYTHONPATH'] - except KeyError: - pass + +@pytest.mark.usefixtures('needs_root') +def test_is_package_manager_busy(): + """Test the behavior of `is-package-manager-busy` in both locked and + unlocked states of the dpkg lock file.""" + + apt_pkg.init() # initialize apt_pkg module + + # In the locked state, the lsof command returns 0. + # Hence no error is thrown. + with apt_pkg.SystemLock(): + superuser_run('packages', ['is-package-manager-busy']) + + # In the unlocked state, the lsof command returns 1. + # An ActionError is raised in this case. + with pytest.raises(ActionError): + superuser_run('packages', ['is-package-manager-busy']) + + +@pytest.mark.usefixtures('develop_mode', 'needs_root') +def test_action_path(monkeypatch): + """Test that in development mode, python action scripts get the + correct PYTHONPATH""" + monkeypatch.setitem(os.environ, 'PYTHONPATH', '') + plinth_path = run('test_path').strip() + su_plinth_path = superuser_run('test_path').strip() + assert plinth_path.startswith(cfg.root) + assert plinth_path == su_plinth_path