diff --git a/_lastrevision b/_lastrevision index 7cf99d5..ca8b3f6 100644 --- a/_lastrevision +++ b/_lastrevision @@ -1 +1 @@ -4b7e8cbf000c33224e9cd7a7eb581954ca3dbbec \ No newline at end of file +1a82ea698915484eb266be064a7ddaf207b4b1e9 \ No newline at end of file diff --git a/add-docker-logout-237.patch b/add-docker-logout-237.patch new file mode 100644 index 0000000..33bf399 --- /dev/null +++ b/add-docker-logout-237.patch @@ -0,0 +1,179 @@ +From 9e6bd24b07cd2424c3805777b07b9ea84adff416 Mon Sep 17 00:00:00 2001 +From: Alexander Graul +Date: Mon, 18 May 2020 16:39:27 +0200 +Subject: [PATCH] Add docker logout (#237) + +Docker logout works analog to login. It takes none, one or more registries as +arguments. If there are no arguments, all known (specified in pillar) +docker registries are logged out of. If arguments are present, they are +interpreted as a list of docker registries to log out of. +--- + salt/modules/dockermod.py | 80 ++++++++++++++++++++++++++++ + tests/unit/modules/test_dockermod.py | 59 ++++++++++++++++++++ + 2 files changed, 139 insertions(+) + +diff --git a/salt/modules/dockermod.py b/salt/modules/dockermod.py +index 28a2107cec..119e9eb170 100644 +--- a/salt/modules/dockermod.py ++++ b/salt/modules/dockermod.py +@@ -1481,6 +1481,86 @@ def login(*registries): + return ret + + ++def logout(*registries): ++ """ ++ .. versionadded:: 3001 ++ ++ Performs a ``docker logout`` to remove the saved authentication details for ++ one or more configured repositories. ++ ++ Multiple registry URLs (matching those configured in Pillar) can be passed, ++ and Salt will attempt to logout of *just* those registries. If no registry ++ URLs are provided, Salt will attempt to logout of *all* configured ++ registries. ++ ++ **RETURN DATA** ++ ++ A dictionary containing the following keys: ++ ++ - ``Results`` - A dictionary mapping registry URLs to the authentication ++ result. ``True`` means a successful logout, ``False`` means a failed ++ logout. ++ - ``Errors`` - A list of errors encountered during the course of this ++ function. ++ ++ CLI Example: ++ ++ .. code-block:: bash ++ ++ salt myminion docker.logout ++ salt myminion docker.logout hub ++ salt myminion docker.logout hub https://mydomain.tld/registry/ ++ """ ++ # NOTE: This function uses the "docker logout" CLI command to remove ++ # authentication information from config.json. docker-py does not support ++ # this usecase (see https://github.com/docker/docker-py/issues/1091) ++ ++ # To logout of all known (to Salt) docker registries, they have to be collected first ++ registry_auth = __salt__["config.get"]("docker-registries", {}) ++ ret = {"retcode": 0} ++ errors = ret.setdefault("Errors", []) ++ if not isinstance(registry_auth, dict): ++ errors.append("'docker-registries' Pillar value must be a dictionary") ++ registry_auth = {} ++ for reg_name, reg_conf in six.iteritems( ++ __salt__["config.option"]("*-docker-registries", wildcard=True) ++ ): ++ try: ++ registry_auth.update(reg_conf) ++ except TypeError: ++ errors.append( ++ "Docker registry '{0}' was not specified as a " ++ "dictionary".format(reg_name) ++ ) ++ ++ # If no registries passed, we will logout of all known registries ++ if not registries: ++ registries = list(registry_auth) ++ ++ results = ret.setdefault("Results", {}) ++ for registry in registries: ++ if registry not in registry_auth: ++ errors.append("No match found for registry '{0}'".format(registry)) ++ continue ++ else: ++ cmd = ["docker", "logout"] ++ if registry.lower() != "hub": ++ cmd.append(registry) ++ log.debug("Attempting to logout of docker registry '%s'", registry) ++ logout_cmd = __salt__["cmd.run_all"]( ++ cmd, python_shell=False, output_loglevel="quiet", ++ ) ++ results[registry] = logout_cmd["retcode"] == 0 ++ if not results[registry]: ++ if logout_cmd["stderr"]: ++ errors.append(logout_cmd["stderr"]) ++ elif logout_cmd["stdout"]: ++ errors.append(logout_cmd["stdout"]) ++ if errors: ++ ret["retcode"] = 1 ++ return ret ++ ++ + # Functions for information gathering + def depends(name): + ''' +diff --git a/tests/unit/modules/test_dockermod.py b/tests/unit/modules/test_dockermod.py +index 191bfc123f..8f4ead2867 100644 +--- a/tests/unit/modules/test_dockermod.py ++++ b/tests/unit/modules/test_dockermod.py +@@ -164,6 +164,65 @@ class DockerTestCase(TestCase, LoaderModuleMockMixin): + self.assertIn('retcode', ret) + self.assertNotEqual(ret['retcode'], 0) + ++ def test_logout_calls_docker_cli_logout_single(self): ++ client = Mock() ++ get_client_mock = MagicMock(return_value=client) ++ ref_out = {"stdout": "", "stderr": "", "retcode": 0} ++ registry_auth_data = { ++ "portus.example.com:5000": { ++ "username": "admin", ++ "password": "linux12345", ++ "email": "tux@example.com", ++ } ++ } ++ docker_mock = MagicMock(return_value=ref_out) ++ with patch.object(docker_mod, "_get_client", get_client_mock): ++ dunder_salt = { ++ "config.get": MagicMock(return_value=registry_auth_data), ++ "cmd.run_all": docker_mock, ++ "config.option": MagicMock(return_value={}), ++ } ++ with patch.dict(docker_mod.__salt__, dunder_salt): ++ ret = docker_mod.logout("portus.example.com:5000") ++ assert "retcode" in ret ++ assert ret["retcode"] == 0 ++ docker_mock.assert_called_with( ++ ["docker", "logout", "portus.example.com:5000"], ++ python_shell=False, ++ output_loglevel="quiet", ++ ) ++ ++ ++ def test_logout_calls_docker_cli_logout_all(self): ++ client = Mock() ++ get_client_mock = MagicMock(return_value=client) ++ ref_out = {"stdout": "", "stderr": "", "retcode": 0} ++ registry_auth_data = { ++ "portus.example.com:5000": { ++ "username": "admin", ++ "password": "linux12345", ++ "email": "tux@example.com", ++ }, ++ "portus2.example.com:5000": { ++ "username": "admin", ++ "password": "linux12345", ++ "email": "tux@example.com", ++ }, ++ } ++ ++ docker_mock = MagicMock(return_value=ref_out) ++ with patch.object(docker_mod, "_get_client", get_client_mock): ++ dunder_salt = { ++ "config.get": MagicMock(return_value=registry_auth_data), ++ "cmd.run_all": docker_mock, ++ "config.option": MagicMock(return_value={}), ++ } ++ with patch.dict(docker_mod.__salt__, dunder_salt): ++ ret = docker_mod.logout() ++ assert "retcode" in ret ++ assert ret["retcode"] == 0 ++ assert docker_mock.call_count == 2 ++ + def test_ps_with_host_true(self): + ''' + Check that docker.ps called with host is ``True``, +-- +2.26.2 + + diff --git a/fix-a-test-and-some-variable-names-229.patch b/fix-a-test-and-some-variable-names-229.patch new file mode 100644 index 0000000..7d20047 --- /dev/null +++ b/fix-a-test-and-some-variable-names-229.patch @@ -0,0 +1,66 @@ +From c1e66b9953c753dc9eff3652aef316e19c22deb4 Mon Sep 17 00:00:00 2001 +From: Alberto Planas +Date: Tue, 12 May 2020 14:16:23 +0200 +Subject: [PATCH] Fix a test and some variable names (#229) + +* loop: fix variable names for until_no_eval + +* Fix test_core tests for fqdns errors +--- + salt/modules/network.py | 2 +- + tests/unit/grains/test_core.py | 24 +++++++++++++----------- + 2 files changed, 14 insertions(+), 12 deletions(-) + +diff --git a/salt/modules/network.py b/salt/modules/network.py +index 880f4f8d5f..9e11eb816e 100644 +--- a/salt/modules/network.py ++++ b/salt/modules/network.py +@@ -1946,4 +1946,4 @@ def fqdns(): + elapsed = time.time() - start + log.debug('Elapsed time getting FQDNs: {} seconds'.format(elapsed)) + +- return {"fqdns": sorted(list(fqdns))} +\ No newline at end of file ++ return {"fqdns": sorted(list(fqdns))} +diff --git a/tests/unit/grains/test_core.py b/tests/unit/grains/test_core.py +index 94e4199814..36aa49f232 100644 +--- a/tests/unit/grains/test_core.py ++++ b/tests/unit/grains/test_core.py +@@ -1122,20 +1122,22 @@ class CoreGrainsTestCase(TestCase, LoaderModuleMockMixin): + + for errno in (0, core.HOST_NOT_FOUND, core.NO_DATA): + mock_log = MagicMock() ++ with patch.dict(core.__salt__, {'network.fqdns': salt.modules.network.fqdns}): ++ with patch.object(socket, 'gethostbyaddr', ++ side_effect=_gen_gethostbyaddr(errno)): ++ with patch('salt.modules.network.log', mock_log): ++ self.assertEqual(core.fqdns(), {'fqdns': []}) ++ mock_log.debug.assert_called() ++ mock_log.error.assert_not_called() ++ ++ mock_log = MagicMock() ++ with patch.dict(core.__salt__, {'network.fqdns': salt.modules.network.fqdns}): + with patch.object(socket, 'gethostbyaddr', +- side_effect=_gen_gethostbyaddr(errno)): +- with patch('salt.grains.core.log', mock_log): ++ side_effect=_gen_gethostbyaddr(-1)): ++ with patch('salt.modules.network.log', mock_log): + self.assertEqual(core.fqdns(), {'fqdns': []}) + mock_log.debug.assert_called_once() +- mock_log.error.assert_not_called() +- +- mock_log = MagicMock() +- with patch.object(socket, 'gethostbyaddr', +- side_effect=_gen_gethostbyaddr(-1)): +- with patch('salt.grains.core.log', mock_log): +- self.assertEqual(core.fqdns(), {'fqdns': []}) +- mock_log.debug.assert_not_called() +- mock_log.error.assert_called_once() ++ mock_log.error.assert_called_once() + + @patch.object(salt.utils.platform, 'is_windows', MagicMock(return_value=False)) + @patch('salt.utils.network.ip_addrs', MagicMock(return_value=['1.2.3.4', '5.6.7.8'])) +-- +2.26.2 + + diff --git a/fix-for-return-value-ret-vs-return-in-batch-mode.patch b/fix-for-return-value-ret-vs-return-in-batch-mode.patch new file mode 100644 index 0000000..04e79d5 --- /dev/null +++ b/fix-for-return-value-ret-vs-return-in-batch-mode.patch @@ -0,0 +1,113 @@ +From 0c988e1db59a255b2f707c4e626cec21ff06d7a3 Mon Sep 17 00:00:00 2001 +From: Jochen Breuer +Date: Thu, 9 Apr 2020 17:12:54 +0200 +Subject: [PATCH] Fix for return value ret vs return in batch mode + +The least intrusive fix for ret vs return in batch mode. +--- + salt/cli/batch.py | 16 ++++++---- + tests/unit/cli/test_batch.py | 62 ++++++++++++++++++++++++++++++++++++ + 2 files changed, 71 insertions(+), 7 deletions(-) + +diff --git a/salt/cli/batch.py b/salt/cli/batch.py +index 10fc81a5f4..d5b8754ad7 100644 +--- a/salt/cli/batch.py ++++ b/salt/cli/batch.py +@@ -234,14 +234,16 @@ class Batch(object): + if not self.quiet: + salt.utils.stringutils.print_cli('\nExecuting run on {0}\n'.format(sorted(next_))) + # create a new iterator for this batch of minions ++ return_value = self.opts.get("return", self.opts.get("ret", "")) + new_iter = self.local.cmd_iter_no_block( +- *args, +- raw=self.opts.get('raw', False), +- ret=self.opts.get('return', ''), +- show_jid=show_jid, +- verbose=show_verbose, +- gather_job_timeout=self.opts['gather_job_timeout'], +- **self.eauth) ++ *args, ++ raw=self.opts.get("raw", False), ++ ret=return_value, ++ show_jid=show_jid, ++ verbose=show_verbose, ++ gather_job_timeout=self.opts["gather_job_timeout"], ++ **self.eauth ++ ) + # add it to our iterators and to the minion_tracker + iters.append(new_iter) + minion_tracker[new_iter] = {} +diff --git a/tests/unit/cli/test_batch.py b/tests/unit/cli/test_batch.py +index acabbe51f5..d7411e8039 100644 +--- a/tests/unit/cli/test_batch.py ++++ b/tests/unit/cli/test_batch.py +@@ -72,3 +72,65 @@ class BatchTestCase(TestCase): + ''' + ret = Batch.get_bnum(self.batch) + self.assertEqual(ret, None) ++ ++ def test_return_value_in_run_for_ret(self): ++ """ ++ cmd_iter_no_block should have been called with a return no matter if ++ the return value was in ret or return. ++ """ ++ self.batch.opts = { ++ "batch": "100%", ++ "timeout": 5, ++ "fun": "test", ++ "arg": "foo", ++ "gather_job_timeout": 5, ++ "ret": "my_return", ++ } ++ self.batch.minions = ["foo", "bar", "baz"] ++ self.batch.local.cmd_iter_no_block = MagicMock(return_value=iter([])) ++ ret = Batch.run(self.batch) ++ # We need to fetch at least one object to trigger the relevant code path. ++ x = next(ret) ++ self.batch.local.cmd_iter_no_block.assert_called_with( ++ ["baz", "bar", "foo"], ++ "test", ++ "foo", ++ 5, ++ "list", ++ raw=False, ++ ret="my_return", ++ show_jid=False, ++ verbose=False, ++ gather_job_timeout=5, ++ ) ++ ++ def test_return_value_in_run_for_return(self): ++ """ ++ cmd_iter_no_block should have been called with a return no matter if ++ the return value was in ret or return. ++ """ ++ self.batch.opts = { ++ "batch": "100%", ++ "timeout": 5, ++ "fun": "test", ++ "arg": "foo", ++ "gather_job_timeout": 5, ++ "return": "my_return", ++ } ++ self.batch.minions = ["foo", "bar", "baz"] ++ self.batch.local.cmd_iter_no_block = MagicMock(return_value=iter([])) ++ ret = Batch.run(self.batch) ++ # We need to fetch at least one object to trigger the relevant code path. ++ x = next(ret) ++ self.batch.local.cmd_iter_no_block.assert_called_with( ++ ["baz", "bar", "foo"], ++ "test", ++ "foo", ++ 5, ++ "list", ++ raw=False, ++ ret="my_return", ++ show_jid=False, ++ verbose=False, ++ gather_job_timeout=5, ++ ) +-- +2.26.1 + + diff --git a/option-to-en-disable-force-refresh-in-zypper-215.patch b/option-to-en-disable-force-refresh-in-zypper-215.patch new file mode 100644 index 0000000..db2e3f2 --- /dev/null +++ b/option-to-en-disable-force-refresh-in-zypper-215.patch @@ -0,0 +1,124 @@ +From bb870d08a0268cb2be5309ee1a1b8facd2c885df Mon Sep 17 00:00:00 2001 +From: darix +Date: Tue, 12 May 2020 13:58:15 +0200 +Subject: [PATCH] Option to en-/disable force refresh in zypper (#215) + +The default will still be force refresh to keep existing setups working. + +1. Pillar option to turn off force refresh + +``` +zypper: + refreshdb_force: false +``` + +2. Cmdline option to force refresh. + +``` +salt '*' pkg.refresh_db [force=true|false] +``` + +The cmdline option will override the pillar as well. + +Co-authored-by: Alexander Graul +--- + salt/modules/zypperpkg.py | 32 ++++++++++++++++++++-------- + tests/unit/modules/test_zypperpkg.py | 24 +++++++++++++++++++-- + 2 files changed, 45 insertions(+), 11 deletions(-) + +diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py +index e3f802a911..ed8420f398 100644 +--- a/salt/modules/zypperpkg.py ++++ b/salt/modules/zypperpkg.py +@@ -1279,25 +1279,39 @@ def mod_repo(repo, **kwargs): + return repo + + +-def refresh_db(root=None): +- ''' +- Force a repository refresh by calling ``zypper refresh --force``, return a dict:: ++def refresh_db(root=None, force=None): ++ """ ++ Trigger a repository refresh by calling ``zypper refresh``. Refresh will run ++ with ``--force`` if the "force=True" flag is passed on the CLI or ++ ``refreshdb_force`` is set to ``true`` in the pillar. The CLI option ++ overrides the pillar setting. + +- {'': Bool} ++ It will return a dict:: + +- root +- operate on a different root directory. ++ {'': Bool} + + CLI Example: + + .. code-block:: bash + +- salt '*' pkg.refresh_db +- ''' ++ salt '*' pkg.refresh_db [force=true|false] ++ ++ Pillar Example: ++ ++ .. code-block:: yaml ++ ++ zypper: ++ refreshdb_force: false ++ """ + # Remove rtag file to keep multiple refreshes from happening in pkg states + salt.utils.pkg.clear_rtag(__opts__) + ret = {} +- out = __zypper__(root=root).refreshable.call('refresh', '--force') ++ refresh_opts = ['refresh'] ++ if force is None: ++ force = __pillar__.get('zypper', {}).get('refreshdb_force', True) ++ if force: ++ refresh_opts.append('--force') ++ out = __zypper__(root=root).refreshable.call(*refresh_opts) + + for line in out.splitlines(): + if not line: +diff --git a/tests/unit/modules/test_zypperpkg.py b/tests/unit/modules/test_zypperpkg.py +index 2a8e753b9d..9a5c59a857 100644 +--- a/tests/unit/modules/test_zypperpkg.py ++++ b/tests/unit/modules/test_zypperpkg.py +@@ -278,12 +278,32 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + 'stderr': '', 'stdout': '\n'.join(ref_out), 'retcode': 0 + } + +- with patch.dict(zypper.__salt__, {'cmd.run_all': MagicMock(return_value=run_out)}): +- with patch.object(salt.utils.pkg, 'clear_rtag', Mock()): ++ zypper_mock = MagicMock(return_value=run_out) ++ call_kwargs = { ++ "output_loglevel": "trace", ++ "python_shell": False, ++ "env": {} ++ } ++ with patch.dict(zypper.__salt__, {"cmd.run_all": zypper_mock}): ++ with patch.object(salt.utils.pkg, "clear_rtag", Mock()): + result = zypper.refresh_db() + self.assertEqual(result.get("openSUSE-Leap-42.1-LATEST"), False) + self.assertEqual(result.get("openSUSE-Leap-42.1-Update"), False) + self.assertEqual(result.get("openSUSE-Leap-42.1-Update-Non-Oss"), True) ++ zypper_mock.assert_called_with( ++ ["zypper", "--non-interactive", "refresh", "--force"], ++ **call_kwargs ++ ) ++ zypper.refresh_db(force=False) ++ zypper_mock.assert_called_with( ++ ["zypper", "--non-interactive", "refresh"], ++ **call_kwargs ++ ) ++ zypper.refresh_db(force=True) ++ zypper_mock.assert_called_with( ++ ["zypper", "--non-interactive", "refresh", "--force"], ++ **call_kwargs ++ ) + + def test_info_installed(self): + ''' +-- +2.26.2 + + diff --git a/salt.changes b/salt.changes index 5278bf7..8514eb4 100644 --- a/salt.changes +++ b/salt.changes @@ -1,3 +1,37 @@ +------------------------------------------------------------------- +Thu May 21 08:35:30 UTC 2020 - Pablo Suárez Hernández + +- zypperpkg: filter patterns that start with dot (bsc#1171906) + +- Added: + * zypperpkg-filter-patterns-that-start-with-dot-244.patch + +------------------------------------------------------------------- +Wed May 20 13:27:23 UTC 2020 - Jochen Breuer + +- Batch mode now also correctly provides return value (bsc#1168340) + +- Added: + * fix-for-return-value-ret-vs-return-in-batch-mode.patch + +------------------------------------------------------------------- +Mon May 18 15:22:58 UTC 2020 - Alexander Graul + +- Add docker.logout to docker execution module (bsc#1165572) + +- Added: + * add-docker-logout-237.patch + +------------------------------------------------------------------- +Tue May 12 15:07:12 UTC 2020 - Jochen Breuer + +- Testsuite fix +- Add option to enable/disable force refresh for zypper + +- Added: + * option-to-en-disable-force-refresh-in-zypper-215.patch + * fix-a-test-and-some-variable-names-229.patch + ------------------------------------------------------------------- Fri May 8 14:24:19 UTC 2020 - Jochen Breuer diff --git a/salt.spec b/salt.spec index e895867..dff3cab 100644 --- a/salt.spec +++ b/salt.spec @@ -308,6 +308,16 @@ Patch111: prevent-logging-deadlock-on-salt-api-subprocesses-bs.patch Patch112: msgpack-support-versions-1.0.0.patch # PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/235 Patch113: python3.8-compatibility-pr-s-235.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/56419 +Patch114: option-to-en-disable-force-refresh-in-zypper-215.patch +# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/229 +Patch115: fix-a-test-and-some-variable-names-229.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/56439 +Patch116: add-docker-logout-237.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/56595 +Patch117: fix-for-return-value-ret-vs-return-in-batch-mode.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/57392 +Patch118: zypperpkg-filter-patterns-that-start-with-dot-244.patch BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildRequires: logrotate @@ -928,6 +938,11 @@ cp %{S:5} ./.travis.yml %patch111 -p1 %patch112 -p1 %patch113 -p1 +%patch114 -p1 +%patch115 -p1 +%patch116 -p1 +%patch117 -p1 +%patch118 -p1 %build %if 0%{?build_py2} diff --git a/zypperpkg-filter-patterns-that-start-with-dot-244.patch b/zypperpkg-filter-patterns-that-start-with-dot-244.patch new file mode 100644 index 0000000..79bc0bd --- /dev/null +++ b/zypperpkg-filter-patterns-that-start-with-dot-244.patch @@ -0,0 +1,80 @@ +From 31bccc548b6f9d894b7c87ade035b1b178c18841 Mon Sep 17 00:00:00 2001 +From: Alberto Planas +Date: Thu, 21 May 2020 10:19:15 +0200 +Subject: [PATCH] zypperpkg: filter patterns that start with dot (#244) + +For versions <=SLE12SP4 some patterns can contain alias, and can appear +duplicated. The alias start with ".", so they can be filtered. + +If the module try to search by the alias name (pattern:.basename, for +example), zypper will not be able to find it and the operation will +fail. + +This patch detect and filter the alias, and remove duplicates. + +Fix bsc#1171906 + +(cherry picked from commit d043db63000df2892b2e7259f580ede81e33724d) +--- + salt/modules/zypperpkg.py | 10 ++++++++-- + tests/unit/modules/test_zypperpkg.py | 22 ++++++++++++++++++++++ + 2 files changed, 30 insertions(+), 2 deletions(-) + +diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py +index ed8420f398b91b3ef76417d2f11ec59c4051d120..96c3eed851b819ec800e733628e2ae255481bb92 100644 +--- a/salt/modules/zypperpkg.py ++++ b/salt/modules/zypperpkg.py +@@ -2302,8 +2302,14 @@ def _get_installed_patterns(root=None): + # a real error. + output = __salt__['cmd.run'](cmd, ignore_retcode=True) + +- installed_patterns = [_pattern_name(line) for line in output.splitlines() +- if line.startswith('pattern() = ')] ++ # On <= SLE12SP4 we have patterns that have multiple names (alias) ++ # and that are duplicated. The alias start with ".", so we filter ++ # them. ++ installed_patterns = { ++ _pattern_name(line) ++ for line in output.splitlines() ++ if line.startswith("pattern() = ") and not _pattern_name(line).startswith(".") ++ } + + patterns = {k: v for k, v in _get_visible_patterns(root=root).items() if v['installed']} + +diff --git a/tests/unit/modules/test_zypperpkg.py b/tests/unit/modules/test_zypperpkg.py +index 9a5c59a8572cb47c947645ed7c0b5c645c48a909..1fce3352c6aa0b5f19c802831bf8583012feb6bf 100644 +--- a/tests/unit/modules/test_zypperpkg.py ++++ b/tests/unit/modules/test_zypperpkg.py +@@ -1493,6 +1493,28 @@ pattern() = package-c'''), + }, + } + ++ @patch("salt.modules.zypperpkg._get_visible_patterns") ++ def test__get_installed_patterns_with_alias(self, get_visible_patterns): ++ """Test installed patterns in the system if they have alias""" ++ get_visible_patterns.return_value = { ++ "package-a": {"installed": True, "summary": "description a"}, ++ "package-b": {"installed": False, "summary": "description b"}, ++ } ++ ++ salt_mock = { ++ "cmd.run": MagicMock( ++ return_value="""pattern() = .package-a-alias ++pattern() = package-a ++pattern-visible() ++pattern() = package-c""" ++ ), ++ } ++ with patch.dict("salt.modules.zypperpkg.__salt__", salt_mock): ++ assert zypper._get_installed_patterns() == { ++ "package-a": {"installed": True, "summary": "description a"}, ++ "package-c": {"installed": True, "summary": "Non-visible pattern"}, ++ } ++ + @patch('salt.modules.zypperpkg._get_visible_patterns') + def test_list_patterns(self, get_visible_patterns): + '''Test available patterns in the repo''' +-- +2.23.0 + +