""" mac_utils tests """ import os import plistlib import subprocess import xml.parsers.expat import salt.modules.cmdmod as cmd import salt.utils.mac_utils as mac_utils import salt.utils.platform from salt.exceptions import CommandExecutionError, SaltInvocationError from tests.support.mixins import LoaderModuleMockMixin from tests.support.mock import MagicMock, MockTimedProc, mock_open, patch from tests.support.unit import TestCase, skipIf @skipIf(not salt.utils.platform.is_darwin(), "These tests run only on mac") class MacUtilsTestCase(TestCase, LoaderModuleMockMixin): """ test mac_utils salt utility """ def setup_loader_modules(self): return {mac_utils: {}} def test_execute_return_success_not_supported(self): """ test execute_return_success function command not supported """ mock_cmd = MagicMock( return_value={"retcode": 0, "stdout": "not supported", "stderr": "error"} ) with patch.object(mac_utils, "_run_all", mock_cmd): self.assertRaises( CommandExecutionError, mac_utils.execute_return_success, "dir c:\\" ) def test_execute_return_success_command_failed(self): """ test execute_return_success function command failed """ mock_cmd = MagicMock( return_value={"retcode": 1, "stdout": "spongebob", "stderr": "error"} ) with patch.object(mac_utils, "_run_all", mock_cmd): self.assertRaises( CommandExecutionError, mac_utils.execute_return_success, "dir c:\\" ) def test_execute_return_success_command_succeeded(self): """ test execute_return_success function command succeeded """ mock_cmd = MagicMock(return_value={"retcode": 0, "stdout": "spongebob"}) with patch.object(mac_utils, "_run_all", mock_cmd): ret = mac_utils.execute_return_success("dir c:\\") self.assertEqual(ret, True) def test_execute_return_result_command_failed(self): """ test execute_return_result function command failed """ mock_cmd = MagicMock( return_value={"retcode": 1, "stdout": "spongebob", "stderr": "squarepants"} ) with patch.object(mac_utils, "_run_all", mock_cmd): self.assertRaises( CommandExecutionError, mac_utils.execute_return_result, "dir c:\\" ) def test_execute_return_result_command_succeeded(self): """ test execute_return_result function command succeeded """ mock_cmd = MagicMock(return_value={"retcode": 0, "stdout": "spongebob"}) with patch.object(mac_utils, "_run_all", mock_cmd): ret = mac_utils.execute_return_result("dir c:\\") self.assertEqual(ret, "spongebob") def test_parse_return_space(self): """ test parse_return function space after colon """ self.assertEqual( mac_utils.parse_return("spongebob: squarepants"), "squarepants" ) def test_parse_return_new_line(self): """ test parse_return function new line after colon """ self.assertEqual( mac_utils.parse_return("spongebob:\nsquarepants"), "squarepants" ) def test_parse_return_no_delimiter(self): """ test parse_return function no delimiter """ self.assertEqual(mac_utils.parse_return("squarepants"), "squarepants") def test_validate_enabled_on(self): """ test validate_enabled function test on """ self.assertEqual(mac_utils.validate_enabled("On"), "on") def test_validate_enabled_off(self): """ test validate_enabled function test off """ self.assertEqual(mac_utils.validate_enabled("Off"), "off") def test_validate_enabled_bad_string(self): """ test validate_enabled function test bad string """ self.assertRaises(SaltInvocationError, mac_utils.validate_enabled, "bad string") def test_validate_enabled_non_zero(self): """ test validate_enabled function test non zero """ for x in range(1, 179, 3): self.assertEqual(mac_utils.validate_enabled(x), "on") def test_validate_enabled_0(self): """ test validate_enabled function test 0 """ self.assertEqual(mac_utils.validate_enabled(0), "off") def test_validate_enabled_true(self): """ test validate_enabled function test True """ self.assertEqual(mac_utils.validate_enabled(True), "on") def test_validate_enabled_false(self): """ test validate_enabled function test False """ self.assertEqual(mac_utils.validate_enabled(False), "off") def test_launchctl(self): """ test launchctl function """ mock_cmd = MagicMock( return_value={"retcode": 0, "stdout": "success", "stderr": "none"} ) with patch("salt.utils.mac_utils.__salt__", {"cmd.run_all": mock_cmd}): ret = mac_utils.launchctl("enable", "org.salt.minion") self.assertEqual(ret, True) def test_launchctl_return_stdout(self): """ test launchctl function and return stdout """ mock_cmd = MagicMock( return_value={"retcode": 0, "stdout": "success", "stderr": "none"} ) with patch("salt.utils.mac_utils.__salt__", {"cmd.run_all": mock_cmd}): ret = mac_utils.launchctl("enable", "org.salt.minion", return_stdout=True) self.assertEqual(ret, "success") def test_launchctl_error(self): """ test launchctl function returning an error """ mock_cmd = MagicMock( return_value={"retcode": 1, "stdout": "failure", "stderr": "test failure"} ) error = ( "Failed to enable service:\n" "stdout: failure\n" "stderr: test failure\n" "retcode: 1" ) with patch("salt.utils.mac_utils.__salt__", {"cmd.run_all": mock_cmd}): try: mac_utils.launchctl("enable", "org.salt.minion") except CommandExecutionError as exc: self.assertEqual(exc.message, error) @patch("salt.utils.path.os_walk") @patch("os.path.exists") def test_available_services_result(self, mock_exists, mock_os_walk): """ test available_services results are properly formed dicts. """ results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]} mock_os_walk.side_effect = _get_walk_side_effects(results) mock_exists.return_value = True plists = [{"Label": "com.apple.lla1"}] ret = _run_available_services(plists) file_path = os.sep + os.path.join( "Library", "LaunchAgents", "com.apple.lla1.plist" ) if salt.utils.platform.is_windows(): file_path = "c:" + file_path expected = { "com.apple.lla1": { "file_name": "com.apple.lla1.plist", "file_path": file_path, "plist": plists[0], } } self.assertEqual(ret, expected) @patch("salt.utils.path.os_walk") @patch("os.path.exists") @patch("os.listdir") @patch("os.path.isdir") def test_available_services_dirs( self, mock_isdir, mock_listdir, mock_exists, mock_os_walk ): """ test available_services checks all of the expected dirs. """ results = { "/Library/LaunchAgents": ["com.apple.lla1.plist"], "/Library/LaunchDaemons": ["com.apple.lld1.plist"], "/System/Library/LaunchAgents": ["com.apple.slla1.plist"], "/System/Library/LaunchDaemons": ["com.apple.slld1.plist"], "/Users/saltymcsaltface/Library/LaunchAgents": ["com.apple.uslla1.plist"], } mock_os_walk.side_effect = _get_walk_side_effects(results) mock_listdir.return_value = ["saltymcsaltface"] mock_isdir.return_value = True mock_exists.return_value = True plists = [ {"Label": "com.apple.lla1"}, {"Label": "com.apple.lld1"}, {"Label": "com.apple.slla1"}, {"Label": "com.apple.slld1"}, {"Label": "com.apple.uslla1"}, ] ret = _run_available_services(plists) self.assertEqual(len(ret), 5) @patch("salt.utils.path.os_walk") @patch("os.path.exists") @patch("plistlib.load") def test_available_services_broken_symlink( self, mock_read_plist, mock_exists, mock_os_walk ): """ test available_services when it encounters a broken symlink. """ results = { "/Library/LaunchAgents": ["com.apple.lla1.plist", "com.apple.lla2.plist"] } mock_os_walk.side_effect = _get_walk_side_effects(results) mock_exists.side_effect = [True, False] plists = [{"Label": "com.apple.lla1"}] ret = _run_available_services(plists) file_path = os.sep + os.path.join( "Library", "LaunchAgents", "com.apple.lla1.plist" ) if salt.utils.platform.is_windows(): file_path = "c:" + file_path expected = { "com.apple.lla1": { "file_name": "com.apple.lla1.plist", "file_path": file_path, "plist": plists[0], } } self.assertEqual(ret, expected) @patch("salt.utils.path.os_walk") @patch("os.path.exists") @patch("salt.utils.mac_utils.__salt__") def test_available_services_binary_plist( self, mock_run, mock_exists, mock_os_walk, ): """ test available_services handles binary plist files. """ results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]} mock_os_walk.side_effect = _get_walk_side_effects(results) mock_exists.return_value = True plists = [{"Label": "com.apple.lla1"}] file_path = os.sep + os.path.join( "Library", "LaunchAgents", "com.apple.lla1.plist" ) if salt.utils.platform.is_windows(): file_path = "c:" + file_path ret = _run_available_services(plists) expected = { "com.apple.lla1": { "file_name": "com.apple.lla1.plist", "file_path": file_path, "plist": plists[0], } } self.assertEqual(ret, expected) @patch("salt.utils.path.os_walk") @patch("os.path.exists") def test_available_services_invalid_file(self, mock_exists, mock_os_walk): """ test available_services excludes invalid files. The py3 plistlib raises an InvalidFileException when a plist file cannot be parsed. """ results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]} mock_os_walk.side_effect = _get_walk_side_effects(results) mock_exists.return_value = True plists = [{"Label": "com.apple.lla1"}] mock_load = MagicMock() mock_load.side_effect = plistlib.InvalidFileException with patch("salt.utils.files.fopen", mock_open()): with patch("plistlib.load", mock_load): ret = mac_utils._available_services() self.assertEqual(len(ret), 0) @patch("salt.utils.mac_utils.__salt__") @patch("salt.utils.path.os_walk") @patch("os.path.exists") def test_available_services_expat_error(self, mock_exists, mock_os_walk, mock_run): """ test available_services excludes files with expat errors. Poorly formed XML will raise an ExpatError on py2. It will also be raised by some almost-correct XML on py3. """ results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]} mock_os_walk.side_effect = _get_walk_side_effects(results) mock_exists.return_value = True file_path = os.sep + os.path.join( "Library", "LaunchAgents", "com.apple.lla1.plist" ) if salt.utils.platform.is_windows(): file_path = "c:" + file_path mock_load = MagicMock() mock_load.side_effect = xml.parsers.expat.ExpatError with patch("salt.utils.files.fopen", mock_open()): with patch("plistlib.load", mock_load): ret = mac_utils._available_services() self.assertEqual(len(ret), 0) @patch("salt.utils.mac_utils.__salt__") @patch("salt.utils.path.os_walk") @patch("os.path.exists") def test_available_services_value_error(self, mock_exists, mock_os_walk, mock_run): """ test available_services excludes files with ValueErrors. """ results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]} mock_os_walk.side_effect = _get_walk_side_effects(results) mock_exists.return_value = True file_path = os.sep + os.path.join( "Library", "LaunchAgents", "com.apple.lla1.plist" ) if salt.utils.platform.is_windows(): file_path = "c:" + file_path mock_load = MagicMock() mock_load.side_effect = ValueError with patch("salt.utils.files.fopen", mock_open()): with patch("plistlib.load", mock_load): ret = mac_utils._available_services() self.assertEqual(len(ret), 0) def test_bootout_retcode_36_success(self): """ Make sure that if we run a `launchctl bootout` cmd and it returns 36 that we treat it as a success. """ proc = MagicMock( return_value=MockTimedProc(stdout=None, stderr=None, returncode=36) ) with patch("salt.utils.timed_subprocess.TimedProc", proc): with patch( "salt.utils.mac_utils.__salt__", {"cmd.run_all": cmd._run_all_quiet} ): ret = mac_utils.launchctl("bootout", "org.salt.minion") self.assertEqual(ret, True) def test_bootout_retcode_99_fail(self): """ Make sure that if we run a `launchctl bootout` cmd and it returns something other than 0 or 36 that we treat it as a fail. """ error = ( "Failed to bootout service:\n" "stdout: failure\n" "stderr: test failure\n" "retcode: 99" ) proc = MagicMock( return_value=MockTimedProc( stdout=b"failure", stderr=b"test failure", returncode=99 ) ) with patch("salt.utils.timed_subprocess.TimedProc", proc): with patch( "salt.utils.mac_utils.__salt__", {"cmd.run_all": cmd._run_all_quiet} ): try: mac_utils.launchctl("bootout", "org.salt.minion") except CommandExecutionError as exc: self.assertEqual(exc.message, error) def test_not_bootout_retcode_36_fail(self): """ Make sure that if we get a retcode 36 on non bootout cmds that we still get a failure. """ error = ( "Failed to bootstrap service:\n" "stdout: failure\n" "stderr: test failure\n" "retcode: 36" ) proc = MagicMock( return_value=MockTimedProc( stdout=b"failure", stderr=b"test failure", returncode=36 ) ) with patch("salt.utils.timed_subprocess.TimedProc", proc): with patch( "salt.utils.mac_utils.__salt__", {"cmd.run_all": cmd._run_all_quiet} ): try: mac_utils.launchctl("bootstrap", "org.salt.minion") except CommandExecutionError as exc: self.assertEqual(exc.message, error) def test_git_is_stub(self): mock_check_call = MagicMock( side_effect=subprocess.CalledProcessError(cmd="", returncode=2) ) with patch("salt.utils.mac_utils.subprocess.check_call", mock_check_call): self.assertEqual(mac_utils.git_is_stub(), True) @patch("salt.utils.mac_utils.subprocess.check_call") def test_git_is_not_stub(self, mock_check_call): self.assertEqual(mac_utils.git_is_stub(), False) def _get_walk_side_effects(results): """ Data generation helper function for service tests. """ def walk_side_effect(*args, **kwargs): return [(args[0], [], results.get(args[0], []))] return walk_side_effect def _run_available_services(plists): mock_load = MagicMock() mock_load.side_effect = plists with patch("salt.utils.files.fopen", mock_open()): with patch("plistlib.load", mock_load): ret = mac_utils._available_services() return ret