1"""
2mac_utils tests
3"""
4
5
6import os
7import plistlib
8import subprocess
9import xml.parsers.expat
10
11import salt.modules.cmdmod as cmd
12import salt.utils.mac_utils as mac_utils
13import salt.utils.platform
14from salt.exceptions import CommandExecutionError, SaltInvocationError
15from tests.support.mixins import LoaderModuleMockMixin
16from tests.support.mock import MagicMock, MockTimedProc, mock_open, patch
17from tests.support.unit import TestCase, skipIf
18
19
20@skipIf(not salt.utils.platform.is_darwin(), "These tests run only on mac")
21class MacUtilsTestCase(TestCase, LoaderModuleMockMixin):
22    """
23    test mac_utils salt utility
24    """
25
26    def setup_loader_modules(self):
27        return {mac_utils: {}}
28
29    def test_execute_return_success_not_supported(self):
30        """
31        test execute_return_success function
32        command not supported
33        """
34        mock_cmd = MagicMock(
35            return_value={"retcode": 0, "stdout": "not supported", "stderr": "error"}
36        )
37        with patch.object(mac_utils, "_run_all", mock_cmd):
38            self.assertRaises(
39                CommandExecutionError, mac_utils.execute_return_success, "dir c:\\"
40            )
41
42    def test_execute_return_success_command_failed(self):
43        """
44        test execute_return_success function
45        command failed
46        """
47        mock_cmd = MagicMock(
48            return_value={"retcode": 1, "stdout": "spongebob", "stderr": "error"}
49        )
50        with patch.object(mac_utils, "_run_all", mock_cmd):
51            self.assertRaises(
52                CommandExecutionError, mac_utils.execute_return_success, "dir c:\\"
53            )
54
55    def test_execute_return_success_command_succeeded(self):
56        """
57        test execute_return_success function
58        command succeeded
59        """
60        mock_cmd = MagicMock(return_value={"retcode": 0, "stdout": "spongebob"})
61        with patch.object(mac_utils, "_run_all", mock_cmd):
62            ret = mac_utils.execute_return_success("dir c:\\")
63            self.assertEqual(ret, True)
64
65    def test_execute_return_result_command_failed(self):
66        """
67        test execute_return_result function
68        command failed
69        """
70        mock_cmd = MagicMock(
71            return_value={"retcode": 1, "stdout": "spongebob", "stderr": "squarepants"}
72        )
73        with patch.object(mac_utils, "_run_all", mock_cmd):
74            self.assertRaises(
75                CommandExecutionError, mac_utils.execute_return_result, "dir c:\\"
76            )
77
78    def test_execute_return_result_command_succeeded(self):
79        """
80        test execute_return_result function
81        command succeeded
82        """
83        mock_cmd = MagicMock(return_value={"retcode": 0, "stdout": "spongebob"})
84        with patch.object(mac_utils, "_run_all", mock_cmd):
85            ret = mac_utils.execute_return_result("dir c:\\")
86            self.assertEqual(ret, "spongebob")
87
88    def test_parse_return_space(self):
89        """
90        test parse_return function
91        space after colon
92        """
93        self.assertEqual(
94            mac_utils.parse_return("spongebob: squarepants"), "squarepants"
95        )
96
97    def test_parse_return_new_line(self):
98        """
99        test parse_return function
100        new line after colon
101        """
102        self.assertEqual(
103            mac_utils.parse_return("spongebob:\nsquarepants"), "squarepants"
104        )
105
106    def test_parse_return_no_delimiter(self):
107        """
108        test parse_return function
109        no delimiter
110        """
111        self.assertEqual(mac_utils.parse_return("squarepants"), "squarepants")
112
113    def test_validate_enabled_on(self):
114        """
115        test validate_enabled function
116        test on
117        """
118        self.assertEqual(mac_utils.validate_enabled("On"), "on")
119
120    def test_validate_enabled_off(self):
121        """
122        test validate_enabled function
123        test off
124        """
125        self.assertEqual(mac_utils.validate_enabled("Off"), "off")
126
127    def test_validate_enabled_bad_string(self):
128        """
129        test validate_enabled function
130        test bad string
131        """
132        self.assertRaises(SaltInvocationError, mac_utils.validate_enabled, "bad string")
133
134    def test_validate_enabled_non_zero(self):
135        """
136        test validate_enabled function
137        test non zero
138        """
139        for x in range(1, 179, 3):
140            self.assertEqual(mac_utils.validate_enabled(x), "on")
141
142    def test_validate_enabled_0(self):
143        """
144        test validate_enabled function
145        test 0
146        """
147        self.assertEqual(mac_utils.validate_enabled(0), "off")
148
149    def test_validate_enabled_true(self):
150        """
151        test validate_enabled function
152        test True
153        """
154        self.assertEqual(mac_utils.validate_enabled(True), "on")
155
156    def test_validate_enabled_false(self):
157        """
158        test validate_enabled function
159        test False
160        """
161        self.assertEqual(mac_utils.validate_enabled(False), "off")
162
163    def test_launchctl(self):
164        """
165        test launchctl function
166        """
167        mock_cmd = MagicMock(
168            return_value={"retcode": 0, "stdout": "success", "stderr": "none"}
169        )
170        with patch("salt.utils.mac_utils.__salt__", {"cmd.run_all": mock_cmd}):
171            ret = mac_utils.launchctl("enable", "org.salt.minion")
172            self.assertEqual(ret, True)
173
174    def test_launchctl_return_stdout(self):
175        """
176        test launchctl function and return stdout
177        """
178        mock_cmd = MagicMock(
179            return_value={"retcode": 0, "stdout": "success", "stderr": "none"}
180        )
181        with patch("salt.utils.mac_utils.__salt__", {"cmd.run_all": mock_cmd}):
182            ret = mac_utils.launchctl("enable", "org.salt.minion", return_stdout=True)
183            self.assertEqual(ret, "success")
184
185    def test_launchctl_error(self):
186        """
187        test launchctl function returning an error
188        """
189        mock_cmd = MagicMock(
190            return_value={"retcode": 1, "stdout": "failure", "stderr": "test failure"}
191        )
192        error = (
193            "Failed to enable service:\n"
194            "stdout: failure\n"
195            "stderr: test failure\n"
196            "retcode: 1"
197        )
198        with patch("salt.utils.mac_utils.__salt__", {"cmd.run_all": mock_cmd}):
199            try:
200                mac_utils.launchctl("enable", "org.salt.minion")
201            except CommandExecutionError as exc:
202                self.assertEqual(exc.message, error)
203
204    @patch("salt.utils.path.os_walk")
205    @patch("os.path.exists")
206    def test_available_services_result(self, mock_exists, mock_os_walk):
207        """
208        test available_services results are properly formed dicts.
209        """
210        results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]}
211        mock_os_walk.side_effect = _get_walk_side_effects(results)
212        mock_exists.return_value = True
213
214        plists = [{"Label": "com.apple.lla1"}]
215        ret = _run_available_services(plists)
216
217        file_path = os.sep + os.path.join(
218            "Library", "LaunchAgents", "com.apple.lla1.plist"
219        )
220        if salt.utils.platform.is_windows():
221            file_path = "c:" + file_path
222
223        expected = {
224            "com.apple.lla1": {
225                "file_name": "com.apple.lla1.plist",
226                "file_path": file_path,
227                "plist": plists[0],
228            }
229        }
230        self.assertEqual(ret, expected)
231
232    @patch("salt.utils.path.os_walk")
233    @patch("os.path.exists")
234    @patch("os.listdir")
235    @patch("os.path.isdir")
236    def test_available_services_dirs(
237        self, mock_isdir, mock_listdir, mock_exists, mock_os_walk
238    ):
239        """
240        test available_services checks all of the expected dirs.
241        """
242        results = {
243            "/Library/LaunchAgents": ["com.apple.lla1.plist"],
244            "/Library/LaunchDaemons": ["com.apple.lld1.plist"],
245            "/System/Library/LaunchAgents": ["com.apple.slla1.plist"],
246            "/System/Library/LaunchDaemons": ["com.apple.slld1.plist"],
247            "/Users/saltymcsaltface/Library/LaunchAgents": ["com.apple.uslla1.plist"],
248        }
249
250        mock_os_walk.side_effect = _get_walk_side_effects(results)
251        mock_listdir.return_value = ["saltymcsaltface"]
252        mock_isdir.return_value = True
253        mock_exists.return_value = True
254
255        plists = [
256            {"Label": "com.apple.lla1"},
257            {"Label": "com.apple.lld1"},
258            {"Label": "com.apple.slla1"},
259            {"Label": "com.apple.slld1"},
260            {"Label": "com.apple.uslla1"},
261        ]
262        ret = _run_available_services(plists)
263
264        self.assertEqual(len(ret), 5)
265
266    @patch("salt.utils.path.os_walk")
267    @patch("os.path.exists")
268    @patch("plistlib.load")
269    def test_available_services_broken_symlink(
270        self, mock_read_plist, mock_exists, mock_os_walk
271    ):
272        """
273        test available_services when it encounters a broken symlink.
274        """
275        results = {
276            "/Library/LaunchAgents": ["com.apple.lla1.plist", "com.apple.lla2.plist"]
277        }
278        mock_os_walk.side_effect = _get_walk_side_effects(results)
279        mock_exists.side_effect = [True, False]
280
281        plists = [{"Label": "com.apple.lla1"}]
282        ret = _run_available_services(plists)
283
284        file_path = os.sep + os.path.join(
285            "Library", "LaunchAgents", "com.apple.lla1.plist"
286        )
287        if salt.utils.platform.is_windows():
288            file_path = "c:" + file_path
289
290        expected = {
291            "com.apple.lla1": {
292                "file_name": "com.apple.lla1.plist",
293                "file_path": file_path,
294                "plist": plists[0],
295            }
296        }
297        self.assertEqual(ret, expected)
298
299    @patch("salt.utils.path.os_walk")
300    @patch("os.path.exists")
301    @patch("salt.utils.mac_utils.__salt__")
302    def test_available_services_binary_plist(
303        self,
304        mock_run,
305        mock_exists,
306        mock_os_walk,
307    ):
308        """
309        test available_services handles binary plist files.
310        """
311        results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]}
312        mock_os_walk.side_effect = _get_walk_side_effects(results)
313        mock_exists.return_value = True
314
315        plists = [{"Label": "com.apple.lla1"}]
316
317        file_path = os.sep + os.path.join(
318            "Library", "LaunchAgents", "com.apple.lla1.plist"
319        )
320        if salt.utils.platform.is_windows():
321            file_path = "c:" + file_path
322
323        ret = _run_available_services(plists)
324
325        expected = {
326            "com.apple.lla1": {
327                "file_name": "com.apple.lla1.plist",
328                "file_path": file_path,
329                "plist": plists[0],
330            }
331        }
332        self.assertEqual(ret, expected)
333
334    @patch("salt.utils.path.os_walk")
335    @patch("os.path.exists")
336    def test_available_services_invalid_file(self, mock_exists, mock_os_walk):
337        """
338        test available_services excludes invalid files.
339        The py3 plistlib raises an InvalidFileException when a plist
340        file cannot be parsed.
341        """
342        results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]}
343        mock_os_walk.side_effect = _get_walk_side_effects(results)
344        mock_exists.return_value = True
345
346        plists = [{"Label": "com.apple.lla1"}]
347
348        mock_load = MagicMock()
349        mock_load.side_effect = plistlib.InvalidFileException
350        with patch("salt.utils.files.fopen", mock_open()):
351            with patch("plistlib.load", mock_load):
352                ret = mac_utils._available_services()
353
354        self.assertEqual(len(ret), 0)
355
356    @patch("salt.utils.mac_utils.__salt__")
357    @patch("salt.utils.path.os_walk")
358    @patch("os.path.exists")
359    def test_available_services_expat_error(self, mock_exists, mock_os_walk, mock_run):
360        """
361        test available_services excludes files with expat errors.
362
363        Poorly formed XML will raise an ExpatError on py2. It will
364        also be raised by some almost-correct XML on py3.
365        """
366        results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]}
367        mock_os_walk.side_effect = _get_walk_side_effects(results)
368        mock_exists.return_value = True
369
370        file_path = os.sep + os.path.join(
371            "Library", "LaunchAgents", "com.apple.lla1.plist"
372        )
373        if salt.utils.platform.is_windows():
374            file_path = "c:" + file_path
375
376        mock_load = MagicMock()
377        mock_load.side_effect = xml.parsers.expat.ExpatError
378        with patch("salt.utils.files.fopen", mock_open()):
379            with patch("plistlib.load", mock_load):
380                ret = mac_utils._available_services()
381
382        self.assertEqual(len(ret), 0)
383
384    @patch("salt.utils.mac_utils.__salt__")
385    @patch("salt.utils.path.os_walk")
386    @patch("os.path.exists")
387    def test_available_services_value_error(self, mock_exists, mock_os_walk, mock_run):
388        """
389        test available_services excludes files with ValueErrors.
390        """
391        results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]}
392        mock_os_walk.side_effect = _get_walk_side_effects(results)
393        mock_exists.return_value = True
394
395        file_path = os.sep + os.path.join(
396            "Library", "LaunchAgents", "com.apple.lla1.plist"
397        )
398        if salt.utils.platform.is_windows():
399            file_path = "c:" + file_path
400
401        mock_load = MagicMock()
402        mock_load.side_effect = ValueError
403        with patch("salt.utils.files.fopen", mock_open()):
404            with patch("plistlib.load", mock_load):
405                ret = mac_utils._available_services()
406
407        self.assertEqual(len(ret), 0)
408
409    def test_bootout_retcode_36_success(self):
410        """
411        Make sure that if we run a `launchctl bootout` cmd and it returns
412        36 that we treat it as a success.
413        """
414        proc = MagicMock(
415            return_value=MockTimedProc(stdout=None, stderr=None, returncode=36)
416        )
417        with patch("salt.utils.timed_subprocess.TimedProc", proc):
418            with patch(
419                "salt.utils.mac_utils.__salt__", {"cmd.run_all": cmd._run_all_quiet}
420            ):
421                ret = mac_utils.launchctl("bootout", "org.salt.minion")
422        self.assertEqual(ret, True)
423
424    def test_bootout_retcode_99_fail(self):
425        """
426        Make sure that if we run a `launchctl bootout` cmd and it returns
427        something other than 0 or 36 that we treat it as a fail.
428        """
429        error = (
430            "Failed to bootout service:\n"
431            "stdout: failure\n"
432            "stderr: test failure\n"
433            "retcode: 99"
434        )
435        proc = MagicMock(
436            return_value=MockTimedProc(
437                stdout=b"failure", stderr=b"test failure", returncode=99
438            )
439        )
440        with patch("salt.utils.timed_subprocess.TimedProc", proc):
441            with patch(
442                "salt.utils.mac_utils.__salt__", {"cmd.run_all": cmd._run_all_quiet}
443            ):
444                try:
445                    mac_utils.launchctl("bootout", "org.salt.minion")
446                except CommandExecutionError as exc:
447                    self.assertEqual(exc.message, error)
448
449    def test_not_bootout_retcode_36_fail(self):
450        """
451        Make sure that if we get a retcode 36 on non bootout cmds
452        that we still get a failure.
453        """
454        error = (
455            "Failed to bootstrap service:\n"
456            "stdout: failure\n"
457            "stderr: test failure\n"
458            "retcode: 36"
459        )
460        proc = MagicMock(
461            return_value=MockTimedProc(
462                stdout=b"failure", stderr=b"test failure", returncode=36
463            )
464        )
465        with patch("salt.utils.timed_subprocess.TimedProc", proc):
466            with patch(
467                "salt.utils.mac_utils.__salt__", {"cmd.run_all": cmd._run_all_quiet}
468            ):
469                try:
470                    mac_utils.launchctl("bootstrap", "org.salt.minion")
471                except CommandExecutionError as exc:
472                    self.assertEqual(exc.message, error)
473
474    def test_git_is_stub(self):
475        mock_check_call = MagicMock(
476            side_effect=subprocess.CalledProcessError(cmd="", returncode=2)
477        )
478        with patch("salt.utils.mac_utils.subprocess.check_call", mock_check_call):
479            self.assertEqual(mac_utils.git_is_stub(), True)
480
481    @patch("salt.utils.mac_utils.subprocess.check_call")
482    def test_git_is_not_stub(self, mock_check_call):
483        self.assertEqual(mac_utils.git_is_stub(), False)
484
485
486def _get_walk_side_effects(results):
487    """
488    Data generation helper function for service tests.
489    """
490
491    def walk_side_effect(*args, **kwargs):
492        return [(args[0], [], results.get(args[0], []))]
493
494    return walk_side_effect
495
496
497def _run_available_services(plists):
498    mock_load = MagicMock()
499    mock_load.side_effect = plists
500    with patch("salt.utils.files.fopen", mock_open()):
501        with patch("plistlib.load", mock_load):
502            ret = mac_utils._available_services()
503    return ret
504