diff --git a/changelog/69496.added.md b/changelog/69496.added.md new file mode 100644 index 000000000000..9700b506a1d5 --- /dev/null +++ b/changelog/69496.added.md @@ -0,0 +1 @@ +Added ``pkg.trust``, ``pkg.untrust``, ``pkg.is_trusted``, and ``pkg.list_trusted`` to ``salt.modules.mac_brew_pkg`` to manage Homebrew's trust list for non-official taps, formulae, casks and external commands. Added ``pkg.trusted`` and ``pkg.untrusted`` state functions to ``salt.states.pkg`` to enforce trust state declaratively on macOS. diff --git a/changelog/70068.added.md b/changelog/70068.added.md new file mode 100644 index 000000000000..2980affbe113 --- /dev/null +++ b/changelog/70068.added.md @@ -0,0 +1 @@ +Added an optional ``name`` argument to ``pkg.homebrew_prefix`` on macOS, so it can return the install location of a specific formula (via ``brew --prefix ``) in addition to the global Homebrew prefix. diff --git a/salt/modules/mac_brew_pkg.py b/salt/modules/mac_brew_pkg.py index 1a10f4944106..51a500f58161 100644 --- a/salt/modules/mac_brew_pkg.py +++ b/salt/modules/mac_brew_pkg.py @@ -34,6 +34,8 @@ # Define the module's virtual name __virtualname__ = "pkg" +_TRUST_TYPES = ("tap", "formula", "cask", "command") + def __virtual__(): """ @@ -134,7 +136,7 @@ def _homebrew_bin(): """ Returns the full path to the homebrew binary in the homebrew installation folder """ - ret = homebrew_prefix() + ret = _homebrew_prefix() if ret is not None: ret += "/bin/brew" else: @@ -181,7 +183,7 @@ def _list_pkgs_from_context(versions_as_list): return ret -def homebrew_prefix(): +def _homebrew_prefix(): """ Returns the full path to the homebrew prefix. @@ -235,6 +237,39 @@ def homebrew_prefix(): return None +def homebrew_prefix(name=None): + """ + Returns the full path to the homebrew prefix. + If ``name`` is provided, displays the location where formula is or would be installed. + + name + The name of the formula to get its prefix. + + .. versionadded:: 3008.3 + + Returns ``Str`` with the location of the given formula. + + CLI Example: + + .. code-block:: bash + + salt '*' pkg.homebrew_prefix + salt '*' pkg.homebrew_prefix vim + """ + + if name is None: + return _homebrew_prefix() + + cmd = ["--prefix", name] + result = _call_brew(*cmd) + if result["retcode"] != 0 or result["stdout"] == "": + raise CommandExecutionError( + f"Error getting brew prefix for formula {name}", info={"result": result} + ) + + return result["stdout"] + + def list_pkgs(versions_as_list=False, **kwargs): """ List the packages currently installed in a dict:: @@ -909,3 +944,156 @@ def unhold(name=None, pkgs=None, sources=None, **kwargs): # pylint: disable=W06 unpin = unhold + + +def list_trusted(type=None): + """ + List trusted taps, formulae, casks and commands. + + .. versionadded:: 3008.3 + + type + Filter by type. Valid values: ``tap``, ``formula``, ``cask``, ``command``. + When ``None`` (default), returns a dict with all trusted items grouped by + type (keys: ``taps``, ``formulae``, ``casks``, ``commands``). When a type + is specified, returns a list of trusted items of that type. + + CLI Example: + + .. code-block:: bash + + salt '*' pkg.list_trusted + salt '*' pkg.list_trusted type=tap + """ + if type is not None and type not in _TRUST_TYPES: + raise SaltInvocationError( + f"Invalid type '{type}'. Valid values: {', '.join(_TRUST_TYPES)}" + ) + + cmd = ["trust", "--json=v1"] + if type is not None: + cmd.append(f"--{type}") + + result = _call_brew(*cmd) + try: + return salt.utils.json.loads(result["stdout"]) + except ValueError as err: + msg = f'Unable to interpret output from "brew trust": {err}' + log.error(msg) + raise CommandExecutionError(msg) + + +def trust(name, type=None): + """ + Trust a tap, formula, cask or command so Homebrew may load it when + ``$HOMEBREW_REQUIRE_TAP_TRUST`` is set. + + .. versionadded:: 3008.3 + + name + The name of the tap, formula, cask or command to trust. Can also be a + remote URL for taps. + + type + The type of the item. Valid values: ``tap``, ``formula``, ``cask``, + ``command``. If not specified, Homebrew will auto-detect the type. + + Returns ``True`` on success (including when the item was already trusted), + ``False`` on failure. + + CLI Example: + + .. code-block:: bash + + salt '*' pkg.trust cdalvaro/tap + salt '*' pkg.trust cdalvaro/tap type=tap + salt '*' pkg.trust cdalvaro/tap/salt type=formula + """ + if type is not None and type not in _TRUST_TYPES: + raise SaltInvocationError( + f"Invalid type '{type}'. Valid values: {', '.join(_TRUST_TYPES)}" + ) + + cmd = ["trust"] + if type is not None: + cmd.append(f"--{type}") + cmd.append(name) + + try: + _call_brew(*cmd) + except CommandExecutionError: + log.error('Failed to trust "%s"', name) + return False + + return True + + +def untrust(name, type=None): + """ + Stop trusting a tap, formula, cask or command. + + .. versionadded:: 3008.3 + + name + The name of the tap, formula, cask or command to untrust. + + type + The type of the item. Valid values: ``tap``, ``formula``, ``cask``, + ``command``. If not specified, Homebrew will auto-detect the type. + + Returns ``True`` on success (including when the item was not trusted), + ``False`` on failure. + + CLI Example: + + .. code-block:: bash + + salt '*' pkg.untrust cdalvaro/tap + salt '*' pkg.untrust cdalvaro/tap type=tap + salt '*' pkg.untrust cdalvaro/tap/salt type=formula + """ + if type is not None and type not in _TRUST_TYPES: + raise SaltInvocationError( + f"Invalid type '{type}'. Valid values: {', '.join(_TRUST_TYPES)}" + ) + + cmd = ["untrust"] + if type is not None: + cmd.append(f"--{type}") + cmd.append(name) + + try: + _call_brew(*cmd) + except CommandExecutionError: + log.error('Failed to untrust "%s"', name) + return False + + return True + + +def is_trusted(name, type=None): + """ + Check whether a tap, formula, cask or command is trusted. + + .. versionadded:: 3008.3 + + name + The name of the tap, formula, cask or command to check. + + type + The type of the item. Valid values: ``tap``, ``formula``, ``cask``, + ``command``. If not specified, checks across all types. + + Returns ``True`` if the item is trusted, ``False`` otherwise. + + CLI Example: + + .. code-block:: bash + + salt '*' pkg.is_trusted cdalvaro/tap + salt '*' pkg.is_trusted cdalvaro/tap type=tap + """ + trusted = list_trusted(type=type) + if isinstance(trusted, list): + return name in trusted + return any(name in items for items in trusted.values()) diff --git a/salt/states/pkg.py b/salt/states/pkg.py index e11787287ba6..b7852d66f3b9 100644 --- a/salt/states/pkg.py +++ b/salt/states/pkg.py @@ -297,9 +297,7 @@ def _find_download_targets( "changes": {}, "result": True, "comment": ( - "Version {} of package '{}' is already downloaded".format( - version, name - ) + f"Version {version} of package '{name}' is already downloaded" ), } @@ -476,8 +474,7 @@ def _find_remove_targets( targets.append(pkgname) else: log.debug( - "Current version (%s) did not match desired version " - "specification (%s), will not remove", + "Current version (%s) did not match desired version specification (%s), will not remove", cver, pkgver, ) @@ -647,9 +644,7 @@ def _find_install_targets( "name": name, "changes": {}, "result": True, - "comment": "Version {} of package '{}' is already installed".format( - version, name - ), + "comment": f"Version {version} of package '{name}' is already installed", } # if cver is not an empty string, the package is already installed @@ -745,7 +740,6 @@ def _find_install_targets( warnings = [] failed_verify = False for package_name, version_string in desired.items(): - # FreeBSD pkg supports `openjdk` and `java/openjdk7` package names origin = bool(re.search("/", package_name)) @@ -1851,8 +1845,9 @@ def installed( "changes": {}, "result": False, "comment": ( - "An error was encountered while " - "holding/unholding package(s): {}".format(hold_ret["comment"]) + "An error was encountered while holding/unholding package(s): {}".format( + hold_ret["comment"] + ) ), } else: @@ -1921,8 +1916,9 @@ def installed( ) if to_unpurge: comment.append( - "The following packages would have their selection status " - "changed from 'purge' to 'install': {}".format(", ".join(to_unpurge)) + "The following packages would have their selection status changed from 'purge' to 'install': {}".format( + ", ".join(to_unpurge) + ) ) changes.update({x: {"new": "installed", "old": ""} for x in to_unpurge}) if to_reinstall: @@ -1950,8 +1946,9 @@ def installed( else: pkgstr = _get_desired_pkg(reinstall_pkg, to_reinstall) comment.append( - "Package '{}' would be reinstalled because the " - "following files have been altered:".format(pkgstr) + "Package '{}' would be reinstalled because the following files have been altered:".format( + pkgstr + ) ) changes.update({reinstall_pkg: {}}) comment.append(_nested_output(altered_files[reinstall_pkg])) @@ -1994,9 +1991,7 @@ def installed( else: ret["changes"] = {} ret["comment"] = ( - "An error was encountered while installing package(s): {}".format( - exc - ) + f"An error was encountered while installing package(s): {exc}" ) if warnings: ret.setdefault("warnings", []).extend(warnings) @@ -2038,8 +2033,9 @@ def installed( "changes": {}, "result": False, "comment": ( - "An error was encountered while " - "holding/unholding package(s): {}".format(hold_ret["comment"]) + "An error was encountered while holding/unholding package(s): {}".format( + hold_ret["comment"] + ) ), } if warnings: @@ -2376,16 +2372,12 @@ def downloaded( return targets elif not isinstance(targets, dict): ret["result"] = False - ret["comment"] = "An error was encountered while checking targets: {}".format( - targets - ) + ret["comment"] = f"An error was encountered while checking targets: {targets}" return ret if __opts__["test"]: summary = ", ".join(targets) - ret["comment"] = "The following packages would be downloaded: {}".format( - summary - ) + ret["comment"] = f"The following packages would be downloaded: {summary}" return ret try: @@ -2481,9 +2473,7 @@ def patch_installed(name, advisory_ids=None, downloadonly=None, **kwargs): return targets elif not isinstance(targets, list): ret["result"] = False - ret["comment"] = "An error was encountered while checking targets: {}".format( - targets - ) + ret["comment"] = f"An error was encountered while checking targets: {targets}" return ret if __opts__["test"]: @@ -2516,9 +2506,7 @@ def patch_installed(name, advisory_ids=None, downloadonly=None, **kwargs): status = "downloaded" if downloadonly else "installed" ret["result"] = True ret["comment"] = ( - "Advisory patch is not needed or related packages are already {}".format( - status - ) + f"Advisory patch is not needed or related packages are already {status}" ) return ret @@ -2777,8 +2765,7 @@ def latest( "changes": {}, "result": False, "comment": ( - "An error was encountered while checking the " - "newest available version of package(s): {}".format(exc) + f"An error was encountered while checking the newest available version of package(s): {exc}" ), } @@ -2887,9 +2874,7 @@ def latest( "changes": {}, "result": False, "comment": ( - "An error was encountered while installing package(s): {}".format( - exc - ) + f"An error was encountered while installing package(s): {exc}" ), } @@ -2912,10 +2897,8 @@ def latest( ) comments.append(msg) if successful: - msg = ( - "The following packages were successfully " - "installed/upgraded: " - "{}".format(", ".join(sorted(successful))) + msg = "The following packages were successfully installed/upgraded: {}".format( + ", ".join(sorted(successful)) ) comments.append(msg) if up_to_date: @@ -2935,20 +2918,14 @@ def latest( } else: if len(targets) > 10: - comment = ( - "{} targeted packages failed to update. " - "See debug log for details.".format(len(targets)) - ) + comment = f"{len(targets)} targeted packages failed to update. See debug log for details." elif len(targets) > 1: - comment = ( - "The following targeted packages failed to update. " - "See debug log for details: ({}).".format( - ", ".join(sorted(targets)) - ) + comment = "The following targeted packages failed to update. See debug log for details: ({}).".format( + ", ".join(sorted(targets)) ) else: - comment = "Package {} failed to update.".format( - next(iter(list(targets.keys()))) + comment = ( + f"Package {next(iter(list(targets.keys())))} failed to update." ) if up_to_date: if len(up_to_date) <= 10: @@ -2958,9 +2935,7 @@ def latest( ) ) else: - comment += "{} packages were already up-to-date".format( - len(up_to_date) - ) + comment += f"{len(up_to_date)} packages were already up-to-date" return { "name": name, @@ -3030,9 +3005,7 @@ def _uninstall( "name": name, "changes": {}, "result": False, - "comment": "An error was encountered while checking targets: {}".format( - targets - ), + "comment": f"An error was encountered while checking targets: {targets}", } if action == "purge": old_removed = __salt__["pkg.list_pkgs"]( @@ -3593,8 +3566,7 @@ def group_installed(name, skip=None, include=None, **kwargs): else: ret["changes"] = {} ret["comment"] = ( - "An error was encountered while " - "installing/updating group '{}': {}".format(name, exc) + f"An error was encountered while installing/updating group '{name}': {exc}" ) return ret @@ -3796,9 +3768,7 @@ def mod_beacon(name, **kwargs): return { "name": name, "changes": {}, - "comment": "pkg.{} does not work with the mod_beacon state function".format( - sfun - ), + "comment": f"pkg.{sfun} does not work with the mod_beacon state function", "result": False, } @@ -3978,6 +3948,120 @@ def held(name, version=None, pkgs=None, replace=False, **kwargs): return ret +def trusted(name, **kwargs): + """ + Ensure a package source or component is marked as trusted by the package + manager. Only available for package managers that implement a trust model + (e.g. Homebrew on macOS). + + .. versionadded:: 3008.3 + + name + The identifier of the package source or component to trust. The exact + format depends on the package manager (e.g. a tap name, a formula, a + URL). + + kwargs + Additional keyword arguments are passed through to the underlying + ``pkg.trust`` and ``pkg.is_trusted`` module functions, allowing + package-manager-specific options to be supplied. For example, on macOS + with Homebrew, ``type`` can be set to ``tap``, ``formula``, ``cask`` + or ``command`` to disambiguate the target. + + Examples: + + .. code-block:: yaml + + # Homebrew: trust a third-party tap + cdalvaro/tap: + pkg.trusted: + - type: tap + + # Homebrew: trust a specific formula from a third-party tap + cdalvaro/tap/salt: + pkg.trusted: + - type: formula + """ + ret = {"name": name, "changes": {}, "result": True, "comment": ""} + + if "pkg.trust" not in __salt__: + ret["result"] = False + ret["comment"] = "`trust` is not available for this package manager." + return ret + + if __salt__["pkg.is_trusted"](name, **kwargs): + ret["comment"] = f"{name} is already trusted." + return ret + + if __opts__["test"]: + ret["result"] = None + ret["comment"] = f"{name} would be trusted." + return ret + + if not __salt__["pkg.trust"](name, **kwargs): + ret["result"] = False + ret["comment"] = f"Failed to trust {name}." + return ret + + ret["changes"] = {name: {"old": "untrusted", "new": "trusted"}} + ret["comment"] = f"{name} is now trusted." + return ret + + +def untrusted(name, **kwargs): + """ + Ensure a package source or component is not marked as trusted by the + package manager. Only available for package managers that implement a + trust model (e.g. Homebrew on macOS). + + .. versionadded:: 3008.3 + + name + The identifier of the package source or component to untrust. The + exact format depends on the package manager. + + kwargs + Additional keyword arguments are passed through to the underlying + ``pkg.untrust`` and ``pkg.is_trusted`` module functions, allowing + package-manager-specific options to be supplied. For example, on macOS + with Homebrew, ``type`` can be set to ``tap``, ``formula``, ``cask`` + or ``command`` to disambiguate the target. + + Example: + + .. code-block:: yaml + + # Homebrew: remove trust from a third-party tap + cdalvaro/tap: + pkg.untrusted: + - type: tap + """ + ret = {"name": name, "changes": {}, "result": True, "comment": ""} + + if "pkg.untrust" not in __salt__: + ret["result"] = False + ret["comment"] = "`untrust` is not available for this package manager." + return ret + + if not __salt__["pkg.is_trusted"](name, **kwargs): + ret["comment"] = f"{name} is already not trusted." + return ret + + if __opts__["test"]: + ret["result"] = None + ret["comment"] = f"{name} would be untrusted." + return ret + + if not __salt__["pkg.untrust"](name, **kwargs): + ret["result"] = False + ret["comment"] = f"Failed to untrust {name}." + return ret + + ret["changes"] = {name: {"old": "trusted", "new": "untrusted"}} + ret["comment"] = f"{name} is no longer trusted." + return ret + + def unheld(name, version=None, pkgs=None, all=False, **kwargs): """ .. versionadded:: 3005 diff --git a/tests/pytests/unit/modules/test_mac_brew_pkg.py b/tests/pytests/unit/modules/test_mac_brew_pkg.py index 62d3c0f008ef..1d5a9e1f1ef0 100644 --- a/tests/pytests/unit/modules/test_mac_brew_pkg.py +++ b/tests/pytests/unit/modules/test_mac_brew_pkg.py @@ -1,5 +1,5 @@ """ - :codeauthor: Nicole Thomas +:codeauthor: Nicole Thomas """ import os @@ -587,14 +587,17 @@ def test_tap_failure(HOMEBREW_BIN): with patch( "salt.modules.mac_brew_pkg._homebrew_bin", MagicMock(return_value=HOMEBREW_BIN) ): - with patch.dict( - mac_brew.__salt__, - { - "cmd.run_all": mock_failure, - "file.get_user": mock_user, - "cmd.run": mock_cmd, - }, - ), patch("salt.modules.mac_brew_pkg._list_taps", MagicMock(return_value={})): + with ( + patch.dict( + mac_brew.__salt__, + { + "cmd.run_all": mock_failure, + "file.get_user": mock_user, + "cmd.run": mock_cmd, + }, + ), + patch("salt.modules.mac_brew_pkg._list_taps", MagicMock(return_value={})), + ): assert not mac_brew._tap("homebrew/test") @@ -608,20 +611,24 @@ def test_tap(TAPS_LIST, HOMEBREW_BIN): with patch( "salt.modules.mac_brew_pkg._homebrew_bin", MagicMock(return_value=HOMEBREW_BIN) ): - with patch.dict( - mac_brew.__salt__, - { - "cmd.run_all": mock_failure, - "file.get_user": mock_user, - "cmd.run": mock_cmd, - }, - ), patch( - "salt.modules.mac_brew_pkg._list_taps", MagicMock(return_value=TAPS_LIST) + with ( + patch.dict( + mac_brew.__salt__, + { + "cmd.run_all": mock_failure, + "file.get_user": mock_user, + "cmd.run": mock_cmd, + }, + ), + patch( + "salt.modules.mac_brew_pkg._list_taps", + MagicMock(return_value=TAPS_LIST), + ), ): assert mac_brew._tap("homebrew/test") -# 'homebrew_prefix' function tests: 4 +# 'homebrew_prefix' function tests: 10 def test_homebrew_prefix_env(HOMEBREW_PREFIX): @@ -647,11 +654,13 @@ def test_homebrew_prefix_command(HOMEBREW_PREFIX, HOMEBREW_BIN): del mock_env["HOMEBREW_PREFIX"] with patch.dict(os.environ, mock_env): - with patch( - "salt.modules.cmdmod.run", MagicMock(return_value=HOMEBREW_PREFIX) - ), patch("salt.modules.file.get_user", MagicMock(return_value="foo")), patch( - "salt.modules.mac_brew_pkg._homebrew_os_bin", - MagicMock(return_value=HOMEBREW_BIN), + with ( + patch("salt.modules.cmdmod.run", MagicMock(return_value=HOMEBREW_PREFIX)), + patch("salt.modules.file.get_user", MagicMock(return_value="foo")), + patch( + "salt.modules.mac_brew_pkg._homebrew_os_bin", + MagicMock(return_value=HOMEBREW_BIN), + ), ): assert mac_brew.homebrew_prefix() == HOMEBREW_PREFIX @@ -684,11 +693,14 @@ def test_homebrew_prefix_returns_none_even_with_execution_errors(): del mock_env["HOMEBREW_PREFIX"] with patch.dict(os.environ, mock_env, clear=True): - with patch( - "salt.modules.cmdmod.run", MagicMock(side_effect=CommandExecutionError) - ), patch( - "salt.modules.mac_brew_pkg._homebrew_os_bin", - MagicMock(return_value=None), + with ( + patch( + "salt.modules.cmdmod.run", MagicMock(side_effect=CommandExecutionError) + ), + patch( + "salt.modules.mac_brew_pkg._homebrew_os_bin", + MagicMock(return_value=None), + ), ): assert mac_brew.homebrew_prefix() is None @@ -716,13 +728,14 @@ def test_homebrew_prefix_no_su_when_brew_owner_is_current_user( current_user = "brewowner" run_mock = MagicMock(return_value=HOMEBREW_PREFIX) with patch.dict(os.environ, mock_env, clear=True): - with patch("salt.modules.cmdmod.run", run_mock), patch( - "salt.modules.file.get_user", MagicMock(return_value=current_user) - ), patch( - "salt.modules.mac_brew_pkg._homebrew_os_bin", - MagicMock(return_value=HOMEBREW_BIN), - ), patch( - "getpass.getuser", MagicMock(return_value=current_user) + with ( + patch("salt.modules.cmdmod.run", run_mock), + patch("salt.modules.file.get_user", MagicMock(return_value=current_user)), + patch( + "salt.modules.mac_brew_pkg._homebrew_os_bin", + MagicMock(return_value=HOMEBREW_BIN), + ), + patch("getpass.getuser", MagicMock(return_value=current_user)), ): assert mac_brew.homebrew_prefix() == HOMEBREW_PREFIX @@ -749,13 +762,14 @@ def test_homebrew_prefix_still_uses_runas_when_brew_owned_by_other_user( run_mock = MagicMock(return_value=HOMEBREW_PREFIX) with patch.dict(os.environ, mock_env, clear=True): - with patch("salt.modules.cmdmod.run", run_mock), patch( - "salt.modules.file.get_user", MagicMock(return_value="brewowner") - ), patch( - "salt.modules.mac_brew_pkg._homebrew_os_bin", - MagicMock(return_value=HOMEBREW_BIN), - ), patch( - "getpass.getuser", MagicMock(return_value="someoneelse") + with ( + patch("salt.modules.cmdmod.run", run_mock), + patch("salt.modules.file.get_user", MagicMock(return_value="brewowner")), + patch( + "salt.modules.mac_brew_pkg._homebrew_os_bin", + MagicMock(return_value=HOMEBREW_BIN), + ), + patch("getpass.getuser", MagicMock(return_value="someoneelse")), ): assert mac_brew.homebrew_prefix() == HOMEBREW_PREFIX @@ -763,6 +777,60 @@ def test_homebrew_prefix_still_uses_runas_when_brew_owned_by_other_user( assert kwargs.get("runas") == "brewowner" +def test_homebrew_prefix_no_name_delegates_to_private_helper(): + """ + Tests that homebrew_prefix() without a name delegates + to the private _homebrew_prefix helper instead of + calling brew directly. + """ + mock_prefix = MagicMock(return_value="/opt/homebrew") + mock_call_brew = MagicMock() + with ( + patch("salt.modules.mac_brew_pkg._homebrew_prefix", mock_prefix), + patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew), + ): + assert mac_brew.homebrew_prefix() == "/opt/homebrew" + mock_prefix.assert_called_once() + mock_call_brew.assert_not_called() + + +def test_homebrew_prefix_with_name(): + """ + Tests that homebrew_prefix(name) returns the prefix + for the given formula by calling 'brew --prefix '. + """ + mock_call_brew = MagicMock( + return_value={"retcode": 0, "stdout": "/opt/homebrew/opt/vim", "stderr": ""} + ) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + assert mac_brew.homebrew_prefix(name="vim") == "/opt/homebrew/opt/vim" + mock_call_brew.assert_called_once_with("--prefix", "vim") + + +def test_homebrew_prefix_with_name_failure(): + """ + Tests that homebrew_prefix(name) raises CommandExecutionError + when brew returns a non-zero retcode. + """ + mock_call_brew = MagicMock( + return_value={"retcode": 1, "stdout": "", "stderr": "Error: No such formula"} + ) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + with pytest.raises(CommandExecutionError, match="vim"): + mac_brew.homebrew_prefix(name="vim") + + +def test_homebrew_prefix_with_name_empty_stdout(): + """ + Tests that homebrew_prefix(name) raises CommandExecutionError + when brew succeeds but returns an empty prefix. + """ + mock_call_brew = MagicMock(return_value={"retcode": 0, "stdout": "", "stderr": ""}) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + with pytest.raises(CommandExecutionError, match="vim"): + mac_brew.homebrew_prefix(name="vim") + + # '_homebrew_os_bin' function tests: 1 @@ -801,7 +869,7 @@ def test_homebrew_bin(HOMEBREW_PREFIX, HOMEBREW_BIN): Tests the path to the homebrew binary """ mock_path = MagicMock(return_value=HOMEBREW_PREFIX) - with patch("salt.modules.mac_brew_pkg.homebrew_prefix", mock_path): + with patch("salt.modules.mac_brew_pkg._homebrew_prefix", mock_path): assert mac_brew._homebrew_bin() == HOMEBREW_BIN @@ -840,12 +908,15 @@ def test_list_pkgs_homebrew_cask_pakages(): "nvim": "0.10.0", } - with patch("salt.modules.mac_brew_pkg._call_brew", custom_call_brew), patch.dict( - mac_brew.__salt__, - { - "pkg_resource.add_pkg": custom_add_pkg, - "pkg_resource.sort_pkglist": MagicMock(), - }, + with ( + patch("salt.modules.mac_brew_pkg._call_brew", custom_call_brew), + patch.dict( + mac_brew.__salt__, + { + "pkg_resource.add_pkg": custom_add_pkg, + "pkg_resource.sort_pkglist": MagicMock(), + }, + ), ): assert mac_brew.list_pkgs(versions_as_list=True) == expected_pkgs @@ -957,13 +1028,17 @@ def test_list_pkgs_no_context(): "homebrew/cask-fonts/font-firacode-nerd-font": "2.0.0", } - with patch("salt.modules.mac_brew_pkg._call_brew", custom_call_brew), patch.dict( - mac_brew.__salt__, - { - "pkg_resource.add_pkg": custom_add_pkg, - "pkg_resource.sort_pkglist": MagicMock(), - }, - ), patch.object(mac_brew, "_list_pkgs_from_context") as list_pkgs_context_mock: + with ( + patch("salt.modules.mac_brew_pkg._call_brew", custom_call_brew), + patch.dict( + mac_brew.__salt__, + { + "pkg_resource.add_pkg": custom_add_pkg, + "pkg_resource.sort_pkglist": MagicMock(), + }, + ), + patch.object(mac_brew, "_list_pkgs_from_context") as list_pkgs_context_mock, + ): pkgs = mac_brew.list_pkgs(versions_as_list=True, use_context=False) list_pkgs_context_mock.assert_not_called() list_pkgs_context_mock.reset_mock() @@ -1025,8 +1100,9 @@ def test_latest_version(): } ) - with patch("salt.modules.mac_brew_pkg.refresh_db", mock_refresh_db), patch( - "salt.modules.mac_brew_pkg._call_brew", mock_call_brew + with ( + patch("salt.modules.mac_brew_pkg.refresh_db", mock_refresh_db), + patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew), ): assert mac_brew.latest_version("neovim") == "0.10.0" mock_refresh_db.assert_called_once() @@ -1094,8 +1170,9 @@ def test_latest_version_multiple_names(): "visual-studio-code": "1.89.1", } - with patch("salt.modules.mac_brew_pkg.refresh_db", mock_refresh_db), patch( - "salt.modules.mac_brew_pkg._call_brew", mock_call_brew + with ( + patch("salt.modules.mac_brew_pkg.refresh_db", mock_refresh_db), + patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew), ): assert ( mac_brew.latest_version("cdalvaro/tap/salt", "nvim", "visual-studio-code") @@ -1132,8 +1209,9 @@ def test_latest_version_with_options(): } ) - with patch("salt.modules.mac_brew_pkg.refresh_db", mock_refresh_db), patch( - "salt.modules.mac_brew_pkg._call_brew", mock_call_brew + with ( + patch("salt.modules.mac_brew_pkg.refresh_db", mock_refresh_db), + patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew), ): assert ( mac_brew.latest_version("cdalvaro/tap/salt", options=["--cask"]) == "3007.1" @@ -1154,9 +1232,10 @@ def test_remove(): Tests if package to be removed exists """ mock_params = MagicMock(return_value=({"foo": None}, "repository")) - with patch( - "salt.modules.mac_brew_pkg.list_pkgs", return_value={"test": "0.1.5"} - ), patch.dict(mac_brew.__salt__, {"pkg_resource.parse_targets": mock_params}): + with ( + patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={"test": "0.1.5"}), + patch.dict(mac_brew.__salt__, {"pkg_resource.parse_targets": mock_params}), + ): assert mac_brew.remove("foo") == {} @@ -1175,9 +1254,11 @@ def mock_list_pkgs(): mock_params = MagicMock(return_value=({"foo": None}, "repository")) mock_call_brew = MagicMock(return_value={"retcode": 0}) - with patch("salt.modules.mac_brew_pkg.list_pkgs", mock_list_pkgs), patch( - "salt.modules.mac_brew_pkg._call_brew", mock_call_brew - ), patch.dict(mac_brew.__salt__, {"pkg_resource.parse_targets": mock_params}): + with ( + patch("salt.modules.mac_brew_pkg.list_pkgs", mock_list_pkgs), + patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew), + patch.dict(mac_brew.__salt__, {"pkg_resource.parse_targets": mock_params}), + ): assert mac_brew.remove("foo", options=["--cask"]) == { "foo": {"new": "", "old": "0.1.5"} } @@ -1194,11 +1275,15 @@ def test_refresh_db_failure(HOMEBREW_BIN): mock_user = MagicMock(return_value="foo") mock_failure = MagicMock(return_value={"stdout": "", "stderr": "", "retcode": 1}) with patch("salt.utils.path.which", MagicMock(return_value="/usr/local/bin/brew")): - with patch.dict( - mac_brew.__salt__, {"file.get_user": mock_user, "cmd.run_all": mock_failure} - ), patch( - "salt.modules.mac_brew_pkg._homebrew_bin", - MagicMock(return_value=HOMEBREW_BIN), + with ( + patch.dict( + mac_brew.__salt__, + {"file.get_user": mock_user, "cmd.run_all": mock_failure}, + ), + patch( + "salt.modules.mac_brew_pkg._homebrew_bin", + MagicMock(return_value=HOMEBREW_BIN), + ), ): with patch.object(salt.utils.pkg, "clear_rtag", Mock()): pytest.raises(CommandExecutionError, mac_brew.refresh_db) @@ -1211,11 +1296,15 @@ def test_refresh_db(HOMEBREW_BIN): mock_user = MagicMock(return_value="foo") mock_success = MagicMock(return_value={"retcode": 0}) with patch("salt.utils.path.which", MagicMock(return_value=HOMEBREW_BIN)): - with patch.dict( - mac_brew.__salt__, {"file.get_user": mock_user, "cmd.run_all": mock_success} - ), patch( - "salt.modules.mac_brew_pkg._homebrew_bin", - MagicMock(return_value=HOMEBREW_BIN), + with ( + patch.dict( + mac_brew.__salt__, + {"file.get_user": mock_user, "cmd.run_all": mock_success}, + ), + patch( + "salt.modules.mac_brew_pkg._homebrew_bin", + MagicMock(return_value=HOMEBREW_BIN), + ), ): with patch.object(salt.utils.pkg, "clear_rtag", Mock()): assert mac_brew.refresh_db() @@ -1262,16 +1351,17 @@ def test_hold(HOMEBREW_BIN): with patch( "salt.modules.mac_brew_pkg._homebrew_bin", MagicMock(return_value=HOMEBREW_BIN) ): - with patch( - "salt.modules.mac_brew_pkg.list_pkgs", return_value={"foo": "0.1.5"} - ), patch.dict( - mac_brew.__salt__, - { - "file.get_user": mock_user, - "pkg_resource.parse_targets": mock_params, - "cmd.run_all": mock_cmd_all, - "cmd.run": mock_cmd, - }, + with ( + patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={"foo": "0.1.5"}), + patch.dict( + mac_brew.__salt__, + { + "file.get_user": mock_user, + "pkg_resource.parse_targets": mock_params, + "cmd.run_all": mock_cmd_all, + "cmd.run": mock_cmd, + }, + ), ): assert mac_brew.hold("foo") == _expected @@ -1298,14 +1388,17 @@ def test_hold_not_installed(HOMEBREW_BIN): with patch( "salt.modules.mac_brew_pkg._homebrew_bin", MagicMock(return_value=HOMEBREW_BIN) ): - with patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={}), patch.dict( - mac_brew.__salt__, - { - "file.get_user": mock_user, - "pkg_resource.parse_targets": mock_params, - "cmd.run_all": mock_cmd_all, - "cmd.run": mock_cmd, - }, + with ( + patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={}), + patch.dict( + mac_brew.__salt__, + { + "file.get_user": mock_user, + "pkg_resource.parse_targets": mock_params, + "cmd.run_all": mock_cmd_all, + "cmd.run": mock_cmd, + }, + ), ): assert mac_brew.hold("foo") == _expected @@ -1329,18 +1422,18 @@ def test_hold_pinned(): } mock_params = MagicMock(return_value=({"foo": None}, "repository")) - with patch( - "salt.modules.mac_brew_pkg.list_pkgs", return_value={"foo": "0.1.5"} - ), patch( - "salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"] - ), patch.dict( - mac_brew.__salt__, - { - "file.get_user": mock_user, - "pkg_resource.parse_targets": mock_params, - "cmd.run_all": mock_cmd_all, - "cmd.run": mock_cmd, - }, + with ( + patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={"foo": "0.1.5"}), + patch("salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"]), + patch.dict( + mac_brew.__salt__, + { + "file.get_user": mock_user, + "pkg_resource.parse_targets": mock_params, + "cmd.run_all": mock_cmd_all, + "cmd.run": mock_cmd, + }, + ), ): assert mac_brew.hold("foo") == _expected @@ -1370,18 +1463,18 @@ def test_unhold(HOMEBREW_BIN): with patch( "salt.modules.mac_brew_pkg._homebrew_bin", MagicMock(return_value=HOMEBREW_BIN) ): - with patch( - "salt.modules.mac_brew_pkg.list_pkgs", return_value={"foo": "0.1.5"} - ), patch( - "salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"] - ), patch.dict( - mac_brew.__salt__, - { - "file.get_user": mock_user, - "pkg_resource.parse_targets": mock_params, - "cmd.run_all": mock_cmd_all, - "cmd.run": mock_cmd, - }, + with ( + patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={"foo": "0.1.5"}), + patch("salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"]), + patch.dict( + mac_brew.__salt__, + { + "file.get_user": mock_user, + "pkg_resource.parse_targets": mock_params, + "cmd.run_all": mock_cmd_all, + "cmd.run": mock_cmd, + }, + ), ): assert mac_brew.unhold("foo") == _expected @@ -1405,16 +1498,18 @@ def test_unhold_not_installed(): } mock_params = MagicMock(return_value=({"foo": None}, "repository")) - with patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={}), patch( - "salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"] - ), patch.dict( - mac_brew.__salt__, - { - "file.get_user": mock_user, - "pkg_resource.parse_targets": mock_params, - "cmd.run_all": mock_cmd_all, - "cmd.run": mock_cmd, - }, + with ( + patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={}), + patch("salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"]), + patch.dict( + mac_brew.__salt__, + { + "file.get_user": mock_user, + "pkg_resource.parse_targets": mock_params, + "cmd.run_all": mock_cmd_all, + "cmd.run": mock_cmd, + }, + ), ): assert mac_brew.unhold("foo") == _expected @@ -1438,16 +1533,18 @@ def test_unhold_not_pinned(): } mock_params = MagicMock(return_value=({"foo": None}, "repository")) - with patch( - "salt.modules.mac_brew_pkg.list_pkgs", return_value={"foo": "0.1.5"} - ), patch("salt.modules.mac_brew_pkg._list_pinned", return_value=[]), patch.dict( - mac_brew.__salt__, - { - "file.get_user": mock_user, - "pkg_resource.parse_targets": mock_params, - "cmd.run_all": mock_cmd_all, - "cmd.run": mock_cmd, - }, + with ( + patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={"foo": "0.1.5"}), + patch("salt.modules.mac_brew_pkg._list_pinned", return_value=[]), + patch.dict( + mac_brew.__salt__, + { + "file.get_user": mock_user, + "pkg_resource.parse_targets": mock_params, + "cmd.run_all": mock_cmd_all, + "cmd.run": mock_cmd, + }, + ), ): assert mac_brew.unhold("foo") == _expected @@ -1520,15 +1617,17 @@ def test_info_installed(HOMEBREW_BIN): with patch( "salt.modules.mac_brew_pkg._homebrew_bin", MagicMock(return_value=HOMEBREW_BIN) ): - with patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={}), patch( - "salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"] - ), patch.dict( - mac_brew.__salt__, - { - "file.get_user": mock_user, - "cmd.run_all": mock_cmd_all, - "cmd.run": mock_cmd, - }, + with ( + patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={}), + patch("salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"]), + patch.dict( + mac_brew.__salt__, + { + "file.get_user": mock_user, + "cmd.run_all": mock_cmd_all, + "cmd.run": mock_cmd, + }, + ), ): assert ( mac_brew.info_installed( @@ -1612,15 +1711,17 @@ def test_list_upgrades(HOMEBREW_BIN): with patch( "salt.modules.mac_brew_pkg._homebrew_bin", MagicMock(return_value=HOMEBREW_BIN) ): - with patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={}), patch( - "salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"] - ), patch.dict( - mac_brew.__salt__, - { - "file.get_user": mock_user, - "cmd.run_all": mock_cmd_all, - "cmd.run": mock_cmd, - }, + with ( + patch("salt.modules.mac_brew_pkg.list_pkgs", return_value={}), + patch("salt.modules.mac_brew_pkg._list_pinned", return_value=["foo"]), + patch.dict( + mac_brew.__salt__, + { + "file.get_user": mock_user, + "cmd.run_all": mock_cmd_all, + "cmd.run": mock_cmd, + }, + ), ): assert ( mac_brew.list_upgrades(refresh=False, include_casks=True) == _expected @@ -1678,3 +1779,190 @@ def test_list_upgrades_with_options(): mock_call_brew.assert_called_once_with( "outdated", "--json=v2", "--greedy", "--fetch-HEAD" ) + + +# 'list_trusted' function tests + + +def test_list_trusted(): + """ + Tests that list_trusted returns all trusted items as a dict. + """ + expected = { + "taps": ["thirdparty/foo"], + "formulae": ["thirdparty/foo/bar"], + "casks": [], + "commands": [], + } + mock_call_brew = MagicMock( + return_value={ + "retcode": 0, + "stdout": '{"taps": ["thirdparty/foo"], "formulae": ["thirdparty/foo/bar"], "casks": [], "commands": []}', + "stderr": "", + } + ) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + assert mac_brew.list_trusted() == expected + mock_call_brew.assert_called_once_with("trust", "--json=v1") + + +def test_list_trusted_by_type(): + """ + Tests that list_trusted with a type returns a list of trusted items. + """ + mock_call_brew = MagicMock( + return_value={ + "retcode": 0, + "stdout": '["thirdparty/foo"]', + "stderr": "", + } + ) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + assert mac_brew.list_trusted(type="tap") == ["thirdparty/foo"] + mock_call_brew.assert_called_once_with("trust", "--json=v1", "--tap") + + +def test_list_trusted_invalid_type(): + """ + Tests that list_trusted raises SaltInvocationError for an invalid type. + """ + with pytest.raises( + salt.exceptions.SaltInvocationError, match="Invalid type 'invalid'" + ): + mac_brew.list_trusted(type="invalid") + + +# 'trust' function tests + + +def test_trust(): + """ + Tests successfully trusting a tap. + """ + mock_call_brew = MagicMock(return_value={"retcode": 0, "stdout": "", "stderr": ""}) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + assert mac_brew.trust("thirdparty/foo") is True + mock_call_brew.assert_called_once_with("trust", "thirdparty/foo") + + +def test_trust_with_type(): + """ + Tests trusting an item with an explicit type flag. + """ + mock_call_brew = MagicMock(return_value={"retcode": 0, "stdout": "", "stderr": ""}) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + assert mac_brew.trust("thirdparty/foo", type="tap") is True + mock_call_brew.assert_called_once_with("trust", "--tap", "thirdparty/foo") + + +def test_trust_failure(): + """ + Tests that trust returns False when brew trust fails. + """ + mock_call_brew = MagicMock(side_effect=CommandExecutionError("brew failed")) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + assert mac_brew.trust("thirdparty/foo") is False + + +def test_trust_invalid_type(): + """ + Tests that trust raises SaltInvocationError for an invalid type. + """ + with pytest.raises( + salt.exceptions.SaltInvocationError, match="Invalid type 'invalid'" + ): + mac_brew.trust("thirdparty/foo", type="invalid") + + +# 'untrust' function tests + + +def test_untrust(): + """ + Tests successfully untrusting a tap. + """ + mock_call_brew = MagicMock(return_value={"retcode": 0, "stdout": "", "stderr": ""}) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + assert mac_brew.untrust("thirdparty/foo") is True + mock_call_brew.assert_called_once_with("untrust", "thirdparty/foo") + + +def test_untrust_with_type(): + """ + Tests untrusting an item with an explicit type flag. + """ + mock_call_brew = MagicMock(return_value={"retcode": 0, "stdout": "", "stderr": ""}) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + assert mac_brew.untrust("thirdparty/foo/bar", type="formula") is True + mock_call_brew.assert_called_once_with( + "untrust", "--formula", "thirdparty/foo/bar" + ) + + +def test_untrust_failure(): + """ + Tests that untrust returns False when brew untrust fails. + """ + mock_call_brew = MagicMock(side_effect=CommandExecutionError("brew failed")) + with patch("salt.modules.mac_brew_pkg._call_brew", mock_call_brew): + assert mac_brew.untrust("thirdparty/foo") is False + + +def test_untrust_invalid_type(): + """ + Tests that untrust raises SaltInvocationError for an invalid type. + """ + with pytest.raises( + salt.exceptions.SaltInvocationError, match="Invalid type 'invalid'" + ): + mac_brew.untrust("thirdparty/foo", type="invalid") + + +# 'is_trusted' function tests + + +def test_is_trusted_found(): + """ + Tests is_trusted returns True when the item is in the trusted list. + """ + trusted = { + "taps": ["thirdparty/foo"], + "formulae": [], + "casks": [], + "commands": [], + } + with patch("salt.modules.mac_brew_pkg.list_trusted", return_value=trusted): + assert mac_brew.is_trusted("thirdparty/foo") is True + + +def test_is_trusted_not_found(): + """ + Tests is_trusted returns False when the item is not trusted. + """ + trusted = { + "taps": [], + "formulae": [], + "casks": [], + "commands": [], + } + with patch("salt.modules.mac_brew_pkg.list_trusted", return_value=trusted): + assert mac_brew.is_trusted("thirdparty/foo") is False + + +def test_is_trusted_with_type(): + """ + Tests is_trusted with a type filter delegates to list_trusted with that type. + """ + with patch( + "salt.modules.mac_brew_pkg.list_trusted", return_value=["thirdparty/foo"] + ) as mock_list: + assert mac_brew.is_trusted("thirdparty/foo", type="tap") is True + mock_list.assert_called_once_with(type="tap") + + +def test_is_trusted_with_type_not_found(): + """ + Tests is_trusted with a type filter returns False when not in list. + """ + with patch("salt.modules.mac_brew_pkg.list_trusted", return_value=["other/tap"]): + assert mac_brew.is_trusted("thirdparty/foo", type="tap") is False diff --git a/tests/pytests/unit/states/test_pkg.py b/tests/pytests/unit/states/test_pkg.py index ad42330dc2d9..3453d337d3e0 100644 --- a/tests/pytests/unit/states/test_pkg.py +++ b/tests/pytests/unit/states/test_pkg.py @@ -577,7 +577,6 @@ def test_installed_with_changes_test_true(list_pkgs): "pkg.list_pkgs": list_pkgs, }, ): - expected = {"dummy": {"new": "some version here", "old": ""}} # Run state with test=true with patch.dict(pkg.__opts__, {"test": True}): @@ -594,15 +593,18 @@ def test_installed_with_sources(list_pkgs, tmp_path): list_pkgs = MagicMock(return_value=list_pkgs) pkg_source = tmp_path / "pkga-package-0.3.0.deb" - with patch.dict( - pkg.__salt__, - { - "cp.cache_file": cp.cache_file, - "pkg.list_pkgs": list_pkgs, - "pkg_resource.pack_sources": pkg_resource.pack_sources, - "lowpkg.bin_pkg_info": MagicMock(), - }, - ), patch("salt.fileclient.get_file_client", return_value=MagicMock()): + with ( + patch.dict( + pkg.__salt__, + { + "cp.cache_file": cp.cache_file, + "pkg.list_pkgs": list_pkgs, + "pkg_resource.pack_sources": pkg_resource.pack_sources, + "lowpkg.bin_pkg_info": MagicMock(), + }, + ), + patch("salt.fileclient.get_file_client", return_value=MagicMock()), + ): try: ret = pkg.installed("install-pkgd", sources=[{"pkga": str(pkg_source)}]) assert ret["result"] is False @@ -843,20 +845,19 @@ def test_installed_with_single_normalize(): "pkg_resource.parse_targets": pkg_resource.parse_targets, } - with patch("salt.modules.yumpkg.list_pkgs", list_pkgs), patch( - "salt.modules.yumpkg.version_cmp", MagicMock(return_value=0) - ), patch( - "salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0}) - ) as call_yum_mock, patch.dict( - pkg.__salt__, salt_dict - ), patch.dict( - pkg_resource.__salt__, salt_dict - ), patch.dict( - yumpkg.__salt__, salt_dict - ), patch.dict( - yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 7} - ), patch.object( - yumpkg, "list_holds", MagicMock() + with ( + patch("salt.modules.yumpkg.list_pkgs", list_pkgs), + patch("salt.modules.yumpkg.version_cmp", MagicMock(return_value=0)), + patch( + "salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0}) + ) as call_yum_mock, + patch.dict(pkg.__salt__, salt_dict), + patch.dict(pkg_resource.__salt__, salt_dict), + patch.dict(yumpkg.__salt__, salt_dict), + patch.dict( + yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 7} + ), + patch.object(yumpkg, "list_holds", MagicMock()), ): expected = { "weird-name-1.2.3-1234.5.6.test7tst.x86_64": { @@ -931,7 +932,6 @@ def test_installed_with_freebsd_origin(): }, ), ): - ret = pkg.installed("test/pkga") install_mock.assert_not_called() assert ret["result"] @@ -965,12 +965,13 @@ def test_installed_preserves_apt_multiarch_pkg_names_for_update_holds(): "pkg_resource.version_clean": pkg_resource.version_clean, } - with patch.dict(pkg.__salt__, salt_dict), patch.dict( - pkg_resource.__salt__, salt_dict - ), patch.dict( - pkg.__grains__, {"os": "Ubuntu", "os_family": "Debian", "osarch": "amd64"} - ), patch.dict( - pkg_resource.__grains__, {"os": "Ubuntu", "os_family": "Debian"} + with ( + patch.dict(pkg.__salt__, salt_dict), + patch.dict(pkg_resource.__salt__, salt_dict), + patch.dict( + pkg.__grains__, {"os": "Ubuntu", "os_family": "Debian", "osarch": "amd64"} + ), + patch.dict(pkg_resource.__grains__, {"os": "Ubuntu", "os_family": "Debian"}), ): ret = pkg.installed( "test_install", @@ -1008,15 +1009,18 @@ def test_verify_install_normalizes_debian_multiarch_names(): desired = {"zlib1g:amd64": "1:1.3.dfsg-3.1ubuntu2.1"} new_pkgs = {"zlib1g": ["1:1.3.dfsg-3.1ubuntu2.1"]} - with patch.dict( - pkg.__salt__, - { - "pkg.normalize_name": lambda name: ( - name.rsplit(":", 1)[0] if name.endswith(":amd64") else name - ), - "pkg_resource.version_clean": pkg_resource.version_clean, - }, - ), patch.dict(pkg.__grains__, {"os": "Ubuntu", "os_family": "Debian"}): + with ( + patch.dict( + pkg.__salt__, + { + "pkg.normalize_name": lambda name: ( + name.rsplit(":", 1)[0] if name.endswith(":amd64") else name + ), + "pkg_resource.version_clean": pkg_resource.version_clean, + }, + ), + patch.dict(pkg.__grains__, {"os": "Ubuntu", "os_family": "Debian"}), + ): ok, failed = pkg._verify_install(desired, new_pkgs) assert ok == ["zlib1g:amd64"] @@ -1030,13 +1034,16 @@ def test_verify_install_normalizes_yum_arch_names(): desired = {"weird-name-1.2.3-1234.5.6.test7tst.x86_64.noarch": "20220214-2.1"} new_pkgs = {"weird-name-1.2.3-1234.5.6.test7tst.x86_64": ["20220214-2.1"]} - with patch.dict( - pkg.__salt__, - { - "pkg.normalize_name": yumpkg.normalize_name, - "pkg_resource.version_clean": pkg_resource.version_clean, - }, - ), patch.dict(pkg.__grains__, {"os": "CentOS", "os_family": "RedHat"}): + with ( + patch.dict( + pkg.__salt__, + { + "pkg.normalize_name": yumpkg.normalize_name, + "pkg_resource.version_clean": pkg_resource.version_clean, + }, + ), + patch.dict(pkg.__grains__, {"os": "CentOS", "os_family": "RedHat"}), + ): ok, failed = pkg._verify_install(desired, new_pkgs) assert ok == ["weird-name-1.2.3-1234.5.6.test7tst.x86_64.noarch"] @@ -1093,16 +1100,15 @@ def test_removed_with_single_normalize(): "pkg_resource.version_clean": pkg_resource.version_clean, } - with patch("salt.modules.yumpkg.list_pkgs", list_pkgs), patch( - "salt.modules.yumpkg.version_cmp", MagicMock(return_value=0) - ), patch( - "salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0}) - ) as call_yum_mock, patch.dict( - pkg.__salt__, salt_dict - ), patch.dict( - pkg_resource.__salt__, salt_dict - ), patch.dict( - yumpkg.__salt__, salt_dict + with ( + patch("salt.modules.yumpkg.list_pkgs", list_pkgs), + patch("salt.modules.yumpkg.version_cmp", MagicMock(return_value=0)), + patch( + "salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0}) + ) as call_yum_mock, + patch.dict(pkg.__salt__, salt_dict), + patch.dict(pkg_resource.__salt__, salt_dict), + patch.dict(yumpkg.__salt__, salt_dict), ): expected = { "weird-name-1.2.3-1234.5.6.test7tst.x86_64": { @@ -1184,18 +1190,18 @@ def test_installed_with_single_normalize_32bit(): "pkg_resource.parse_targets": pkg_resource.parse_targets, } - with patch("salt.modules.yumpkg.list_pkgs", list_pkgs), patch( - "salt.modules.yumpkg.version_cmp", MagicMock(return_value=0) - ), patch( - "salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0}) - ) as call_yum_mock, patch.dict( - pkg.__salt__, salt_dict - ), patch.dict( - pkg_resource.__salt__, salt_dict - ), patch.dict( - yumpkg.__salt__, salt_dict - ), patch.dict( - yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 7} + with ( + patch("salt.modules.yumpkg.list_pkgs", list_pkgs), + patch("salt.modules.yumpkg.version_cmp", MagicMock(return_value=0)), + patch( + "salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0}) + ) as call_yum_mock, + patch.dict(pkg.__salt__, salt_dict), + patch.dict(pkg_resource.__salt__, salt_dict), + patch.dict(yumpkg.__salt__, salt_dict), + patch.dict( + yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 7} + ), ): expected = { "xz-devel.i686": { @@ -1216,13 +1222,16 @@ def test_installed_with_single_normalize_32bit(): def test__get_installable_versions_no_version_found(): mock_latest_versions = MagicMock(return_value={}) mock_list_repo_pkgs = MagicMock(return_value={}) - with patch.dict( - pkg.__salt__, - { - "pkg.latest_version": mock_latest_versions, - "pkg.list_pkgs": mock_list_repo_pkgs, - }, - ), patch.dict(pkg.__opts__, {"test": True}): + with ( + patch.dict( + pkg.__salt__, + { + "pkg.latest_version": mock_latest_versions, + "pkg.list_pkgs": mock_list_repo_pkgs, + }, + ), + patch.dict(pkg.__opts__, {"test": True}), + ): expected = {"dummy": {"new": "installed", "old": ""}} ret = pkg._get_installable_versions({"dummy": None}, current=None) assert ret == expected @@ -1231,13 +1240,16 @@ def test__get_installable_versions_no_version_found(): def test__get_installable_versions_version_found(): mock_latest_versions = MagicMock(return_value={"dummy": "1.0.1"}) mock_list_repo_pkgs = MagicMock(return_value={}) - with patch.dict( - pkg.__salt__, - { - "pkg.latest_version": mock_latest_versions, - "pkg.list_pkgs": mock_list_repo_pkgs, - }, - ), patch.dict(pkg.__opts__, {"test": True}): + with ( + patch.dict( + pkg.__salt__, + { + "pkg.latest_version": mock_latest_versions, + "pkg.list_pkgs": mock_list_repo_pkgs, + }, + ), + patch.dict(pkg.__opts__, {"test": True}), + ): expected = {"dummy": {"new": "1.0.1", "old": ""}} ret = pkg._get_installable_versions({"dummy": None}, current=None) assert ret == expected @@ -1315,12 +1327,15 @@ def test_installed_arch_qualified_native_name_already_installed_69604(): "pkg_resource.version_clean": pkg_resource.version_clean, } - with patch.dict(pkg.__salt__, salt_dict), patch.dict( - pkg_resource.__salt__, salt_dict - ), patch.dict( - pkg.__grains__, {"os": "CentOS", "os_family": "RedHat", "osarch": "x86_64"} - ), patch.dict( - yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 8} + with ( + patch.dict(pkg.__salt__, salt_dict), + patch.dict(pkg_resource.__salt__, salt_dict), + patch.dict( + pkg.__grains__, {"os": "CentOS", "os_family": "RedHat", "osarch": "x86_64"} + ), + patch.dict( + yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 8} + ), ): ret = pkg.installed( "test_install", @@ -1365,12 +1380,15 @@ def test_find_install_targets_arch_qualified_native_already_installed_69604(): "pkg_resource.version_clean": pkg_resource.version_clean, } - with patch.dict(pkg.__salt__, salt_dict), patch.dict( - pkg_resource.__salt__, salt_dict - ), patch.dict( - pkg.__grains__, {"os": "CentOS", "os_family": "RedHat", "osarch": "x86_64"} - ), patch.dict( - yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 8} + with ( + patch.dict(pkg.__salt__, salt_dict), + patch.dict(pkg_resource.__salt__, salt_dict), + patch.dict( + pkg.__grains__, {"os": "CentOS", "os_family": "RedHat", "osarch": "x86_64"} + ), + patch.dict( + yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 8} + ), ): # split_arch=False is the key trigger: pkg.installed passes this to # preserve APT multiarch names (e.g. foo:amd64). With split_arch=False, @@ -1426,12 +1444,15 @@ def test_installed_arch_qualified_foreign_arch_not_confused_with_native_69604(): "pkg_resource.version_clean": pkg_resource.version_clean, } - with patch.dict(pkg.__salt__, salt_dict), patch.dict( - pkg_resource.__salt__, salt_dict - ), patch.dict( - pkg.__grains__, {"os": "CentOS", "os_family": "RedHat", "osarch": "x86_64"} - ), patch.dict( - yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 8} + with ( + patch.dict(pkg.__salt__, salt_dict), + patch.dict(pkg_resource.__salt__, salt_dict), + patch.dict( + pkg.__grains__, {"os": "CentOS", "os_family": "RedHat", "osarch": "x86_64"} + ), + patch.dict( + yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 8} + ), ): ret = pkg.installed( "test_install", @@ -1500,12 +1521,14 @@ def test_yumpkg_group_installed_with_repo_options( } name = "MyGroup" - with patch.dict(pkg.__salt__, salt_dict), patch.dict( - yumpkg.__salt__, salt_dict - ), patch.object( - yumpkg, - "list_pkgs", - MagicMock(return_value=list_pkgs), + with ( + patch.dict(pkg.__salt__, salt_dict), + patch.dict(yumpkg.__salt__, salt_dict), + patch.object( + yumpkg, + "list_pkgs", + MagicMock(return_value=list_pkgs), + ), ): ret = pkg.group_installed(name, **kwargs) assert ret["result"] @@ -1808,3 +1831,181 @@ def test_downloaded_empty_pkgs_list(): ret = pkg.downloaded("foo", pkgs=[]) assert ret["result"] is True assert ret["comment"] == "No packages to download provided" + + +# 'trusted' state tests + + +def test_trusted_not_available(): + """ + Test pkg.trusted when pkg.trust is not available for the package manager. + """ + with patch.dict(pkg.__salt__, {}): + ret = pkg.trusted("cdalvaro/tap") + assert ret["result"] is False + assert "not available" in ret["comment"] + assert ret["changes"] == {} + + +def test_trusted_already_trusted(): + """ + Test pkg.trusted when the item is already trusted. + """ + with patch.dict( + pkg.__salt__, + { + "pkg.trust": MagicMock(return_value=True), + "pkg.is_trusted": MagicMock(return_value=True), + }, + ): + ret = pkg.trusted("cdalvaro/tap", type="tap") + assert ret["result"] is True + assert "already trusted" in ret["comment"] + assert ret["changes"] == {} + + +def test_trusted_test_mode(): + """ + Test pkg.trusted in test mode when the item would be trusted. + """ + with ( + patch.dict( + pkg.__salt__, + { + "pkg.trust": MagicMock(return_value=True), + "pkg.is_trusted": MagicMock(return_value=False), + }, + ), + patch.dict(pkg.__opts__, {"test": True}), + ): + ret = pkg.trusted("cdalvaro/tap") + assert ret["result"] is None + assert "would be trusted" in ret["comment"] + assert ret["changes"] == {} + + +def test_trusted_success(): + """ + Test pkg.trusted successfully trusts an item. + """ + trust_mock = MagicMock(return_value=True) + with patch.dict( + pkg.__salt__, + { + "pkg.trust": trust_mock, + "pkg.is_trusted": MagicMock(return_value=False), + }, + ): + ret = pkg.trusted("cdalvaro/tap", type="tap") + assert ret["result"] is True + assert ret["changes"] == { + "cdalvaro/tap": {"old": "untrusted", "new": "trusted"} + } + assert "now trusted" in ret["comment"] + trust_mock.assert_called_once_with("cdalvaro/tap", type="tap") + + +def test_trusted_failure(): + """ + Test pkg.trusted when the brew trust command fails. + """ + with patch.dict( + pkg.__salt__, + { + "pkg.trust": MagicMock(return_value=False), + "pkg.is_trusted": MagicMock(return_value=False), + }, + ): + ret = pkg.trusted("cdalvaro/tap") + assert ret["result"] is False + assert "Failed to trust" in ret["comment"] + assert ret["changes"] == {} + + +# 'untrusted' state tests + + +def test_untrusted_not_available(): + """ + Test pkg.untrusted when pkg.untrust is not available for the package manager. + """ + with patch.dict(pkg.__salt__, {}): + ret = pkg.untrusted("cdalvaro/tap") + assert ret["result"] is False + assert "not available" in ret["comment"] + assert ret["changes"] == {} + + +def test_untrusted_already_not_trusted(): + """ + Test pkg.untrusted when the item is already not trusted. + """ + with patch.dict( + pkg.__salt__, + { + "pkg.untrust": MagicMock(return_value=True), + "pkg.is_trusted": MagicMock(return_value=False), + }, + ): + ret = pkg.untrusted("cdalvaro/tap", type="tap") + assert ret["result"] is True + assert "already not trusted" in ret["comment"] + assert ret["changes"] == {} + + +def test_untrusted_test_mode(): + """ + Test pkg.untrusted in test mode when the item would be untrusted. + """ + with ( + patch.dict( + pkg.__salt__, + { + "pkg.untrust": MagicMock(return_value=True), + "pkg.is_trusted": MagicMock(return_value=True), + }, + ), + patch.dict(pkg.__opts__, {"test": True}), + ): + ret = pkg.untrusted("cdalvaro/tap") + assert ret["result"] is None + assert "would be untrusted" in ret["comment"] + assert ret["changes"] == {} + + +def test_untrusted_success(): + """ + Test pkg.untrusted successfully untrusts an item. + """ + untrust_mock = MagicMock(return_value=True) + with patch.dict( + pkg.__salt__, + { + "pkg.untrust": untrust_mock, + "pkg.is_trusted": MagicMock(return_value=True), + }, + ): + ret = pkg.untrusted("cdalvaro/tap", type="tap") + assert ret["result"] is True + assert ret["changes"] == { + "cdalvaro/tap": {"old": "trusted", "new": "untrusted"} + } + assert "no longer trusted" in ret["comment"] + untrust_mock.assert_called_once_with("cdalvaro/tap", type="tap") + + +def test_untrusted_failure(): + """ + Test pkg.untrusted when the brew untrust command fails. + """ + with patch.dict( + pkg.__salt__, + { + "pkg.untrust": MagicMock(return_value=False), + "pkg.is_trusted": MagicMock(return_value=True), + }, + ): + ret = pkg.untrusted("cdalvaro/tap") + assert ret["result"] is False + assert "Failed to untrust" in ret["comment"] + assert ret["changes"] == {}