summaryrefslogtreecommitdiffstats
path: root/test/integration/targets/module_utils
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--test/integration/targets/module_utils/aliases6
-rw-r--r--test/integration/targets/module_utils/callback/pure_json.py31
-rw-r--r--test/integration/targets/module_utils/collections/ansible_collections/testns/testcoll/plugins/module_utils/legit.py6
-rw-r--r--test/integration/targets/module_utils/library/test.py87
-rw-r--r--test/integration/targets/module_utils/library/test_alias_deprecation.py16
-rw-r--r--test/integration/targets/module_utils/library/test_cwd_missing.py33
-rw-r--r--test/integration/targets/module_utils/library/test_cwd_unreadable.py28
-rw-r--r--test/integration/targets/module_utils/library/test_datetime.py19
-rw-r--r--test/integration/targets/module_utils/library/test_env_override.py14
-rw-r--r--test/integration/targets/module_utils/library/test_failure.py14
-rw-r--r--test/integration/targets/module_utils/library/test_network.py28
-rw-r--r--test/integration/targets/module_utils/library/test_no_log.py35
-rw-r--r--test/integration/targets/module_utils/library/test_optional.py84
-rw-r--r--test/integration/targets/module_utils/library/test_override.py11
-rw-r--r--test/integration/targets/module_utils/library/test_recursive_diff.py29
-rw-r--r--test/integration/targets/module_utils/module_utils/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/d/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/d/e/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/h/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/ansible_release.py4
-rw-r--r--test/integration/targets/module_utils/module_utils/bar0/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/bar0/foo.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/bar1/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/bar2/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/baz1/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/baz1/one.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/baz2/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/baz2/one.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/foo.py3
-rw-r--r--test/integration/targets/module_utils/module_utils/foo0.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/foo1.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/foo2.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/qux1/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/qux1/quux.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/qux2/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/qux2/quux.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/qux2/quuz.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/service.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam1/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam1/ham/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam1/ham/eggs/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam2/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam2/ham/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam2/ham/eggs/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam3/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam3/ham/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam3/ham/bacon.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam4/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam4/ham/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam4/ham/bacon.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam5/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam5/ham/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam5/ham/bacon.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam5/ham/eggs.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam6/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam6/ham/__init__.py2
-rw-r--r--test/integration/targets/module_utils/module_utils/spam7/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam7/ham/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam7/ham/bacon.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam8/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/spam8/ham/__init__.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/spam8/ham/bacon.py1
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bam.py3
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bam/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bam/bam.py3
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bar/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bar/bam.py3
-rw-r--r--test/integration/targets/module_utils/module_utils/sub/bar/bar.py3
-rw-r--r--test/integration/targets/module_utils/module_utils/yak/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/yak/zebra/__init__.py0
-rw-r--r--test/integration/targets/module_utils/module_utils/yak/zebra/foo.py1
-rw-r--r--test/integration/targets/module_utils/module_utils_basic_setcwd.yml53
-rw-r--r--test/integration/targets/module_utils/module_utils_common_dict_transformation.yml34
-rw-r--r--test/integration/targets/module_utils/module_utils_common_network.yml10
-rw-r--r--test/integration/targets/module_utils/module_utils_envvar.yml51
-rw-r--r--test/integration/targets/module_utils/module_utils_test.yml121
-rw-r--r--test/integration/targets/module_utils/module_utils_test_no_log.yml12
-rw-r--r--test/integration/targets/module_utils/module_utils_vvvvv.yml29
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/d/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/__init__.py0
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/h/__init__.py1
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/facts.py1
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/json_utils.py1
-rw-r--r--test/integration/targets/module_utils/other_mu_dir/mork.py1
-rwxr-xr-xtest/integration/targets/module_utils/runme.sh19
-rw-r--r--test/integration/targets/module_utils_Ansible.AccessToken/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.AccessToken/library/ansible_access_token_tests.ps1407
-rw-r--r--test/integration/targets/module_utils_Ansible.AccessToken/tasks/main.yml29
-rw-r--r--test/integration/targets/module_utils_Ansible.Basic/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps13206
-rw-r--r--test/integration/targets/module_utils_Ansible.Basic/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_Ansible.Become/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.Become/library/ansible_become_tests.ps11022
-rw-r--r--test/integration/targets/module_utils_Ansible.Become/tasks/main.yml28
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1332
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/tasks/main.yml10
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/library/argv_parser_test.ps193
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/meta/main.yml3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/library/backup_file_test.ps192
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/tasks/main.yml10
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/library/camel_conversion_test.ps181
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/tasks/main.yml8
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/library/command_util_test.ps1139
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/meta/main.yml3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/library/file_util_test.ps1114
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/tasks/main.yml8
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testlist.ps112
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testpath.ps19
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/tasks/main.yml41
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/library/symbolic_link_test.ps1174
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/tasks/main.yml8
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/library/privilege_util_test.ps1113
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/tasks/main.yml8
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.SID/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.SID/library/sid_utils_test.ps1101
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.SID/tasks/main.yml22
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/aliases4
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/library/web_request_test.ps1473
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/meta/main.yml3
-rw-r--r--test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/tasks/main.yml10
-rw-r--r--test/integration/targets/module_utils_Ansible.Privilege/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.Privilege/library/ansible_privilege_tests.ps1278
-rw-r--r--test/integration/targets/module_utils_Ansible.Privilege/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_Ansible.Process/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.Process/library/ansible_process_tests.ps1242
-rw-r--r--test/integration/targets/module_utils_Ansible.Process/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_Ansible.Service/aliases3
-rw-r--r--test/integration/targets/module_utils_Ansible.Service/library/ansible_service_tests.ps1953
-rw-r--r--test/integration/targets/module_utils_Ansible.Service/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_ansible_release/aliases2
-rw-r--r--test/integration/targets/module_utils_ansible_release/library/ansible_release.py40
-rw-r--r--test/integration/targets/module_utils_ansible_release/tasks/main.yml9
-rw-r--r--test/integration/targets/module_utils_common.respawn/aliases1
-rw-r--r--test/integration/targets/module_utils_common.respawn/library/respawnme.py44
-rw-r--r--test/integration/targets/module_utils_common.respawn/tasks/main.yml24
-rw-r--r--test/integration/targets/module_utils_distro/aliases2
-rw-r--r--test/integration/targets/module_utils_distro/meta/main.yml2
-rwxr-xr-xtest/integration/targets/module_utils_distro/runme.sh24
-rw-r--r--test/integration/targets/module_utils_facts.system.selinux/aliases1
-rw-r--r--test/integration/targets/module_utils_facts.system.selinux/tasks/main.yml38
-rw-r--r--test/integration/targets/module_utils_facts.system.selinux/tasks/selinux.yml93
-rw-r--r--test/integration/targets/module_utils_urls/aliases2
-rw-r--r--test/integration/targets/module_utils_urls/library/test_peercert.py98
-rw-r--r--test/integration/targets/module_utils_urls/meta/main.yml3
-rw-r--r--test/integration/targets/module_utils_urls/tasks/main.yml32
168 files changed, 9372 insertions, 0 deletions
diff --git a/test/integration/targets/module_utils/aliases b/test/integration/targets/module_utils/aliases
new file mode 100644
index 0000000..a1fba96
--- /dev/null
+++ b/test/integration/targets/module_utils/aliases
@@ -0,0 +1,6 @@
+shippable/posix/group2
+needs/root
+needs/target/setup_test_user
+needs/target/setup_remote_tmp_dir
+context/target
+destructive
diff --git a/test/integration/targets/module_utils/callback/pure_json.py b/test/integration/targets/module_utils/callback/pure_json.py
new file mode 100644
index 0000000..1723d7b
--- /dev/null
+++ b/test/integration/targets/module_utils/callback/pure_json.py
@@ -0,0 +1,31 @@
+# (c) 2021 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ name: pure_json
+ type: stdout
+ short_description: only outputs the module results as json
+'''
+
+import json
+
+from ansible.plugins.callback import CallbackBase
+
+
+class CallbackModule(CallbackBase):
+
+ CALLBACK_VERSION = 2.0
+ CALLBACK_TYPE = 'stdout'
+ CALLBACK_NAME = 'pure_json'
+
+ def v2_runner_on_failed(self, result, ignore_errors=False):
+ self._display.display(json.dumps(result._result))
+
+ def v2_runner_on_ok(self, result):
+ self._display.display(json.dumps(result._result))
+
+ def v2_runner_on_skipped(self, result):
+ self._display.display(json.dumps(result._result))
diff --git a/test/integration/targets/module_utils/collections/ansible_collections/testns/testcoll/plugins/module_utils/legit.py b/test/integration/targets/module_utils/collections/ansible_collections/testns/testcoll/plugins/module_utils/legit.py
new file mode 100644
index 0000000..b9d6348
--- /dev/null
+++ b/test/integration/targets/module_utils/collections/ansible_collections/testns/testcoll/plugins/module_utils/legit.py
@@ -0,0 +1,6 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+def importme():
+ return "successfully imported from testns.testcoll"
diff --git a/test/integration/targets/module_utils/library/test.py b/test/integration/targets/module_utils/library/test.py
new file mode 100644
index 0000000..fb6c8a8
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test.py
@@ -0,0 +1,87 @@
+#!/usr/bin/python
+# Most of these names are only available via PluginLoader so pylint doesn't
+# know they exist
+# pylint: disable=no-name-in-module
+__metaclass__ = type
+
+results = {}
+
+# Test import with no from
+import ansible.module_utils.foo0
+results['foo0'] = ansible.module_utils.foo0.data
+
+# Test depthful import with no from
+import ansible.module_utils.bar0.foo
+results['bar0'] = ansible.module_utils.bar0.foo.data
+
+# Test import of module_utils/foo1.py
+from ansible.module_utils import foo1
+results['foo1'] = foo1.data
+
+# Test import of an identifier inside of module_utils/foo2.py
+from ansible.module_utils.foo2 import data
+results['foo2'] = data
+
+# Test import of module_utils/bar1/__init__.py
+from ansible.module_utils import bar1
+results['bar1'] = bar1.data
+
+# Test import of an identifier inside of module_utils/bar2/__init__.py
+from ansible.module_utils.bar2 import data
+results['bar2'] = data
+
+# Test import of module_utils/baz1/one.py
+from ansible.module_utils.baz1 import one
+results['baz1'] = one.data
+
+# Test import of an identifier inside of module_utils/baz2/one.py
+from ansible.module_utils.baz2.one import data
+results['baz2'] = data
+
+# Test import of module_utils/spam1/ham/eggs/__init__.py
+from ansible.module_utils.spam1.ham import eggs
+results['spam1'] = eggs.data
+
+# Test import of an identifier inside module_utils/spam2/ham/eggs/__init__.py
+from ansible.module_utils.spam2.ham.eggs import data
+results['spam2'] = data
+
+# Test import of module_utils/spam3/ham/bacon.py
+from ansible.module_utils.spam3.ham import bacon
+results['spam3'] = bacon.data
+
+# Test import of an identifier inside of module_utils/spam4/ham/bacon.py
+from ansible.module_utils.spam4.ham.bacon import data
+results['spam4'] = data
+
+# Test import of module_utils.spam5.ham bacon and eggs (modules)
+from ansible.module_utils.spam5.ham import bacon, eggs
+results['spam5'] = (bacon.data, eggs.data)
+
+# Test import of module_utils.spam6.ham bacon and eggs (identifiers)
+from ansible.module_utils.spam6.ham import bacon, eggs
+results['spam6'] = (bacon, eggs)
+
+# Test import of module_utils.spam7.ham bacon and eggs (module and identifier)
+from ansible.module_utils.spam7.ham import bacon, eggs
+results['spam7'] = (bacon.data, eggs)
+
+# Test import of module_utils/spam8/ham/bacon.py and module_utils/spam8/ham/eggs.py separately
+from ansible.module_utils.spam8.ham import bacon
+from ansible.module_utils.spam8.ham import eggs
+results['spam8'] = (bacon.data, eggs)
+
+# Test that import of module_utils/qux1/quux.py using as works
+from ansible.module_utils.qux1 import quux as one
+results['qux1'] = one.data
+
+# Test that importing qux2/quux.py and qux2/quuz.py using as works
+from ansible.module_utils.qux2 import quux as one, quuz as two
+results['qux2'] = (one.data, two.data)
+
+# Test depth
+from ansible.module_utils.a.b.c.d.e.f.g.h import data
+
+results['abcdefgh'] = data
+from ansible.module_utils.basic import AnsibleModule
+AnsibleModule(argument_spec=dict()).exit_json(**results)
diff --git a/test/integration/targets/module_utils/library/test_alias_deprecation.py b/test/integration/targets/module_utils/library/test_alias_deprecation.py
new file mode 100644
index 0000000..dc36aba
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_alias_deprecation.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+# overridden
+from ansible.module_utils.ansible_release import data
+
+results = {"data": data}
+
+arg_spec = dict(
+ foo=dict(type='str', aliases=['baz'], deprecated_aliases=[dict(name='baz', version='9.99')])
+)
+
+AnsibleModule(argument_spec=arg_spec).exit_json(**results)
diff --git a/test/integration/targets/module_utils/library/test_cwd_missing.py b/test/integration/targets/module_utils/library/test_cwd_missing.py
new file mode 100644
index 0000000..cd1f9c7
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_cwd_missing.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ # This module verifies that AnsibleModule works when cwd does not exist.
+ # This situation can occur as a race condition when the following conditions are met:
+ #
+ # 1) Execute a module which has high startup overhead prior to instantiating AnsibleModule (0.5s is enough in many cases).
+ # 2) Run the module async as the last task in a playbook using connection=local (a fire-and-forget task).
+ # 3) Remove the directory containing the playbook immediately after playbook execution ends (playbook in a temp dir).
+ #
+ # To ease testing of this race condition the deletion of cwd is handled in this module.
+ # This avoids race conditions in the test, including timing cwd deletion between AnsiballZ wrapper execution and AnsibleModule instantiation.
+ # The timing issue with AnsiballZ is due to cwd checking in the wrapper when code coverage is enabled.
+
+ temp = os.path.abspath('temp')
+
+ os.mkdir(temp)
+ os.chdir(temp)
+ os.rmdir(temp)
+
+ module = AnsibleModule(argument_spec=dict())
+ module.exit_json(before=temp, after=os.getcwd())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils/library/test_cwd_unreadable.py b/test/integration/targets/module_utils/library/test_cwd_unreadable.py
new file mode 100644
index 0000000..d65f31a
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_cwd_unreadable.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ # This module verifies that AnsibleModule works when cwd exists but is unreadable.
+ # This situation can occur when running tasks as an unprivileged user.
+
+ try:
+ cwd = os.getcwd()
+ except OSError:
+ # Compensate for macOS being unable to access cwd as an unprivileged user.
+ # This test is a no-op in this case.
+ # Testing for os.getcwd() failures is handled by the test_cwd_missing module.
+ cwd = '/'
+ os.chdir(cwd)
+
+ module = AnsibleModule(argument_spec=dict())
+ module.exit_json(before=cwd, after=os.getcwd())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils/library/test_datetime.py b/test/integration/targets/module_utils/library/test_datetime.py
new file mode 100644
index 0000000..493a186
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_datetime.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+# Most of these names are only available via PluginLoader so pylint doesn't
+# know they exist
+# pylint: disable=no-name-in-module
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+import datetime
+
+module = AnsibleModule(argument_spec=dict(
+ datetime=dict(type=str, required=True),
+ date=dict(type=str, required=True),
+))
+result = {
+ 'datetime': datetime.datetime.strptime(module.params.get('datetime'), '%Y-%m-%dT%H:%M:%S'),
+ 'date': datetime.datetime.strptime(module.params.get('date'), '%Y-%m-%d').date(),
+}
+module.exit_json(**result)
diff --git a/test/integration/targets/module_utils/library/test_env_override.py b/test/integration/targets/module_utils/library/test_env_override.py
new file mode 100644
index 0000000..ebfb5dd
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_env_override.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+# Most of these names are only available via PluginLoader so pylint doesn't
+# know they exist
+# pylint: disable=no-name-in-module
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.json_utils import data
+from ansible.module_utils.mork import data as mork_data
+
+results = {"json_utils": data, "mork": mork_data}
+
+AnsibleModule(argument_spec=dict()).exit_json(**results)
diff --git a/test/integration/targets/module_utils/library/test_failure.py b/test/integration/targets/module_utils/library/test_failure.py
new file mode 100644
index 0000000..efb3dda
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_failure.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+results = {}
+# Test that we are rooted correctly
+# Following files:
+# module_utils/yak/zebra/foo.py
+from ansible.module_utils.zebra import foo
+
+results['zebra'] = foo.data
+
+from ansible.module_utils.basic import AnsibleModule
+AnsibleModule(argument_spec=dict()).exit_json(**results)
diff --git a/test/integration/targets/module_utils/library/test_network.py b/test/integration/targets/module_utils/library/test_network.py
new file mode 100644
index 0000000..c6a5390
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_network.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2021, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.network import to_subnet
+
+
+def main():
+ module = AnsibleModule(argument_spec=dict(
+ subnet=dict(),
+ ))
+
+ subnet = module.params['subnet']
+
+ if subnet is not None:
+ split_addr = subnet.split('/')
+ if len(split_addr) != 2:
+ module.fail_json("Invalid CIDR notation: expected a subnet mask (e.g. 10.0.0.0/32)")
+ module.exit_json(resolved=to_subnet(split_addr[0], split_addr[1]))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils/library/test_no_log.py b/test/integration/targets/module_utils/library/test_no_log.py
new file mode 100644
index 0000000..770e0b3
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_no_log.py
@@ -0,0 +1,35 @@
+#!/usr/bin/python
+# (c) 2021 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ explicit_pass=dict(type='str', no_log=True),
+ fallback_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_ENV'])),
+ default_pass=dict(type='str', no_log=True, default='zyx'),
+ normal=dict(type='str', default='plaintext'),
+ suboption=dict(
+ type='dict',
+ options=dict(
+ explicit_sub_pass=dict(type='str', no_log=True),
+ fallback_sub_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_SUB_ENV'])),
+ default_sub_pass=dict(type='str', no_log=True, default='xvu'),
+ normal=dict(type='str', default='plaintext'),
+ ),
+ ),
+ ),
+ )
+
+ module.exit_json(changed=False)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils/library/test_optional.py b/test/integration/targets/module_utils/library/test_optional.py
new file mode 100644
index 0000000..4d0225d
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_optional.py
@@ -0,0 +1,84 @@
+#!/usr/bin/python
+# Most of these names are only available via PluginLoader so pylint doesn't
+# know they exist
+# pylint: disable=no-name-in-module
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+# internal constants to keep pylint from griping about constant-valued conditionals
+_private_false = False
+_private_true = True
+
+# module_utils import statements nested below any block are considered optional "best-effort" for AnsiballZ to include.
+# test a number of different import shapes and nesting types to exercise this...
+
+# first, some nested imports that should succeed...
+try:
+ from ansible.module_utils.urls import fetch_url as yep1
+except ImportError:
+ yep1 = None
+
+try:
+ import ansible.module_utils.common.text.converters as yep2
+except ImportError:
+ yep2 = None
+
+try:
+ # optional import from a legit collection
+ from ansible_collections.testns.testcoll.plugins.module_utils.legit import importme as yep3
+except ImportError:
+ yep3 = None
+
+# and a bunch that should fail to be found, but not break the module_utils payload build in the process...
+try:
+ from ansible.module_utils.bogus import fromnope1
+except ImportError:
+ fromnope1 = None
+
+if _private_false:
+ from ansible.module_utils.alsobogus import fromnope2
+else:
+ fromnope2 = None
+
+try:
+ import ansible.module_utils.verybogus
+ nope1 = ansible.module_utils.verybogus
+except ImportError:
+ nope1 = None
+
+# deepish nested with multiple block types- make sure the AST walker made it all the way down
+try:
+ if _private_true:
+ if _private_true:
+ if _private_true:
+ if _private_true:
+ try:
+ import ansible.module_utils.stillbogus as nope2
+ except ImportError:
+ raise
+except ImportError:
+ nope2 = None
+
+try:
+ # optional import from a valid collection with an invalid package
+ from ansible_collections.testns.testcoll.plugins.module_utils.bogus import collnope1
+except ImportError:
+ collnope1 = None
+
+try:
+ # optional import from a bogus collection
+ from ansible_collections.bogusns.boguscoll.plugins.module_utils.bogus import collnope2
+except ImportError:
+ collnope2 = None
+
+module = AnsibleModule(argument_spec={})
+
+if not all([yep1, yep2, yep3]):
+ module.fail_json(msg='one or more existing optional imports did not resolve')
+
+if any([fromnope1, fromnope2, nope1, nope2, collnope1, collnope2]):
+ module.fail_json(msg='one or more missing optional imports resolved unexpectedly')
+
+module.exit_json(msg='all missing optional imports behaved as expected')
diff --git a/test/integration/targets/module_utils/library/test_override.py b/test/integration/targets/module_utils/library/test_override.py
new file mode 100644
index 0000000..7f6e7a5
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_override.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+# overridden
+from ansible.module_utils.ansible_release import data
+
+results = {"data": data}
+
+AnsibleModule(argument_spec=dict()).exit_json(**results)
diff --git a/test/integration/targets/module_utils/library/test_recursive_diff.py b/test/integration/targets/module_utils/library/test_recursive_diff.py
new file mode 100644
index 0000000..0cf39d9
--- /dev/null
+++ b/test/integration/targets/module_utils/library/test_recursive_diff.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python
+# Copyright: (c) 2020, Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.dict_transformations import recursive_diff
+
+
+def main():
+ module = AnsibleModule(
+ {
+ 'a': {'type': 'dict'},
+ 'b': {'type': 'dict'},
+ }
+ )
+
+ module.exit_json(
+ the_diff=recursive_diff(
+ module.params['a'],
+ module.params['b'],
+ ),
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils/module_utils/__init__.py b/test/integration/targets/module_utils/module_utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/__init__.py b/test/integration/targets/module_utils/module_utils/a/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/d/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/d/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/d/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/d/e/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/h/__init__.py b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/h/__init__.py
new file mode 100644
index 0000000..722f4b7
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/h/__init__.py
@@ -0,0 +1 @@
+data = 'abcdefgh'
diff --git a/test/integration/targets/module_utils/module_utils/ansible_release.py b/test/integration/targets/module_utils/module_utils/ansible_release.py
new file mode 100644
index 0000000..7d43bf8
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/ansible_release.py
@@ -0,0 +1,4 @@
+# This file overrides the builtin ansible.module_utils.ansible_release file
+# to test that it can be overridden. Previously this was facts.py but caused issues
+# with dependencies that may need to execute a module that makes use of facts
+data = 'overridden ansible_release.py'
diff --git a/test/integration/targets/module_utils/module_utils/bar0/__init__.py b/test/integration/targets/module_utils/module_utils/bar0/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/bar0/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/bar0/foo.py b/test/integration/targets/module_utils/module_utils/bar0/foo.py
new file mode 100644
index 0000000..1072dcc
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/bar0/foo.py
@@ -0,0 +1 @@
+data = 'bar0'
diff --git a/test/integration/targets/module_utils/module_utils/bar1/__init__.py b/test/integration/targets/module_utils/module_utils/bar1/__init__.py
new file mode 100644
index 0000000..68e4350
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/bar1/__init__.py
@@ -0,0 +1 @@
+data = 'bar1'
diff --git a/test/integration/targets/module_utils/module_utils/bar2/__init__.py b/test/integration/targets/module_utils/module_utils/bar2/__init__.py
new file mode 100644
index 0000000..59e86af
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/bar2/__init__.py
@@ -0,0 +1 @@
+data = 'bar2'
diff --git a/test/integration/targets/module_utils/module_utils/baz1/__init__.py b/test/integration/targets/module_utils/module_utils/baz1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/baz1/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/baz1/one.py b/test/integration/targets/module_utils/module_utils/baz1/one.py
new file mode 100644
index 0000000..e5d7894
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/baz1/one.py
@@ -0,0 +1 @@
+data = 'baz1'
diff --git a/test/integration/targets/module_utils/module_utils/baz2/__init__.py b/test/integration/targets/module_utils/module_utils/baz2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/baz2/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/baz2/one.py b/test/integration/targets/module_utils/module_utils/baz2/one.py
new file mode 100644
index 0000000..1efe196
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/baz2/one.py
@@ -0,0 +1 @@
+data = 'baz2'
diff --git a/test/integration/targets/module_utils/module_utils/foo.py b/test/integration/targets/module_utils/module_utils/foo.py
new file mode 100644
index 0000000..20698f1
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/foo.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+foo = "FOO FROM foo.py"
diff --git a/test/integration/targets/module_utils/module_utils/foo0.py b/test/integration/targets/module_utils/module_utils/foo0.py
new file mode 100644
index 0000000..4b528b6
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/foo0.py
@@ -0,0 +1 @@
+data = 'foo0'
diff --git a/test/integration/targets/module_utils/module_utils/foo1.py b/test/integration/targets/module_utils/module_utils/foo1.py
new file mode 100644
index 0000000..18e0cef
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/foo1.py
@@ -0,0 +1 @@
+data = 'foo1'
diff --git a/test/integration/targets/module_utils/module_utils/foo2.py b/test/integration/targets/module_utils/module_utils/foo2.py
new file mode 100644
index 0000000..feb142d
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/foo2.py
@@ -0,0 +1 @@
+data = 'foo2'
diff --git a/test/integration/targets/module_utils/module_utils/qux1/__init__.py b/test/integration/targets/module_utils/module_utils/qux1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/qux1/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/qux1/quux.py b/test/integration/targets/module_utils/module_utils/qux1/quux.py
new file mode 100644
index 0000000..3d288c9
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/qux1/quux.py
@@ -0,0 +1 @@
+data = 'qux1'
diff --git a/test/integration/targets/module_utils/module_utils/qux2/__init__.py b/test/integration/targets/module_utils/module_utils/qux2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/qux2/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/qux2/quux.py b/test/integration/targets/module_utils/module_utils/qux2/quux.py
new file mode 100644
index 0000000..496d446
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/qux2/quux.py
@@ -0,0 +1 @@
+data = 'qux2:quux'
diff --git a/test/integration/targets/module_utils/module_utils/qux2/quuz.py b/test/integration/targets/module_utils/module_utils/qux2/quuz.py
new file mode 100644
index 0000000..cdc0fad
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/qux2/quuz.py
@@ -0,0 +1 @@
+data = 'qux2:quuz'
diff --git a/test/integration/targets/module_utils/module_utils/service.py b/test/integration/targets/module_utils/module_utils/service.py
new file mode 100644
index 0000000..1492f46
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/service.py
@@ -0,0 +1 @@
+sysv_is_enabled = 'sysv_is_enabled'
diff --git a/test/integration/targets/module_utils/module_utils/spam1/__init__.py b/test/integration/targets/module_utils/module_utils/spam1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam1/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam1/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam1/ham/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam1/ham/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam1/ham/eggs/__init__.py b/test/integration/targets/module_utils/module_utils/spam1/ham/eggs/__init__.py
new file mode 100644
index 0000000..f290e15
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam1/ham/eggs/__init__.py
@@ -0,0 +1 @@
+data = 'spam1'
diff --git a/test/integration/targets/module_utils/module_utils/spam2/__init__.py b/test/integration/targets/module_utils/module_utils/spam2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam2/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam2/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam2/ham/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam2/ham/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam2/ham/eggs/__init__.py b/test/integration/targets/module_utils/module_utils/spam2/ham/eggs/__init__.py
new file mode 100644
index 0000000..5e053d8
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam2/ham/eggs/__init__.py
@@ -0,0 +1 @@
+data = 'spam2'
diff --git a/test/integration/targets/module_utils/module_utils/spam3/__init__.py b/test/integration/targets/module_utils/module_utils/spam3/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam3/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam3/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam3/ham/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam3/ham/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam3/ham/bacon.py b/test/integration/targets/module_utils/module_utils/spam3/ham/bacon.py
new file mode 100644
index 0000000..9107508
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam3/ham/bacon.py
@@ -0,0 +1 @@
+data = 'spam3'
diff --git a/test/integration/targets/module_utils/module_utils/spam4/__init__.py b/test/integration/targets/module_utils/module_utils/spam4/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam4/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam4/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam4/ham/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam4/ham/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam4/ham/bacon.py b/test/integration/targets/module_utils/module_utils/spam4/ham/bacon.py
new file mode 100644
index 0000000..7d55288
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam4/ham/bacon.py
@@ -0,0 +1 @@
+data = 'spam4'
diff --git a/test/integration/targets/module_utils/module_utils/spam5/__init__.py b/test/integration/targets/module_utils/module_utils/spam5/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam5/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam5/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam5/ham/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam5/ham/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam5/ham/bacon.py b/test/integration/targets/module_utils/module_utils/spam5/ham/bacon.py
new file mode 100644
index 0000000..cc947b8
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam5/ham/bacon.py
@@ -0,0 +1 @@
+data = 'spam5:bacon'
diff --git a/test/integration/targets/module_utils/module_utils/spam5/ham/eggs.py b/test/integration/targets/module_utils/module_utils/spam5/ham/eggs.py
new file mode 100644
index 0000000..f0394c8
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam5/ham/eggs.py
@@ -0,0 +1 @@
+data = 'spam5:eggs'
diff --git a/test/integration/targets/module_utils/module_utils/spam6/__init__.py b/test/integration/targets/module_utils/module_utils/spam6/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam6/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam6/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam6/ham/__init__.py
new file mode 100644
index 0000000..8c1a70e
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam6/ham/__init__.py
@@ -0,0 +1,2 @@
+bacon = 'spam6:bacon'
+eggs = 'spam6:eggs'
diff --git a/test/integration/targets/module_utils/module_utils/spam7/__init__.py b/test/integration/targets/module_utils/module_utils/spam7/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam7/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam7/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam7/ham/__init__.py
new file mode 100644
index 0000000..cd9a05d
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam7/ham/__init__.py
@@ -0,0 +1 @@
+eggs = 'spam7:eggs'
diff --git a/test/integration/targets/module_utils/module_utils/spam7/ham/bacon.py b/test/integration/targets/module_utils/module_utils/spam7/ham/bacon.py
new file mode 100644
index 0000000..490121f
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam7/ham/bacon.py
@@ -0,0 +1 @@
+data = 'spam7:bacon'
diff --git a/test/integration/targets/module_utils/module_utils/spam8/__init__.py b/test/integration/targets/module_utils/module_utils/spam8/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam8/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/spam8/ham/__init__.py b/test/integration/targets/module_utils/module_utils/spam8/ham/__init__.py
new file mode 100644
index 0000000..c02bf5f
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam8/ham/__init__.py
@@ -0,0 +1 @@
+eggs = 'spam8:eggs'
diff --git a/test/integration/targets/module_utils/module_utils/spam8/ham/bacon.py b/test/integration/targets/module_utils/module_utils/spam8/ham/bacon.py
new file mode 100644
index 0000000..28ea285
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/spam8/ham/bacon.py
@@ -0,0 +1 @@
+data = 'spam8:bacon'
diff --git a/test/integration/targets/module_utils/module_utils/sub/__init__.py b/test/integration/targets/module_utils/module_utils/sub/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/sub/bam.py b/test/integration/targets/module_utils/module_utils/sub/bam.py
new file mode 100644
index 0000000..566f8b7
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bam.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+bam = "BAM FROM sub/bam.py"
diff --git a/test/integration/targets/module_utils/module_utils/sub/bam/__init__.py b/test/integration/targets/module_utils/module_utils/sub/bam/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bam/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/sub/bam/bam.py b/test/integration/targets/module_utils/module_utils/sub/bam/bam.py
new file mode 100644
index 0000000..b7ed707
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bam/bam.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+bam = "BAM FROM sub/bam/bam.py"
diff --git a/test/integration/targets/module_utils/module_utils/sub/bar/__init__.py b/test/integration/targets/module_utils/module_utils/sub/bar/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bar/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/sub/bar/bam.py b/test/integration/targets/module_utils/module_utils/sub/bar/bam.py
new file mode 100644
index 0000000..02fafd4
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bar/bam.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+bam = "BAM FROM sub/bar/bam.py"
diff --git a/test/integration/targets/module_utils/module_utils/sub/bar/bar.py b/test/integration/targets/module_utils/module_utils/sub/bar/bar.py
new file mode 100644
index 0000000..8566901
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/sub/bar/bar.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+bar = "BAR FROM sub/bar/bar.py"
diff --git a/test/integration/targets/module_utils/module_utils/yak/__init__.py b/test/integration/targets/module_utils/module_utils/yak/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/yak/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/yak/zebra/__init__.py b/test/integration/targets/module_utils/module_utils/yak/zebra/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/yak/zebra/__init__.py
diff --git a/test/integration/targets/module_utils/module_utils/yak/zebra/foo.py b/test/integration/targets/module_utils/module_utils/yak/zebra/foo.py
new file mode 100644
index 0000000..89b2bfe
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils/yak/zebra/foo.py
@@ -0,0 +1 @@
+data = 'yak'
diff --git a/test/integration/targets/module_utils/module_utils_basic_setcwd.yml b/test/integration/targets/module_utils/module_utils_basic_setcwd.yml
new file mode 100644
index 0000000..71317f9
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_basic_setcwd.yml
@@ -0,0 +1,53 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: make sure the test user is available
+ include_role:
+ name: setup_test_user
+
+ - name: verify AnsibleModule works when cwd is missing
+ test_cwd_missing:
+ register: missing
+
+ - name: record the mode of the connection user's home directory
+ stat:
+ path: "~"
+ vars:
+ ansible_become: no
+ register: connection_user_home
+
+ - name: limit access to the connection user's home directory
+ file:
+ state: directory
+ path: "{{ connection_user_home.stat.path }}"
+ mode: "0700"
+ vars:
+ ansible_become: no
+
+ - block:
+ - name: verify AnsibleModule works when cwd is unreadable
+ test_cwd_unreadable:
+ register: unreadable
+ vars: &test_user_become
+ ansible_become: yes
+ ansible_become_user: "{{ test_user_name }}" # root can read cwd regardless of permissions, so a non-root user is required here
+ ansible_become_password: "{{ test_user_plaintext_password }}"
+ always:
+ - name: restore access to the connection user's home directory
+ file:
+ state: directory
+ path: "{{ connection_user_home.stat.path }}"
+ mode: "{{ connection_user_home.stat.mode }}"
+ vars:
+ ansible_become: no
+
+ - name: get real path of home directory of the unprivileged user
+ raw: "{{ ansible_python_interpreter }} -c 'import os.path; print(os.path.realpath(os.path.expanduser(\"~\")))'"
+ register: home
+ vars: *test_user_become
+
+ - name: verify AnsibleModule was able to adjust cwd as expected
+ assert:
+ that:
+ - missing.before != missing.after
+ - unreadable.before != unreadable.after or unreadable.before == '/' or unreadable.before == home.stdout.strip() # allow / and $HOME fallback on macOS when using an unprivileged user
diff --git a/test/integration/targets/module_utils/module_utils_common_dict_transformation.yml b/test/integration/targets/module_utils/module_utils_common_dict_transformation.yml
new file mode 100644
index 0000000..7d961c4
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_common_dict_transformation.yml
@@ -0,0 +1,34 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - test_recursive_diff:
+ a:
+ foo:
+ bar:
+ - baz:
+ qux: ham_sandwich
+ b:
+ foo:
+ bar:
+ - baz:
+ qux: turkey_sandwich
+ register: recursive_diff_diff
+
+ - test_recursive_diff:
+ a:
+ foo:
+ bar:
+ - baz:
+ qux: ham_sandwich
+ b:
+ foo:
+ bar:
+ - baz:
+ qux: ham_sandwich
+ register: recursive_diff_same
+
+ - assert:
+ that:
+ - recursive_diff_diff.the_diff is not none
+ - recursive_diff_diff.the_diff|length == 2
+ - recursive_diff_same.the_diff is none
diff --git a/test/integration/targets/module_utils/module_utils_common_network.yml b/test/integration/targets/module_utils/module_utils_common_network.yml
new file mode 100644
index 0000000..e1b953f
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_common_network.yml
@@ -0,0 +1,10 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - test_network:
+ subnet: "10.0.0.2/24"
+ register: subnet
+
+ - assert:
+ that:
+ - subnet.resolved == "10.0.0.0/24"
diff --git a/test/integration/targets/module_utils/module_utils_envvar.yml b/test/integration/targets/module_utils/module_utils_envvar.yml
new file mode 100644
index 0000000..8c37940
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_envvar.yml
@@ -0,0 +1,51 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Use a specially crafted module to see if things were imported correctly
+ test:
+ register: result
+
+ - name: Check that these are all loaded from playbook dir's module_utils
+ assert:
+ that:
+ - 'result["abcdefgh"] == "abcdefgh"'
+ - 'result["bar0"] == "bar0"'
+ - 'result["bar1"] == "bar1"'
+ - 'result["bar2"] == "bar2"'
+ - 'result["baz1"] == "baz1"'
+ - 'result["baz2"] == "baz2"'
+ - 'result["foo0"] == "foo0"'
+ - 'result["foo1"] == "foo1"'
+ - 'result["foo2"] == "foo2"'
+ - 'result["qux1"] == "qux1"'
+ - 'result["qux2"] == ["qux2:quux", "qux2:quuz"]'
+ - 'result["spam1"] == "spam1"'
+ - 'result["spam2"] == "spam2"'
+ - 'result["spam3"] == "spam3"'
+ - 'result["spam4"] == "spam4"'
+ - 'result["spam5"] == ["spam5:bacon", "spam5:eggs"]'
+ - 'result["spam6"] == ["spam6:bacon", "spam6:eggs"]'
+ - 'result["spam7"] == ["spam7:bacon", "spam7:eggs"]'
+ - 'result["spam8"] == ["spam8:bacon", "spam8:eggs"]'
+
+ # Test that overriding something in module_utils with something in the local library works
+ - name: Test that playbook dir's module_utils overrides facts.py
+ test_override:
+ register: result
+
+ - name: Make sure the we used the local ansible_release.py, not the one shipped with ansible
+ assert:
+ that:
+ - 'result["data"] == "overridden ansible_release.py"'
+
+ - name: Test that importing something from the module_utils in the env_vars works
+ test_env_override:
+ register: result
+
+ - name: Make sure we used the module_utils from the env_var for these
+ assert:
+ that:
+ # Override of shipped module_utils
+ - 'result["json_utils"] == "overridden json_utils"'
+ # Only i nthe env vars directory
+ - 'result["mork"] == "mork"'
diff --git a/test/integration/targets/module_utils/module_utils_test.yml b/test/integration/targets/module_utils/module_utils_test.yml
new file mode 100644
index 0000000..4e948bd
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_test.yml
@@ -0,0 +1,121 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Use a specially crafted module to see if things were imported correctly
+ test:
+ register: result
+
+ - name: Check that the module imported the correct version of each module_util
+ assert:
+ that:
+ - 'result["abcdefgh"] == "abcdefgh"'
+ - 'result["bar0"] == "bar0"'
+ - 'result["bar1"] == "bar1"'
+ - 'result["bar2"] == "bar2"'
+ - 'result["baz1"] == "baz1"'
+ - 'result["baz2"] == "baz2"'
+ - 'result["foo0"] == "foo0"'
+ - 'result["foo1"] == "foo1"'
+ - 'result["foo2"] == "foo2"'
+ - 'result["qux1"] == "qux1"'
+ - 'result["qux2"] == ["qux2:quux", "qux2:quuz"]'
+ - 'result["spam1"] == "spam1"'
+ - 'result["spam2"] == "spam2"'
+ - 'result["spam3"] == "spam3"'
+ - 'result["spam4"] == "spam4"'
+ - 'result["spam5"] == ["spam5:bacon", "spam5:eggs"]'
+ - 'result["spam6"] == ["spam6:bacon", "spam6:eggs"]'
+ - 'result["spam7"] == ["spam7:bacon", "spam7:eggs"]'
+ - 'result["spam8"] == ["spam8:bacon", "spam8:eggs"]'
+
+ # Test that overriding something in module_utils with something in the local library works
+ - name: Test that local module_utils overrides facts.py
+ test_override:
+ register: result
+
+ - name: Make sure the we used the local ansible_release.py, not the one shipped with ansible
+ assert:
+ that:
+ - result["data"] == "overridden ansible_release.py"
+
+ - name: Test that importing a module that only exists inside of a submodule does not work
+ test_failure:
+ ignore_errors: True
+ register: result
+
+ - name: Make sure we failed in AnsiBallZ
+ assert:
+ that:
+ - result is failed
+ - result['msg'] == "Could not find imported module support code for ansible.modules.test_failure. Looked for (['ansible.module_utils.zebra.foo', 'ansible.module_utils.zebra'])"
+
+ - name: Test that alias deprecation works
+ test_alias_deprecation:
+ baz: 'bar'
+ register: result
+
+ - name: Assert that the deprecation message is given correctly
+ assert:
+ that:
+ - result.deprecations[-1].msg == "Alias 'baz' is deprecated. See the module docs for more information"
+ - result.deprecations[-1].version == '9.99'
+
+ - block:
+ - import_role:
+ name: setup_remote_tmp_dir
+
+ - name: Get a string with a \0 in it
+ command: echo -e 'hi\0foo'
+ register: string_with_null
+
+ - name: Use the null string as a module parameter
+ lineinfile:
+ path: "{{ remote_tmp_dir }}/nulltest"
+ line: "{{ string_with_null.stdout }}"
+ create: yes
+ ignore_errors: yes
+ register: nulltest
+
+ - name: See if the file exists
+ stat:
+ path: "{{ remote_tmp_dir }}/nulltest"
+ register: nullstat
+
+ - assert:
+ that:
+ - nulltest is failed
+ - nulltest.msg_to_log.startswith('Invoked ')
+ - nulltest.msg.startswith('Failed to log to syslog')
+ # Conditionalize this, because when we log with something other than
+ # syslog, it's probably successful and these assertions will fail.
+ when: nulltest is failed
+
+ # Ensure we fail out early and don't actually run the module if logging
+ # failed.
+ - assert:
+ that:
+ - nullstat.stat.exists == nulltest is successful
+ always:
+ - file:
+ path: "{{ remote_tmp_dir }}/nulltest"
+ state: absent
+
+ - name: Test that date and datetime in module output works
+ test_datetime:
+ date: "2020-10-05"
+ datetime: "2020-10-05T10:05:05"
+ register: datetimetest
+
+ - assert:
+ that:
+ - datetimetest.date == '2020-10-05'
+ - datetimetest.datetime == '2020-10-05T10:05:05'
+
+ - name: Test that optional imports behave properly
+ test_optional:
+ register: optionaltest
+
+ - assert:
+ that:
+ - optionaltest is success
+ - optionaltest.msg == 'all missing optional imports behaved as expected'
diff --git a/test/integration/targets/module_utils/module_utils_test_no_log.yml b/test/integration/targets/module_utils/module_utils_test_no_log.yml
new file mode 100644
index 0000000..2fa3e10
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_test_no_log.yml
@@ -0,0 +1,12 @@
+# This is called by module_utils_vvvvv.yml with a custom callback
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Check no_log invocation results
+ test_no_log:
+ explicit_pass: abc
+ suboption:
+ explicit_sub_pass: def
+ environment:
+ SECRET_ENV: ghi
+ SECRET_SUB_ENV: jkl
diff --git a/test/integration/targets/module_utils/module_utils_vvvvv.yml b/test/integration/targets/module_utils/module_utils_vvvvv.yml
new file mode 100644
index 0000000..fc2b0c1
--- /dev/null
+++ b/test/integration/targets/module_utils/module_utils_vvvvv.yml
@@ -0,0 +1,29 @@
+- hosts: testhost
+ gather_facts: no
+ tasks:
+ - name: Use a specially crafted module to see if things were imported correctly
+ test:
+
+ # Invocation usually is output with 3vs or more, our callback plugin displays it anyway
+ - name: Check no_log invocation results
+ command: ansible-playbook -i {{ inventory_file }} module_utils_test_no_log.yml
+ delegate_to: localhost
+ environment:
+ ANSIBLE_CALLBACK_PLUGINS: callback
+ ANSIBLE_STDOUT_CALLBACK: pure_json
+ register: no_log_invocation
+
+ - set_fact:
+ no_log_invocation: '{{ no_log_invocation.stdout | trim | from_json }}'
+
+ - name: check no log values from fallback or default are masked
+ assert:
+ that:
+ - no_log_invocation.invocation.module_args.default_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.explicit_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.fallback_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.normal == 'plaintext'
+ - no_log_invocation.invocation.module_args.suboption.default_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.suboption.explicit_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.suboption.fallback_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ - no_log_invocation.invocation.module_args.suboption.normal == 'plaintext'
diff --git a/test/integration/targets/module_utils/other_mu_dir/__init__.py b/test/integration/targets/module_utils/other_mu_dir/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/__init__.py
diff --git a/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/h/__init__.py b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/h/__init__.py
new file mode 100644
index 0000000..796fed3
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/h/__init__.py
@@ -0,0 +1 @@
+data = 'should not be visible abcdefgh'
diff --git a/test/integration/targets/module_utils/other_mu_dir/facts.py b/test/integration/targets/module_utils/other_mu_dir/facts.py
new file mode 100644
index 0000000..dbfab27
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/facts.py
@@ -0,0 +1 @@
+data = 'should not be visible facts.py'
diff --git a/test/integration/targets/module_utils/other_mu_dir/json_utils.py b/test/integration/targets/module_utils/other_mu_dir/json_utils.py
new file mode 100644
index 0000000..59757e4
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/json_utils.py
@@ -0,0 +1 @@
+data = 'overridden json_utils'
diff --git a/test/integration/targets/module_utils/other_mu_dir/mork.py b/test/integration/targets/module_utils/other_mu_dir/mork.py
new file mode 100644
index 0000000..3b700fc
--- /dev/null
+++ b/test/integration/targets/module_utils/other_mu_dir/mork.py
@@ -0,0 +1 @@
+data = 'mork'
diff --git a/test/integration/targets/module_utils/runme.sh b/test/integration/targets/module_utils/runme.sh
new file mode 100755
index 0000000..15f022b
--- /dev/null
+++ b/test/integration/targets/module_utils/runme.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_ROLES_PATH=../
+
+ansible-playbook module_utils_basic_setcwd.yml -i ../../inventory "$@"
+
+# Keep the -vvvvv here. This acts as a test for testing that higher verbosity
+# doesn't traceback with unicode in the custom module_utils directory path.
+ansible-playbook module_utils_vvvvv.yml -i ../../inventory -vvvvv "$@"
+
+ansible-playbook module_utils_test.yml -i ../../inventory -v "$@"
+
+ANSIBLE_MODULE_UTILS=other_mu_dir ansible-playbook module_utils_envvar.yml -i ../../inventory -v "$@"
+
+ansible-playbook module_utils_common_dict_transformation.yml -i ../../inventory "$@"
+
+ansible-playbook module_utils_common_network.yml -i ../../inventory "$@"
diff --git a/test/integration/targets/module_utils_Ansible.AccessToken/aliases b/test/integration/targets/module_utils_Ansible.AccessToken/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.AccessToken/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.AccessToken/library/ansible_access_token_tests.ps1 b/test/integration/targets/module_utils_Ansible.AccessToken/library/ansible_access_token_tests.ps1
new file mode 100644
index 0000000..a1de2b4
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.AccessToken/library/ansible_access_token_tests.ps1
@@ -0,0 +1,407 @@
+# End of the setup code and start of the module code
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.AccessToken
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ test_username = @{ type = "str"; required = $true }
+ test_password = @{ type = "str"; required = $true; no_log = $true }
+ }
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$test_username = $module.Params.test_username
+$test_password = $module.Params.test_password
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+$current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+
+$tests = [Ordered]@{
+ "Open process token" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Query")
+ try {
+ $h_token.IsClosed | Assert-Equal -Expected $false
+ $h_token.IsInvalid | Assert-Equal -Expected $false
+
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($h_token)
+ $actual_user | Assert-Equal -Expected $current_user
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ $h_token.IsClosed | Assert-Equal -Expected $true
+ }
+
+ "Open process token of another process" = {
+ $proc_info = Start-Process -FilePath "powershell.exe" -ArgumentList "-Command Start-Sleep -Seconds 60" -WindowStyle Hidden -PassThru
+ try {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess($proc_info.Id, "QueryInformation", $false)
+ try {
+ $h_process.IsClosed | Assert-Equal -Expected $false
+ $h_process.IsInvalid | Assert-Equal -Expected $false
+
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Query")
+ try {
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($h_token)
+ $actual_user | Assert-Equal -Expected $current_user
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+ finally {
+ $h_process.Dispose()
+ }
+ $h_process.IsClosed | Assert-Equal -Expected $true
+ }
+ finally {
+ $proc_info | Stop-Process
+ }
+ }
+
+ "Failed to open process token" = {
+ $failed = $false
+ try {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess(4, "QueryInformation", $false)
+ $h_process.Dispose() # Incase this doesn't fail, make sure we still dispose of it
+ }
+ catch [Ansible.AccessToken.Win32Exception] {
+ $failed = $true
+ $msg = "Failed to open process 4 with access QueryInformation (Access is denied, Win32ErrorCode 5 - 0x00000005)"
+ $_.Exception.Message | Assert-Equal -Expected $msg
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Duplicate access token primary" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Duplicate")
+ try {
+ $dup_token = [Ansible.AccessToken.TokenUtil]::DuplicateToken($h_token, "Query", "Anonymous", "Primary")
+ try {
+ $dup_token.IsClosed | Assert-Equal -Expected $false
+ $dup_token.IsInvalid | Assert-Equal -Expected $false
+
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($dup_token)
+
+ $actual_user | Assert-Equal -Expected $current_user
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($dup_token)
+
+ $actual_stat.TokenType | Assert-Equal -Expected ([Ansible.AccessToken.TokenType]::Primary)
+ $actual_stat.ImpersonationLevel | Assert-Equal -Expected ([Ansible.AccessToken.SecurityImpersonationLevel]::Anonymous)
+ }
+ finally {
+ $dup_token.Dispose()
+ }
+
+ $dup_token.IsClosed | Assert-Equal -Expected $true
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+
+ "Duplicate access token impersonation" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Duplicate")
+ try {
+ "Anonymous", "Identification", "Impersonation", "Delegation" | ForEach-Object -Process {
+ $dup_token = [Ansible.AccessToken.TokenUtil]::DuplicateToken($h_token, "Query", $_, "Impersonation")
+ try {
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($dup_token)
+
+ $actual_user | Assert-Equal -Expected $current_user
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($dup_token)
+
+ $actual_stat.TokenType | Assert-Equal -Expected ([Ansible.AccessToken.TokenType]::Impersonation)
+ $actual_stat.ImpersonationLevel | Assert-Equal -Expected ([Ansible.AccessToken.SecurityImpersonationLevel]"$_")
+ }
+ finally {
+ $dup_token.Dispose()
+ }
+ }
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+
+ "Impersonate SYSTEM token" = {
+ $system_sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @(
+ [System.Security.Principal.WellKnownSidType]::LocalSystemSid,
+ $null
+ )
+ $tested = $false
+ foreach ($h_token in [Ansible.AccessToken.TokenUtil]::EnumerateUserTokens($system_sid, "Duplicate, Impersonate, Query")) {
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($h_token)
+ $actual_user | Assert-Equal -Expected $system_sid
+
+ [Ansible.AccessToken.TokenUtil]::ImpersonateToken($h_token)
+ try {
+ $current_sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+ $current_sid | Assert-Equal -Expected $system_sid
+ }
+ finally {
+ [Ansible.AccessToken.TokenUtil]::RevertToSelf()
+ }
+
+ $current_sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+ $current_sid | Assert-Equal -Expected $current_user
+
+ # Will keep on looping for each SYSTEM token it can retrieve, we only want to test 1
+ $tested = $true
+ break
+ }
+
+ $tested | Assert-Equal -Expected $true
+ }
+
+ "Get token privileges" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Query")
+ try {
+ $priv_info = &whoami.exe /priv | Where-Object { $_.StartsWith("Se") }
+ $actual_privs = [Ansible.AccessToken.Tokenutil]::GetTokenPrivileges($h_token)
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($h_token)
+
+ $actual_privs.Count | Assert-Equal -Expected $priv_info.Count
+ $actual_privs.Count | Assert-Equal -Expected $actual_stat.PrivilegeCount
+
+ foreach ($info in $priv_info) {
+ $info_split = $info.Split(" ", [System.StringSplitOptions]::RemoveEmptyEntries)
+ $priv_name = $info_split[0]
+ $priv_enabled = $info_split[-1] -eq "Enabled"
+ $actual_priv = $actual_privs | Where-Object { $_.Name -eq $priv_name }
+
+ $actual_priv -eq $null | Assert-Equal -Expected $false
+ if ($priv_enabled) {
+ $actual_priv.Attributes.HasFlag([Ansible.AccessToken.PrivilegeAttributes]::Enabled) | Assert-Equal -Expected $true
+ }
+ else {
+ $actual_priv.Attributes.HasFlag([Ansible.AccessToken.PrivilegeAttributes]::Disabled) | Assert-Equal -Expected $true
+ }
+ }
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+
+ "Get token statistics" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Query")
+ try {
+ $actual_priv = [Ansible.AccessToken.Tokenutil]::GetTokenPrivileges($h_token)
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($h_token)
+
+ $actual_stat.TokenId.GetType().FullName | Assert-Equal -Expected "Ansible.AccessToken.Luid"
+ $actual_stat.AuthenticationId.GetType().FullName | Assert-Equal -Expected "Ansible.AccessToken.Luid"
+ $actual_stat.ExpirationTime.GetType().FullName | Assert-Equal -Expected "System.Int64"
+
+ $actual_stat.TokenType | Assert-Equal -Expected ([Ansible.AccessToken.TokenType]::Primary)
+
+ $os_version = [Version](Get-Item -LiteralPath $env:SystemRoot\System32\kernel32.dll).VersionInfo.ProductVersion
+ if ($os_version -lt [Version]"6.1") {
+ # While the token is a primary token, Server 2008 reports the SecurityImpersonationLevel for a primary token as Impersonation
+ $actual_stat.ImpersonationLevel | Assert-Equal -Expected ([Ansible.AccessToken.SecurityImpersonationLevel]::Impersonation)
+ }
+ else {
+ $actual_stat.ImpersonationLevel | Assert-Equal -Expected ([Ansible.AccessToken.SecurityImpersonationLevel]::Anonymous)
+ }
+ $actual_stat.DynamicCharged.GetType().FullName | Assert-Equal -Expected "System.UInt32"
+ $actual_stat.DynamicAvailable.GetType().FullName | Assert-Equal -Expected "System.UInt32"
+ $actual_stat.GroupCount.GetType().FullName | Assert-Equal -Expected "System.UInt32"
+ $actual_stat.PrivilegeCount | Assert-Equal -Expected $actual_priv.Count
+ $actual_stat.ModifiedId.GetType().FullName | Assert-Equal -Expected "Ansible.AccessToken.Luid"
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+
+ "Get token linked token impersonation" = {
+ $h_token = [Ansible.AccessToken.TokenUtil]::LogonUser($test_username, $null, $test_password, "Interactive", "Default")
+ try {
+ $actual_elevation_type = [Ansible.AccessToken.TokenUtil]::GetTokenElevationType($h_token)
+ $actual_elevation_type | Assert-Equal -Expected ([Ansible.AccessToken.TokenElevationType]::Limited)
+
+ $actual_linked = [Ansible.AccessToken.TokenUtil]::GetTokenLinkedToken($h_token)
+ try {
+ $actual_linked.IsClosed | Assert-Equal -Expected $false
+ $actual_linked.IsInvalid | Assert-Equal -Expected $false
+
+ $actual_elevation_type = [Ansible.AccessToken.TokenUtil]::GetTokenElevationType($actual_linked)
+ $actual_elevation_type | Assert-Equal -Expected ([Ansible.AccessToken.TokenElevationType]::Full)
+
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($actual_linked)
+ $actual_stat.TokenType | Assert-Equal -Expected ([Ansible.AccessToken.TokenType]::Impersonation)
+ }
+ finally {
+ $actual_linked.Dispose()
+ }
+ $actual_linked.IsClosed | Assert-Equal -Expected $true
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ }
+
+ "Get token linked token primary" = {
+ # We need a token with the SeTcbPrivilege for this to work.
+ $system_sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @(
+ [System.Security.Principal.WellKnownSidType]::LocalSystemSid,
+ $null
+ )
+ $tested = $false
+ foreach ($system_token in [Ansible.AccessToken.TokenUtil]::EnumerateUserTokens($system_sid, "Duplicate, Impersonate, Query")) {
+ $privileges = [Ansible.AccessToken.TokenUtil]::GetTokenPrivileges($system_token)
+ if ($null -eq ($privileges | Where-Object { $_.Name -eq "SeTcbPrivilege" })) {
+ continue
+ }
+
+ $h_token = [Ansible.AccessToken.TokenUtil]::LogonUser($test_username, $null, $test_password, "Interactive", "Default")
+ try {
+ [Ansible.AccessToken.TokenUtil]::ImpersonateToken($system_token)
+ try {
+ $actual_linked = [Ansible.AccessToken.TokenUtil]::GetTokenLinkedToken($h_token)
+ try {
+ $actual_linked.IsClosed | Assert-Equal -Expected $false
+ $actual_linked.IsInvalid | Assert-Equal -Expected $false
+
+ $actual_elevation_type = [Ansible.AccessToken.TokenUtil]::GetTokenElevationType($actual_linked)
+ $actual_elevation_type | Assert-Equal -Expected ([Ansible.AccessToken.TokenElevationType]::Full)
+
+ $actual_stat = [Ansible.AccessToken.TokenUtil]::GetTokenStatistics($actual_linked)
+ $actual_stat.TokenType | Assert-Equal -Expected ([Ansible.AccessToken.TokenType]::Primary)
+ }
+ finally {
+ $actual_linked.Dispose()
+ }
+ $actual_linked.IsClosed | Assert-Equal -Expected $true
+ }
+ finally {
+ [Ansible.AccessToken.TokenUtil]::RevertToSelf()
+ }
+ }
+ finally {
+ $h_token.Dispose()
+ }
+
+ $tested = $true
+ break
+ }
+ $tested | Assert-Equal -Expected $true
+ }
+
+ "Failed to get token information" = {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, 'Duplicate') # Without Query the below will fail
+
+ $failed = $false
+ try {
+ [Ansible.AccessToken.TokenUtil]::GetTokenUser($h_token)
+ }
+ catch [Ansible.AccessToken.Win32Exception] {
+ $failed = $true
+ $msg = "GetTokenInformation(TokenUser) failed to get buffer length (Access is denied, Win32ErrorCode 5 - 0x00000005)"
+ $_.Exception.Message | Assert-Equal -Expected $msg
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Logon with valid credentials" = {
+ $expected_user = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $test_username
+ $expected_sid = $expected_user.Translate([System.Security.Principal.SecurityIdentifier])
+
+ $h_token = [Ansible.AccessToken.TokenUtil]::LogonUser($test_username, $null, $test_password, "Network", "Default")
+ try {
+ $h_token.IsClosed | Assert-Equal -Expected $false
+ $h_token.IsInvalid | Assert-Equal -Expected $false
+
+ $actual_user = [Ansible.AccessToken.TokenUtil]::GetTokenUser($h_token)
+ $actual_user | Assert-Equal -Expected $expected_sid
+ }
+ finally {
+ $h_token.Dispose()
+ }
+ $h_token.IsClosed | Assert-Equal -Expected $true
+ }
+
+ "Logon with invalid credentials" = {
+ $failed = $false
+ try {
+ [Ansible.AccessToken.TokenUtil]::LogonUser("fake-user", $null, "fake-pass", "Network", "Default")
+ }
+ catch [Ansible.AccessToken.Win32Exception] {
+ $failed = $true
+ $_.Exception.Message.Contains("Failed to logon fake-user") | Assert-Equal -Expected $true
+ $_.Exception.Message.Contains("Win32ErrorCode 1326 - 0x0000052E)") | Assert-Equal -Expected $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Logon with invalid credential with domain account" = {
+ $failed = $false
+ try {
+ [Ansible.AccessToken.TokenUtil]::LogonUser("fake-user", "fake-domain", "fake-pass", "Network", "Default")
+ }
+ catch [Ansible.AccessToken.Win32Exception] {
+ $failed = $true
+ $_.Exception.Message.Contains("Failed to logon fake-domain\fake-user") | Assert-Equal -Expected $true
+ $_.Exception.Message.Contains("Win32ErrorCode 1326 - 0x0000052E)") | Assert-Equal -Expected $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+}
+
+foreach ($test_impl in $tests.GetEnumerator()) {
+ $test = $test_impl.Key
+ &$test_impl.Value
+}
+
+$module.Result.data = "success"
+$module.ExitJson()
diff --git a/test/integration/targets/module_utils_Ansible.AccessToken/tasks/main.yml b/test/integration/targets/module_utils_Ansible.AccessToken/tasks/main.yml
new file mode 100644
index 0000000..dbd64b0
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.AccessToken/tasks/main.yml
@@ -0,0 +1,29 @@
+---
+- set_fact:
+ test_username: ansible-test
+ test_password: Password123{{ lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}
+
+- name: create test Admin user
+ win_user:
+ name: '{{ test_username }}'
+ password: '{{ test_password }}'
+ state: present
+ groups:
+ - Administrators
+
+- block:
+ - name: test Ansible.AccessToken.cs
+ ansible_access_token_tests:
+ test_username: '{{ test_username }}'
+ test_password: '{{ test_password }}'
+ register: ansible_access_token_test
+
+ - name: assert test Ansible.AccessToken.cs
+ assert:
+ that:
+ - ansible_access_token_test.data == "success"
+ always:
+ - name: remove test Admin user
+ win_user:
+ name: '{{ test_username }}'
+ state: absent
diff --git a/test/integration/targets/module_utils_Ansible.Basic/aliases b/test/integration/targets/module_utils_Ansible.Basic/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Basic/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 b/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1
new file mode 100644
index 0000000..cfa73c6
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1
@@ -0,0 +1,3206 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.failed = $true
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.Result.msg = "AssertionError: actual != expected"
+
+ Exit-Module
+ }
+ }
+}
+
+Function Assert-DictionaryEqual {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $actual_keys = $Actual.Keys
+ $expected_keys = $Expected.Keys
+
+ $actual_keys.Count | Assert-Equal -Expected $expected_keys.Count
+ foreach ($actual_entry in $Actual.GetEnumerator()) {
+ $actual_key = $actual_entry.Key
+ ($actual_key -cin $expected_keys) | Assert-Equal -Expected $true
+ $actual_value = $actual_entry.Value
+ $expected_value = $Expected.$actual_key
+
+ if ($actual_value -is [System.Collections.IDictionary]) {
+ $actual_value | Assert-DictionaryEqual -Expected $expected_value
+ }
+ elseif ($actual_value -is [System.Collections.ArrayList] -or $actual_value -is [Array]) {
+ for ($i = 0; $i -lt $actual_value.Count; $i++) {
+ $actual_entry = $actual_value[$i]
+ $expected_entry = $expected_value[$i]
+ if ($actual_entry -is [System.Collections.IDictionary]) {
+ $actual_entry | Assert-DictionaryEqual -Expected $expected_entry
+ }
+ else {
+ Assert-Equal -Actual $actual_entry -Expected $expected_entry
+ }
+ }
+ }
+ else {
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ }
+ foreach ($expected_key in $expected_keys) {
+ ($expected_key -cin $actual_keys) | Assert-Equal -Expected $true
+ }
+ }
+}
+
+Function Exit-Module {
+ # Make sure Exit actually calls exit and not our overriden test behaviour
+ [Ansible.Basic.AnsibleModule]::Exit = { param([Int32]$rc) exit $rc }
+ Write-Output -InputObject (ConvertTo-Json -InputObject $module.Result -Compress -Depth 99)
+ $module.ExitJson()
+}
+
+$tmpdir = $module.Tmpdir
+
+# Override the Exit and WriteLine behaviour to throw an exception instead of exiting the module
+[Ansible.Basic.AnsibleModule]::Exit = {
+ param([Int32]$rc)
+ $exp = New-Object -TypeName System.Exception -ArgumentList "exit: $rc"
+ $exp | Add-Member -Type NoteProperty -Name Output -Value $_test_out
+ throw $exp
+}
+[Ansible.Basic.AnsibleModule]::WriteLine = {
+ param([String]$line)
+ Set-Variable -Name _test_out -Scope Global -Value $line
+}
+
+$tests = @{
+ "Empty spec and no options - args file" = {
+ $args_file = Join-Path -Path $tmpdir -ChildPath "args-$(Get-Random).json"
+ [System.IO.File]::WriteAllText($args_file, '{ "ANSIBLE_MODULE_ARGS": {} }')
+ $m = [Ansible.Basic.AnsibleModule]::Create(@($args_file), @{})
+
+ $m.CheckMode | Assert-Equal -Expected $false
+ $m.DebugMode | Assert-Equal -Expected $false
+ $m.DiffMode | Assert-Equal -Expected $false
+ $m.KeepRemoteFiles | Assert-Equal -Expected $false
+ $m.ModuleName | Assert-Equal -Expected "undefined win module"
+ $m.NoLog | Assert-Equal -Expected $false
+ $m.Verbosity | Assert-Equal -Expected 0
+ $m.AnsibleVersion | Assert-Equal -Expected $null
+ }
+
+ "Empty spec and no options - complex_args" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{}
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ $m.CheckMode | Assert-Equal -Expected $false
+ $m.DebugMode | Assert-Equal -Expected $false
+ $m.DiffMode | Assert-Equal -Expected $false
+ $m.KeepRemoteFiles | Assert-Equal -Expected $false
+ $m.ModuleName | Assert-Equal -Expected "undefined win module"
+ $m.NoLog | Assert-Equal -Expected $false
+ $m.Verbosity | Assert-Equal -Expected 0
+ $m.AnsibleVersion | Assert-Equal -Expected $null
+ }
+
+ "Internal param changes - args file" = {
+ $m_tmpdir = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
+ New-Item -Path $m_tmpdir -ItemType Directory > $null
+ $args_file = Join-Path -Path $tmpdir -ChildPath "args-$(Get-Random).json"
+ [System.IO.File]::WriteAllText($args_file, @"
+{
+ "ANSIBLE_MODULE_ARGS": {
+ "_ansible_check_mode": true,
+ "_ansible_debug": true,
+ "_ansible_diff": true,
+ "_ansible_keep_remote_files": true,
+ "_ansible_module_name": "ansible_basic_tests",
+ "_ansible_no_log": true,
+ "_ansible_remote_tmp": "%TEMP%",
+ "_ansible_selinux_special_fs": "ignored",
+ "_ansible_shell_executable": "ignored",
+ "_ansible_socket": "ignored",
+ "_ansible_syslog_facility": "ignored",
+ "_ansible_tmpdir": "$($m_tmpdir -replace "\\", "\\")",
+ "_ansible_verbosity": 3,
+ "_ansible_version": "2.8.0"
+ }
+}
+"@)
+ $m = [Ansible.Basic.AnsibleModule]::Create(@($args_file), @{supports_check_mode = $true })
+ $m.CheckMode | Assert-Equal -Expected $true
+ $m.DebugMode | Assert-Equal -Expected $true
+ $m.DiffMode | Assert-Equal -Expected $true
+ $m.KeepRemoteFiles | Assert-Equal -Expected $true
+ $m.ModuleName | Assert-Equal -Expected "ansible_basic_tests"
+ $m.NoLog | Assert-Equal -Expected $true
+ $m.Verbosity | Assert-Equal -Expected 3
+ $m.AnsibleVersion | Assert-Equal -Expected "2.8.0"
+ $m.Tmpdir | Assert-Equal -Expected $m_tmpdir
+ }
+
+ "Internal param changes - complex_args" = {
+ $m_tmpdir = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
+ New-Item -Path $m_tmpdir -ItemType Directory > $null
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_check_mode = $true
+ _ansible_debug = $true
+ _ansible_diff = $true
+ _ansible_keep_remote_files = $true
+ _ansible_module_name = "ansible_basic_tests"
+ _ansible_no_log = $true
+ _ansible_remote_tmp = "%TEMP%"
+ _ansible_selinux_special_fs = "ignored"
+ _ansible_shell_executable = "ignored"
+ _ansible_socket = "ignored"
+ _ansible_syslog_facility = "ignored"
+ _ansible_tmpdir = $m_tmpdir.ToString()
+ _ansible_verbosity = 3
+ _ansible_version = "2.8.0"
+ }
+ $spec = @{
+ supports_check_mode = $true
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $m.CheckMode | Assert-Equal -Expected $true
+ $m.DebugMode | Assert-Equal -Expected $true
+ $m.DiffMode | Assert-Equal -Expected $true
+ $m.KeepRemoteFiles | Assert-Equal -Expected $true
+ $m.ModuleName | Assert-Equal -Expected "ansible_basic_tests"
+ $m.NoLog | Assert-Equal -Expected $true
+ $m.Verbosity | Assert-Equal -Expected 3
+ $m.AnsibleVersion | Assert-Equal -Expected "2.8.0"
+ $m.Tmpdir | Assert-Equal -Expected $m_tmpdir
+ }
+
+ "Parse complex module options" = {
+ $spec = @{
+ options = @{
+ option_default = @{}
+ missing_option_default = @{}
+ string_option = @{type = "str" }
+ required_option = @{required = $true }
+ missing_choices = @{choices = "a", "b" }
+ choices = @{choices = "a", "b" }
+ one_choice = @{choices = , "b" }
+ choice_with_default = @{choices = "a", "b"; default = "b" }
+ alias_direct = @{aliases = , "alias_direct1" }
+ alias_as_alias = @{aliases = "alias_as_alias1", "alias_as_alias2" }
+ bool_type = @{type = "bool" }
+ bool_from_str = @{type = "bool" }
+ dict_type = @{
+ type = "dict"
+ options = @{
+ int_type = @{type = "int" }
+ str_type = @{type = "str"; default = "str_sub_type" }
+ }
+ }
+ dict_type_missing = @{
+ type = "dict"
+ options = @{
+ int_type = @{type = "int" }
+ str_type = @{type = "str"; default = "str_sub_type" }
+ }
+ }
+ dict_type_defaults = @{
+ type = "dict"
+ apply_defaults = $true
+ options = @{
+ int_type = @{type = "int" }
+ str_type = @{type = "str"; default = "str_sub_type" }
+ }
+ }
+ dict_type_json = @{type = "dict" }
+ dict_type_str = @{type = "dict" }
+ float_type = @{type = "float" }
+ int_type = @{type = "int" }
+ json_type = @{type = "json" }
+ json_type_dict = @{type = "json" }
+ list_type = @{type = "list" }
+ list_type_str = @{type = "list" }
+ list_with_int = @{type = "list"; elements = "int" }
+ list_type_single = @{type = "list" }
+ list_with_dict = @{
+ type = "list"
+ elements = "dict"
+ options = @{
+ int_type = @{type = "int" }
+ str_type = @{type = "str"; default = "str_sub_type" }
+ }
+ }
+ path_type = @{type = "path" }
+ path_type_nt = @{type = "path" }
+ path_type_missing = @{type = "path" }
+ raw_type_str = @{type = "raw" }
+ raw_type_int = @{type = "raw" }
+ sid_type = @{type = "sid" }
+ sid_from_name = @{type = "sid" }
+ str_type = @{type = "str" }
+ delegate_type = @{type = [Func[[Object], [UInt64]]] { [System.UInt64]::Parse($args[0]) } }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_default = 1
+ string_option = 1
+ required_option = "required"
+ choices = "a"
+ one_choice = "b"
+ alias_direct = "a"
+ alias_as_alias2 = "a"
+ bool_type = $true
+ bool_from_str = "false"
+ dict_type = @{
+ int_type = "10"
+ }
+ dict_type_json = '{"a":"a","b":1,"c":["a","b"]}'
+ dict_type_str = 'a=a b="b 2" c=c'
+ float_type = "3.14159"
+ int_type = 0
+ json_type = '{"a":"a","b":1,"c":["a","b"]}'
+ json_type_dict = @{
+ a = "a"
+ b = 1
+ c = @("a", "b")
+ }
+ list_type = @("a", "b", 1, 2)
+ list_type_str = "a, b,1,2 "
+ list_with_int = @("1", 2)
+ list_type_single = "single"
+ list_with_dict = @(
+ @{
+ int_type = 2
+ str_type = "dict entry"
+ },
+ @{ int_type = 1 },
+ @{}
+ )
+ path_type = "%SystemRoot%\System32"
+ path_type_nt = "\\?\%SystemRoot%\System32"
+ path_type_missing = "T:\missing\path"
+ raw_type_str = "str"
+ raw_type_int = 1
+ sid_type = "S-1-5-18"
+ sid_from_name = "SYSTEM"
+ str_type = "str"
+ delegate_type = "1234"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $m.Params.option_default | Assert-Equal -Expected "1"
+ $m.Params.option_default.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.missing_option_default | Assert-Equal -Expected $null
+ $m.Params.string_option | Assert-Equal -Expected "1"
+ $m.Params.string_option.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.required_option | Assert-Equal -Expected "required"
+ $m.Params.required_option.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.missing_choices | Assert-Equal -Expected $null
+ $m.Params.choices | Assert-Equal -Expected "a"
+ $m.Params.choices.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.one_choice | Assert-Equal -Expected "b"
+ $m.Params.one_choice.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.choice_with_default | Assert-Equal -Expected "b"
+ $m.Params.choice_with_default.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.alias_direct | Assert-Equal -Expected "a"
+ $m.Params.alias_direct.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.alias_as_alias | Assert-Equal -Expected "a"
+ $m.Params.alias_as_alias.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.bool_type | Assert-Equal -Expected $true
+ $m.Params.bool_type.GetType().ToString() | Assert-Equal -Expected "System.Boolean"
+ $m.Params.bool_from_str | Assert-Equal -Expected $false
+ $m.Params.bool_from_str.GetType().ToString() | Assert-Equal -Expected "System.Boolean"
+ $m.Params.dict_type | Assert-DictionaryEqual -Expected @{int_type = 10; str_type = "str_sub_type" }
+ $m.Params.dict_type.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
+ $m.Params.dict_type.int_type.GetType().ToString() | Assert-Equal -Expected "System.Int32"
+ $m.Params.dict_type.str_type.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.dict_type_missing | Assert-Equal -Expected $null
+ $m.Params.dict_type_defaults | Assert-DictionaryEqual -Expected @{int_type = $null; str_type = "str_sub_type" }
+ $m.Params.dict_type_defaults.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
+ $m.Params.dict_type_defaults.str_type.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.dict_type_json | Assert-DictionaryEqual -Expected @{
+ a = "a"
+ b = 1
+ c = @("a", "b")
+ }
+ $m.Params.dict_type_json.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
+ $m.Params.dict_type_json.a.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.dict_type_json.b.GetType().ToString() | Assert-Equal -Expected "System.Int32"
+ $m.Params.dict_type_json.c.GetType().ToString() | Assert-Equal -Expected "System.Collections.ArrayList"
+ $m.Params.dict_type_str | Assert-DictionaryEqual -Expected @{a = "a"; b = "b 2"; c = "c" }
+ $m.Params.dict_type_str.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
+ $m.Params.dict_type_str.a.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.dict_type_str.b.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.dict_type_str.c.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.float_type | Assert-Equal -Expected ([System.Single]3.14159)
+ $m.Params.float_type.GetType().ToString() | Assert-Equal -Expected "System.Single"
+ $m.Params.int_type | Assert-Equal -Expected 0
+ $m.Params.int_type.GetType().ToString() | Assert-Equal -Expected "System.Int32"
+ $m.Params.json_type | Assert-Equal -Expected '{"a":"a","b":1,"c":["a","b"]}'
+ $m.Params.json_type.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $jsonValue = ([Ansible.Basic.AnsibleModule]::FromJson('{"a":"a","b":1,"c":["a","b"]}'))
+ [Ansible.Basic.AnsibleModule]::FromJson($m.Params.json_type_dict) | Assert-DictionaryEqual -Expected $jsonValue
+ $m.Params.json_type_dict.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.list_type.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.List``1[System.Object]"
+ $m.Params.list_type.Count | Assert-Equal -Expected 4
+ $m.Params.list_type[0] | Assert-Equal -Expected "a"
+ $m.Params.list_type[0].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_type[1] | Assert-Equal -Expected "b"
+ $m.Params.list_type[1].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_type[2] | Assert-Equal -Expected 1
+ $m.Params.list_type[2].GetType().FullName | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_type[3] | Assert-Equal -Expected 2
+ $m.Params.list_type[3].GetType().FullName | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_type_str.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.List``1[System.Object]"
+ $m.Params.list_type_str.Count | Assert-Equal -Expected 4
+ $m.Params.list_type_str[0] | Assert-Equal -Expected "a"
+ $m.Params.list_type_str[0].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_type_str[1] | Assert-Equal -Expected "b"
+ $m.Params.list_type_str[1].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_type_str[2] | Assert-Equal -Expected "1"
+ $m.Params.list_type_str[2].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_type_str[3] | Assert-Equal -Expected "2"
+ $m.Params.list_type_str[3].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_with_int.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.List``1[System.Object]"
+ $m.Params.list_with_int.Count | Assert-Equal -Expected 2
+ $m.Params.list_with_int[0] | Assert-Equal -Expected 1
+ $m.Params.list_with_int[0].GetType().FullName | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_with_int[1] | Assert-Equal -Expected 2
+ $m.Params.list_with_int[1].GetType().FullName | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_type_single.GetType().ToString() | Assert-Equal -Expected "System.Collections.Generic.List``1[System.Object]"
+ $m.Params.list_type_single.Count | Assert-Equal -Expected 1
+ $m.Params.list_type_single[0] | Assert-Equal -Expected "single"
+ $m.Params.list_type_single[0].GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.list_with_dict.GetType().FullName.StartsWith("System.Collections.Generic.List``1[[System.Object") | Assert-Equal -Expected $true
+ $m.Params.list_with_dict.Count | Assert-Equal -Expected 3
+ $m.Params.list_with_dict[0].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equal -Expected $true
+ $m.Params.list_with_dict[0] | Assert-DictionaryEqual -Expected @{int_type = 2; str_type = "dict entry" }
+ $m.Params.list_with_dict[0].int_type.GetType().FullName.ToString() | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_with_dict[0].str_type.GetType().FullName.ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.list_with_dict[1].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equal -Expected $true
+ $m.Params.list_with_dict[1] | Assert-DictionaryEqual -Expected @{int_type = 1; str_type = "str_sub_type" }
+ $m.Params.list_with_dict[1].int_type.GetType().FullName.ToString() | Assert-Equal -Expected "System.Int32"
+ $m.Params.list_with_dict[1].str_type.GetType().FullName.ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.list_with_dict[2].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equal -Expected $true
+ $m.Params.list_with_dict[2] | Assert-DictionaryEqual -Expected @{int_type = $null; str_type = "str_sub_type" }
+ $m.Params.list_with_dict[2].str_type.GetType().FullName.ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.path_type | Assert-Equal -Expected "$($env:SystemRoot)\System32"
+ $m.Params.path_type.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.path_type_nt | Assert-Equal -Expected "\\?\%SystemRoot%\System32"
+ $m.Params.path_type_nt.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.path_type_missing | Assert-Equal -Expected "T:\missing\path"
+ $m.Params.path_type_missing.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.raw_type_str | Assert-Equal -Expected "str"
+ $m.Params.raw_type_str.GetType().FullName | Assert-Equal -Expected "System.String"
+ $m.Params.raw_type_int | Assert-Equal -Expected 1
+ $m.Params.raw_type_int.GetType().FullName | Assert-Equal -Expected "System.Int32"
+ $m.Params.sid_type | Assert-Equal -Expected (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList "S-1-5-18")
+ $m.Params.sid_type.GetType().ToString() | Assert-Equal -Expected "System.Security.Principal.SecurityIdentifier"
+ $m.Params.sid_from_name | Assert-Equal -Expected (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList "S-1-5-18")
+ $m.Params.sid_from_name.GetType().ToString() | Assert-Equal -Expected "System.Security.Principal.SecurityIdentifier"
+ $m.Params.str_type | Assert-Equal -Expected "str"
+ $m.Params.str_type.GetType().ToString() | Assert-Equal -Expected "System.String"
+ $m.Params.delegate_type | Assert-Equal -Expected 1234
+ $m.Params.delegate_type.GetType().ToString() | Assert-Equal -Expected "System.UInt64"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_module_args = @{
+ option_default = "1"
+ missing_option_default = $null
+ string_option = "1"
+ required_option = "required"
+ missing_choices = $null
+ choices = "a"
+ one_choice = "b"
+ choice_with_default = "b"
+ alias_direct = "a"
+ alias_as_alias = "a"
+ alias_as_alias2 = "a"
+ bool_type = $true
+ bool_from_str = $false
+ dict_type = @{
+ int_type = 10
+ str_type = "str_sub_type"
+ }
+ dict_type_missing = $null
+ dict_type_defaults = @{
+ int_type = $null
+ str_type = "str_sub_type"
+ }
+ dict_type_json = @{
+ a = "a"
+ b = 1
+ c = @("a", "b")
+ }
+ dict_type_str = @{
+ a = "a"
+ b = "b 2"
+ c = "c"
+ }
+ float_type = 3.14159
+ int_type = 0
+ json_type = $m.Params.json_type.ToString()
+ json_type_dict = $m.Params.json_type_dict.ToString()
+ list_type = @("a", "b", 1, 2)
+ list_type_str = @("a", "b", "1", "2")
+ list_with_int = @(1, 2)
+ list_type_single = @("single")
+ list_with_dict = @(
+ @{
+ int_type = 2
+ str_type = "dict entry"
+ },
+ @{
+ int_type = 1
+ str_type = "str_sub_type"
+ },
+ @{
+ int_type = $null
+ str_type = "str_sub_type"
+ }
+ )
+ path_type = "$($env:SystemRoot)\System32"
+ path_type_nt = "\\?\%SystemRoot%\System32"
+ path_type_missing = "T:\missing\path"
+ raw_type_str = "str"
+ raw_type_int = 1
+ sid_type = "S-1-5-18"
+ sid_from_name = "S-1-5-18"
+ str_type = "str"
+ delegate_type = 1234
+ }
+ $actual.Keys.Count | Assert-Equal -Expected 2
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $expected_module_args }
+ }
+
+ "Parse module args with list elements and delegate type" = {
+ $spec = @{
+ options = @{
+ list_delegate_type = @{
+ type = "list"
+ elements = [Func[[Object], [UInt16]]] { [System.UInt16]::Parse($args[0]) }
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ list_delegate_type = @(
+ "1234",
+ 4321
+ )
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $m.Params.list_delegate_type.GetType().Name | Assert-Equal -Expected 'List`1'
+ $m.Params.list_delegate_type[0].GetType().FullName | Assert-Equal -Expected "System.UInt16"
+ $m.Params.list_delegate_Type[1].GetType().FullName | Assert-Equal -Expected "System.UInt16"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_module_args = @{
+ list_delegate_type = @(
+ 1234,
+ 4321
+ )
+ }
+ $actual.Keys.Count | Assert-Equal -Expected 2
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $expected_module_args }
+ }
+
+ "Parse module args with case insensitive input" = {
+ $spec = @{
+ options = @{
+ option1 = @{ type = "int"; required = $true }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_module_name = "win_test"
+ Option1 = "1"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ # Verifies the case of the params key is set to the module spec not actual input
+ $m.Params.Keys | Assert-Equal -Expected @("option1")
+ $m.Params.option1 | Assert-Equal -Expected 1
+
+ # Verifies the type conversion happens even on a case insensitive match
+ $m.Params.option1.GetType().FullName | Assert-Equal -Expected "System.Int32"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_warnings = "Parameters for (win_test) was a case insensitive match: Option1. "
+ $expected_warnings += "Module options will become case sensitive in a future Ansible release. "
+ $expected_warnings += "Supported parameters include: option1"
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ option1 = 1
+ }
+ }
+ # We have disabled the warning for now
+ #warnings = @($expected_warnings)
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "No log values" = {
+ $spec = @{
+ options = @{
+ username = @{type = "str" }
+ password = @{type = "str"; no_log = $true }
+ password2 = @{type = "int"; no_log = $true }
+ dict = @{type = "dict" }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_module_name = "test_no_log"
+ username = "user - pass - name"
+ password = "pass"
+ password2 = 1234
+ dict = @{
+ data = "Oops this is secret: pass"
+ dict = @{
+ pass = "plain"
+ hide = "pass"
+ sub_hide = "password"
+ int_hide = 123456
+ }
+ list = @(
+ "pass",
+ "password",
+ 1234567,
+ "pa ss",
+ @{
+ pass = "plain"
+ hide = "pass"
+ sub_hide = "password"
+ int_hide = 123456
+ }
+ )
+ custom = "pass"
+ }
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $m.Result.data = $complex_args.dict
+
+ # verify params internally aren't masked
+ $m.Params.username | Assert-Equal -Expected "user - pass - name"
+ $m.Params.password | Assert-Equal -Expected "pass"
+ $m.Params.password2 | Assert-Equal -Expected 1234
+ $m.Params.dict.custom | Assert-Equal -Expected "pass"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ # verify no_log params are masked in invocation
+ $expected = @{
+ invocation = @{
+ module_args = @{
+ password2 = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ dict = @{
+ dict = @{
+ pass = "plain"
+ hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ sub_hide = "********word"
+ int_hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ }
+ custom = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ list = @(
+ "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
+ "********word",
+ "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
+ "pa ss",
+ @{
+ pass = "plain"
+ hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ sub_hide = "********word"
+ int_hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ }
+ )
+ data = "Oops this is secret: ********"
+ }
+ username = "user - ******** - name"
+ password = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ }
+ }
+ changed = $false
+ data = $complex_args.dict
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+
+ $expected_event = @'
+test_no_log - Invoked with:
+ username: user - ******** - name
+ dict: dict: sub_hide: ****word
+ pass: plain
+ int_hide: ********56
+ hide: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ data: Oops this is secret: ********
+ custom: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ list:
+ - VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ - ********word
+ - ********567
+ - pa ss
+ - sub_hide: ********word
+ pass: plain
+ int_hide: ********56
+ hide: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ password2: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ password: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+'@
+ $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message
+ $actual_event | Assert-DictionaryEqual -Expected $expected_event
+ }
+
+ "No log value with an empty string" = {
+ $spec = @{
+ options = @{
+ password1 = @{type = "str"; no_log = $true }
+ password2 = @{type = "str"; no_log = $true }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_module_name = "test_no_log"
+ password1 = ""
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $m.Result.data = $complex_args.dict
+
+ # verify params internally aren't masked
+ $m.Params.password1 | Assert-Equal -Expected ""
+ $m.Params.password2 | Assert-Equal -Expected $null
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ invocation = @{
+ module_args = @{
+ password1 = ""
+ password2 = $null
+ }
+ }
+ changed = $false
+ data = $complex_args.dict
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Removed in version" = {
+ $spec = @{
+ options = @{
+ removed1 = @{removed_in_version = "2.1" }
+ removed2 = @{removed_in_version = "2.2" }
+ removed3 = @{removed_in_version = "2.3"; removed_from_collection = "ansible.builtin" }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ removed1 = "value"
+ removed3 = "value"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ removed1 = "value"
+ removed2 = $null
+ removed3 = "value"
+ }
+ }
+ deprecations = @(
+ @{
+ msg = "Param 'removed3' is deprecated. See the module docs for more information"
+ version = "2.3"
+ collection_name = "ansible.builtin"
+ },
+ @{
+ msg = "Param 'removed1' is deprecated. See the module docs for more information"
+ version = "2.1"
+ collection_name = $null
+ }
+ )
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Removed at date" = {
+ $spec = @{
+ options = @{
+ removed1 = @{removed_at_date = [DateTime]"2020-03-10" }
+ removed2 = @{removed_at_date = [DateTime]"2020-03-11" }
+ removed3 = @{removed_at_date = [DateTime]"2020-06-07"; removed_from_collection = "ansible.builtin" }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ removed1 = "value"
+ removed3 = "value"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ removed1 = "value"
+ removed2 = $null
+ removed3 = "value"
+ }
+ }
+ deprecations = @(
+ @{
+ msg = "Param 'removed3' is deprecated. See the module docs for more information"
+ date = "2020-06-07"
+ collection_name = "ansible.builtin"
+ },
+ @{
+ msg = "Param 'removed1' is deprecated. See the module docs for more information"
+ date = "2020-03-10"
+ collection_name = $null
+ }
+ )
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Deprecated aliases" = {
+ $spec = @{
+ options = @{
+ option1 = @{ type = "str"; aliases = "alias1"; deprecated_aliases = @(@{name = "alias1"; version = "2.10" }) }
+ option2 = @{ type = "str"; aliases = "alias2"; deprecated_aliases = @(@{name = "alias2"; version = "2.11" }) }
+ option3 = @{
+ type = "dict"
+ options = @{
+ option1 = @{ type = "str"; aliases = "alias1"; deprecated_aliases = @(@{name = "alias1"; version = "2.10" }) }
+ option2 = @{ type = "str"; aliases = "alias2"; deprecated_aliases = @(@{name = "alias2"; version = "2.11" }) }
+ option3 = @{
+ type = "str"
+ aliases = "alias3"
+ deprecated_aliases = @(
+ @{name = "alias3"; version = "2.12"; collection_name = "ansible.builtin" }
+ )
+ }
+ option4 = @{ type = "str"; aliases = "alias4"; deprecated_aliases = @(@{name = "alias4"; date = [DateTime]"2020-03-11" }) }
+ option5 = @{ type = "str"; aliases = "alias5"; deprecated_aliases = @(@{name = "alias5"; date = [DateTime]"2020-03-09" }) }
+ option6 = @{
+ type = "str"
+ aliases = "alias6"
+ deprecated_aliases = @(
+ @{name = "alias6"; date = [DateTime]"2020-06-01"; collection_name = "ansible.builtin" }
+ )
+ }
+ }
+ }
+ option4 = @{ type = "str"; aliases = "alias4"; deprecated_aliases = @(@{name = "alias4"; date = [DateTime]"2020-03-10" }) }
+ option5 = @{ type = "str"; aliases = "alias5"; deprecated_aliases = @(@{name = "alias5"; date = [DateTime]"2020-03-12" }) }
+ option6 = @{
+ type = "str"
+ aliases = "alias6"
+ deprecated_aliases = @(
+ @{name = "alias6"; version = "2.12"; collection_name = "ansible.builtin" }
+ )
+ }
+ option7 = @{
+ type = "str"
+ aliases = "alias7"
+ deprecated_aliases = @(
+ @{name = "alias7"; date = [DateTime]"2020-06-07"; collection_name = "ansible.builtin" }
+ )
+ }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ alias1 = "alias1"
+ option2 = "option2"
+ option3 = @{
+ option1 = "option1"
+ alias2 = "alias2"
+ alias3 = "alias3"
+ option4 = "option4"
+ alias5 = "alias5"
+ alias6 = "alias6"
+ }
+ option4 = "option4"
+ alias5 = "alias5"
+ alias6 = "alias6"
+ alias7 = "alias7"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ alias1 = "alias1"
+ option1 = "alias1"
+ option2 = "option2"
+ option3 = @{
+ option1 = "option1"
+ option2 = "alias2"
+ alias2 = "alias2"
+ option3 = "alias3"
+ alias3 = "alias3"
+ option4 = "option4"
+ option5 = "alias5"
+ alias5 = "alias5"
+ option6 = "alias6"
+ alias6 = "alias6"
+ }
+ option4 = "option4"
+ option5 = "alias5"
+ alias5 = "alias5"
+ option6 = "alias6"
+ alias6 = "alias6"
+ option7 = "alias7"
+ alias7 = "alias7"
+ }
+ }
+ deprecations = @(
+ @{
+ msg = "Alias 'alias7' is deprecated. See the module docs for more information"
+ date = "2020-06-07"
+ collection_name = "ansible.builtin"
+ },
+ @{
+ msg = "Alias 'alias1' is deprecated. See the module docs for more information"
+ version = "2.10"
+ collection_name = $null
+ },
+ @{
+ msg = "Alias 'alias5' is deprecated. See the module docs for more information"
+ date = "2020-03-12"
+ collection_name = $null
+ },
+ @{
+ msg = "Alias 'alias6' is deprecated. See the module docs for more information"
+ version = "2.12"
+ collection_name = "ansible.builtin"
+ },
+ @{
+ msg = "Alias 'alias2' is deprecated. See the module docs for more information - found in option3"
+ version = "2.11"
+ collection_name = $null
+ },
+ @{
+ msg = "Alias 'alias5' is deprecated. See the module docs for more information - found in option3"
+ date = "2020-03-09"
+ collection_name = $null
+ },
+ @{
+ msg = "Alias 'alias3' is deprecated. See the module docs for more information - found in option3"
+ version = "2.12"
+ collection_name = "ansible.builtin"
+ },
+ @{
+ msg = "Alias 'alias6' is deprecated. See the module docs for more information - found in option3"
+ date = "2020-06-01"
+ collection_name = "ansible.builtin"
+ }
+ )
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Required by - single value" = {
+ $spec = @{
+ options = @{
+ option1 = @{type = "str" }
+ option2 = @{type = "str" }
+ option3 = @{type = "str" }
+ }
+ required_by = @{
+ option1 = "option2"
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ option2 = "option2"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ option1 = "option1"
+ option2 = "option2"
+ option3 = $null
+ }
+ }
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Required by - multiple values" = {
+ $spec = @{
+ options = @{
+ option1 = @{type = "str" }
+ option2 = @{type = "str" }
+ option3 = @{type = "str" }
+ }
+ required_by = @{
+ option1 = "option2", "option3"
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ option2 = "option2"
+ option3 = "option3"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ option1 = "option1"
+ option2 = "option2"
+ option3 = "option3"
+ }
+ }
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Required by explicit null" = {
+ $spec = @{
+ options = @{
+ option1 = @{type = "str" }
+ option2 = @{type = "str" }
+ option3 = @{type = "str" }
+ }
+ required_by = @{
+ option1 = "option2"
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ option2 = $null
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{
+ option1 = "option1"
+ option2 = $null
+ option3 = $null
+ }
+ }
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Required by failed - single value" = {
+ $spec = @{
+ options = @{
+ option1 = @{type = "str" }
+ option2 = @{type = "str" }
+ option3 = @{type = "str" }
+ }
+ required_by = @{
+ option1 = "option2"
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ failed = $true
+ invocation = @{
+ module_args = @{
+ option1 = "option1"
+ }
+ }
+ msg = "missing parameter(s) required by 'option1': option2"
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Required by failed - multiple values" = {
+ $spec = @{
+ options = @{
+ option1 = @{type = "str" }
+ option2 = @{type = "str" }
+ option3 = @{type = "str" }
+ }
+ required_by = @{
+ option1 = "option2", "option3"
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ failed = $true
+ invocation = @{
+ module_args = @{
+ option1 = "option1"
+ }
+ }
+ msg = "missing parameter(s) required by 'option1': option2, option3"
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Debug without debug set" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_debug = $false
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Debug("debug message")
+ $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message
+ $actual_event | Assert-Equal -Expected "undefined win module - Invoked with:`r`n "
+ }
+
+ "Debug with debug set" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_debug = $true
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Debug("debug message")
+ $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message
+ $actual_event | Assert-Equal -Expected "undefined win module - [DEBUG] debug message"
+ }
+
+ "Deprecate and warn with version" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Deprecate("message", "2.7")
+ $actual_deprecate_event_1 = Get-EventLog -LogName Application -Source Ansible -Newest 1
+ $m.Deprecate("message w collection", "2.8", "ansible.builtin")
+ $actual_deprecate_event_2 = Get-EventLog -LogName Application -Source Ansible -Newest 1
+ $m.Warn("warning")
+ $actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1
+
+ $actual_deprecate_event_1.Message | Assert-Equal -Expected "undefined win module - [DEPRECATION WARNING] message 2.7"
+ $actual_deprecate_event_2.Message | Assert-Equal -Expected "undefined win module - [DEPRECATION WARNING] message w collection 2.8"
+ $actual_warn_event.EntryType | Assert-Equal -Expected "Warning"
+ $actual_warn_event.Message | Assert-Equal -Expected "undefined win module - [WARNING] warning"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ warnings = @("warning")
+ deprecations = @(
+ @{msg = "message"; version = "2.7"; collection_name = $null },
+ @{msg = "message w collection"; version = "2.8"; collection_name = "ansible.builtin" }
+ )
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Deprecate and warn with date" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Deprecate("message", [DateTime]"2020-01-01")
+ $actual_deprecate_event_1 = Get-EventLog -LogName Application -Source Ansible -Newest 1
+ $m.Deprecate("message w collection", [DateTime]"2020-01-02", "ansible.builtin")
+ $actual_deprecate_event_2 = Get-EventLog -LogName Application -Source Ansible -Newest 1
+ $m.Warn("warning")
+ $actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1
+
+ $actual_deprecate_event_1.Message | Assert-Equal -Expected "undefined win module - [DEPRECATION WARNING] message 2020-01-01"
+ $actual_deprecate_event_2.Message | Assert-Equal -Expected "undefined win module - [DEPRECATION WARNING] message w collection 2020-01-02"
+ $actual_warn_event.EntryType | Assert-Equal -Expected "Warning"
+ $actual_warn_event.Message | Assert-Equal -Expected "undefined win module - [WARNING] warning"
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ warnings = @("warning")
+ deprecations = @(
+ @{msg = "message"; date = "2020-01-01"; collection_name = $null },
+ @{msg = "message w collection"; date = "2020-01-02"; collection_name = "ansible.builtin" }
+ )
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "FailJson with message" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ $failed = $false
+ try {
+ $m.FailJson("fail message")
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ failed = $true
+ msg = "fail message"
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "FailJson with Exception" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ try {
+ [System.IO.Path]::GetFullPath($null)
+ }
+ catch {
+ $excp = $_.Exception
+ }
+
+ $failed = $false
+ try {
+ $m.FailJson("fail message", $excp)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ failed = $true
+ msg = "fail message"
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "FailJson with ErrorRecord" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ try {
+ Get-Item -LiteralPath $null
+ }
+ catch {
+ $error_record = $_
+ }
+
+ $failed = $false
+ try {
+ $m.FailJson("fail message", $error_record)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ failed = $true
+ msg = "fail message"
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "FailJson with Exception and verbosity 3" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_verbosity = 3
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ try {
+ [System.IO.Path]::GetFullPath($null)
+ }
+ catch {
+ $excp = $_.Exception
+ }
+
+ $failed = $false
+ try {
+ $m.FailJson("fail message", $excp)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = @{} }
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected "fail message"
+ $expected = 'System.Management.Automation.MethodInvocationException: Exception calling "GetFullPath" with "1" argument(s)'
+ $actual.exception.Contains($expected) | Assert-Equal -Expected $true
+ }
+
+ "FailJson with ErrorRecord and verbosity 3" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_verbosity = 3
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ try {
+ Get-Item -LiteralPath $null
+ }
+ catch {
+ $error_record = $_
+ }
+
+ $failed = $false
+ try {
+ $m.FailJson("fail message", $error_record)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = @{} }
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected "fail message"
+ $actual.exception.Contains("Cannot bind argument to parameter 'LiteralPath' because it is null") | Assert-Equal -Expected $true
+ $actual.exception.Contains("+ Get-Item -LiteralPath `$null") | Assert-Equal -Expected $true
+ $actual.exception.Contains("ScriptStackTrace:") | Assert-Equal -Expected $true
+ }
+
+ "Diff entry without diff set" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Diff.before = @{a = "a" }
+ $m.Diff.after = @{b = "b" }
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "Diff entry with diff set" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_diff = $true
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ $m.Diff.before = @{a = "a" }
+ $m.Diff.after = @{b = "b" }
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $failed
+
+ $expected = @{
+ changed = $false
+ invocation = @{
+ module_args = @{}
+ }
+ diff = @{
+ before = @{a = "a" }
+ after = @{b = "b" }
+ }
+ }
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+
+ "ParseBool tests" = {
+ $mapping = New-Object -TypeName 'System.Collections.Generic.Dictionary`2[[Object], [Bool]]'
+ $mapping.Add("y", $true)
+ $mapping.Add("Y", $true)
+ $mapping.Add("yes", $true)
+ $mapping.Add("Yes", $true)
+ $mapping.Add("on", $true)
+ $mapping.Add("On", $true)
+ $mapping.Add("1", $true)
+ $mapping.Add(1, $true)
+ $mapping.Add("true", $true)
+ $mapping.Add("True", $true)
+ $mapping.Add("t", $true)
+ $mapping.Add("T", $true)
+ $mapping.Add("1.0", $true)
+ $mapping.Add(1.0, $true)
+ $mapping.Add($true, $true)
+ $mapping.Add("n", $false)
+ $mapping.Add("N", $false)
+ $mapping.Add("no", $false)
+ $mapping.Add("No", $false)
+ $mapping.Add("off", $false)
+ $mapping.Add("Off", $false)
+ $mapping.Add("0", $false)
+ $mapping.Add(0, $false)
+ $mapping.Add("false", $false)
+ $mapping.Add("False", $false)
+ $mapping.Add("f", $false)
+ $mapping.Add("F", $false)
+ $mapping.Add("0.0", $false)
+ $mapping.Add(0.0, $false)
+ $mapping.Add($false, $false)
+
+ foreach ($map in $mapping.GetEnumerator()) {
+ $expected = $map.Value
+ $actual = [Ansible.Basic.AnsibleModule]::ParseBool($map.Key)
+ $actual | Assert-Equal -Expected $expected
+ $actual.GetType().FullName | Assert-Equal -Expected "System.Boolean"
+ }
+
+ $fail_bools = @(
+ "falsey",
+ "abc",
+ 2,
+ "2",
+ -1
+ )
+ foreach ($fail_bool in $fail_bools) {
+ $failed = $false
+ try {
+ [Ansible.Basic.AnsibleModule]::ParseBool($fail_bool)
+ }
+ catch {
+ $failed = $true
+ $_.Exception.Message.Contains("The value '$fail_bool' is not a valid boolean") | Assert-Equal -Expected $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+ }
+
+ "Unknown internal key" = {
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_invalid = "invalid"
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+
+ $expected = @{
+ invocation = @{
+ module_args = @{
+ _ansible_invalid = "invalid"
+ }
+ }
+ changed = $false
+ failed = $true
+ msg = "Unsupported parameters for (undefined win module) module: _ansible_invalid. Supported parameters include: "
+ }
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ $actual | Assert-DictionaryEqual -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Module tmpdir with present remote tmp" = {
+ $current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+ $dir_security = New-Object -TypeName System.Security.AccessControl.DirectorySecurity
+ $dir_security.SetOwner($current_user)
+ $dir_security.SetAccessRuleProtection($true, $false)
+ $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
+ $current_user, [System.Security.AccessControl.FileSystemRights]::FullControl,
+ [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
+ [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow
+ )
+ $dir_security.AddAccessRule($ace)
+ $expected_sd = $dir_security.GetSecurityDescriptorSddlForm("Access, Owner")
+
+ $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
+ New-Item -Path $remote_tmp -ItemType Directory > $null
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_remote_tmp = $remote_tmp.ToString()
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+
+ $actual_tmpdir = $m.Tmpdir
+ $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent
+ $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf
+
+ $parent_tmpdir | Assert-Equal -Expected $remote_tmp
+ $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+ $children = [System.IO.Directory]::EnumerateDirectories($remote_tmp)
+ $children.Count | Assert-Equal -Expected 1
+ $actual_tmpdir_sd = (Get-Acl -Path $actual_tmpdir).GetSecurityDescriptorSddlForm("Access, Owner")
+ $actual_tmpdir_sd | Assert-Equal -Expected $expected_sd
+
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $false
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+ $output.warnings.Count | Assert-Equal -Expected 0
+ }
+
+ "Module tmpdir with missing remote_tmp" = {
+ $current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+ $dir_security = New-Object -TypeName System.Security.AccessControl.DirectorySecurity
+ $dir_security.SetOwner($current_user)
+ $dir_security.SetAccessRuleProtection($true, $false)
+ $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
+ $current_user, [System.Security.AccessControl.FileSystemRights]::FullControl,
+ [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
+ [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow
+ )
+ $dir_security.AddAccessRule($ace)
+ $expected_sd = $dir_security.GetSecurityDescriptorSddlForm("Access, Owner")
+
+ $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_remote_tmp = $remote_tmp.ToString()
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $false
+
+ $actual_tmpdir = $m.Tmpdir
+ $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent
+ $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf
+
+ $parent_tmpdir | Assert-Equal -Expected $remote_tmp
+ $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+ $children = [System.IO.Directory]::EnumerateDirectories($remote_tmp)
+ $children.Count | Assert-Equal -Expected 1
+ $actual_remote_sd = (Get-Acl -Path $remote_tmp).GetSecurityDescriptorSddlForm("Access, Owner")
+ $actual_tmpdir_sd = (Get-Acl -Path $actual_tmpdir).GetSecurityDescriptorSddlForm("Access, Owner")
+ $actual_remote_sd | Assert-Equal -Expected $expected_sd
+ $actual_tmpdir_sd | Assert-Equal -Expected $expected_sd
+
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $false
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+ $output.warnings.Count | Assert-Equal -Expected 1
+ $nt_account = $current_user.Translate([System.Security.Principal.NTAccount])
+ $actual_warning = "Module remote_tmp $remote_tmp did not exist and was created with FullControl to $nt_account, "
+ $actual_warning += "this may cause issues when running as another user. To avoid this, "
+ $actual_warning += "create the remote_tmp dir with the correct permissions manually"
+ $actual_warning | Assert-Equal -Expected $output.warnings[0]
+ }
+
+ "Module tmp, keep remote files" = {
+ $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
+ New-Item -Path $remote_tmp -ItemType Directory > $null
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_remote_tmp = $remote_tmp.ToString()
+ _ansible_keep_remote_files = $true
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ $actual_tmpdir = $m.Tmpdir
+ $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent
+ $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf
+
+ $parent_tmpdir | Assert-Equal -Expected $remote_tmp
+ $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equal -Expected $true
+ (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equal -Expected $true
+ $output.warnings.Count | Assert-Equal -Expected 0
+ Remove-Item -LiteralPath $actual_tmpdir -Force -Recurse
+ }
+
+ "Invalid argument spec key" = {
+ $spec = @{
+ invalid = $true
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, "
+ $expected_msg += "aliases, choices, default, deprecated_aliases, elements, mutually_exclusive, no_log, options, "
+ $expected_msg += "removed_in_version, removed_at_date, removed_from_collection, required, required_by, required_if, "
+ $expected_msg += "required_one_of, required_together, supports_check_mode, type"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid argument spec key - nested" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ options = @{
+ sub_option_key = @{
+ invalid = $true
+ }
+ }
+ }
+ }
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, "
+ $expected_msg += "aliases, choices, default, deprecated_aliases, elements, mutually_exclusive, no_log, options, "
+ $expected_msg += "removed_in_version, removed_at_date, removed_from_collection, required, required_by, required_if, "
+ $expected_msg += "required_one_of, required_together, supports_check_mode, type - found in option_key -> sub_option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid argument spec value type" = {
+ $spec = @{
+ apply_defaults = "abc"
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: argument spec for 'apply_defaults' did not match expected "
+ $expected_msg += "type System.Boolean: actual type System.String"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid argument spec option type" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "invalid type"
+ }
+ }
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: type 'invalid type' is unsupported - found in option_key. "
+ $expected_msg += "Valid types are: bool, dict, float, int, json, list, path, raw, sid, str"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid argument spec option element type" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "list"
+ elements = "invalid type"
+ }
+ }
+ }
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: elements 'invalid type' is unsupported - found in option_key. "
+ $expected_msg += "Valid types are: bool, dict, float, int, json, list, path, raw, sid, str"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid deprecated aliases entry - no version and date" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "str"
+ aliases = , "alias_name"
+ deprecated_aliases = @(
+ @{name = "alias_name" }
+ )
+ }
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: One of version or date is required in a deprecated_aliases entry"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid deprecated aliases entry - no name (nested)" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "dict"
+ options = @{
+ sub_option_key = @{
+ type = "str"
+ aliases = , "alias_name"
+ deprecated_aliases = @(
+ @{version = "2.10" }
+ )
+ }
+ }
+ }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = @{
+ sub_option_key = "a"
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.ArgumentException] {
+ $failed = $true
+ $expected_msg = "name is required in a deprecated_aliases entry - found in option_key"
+ $_.Exception.Message | Assert-Equal -Expected $expected_msg
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Invalid deprecated aliases entry - both version and date" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "str"
+ aliases = , "alias_name"
+ deprecated_aliases = @(
+ @{
+ name = "alias_name"
+ date = [DateTime]"2020-03-10"
+ version = "2.11"
+ }
+ )
+ }
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: Only one of version or date is allowed in a deprecated_aliases entry"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Invalid deprecated aliases entry - wrong date type" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "str"
+ aliases = , "alias_name"
+ deprecated_aliases = @(
+ @{
+ name = "alias_name"
+ date = "2020-03-10"
+ }
+ )
+ }
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: A deprecated_aliases date must be a DateTime object"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Spec required and default set at the same time" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ required = $true
+ default = "default value"
+ }
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: required and default are mutually exclusive for option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ ("exception" -cin $actual.Keys) | Assert-Equal -Expected $true
+ }
+
+ "Unsupported options" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "str"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "abc"
+ invalid_key = "def"
+ another_key = "ghi"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "Unsupported parameters for (undefined win module) module: another_key, invalid_key. "
+ $expected_msg += "Supported parameters include: option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Check mode and module doesn't support check mode" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "str"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_check_mode = $true
+ option_key = "abc"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "remote module (undefined win module) does not support check mode"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.skipped | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = "abc" } }
+ }
+
+ "Check mode with suboption without supports_check_mode" = {
+ $spec = @{
+ options = @{
+ sub_options = @{
+ # This tests the situation where a sub key doesn't set supports_check_mode, the logic in
+ # Ansible.Basic automatically sets that to $false and we want it to ignore it for a nested check
+ type = "dict"
+ options = @{
+ sub_option = @{ type = "str"; default = "value" }
+ }
+ }
+ }
+ supports_check_mode = $true
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ _ansible_check_mode = $true
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $m.CheckMode | Assert-Equal -Expected $true
+ }
+
+ "Type conversion error" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "int"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "a"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "argument for option_key is of type System.String and we were unable to convert to int: "
+ $expected_msg += "Input string was not in a correct format."
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Type conversion error - delegate" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "dict"
+ options = @{
+ sub_option_key = @{
+ type = [Func[[Object], [UInt64]]] { [System.UInt64]::Parse($args[0]) }
+ }
+ }
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = @{
+ sub_option_key = "a"
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "argument for sub_option_key is of type System.String and we were unable to convert to delegate: "
+ $expected_msg += "Exception calling `"Parse`" with `"1`" argument(s): `"Input string was not in a correct format.`" "
+ $expected_msg += "found in option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Numeric choices" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = 1, 2, 3
+ type = "int"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "2"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $output.Keys.Count | Assert-Equal -Expected 2
+ $output.changed | Assert-Equal -Expected $false
+ $output.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = 2 } }
+ }
+
+ "Case insensitive choice" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "abc", "def"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "ABC"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $expected_warning = "value of option_key was a case insensitive match of one of: abc, def. "
+ $expected_warning += "Checking of choices will be case sensitive in a future Ansible release. "
+ $expected_warning += "Case insensitive matches were: ABC"
+
+ $output.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = "ABC" } }
+ # We have disabled the warnings for now
+ #$output.warnings.Count | Assert-Equal -Expected 1
+ #$output.warnings[0] | Assert-Equal -Expected $expected_warning
+ }
+
+ "Case insensitive choice no_log" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "abc", "def"
+ no_log = $true
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "ABC"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $expected_warning = "value of option_key was a case insensitive match of one of: abc, def. "
+ $expected_warning += "Checking of choices will be case sensitive in a future Ansible release. "
+ $expected_warning += "Case insensitive matches were: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+
+ $output.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" } }
+ # We have disabled the warnings for now
+ #$output.warnings.Count | Assert-Equal -Expected 1
+ #$output.warnings[0] | Assert-Equal -Expected $expected_warning
+ }
+
+ "Case insentitive choice as list" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "abc", "def", "ghi", "JKL"
+ type = "list"
+ elements = "str"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "AbC", "ghi", "jkl"
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $expected_warning = "value of option_key was a case insensitive match of one or more of: abc, def, ghi, JKL. "
+ $expected_warning += "Checking of choices will be case sensitive in a future Ansible release. "
+ $expected_warning += "Case insensitive matches were: AbC, jkl"
+
+ $output.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ # We have disabled the warnings for now
+ #$output.warnings.Count | Assert-Equal -Expected 1
+ #$output.warnings[0] | Assert-Equal -Expected $expected_warning
+ }
+
+ "Invalid choice" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "a", "b"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "c"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "value of option_key must be one of: a, b. Got no match for: c"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Invalid choice with no_log" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "a", "b"
+ no_log = $true
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "abc"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "value of option_key must be one of: a, b. Got no match for: ********"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = @{option_key = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" } }
+ }
+
+ "Invalid choice in list" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ choices = "a", "b"
+ type = "list"
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = "a", "c"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "value of option_key must be one or more of: a, b. Got no match for: c"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Mutually exclusive options" = {
+ $spec = @{
+ options = @{
+ option1 = @{}
+ option2 = @{}
+ }
+ mutually_exclusive = @(, @("option1", "option2"))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "a"
+ option2 = "b"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "parameters are mutually exclusive: option1, option2"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Missing required argument" = {
+ $spec = @{
+ options = @{
+ option1 = @{}
+ option2 = @{required = $true }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "a"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "missing required arguments: option2"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Missing required argument subspec - no value defined" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "dict"
+ options = @{
+ sub_option_key = @{
+ required = $true
+ }
+ }
+ }
+ }
+ }
+
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.Keys.Count | Assert-Equal -Expected 2
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Missing required argument subspec" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "dict"
+ options = @{
+ sub_option_key = @{
+ required = $true
+ }
+ another_key = @{}
+ }
+ }
+ }
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = @{
+ another_key = "abc"
+ }
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "missing required arguments: sub_option_key found in option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required together not set" = {
+ $spec = @{
+ options = @{
+ option1 = @{}
+ option2 = @{}
+ }
+ required_together = @(, @("option1", "option2"))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "abc"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "parameters are required together: option1, option2"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required together not set - subspec" = {
+ $spec = @{
+ options = @{
+ option_key = @{
+ type = "dict"
+ options = @{
+ option1 = @{}
+ option2 = @{}
+ }
+ required_together = @(, @("option1", "option2"))
+ }
+ another_option = @{}
+ }
+ required_together = @(, @("option_key", "another_option"))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option_key = @{
+ option1 = "abc"
+ }
+ another_option = "def"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "parameters are required together: option1, option2 found in option_key"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required one of not set" = {
+ $spec = @{
+ options = @{
+ option1 = @{}
+ option2 = @{}
+ option3 = @{}
+ }
+ required_one_of = @(@("option1", "option2"), @("option2", "option3"))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "abc"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "one of the following is required: option2, option3"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required if invalid entries" = {
+ $spec = @{
+ options = @{
+ state = @{choices = "absent", "present"; default = "present" }
+ path = @{type = "path" }
+ }
+ required_if = @(, @("state", "absent"))
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "internal error: invalid required_if value count of 2, expecting 3 or 4 entries"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required if no missing option" = {
+ $spec = @{
+ options = @{
+ state = @{choices = "absent", "present"; default = "present" }
+ name = @{}
+ path = @{type = "path" }
+ }
+ required_if = @(, @("state", "absent", @("name", "path")))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ name = "abc"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.Keys.Count | Assert-Equal -Expected 2
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required if missing option" = {
+ $spec = @{
+ options = @{
+ state = @{choices = "absent", "present"; default = "present" }
+ name = @{}
+ path = @{type = "path" }
+ }
+ required_if = @(, @("state", "absent", @("name", "path")))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ state = "absent"
+ name = "abc"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "state is absent but all of the following are missing: path"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required if missing option and required one is set" = {
+ $spec = @{
+ options = @{
+ state = @{choices = "absent", "present"; default = "present" }
+ name = @{}
+ path = @{type = "path" }
+ }
+ required_if = @(, @("state", "absent", @("name", "path"), $true))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ state = "absent"
+ }
+
+ $failed = $false
+ try {
+ $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $expected_msg = "state is absent but any of the following are missing: name, path"
+
+ $actual.Keys.Count | Assert-Equal -Expected 4
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected $expected_msg
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Required if missing option but one required set" = {
+ $spec = @{
+ options = @{
+ state = @{choices = "absent", "present"; default = "present" }
+ name = @{}
+ path = @{type = "path" }
+ }
+ required_if = @(, @("state", "absent", @("name", "path"), $true))
+ }
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ state = "absent"
+ name = "abc"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.Keys.Count | Assert-Equal -Expected 2
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "PS Object in return result" = {
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
+
+ # JavaScriptSerializer struggles with PS Object like PSCustomObject due to circular references, this test makes
+ # sure we can handle these types of objects without bombing
+ $m.Result.output = [PSCustomObject]@{a = "a"; b = "b" }
+ $failed = $true
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.Keys.Count | Assert-Equal -Expected 3
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = @{} }
+ $actual.output | Assert-DictionaryEqual -Expected @{a = "a"; b = "b" }
+ }
+
+ "String json array to object" = {
+ $input_json = '["abc", "def"]'
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($input_json)
+ $actual -is [Array] | Assert-Equal -Expected $true
+ $actual.Length | Assert-Equal -Expected 2
+ $actual[0] | Assert-Equal -Expected "abc"
+ $actual[1] | Assert-Equal -Expected "def"
+ }
+
+ "String json array of dictionaries to object" = {
+ $input_json = '[{"abc":"def"}]'
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($input_json)
+ $actual -is [Array] | Assert-Equal -Expected $true
+ $actual.Length | Assert-Equal -Expected 1
+ $actual[0] | Assert-DictionaryEqual -Expected @{"abc" = "def" }
+ }
+
+ "Spec with fragments" = {
+ $spec = @{
+ options = @{
+ option1 = @{ type = "str" }
+ }
+ }
+ $fragment1 = @{
+ options = @{
+ option2 = @{ type = "str" }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ option2 = "option2"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1))
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{module_args = $complex_args }
+ }
+
+ "Fragment spec that with a deprecated alias" = {
+ $spec = @{
+ options = @{
+ option1 = @{
+ aliases = @("alias1_spec")
+ type = "str"
+ deprecated_aliases = @(
+ @{name = "alias1_spec"; version = "2.0" }
+ )
+ }
+ option2 = @{
+ aliases = @("alias2_spec")
+ deprecated_aliases = @(
+ @{name = "alias2_spec"; version = "2.0"; collection_name = "ansible.builtin" }
+ )
+ }
+ }
+ }
+ $fragment1 = @{
+ options = @{
+ option1 = @{
+ aliases = @("alias1")
+ deprecated_aliases = @() # Makes sure it doesn't overwrite the spec, just adds to it.
+ }
+ option2 = @{
+ aliases = @("alias2")
+ deprecated_aliases = @(
+ @{name = "alias2"; version = "2.0"; collection_name = "foo.bar" }
+ )
+ type = "str"
+ }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ alias1_spec = "option1"
+ alias2 = "option2"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1))
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.deprecations.Count | Assert-Equal -Expected 2
+ $actual.deprecations[0] | Assert-DictionaryEqual -Expected @{
+ msg = "Alias 'alias1_spec' is deprecated. See the module docs for more information"; version = "2.0"; collection_name = $null
+ }
+ $actual.deprecations[1] | Assert-DictionaryEqual -Expected @{
+ msg = "Alias 'alias2' is deprecated. See the module docs for more information"; version = "2.0"; collection_name = "foo.bar"
+ }
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{
+ module_args = @{
+ option1 = "option1"
+ alias1_spec = "option1"
+ option2 = "option2"
+ alias2 = "option2"
+ }
+ }
+ }
+
+ "Fragment spec with mutual args" = {
+ $spec = @{
+ options = @{
+ option1 = @{ type = "str" }
+ option2 = @{ type = "str" }
+ }
+ mutually_exclusive = @(
+ , @('option1', 'option2')
+ )
+ }
+ $fragment1 = @{
+ options = @{
+ fragment1_1 = @{ type = "str" }
+ fragment1_2 = @{ type = "str" }
+ }
+ mutually_exclusive = @(
+ , @('fragment1_1', 'fragment1_2')
+ )
+ }
+ $fragment2 = @{
+ options = @{
+ fragment2 = @{ type = "str" }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ fragment1_1 = "fragment1_1"
+ fragment1_2 = "fragment1_2"
+ fragment2 = "fragment2"
+ }
+
+ $failed = $false
+ try {
+ [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1, $fragment2))
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg | Assert-Equal -Expected "parameters are mutually exclusive: fragment1_1, fragment1_2"
+ $actual.invocation | Assert-DictionaryEqual -Expected @{ module_args = $complex_args }
+ }
+
+ "Fragment spec with no_log" = {
+ $spec = @{
+ options = @{
+ option1 = @{
+ aliases = @("alias")
+ }
+ }
+ }
+ $fragment1 = @{
+ options = @{
+ option1 = @{
+ no_log = $true # Makes sure that a value set in the fragment but not in the spec is respected.
+ type = "str"
+ }
+ }
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ alias = "option1"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1))
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.invocation | Assert-DictionaryEqual -Expected @{
+ module_args = @{
+ option1 = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ alias = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ }
+ }
+ }
+
+ "Catch invalid fragment spec format" = {
+ $spec = @{
+ options = @{
+ option1 = @{ type = "str" }
+ }
+ }
+ $fragment = @{
+ options = @{}
+ invalid = "will fail"
+ }
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ option1 = "option1"
+ }
+
+ $failed = $false
+ try {
+ [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment))
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 1"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.failed | Assert-Equal -Expected $true
+ $actual.msg.StartsWith("internal error: argument spec entry contains an invalid key 'invalid', valid keys: ") | Assert-Equal -Expected $true
+ }
+
+ "Spec with different list types" = {
+ $spec = @{
+ options = @{
+ # Single element of the same list type not in a list
+ option1 = @{
+ aliases = "alias1"
+ deprecated_aliases = @{name = "alias1"; version = "2.0"; collection_name = "foo.bar" }
+ }
+
+ # Arrays
+ option2 = @{
+ aliases = , "alias2"
+ deprecated_aliases = , @{name = "alias2"; version = "2.0"; collection_name = "foo.bar" }
+ }
+
+ # ArrayList
+ option3 = @{
+ aliases = [System.Collections.ArrayList]@("alias3")
+ deprecated_aliases = [System.Collections.ArrayList]@(@{name = "alias3"; version = "2.0"; collection_name = "foo.bar" })
+ }
+
+ # Generic.List[Object]
+ option4 = @{
+ aliases = [System.Collections.Generic.List[Object]]@("alias4")
+ deprecated_aliases = [System.Collections.Generic.List[Object]]@(@{name = "alias4"; version = "2.0"; collection_name = "foo.bar" })
+ }
+
+ # Generic.List[T]
+ option5 = @{
+ aliases = [System.Collections.Generic.List[String]]@("alias5")
+ deprecated_aliases = [System.Collections.Generic.List[Hashtable]]@()
+ }
+ }
+ }
+ $spec.options.option5.deprecated_aliases.Add(@{name = "alias5"; version = "2.0"; collection_name = "foo.bar" })
+
+ Set-Variable -Name complex_args -Scope Global -Value @{
+ alias1 = "option1"
+ alias2 = "option2"
+ alias3 = "option3"
+ alias4 = "option4"
+ alias5 = "option5"
+ }
+ $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
+
+ $failed = $false
+ try {
+ $m.ExitJson()
+ }
+ catch [System.Management.Automation.RuntimeException] {
+ $failed = $true
+ $_.Exception.Message | Assert-Equal -Expected "exit: 0"
+ $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual.changed | Assert-Equal -Expected $false
+ $actual.deprecations.Count | Assert-Equal -Expected 5
+ foreach ($dep in $actual.deprecations) {
+ $dep.msg -like "Alias 'alias?' is deprecated. See the module docs for more information" | Assert-Equal -Expected $true
+ $dep.version | Assert-Equal -Expected '2.0'
+ $dep.collection_name | Assert-Equal -Expected 'foo.bar'
+ }
+ $actual.invocation | Assert-DictionaryEqual -Expected @{
+ module_args = @{
+ alias1 = "option1"
+ option1 = "option1"
+ alias2 = "option2"
+ option2 = "option2"
+ alias3 = "option3"
+ option3 = "option3"
+ alias4 = "option4"
+ option4 = "option4"
+ alias5 = "option5"
+ option5 = "option5"
+ }
+ }
+ }
+}
+
+try {
+ foreach ($test_impl in $tests.GetEnumerator()) {
+ # Reset the variables before each test
+ Set-Variable -Name complex_args -Value @{} -Scope Global
+
+ $test = $test_impl.Key
+ &$test_impl.Value
+ }
+ $module.Result.data = "success"
+}
+catch [System.Management.Automation.RuntimeException] {
+ $module.Result.failed = $true
+ $module.Result.test = $test
+ $module.Result.line = $_.InvocationInfo.ScriptLineNumber
+ $module.Result.method = $_.InvocationInfo.Line.Trim()
+
+ if ($_.Exception.Message.StartSwith("exit: ")) {
+ # The exception was caused by an unexpected Exit call, log that on the output
+ $module.Result.output = (ConvertFrom-Json -InputObject $_.Exception.InnerException.Output)
+ $module.Result.msg = "Uncaught AnsibleModule exit in tests, see output"
+ }
+ else {
+ # Unrelated exception
+ $module.Result.exception = $_.Exception.ToString()
+ $module.Result.msg = "Uncaught exception: $(($_ | Out-String).ToString())"
+ }
+}
+
+Exit-Module
diff --git a/test/integration/targets/module_utils_Ansible.Basic/tasks/main.yml b/test/integration/targets/module_utils_Ansible.Basic/tasks/main.yml
new file mode 100644
index 0000000..010c2d5
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Basic/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: test Ansible.Basic.cs
+ ansible_basic_tests:
+ register: ansible_basic_test
+
+- name: assert test Ansible.Basic.cs
+ assert:
+ that:
+ - ansible_basic_test.data == "success"
diff --git a/test/integration/targets/module_utils_Ansible.Become/aliases b/test/integration/targets/module_utils_Ansible.Become/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Become/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.Become/library/ansible_become_tests.ps1 b/test/integration/targets/module_utils_Ansible.Become/library/ansible_become_tests.ps1
new file mode 100644
index 0000000..6e36321
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Become/library/ansible_become_tests.ps1
@@ -0,0 +1,1022 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -CSharpUtil Ansible.Become
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+# Would be great to move win_whomai out into it's own module util and share the
+# code here, for now just rely on a cut down version
+$test_whoami = {
+ Add-Type -TypeDefinition @'
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+using System.Text;
+
+namespace Ansible
+{
+ internal class NativeHelpers
+ {
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct LSA_UNICODE_STRING
+ {
+ public UInt16 Length;
+ public UInt16 MaximumLength;
+ public IntPtr Buffer;
+
+ public override string ToString()
+ {
+ return Marshal.PtrToStringUni(Buffer, Length / sizeof(char));
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LUID
+ {
+ public UInt32 LowPart;
+ public Int32 HighPart;
+
+ public static explicit operator UInt64(LUID l)
+ {
+ return (UInt64)((UInt64)l.HighPart << 32) | (UInt64)l.LowPart;
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SECURITY_LOGON_SESSION_DATA
+ {
+ public UInt32 Size;
+ public LUID LogonId;
+ public LSA_UNICODE_STRING UserName;
+ public LSA_UNICODE_STRING LogonDomain;
+ public LSA_UNICODE_STRING AuthenticationPackage;
+ public SECURITY_LOGON_TYPE LogonType;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SID_AND_ATTRIBUTES
+ {
+ public IntPtr Sid;
+ public int Attributes;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_MANDATORY_LABEL
+ {
+ public SID_AND_ATTRIBUTES Label;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_SOURCE
+ {
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public char[] SourceName;
+ public LUID SourceIdentifier;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_STATISTICS
+ {
+ public LUID TokenId;
+ public LUID AuthenticationId;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_USER
+ {
+ public SID_AND_ATTRIBUTES User;
+ }
+
+ public enum SECURITY_LOGON_TYPE
+ {
+ System = 0, // Used only by the Sytem account
+ Interactive = 2,
+ Network,
+ Batch,
+ Service,
+ Proxy,
+ Unlock,
+ NetworkCleartext,
+ NewCredentials,
+ RemoteInteractive,
+ CachedInteractive,
+ CachedRemoteInteractive,
+ CachedUnlock
+ }
+
+ public enum TokenInformationClass
+ {
+ TokenUser = 1,
+ TokenSource = 7,
+ TokenStatistics = 10,
+ TokenIntegrityLevel = 25,
+ }
+ }
+
+ internal class NativeMethods
+ {
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern bool CloseHandle(
+ IntPtr hObject);
+
+ [DllImport("kernel32.dll")]
+ public static extern SafeNativeHandle GetCurrentProcess();
+
+ [DllImport("userenv.dll", SetLastError = true)]
+ public static extern bool GetProfileType(
+ out UInt32 dwFlags);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ public static extern bool GetTokenInformation(
+ SafeNativeHandle TokenHandle,
+ NativeHelpers.TokenInformationClass TokenInformationClass,
+ SafeMemoryBuffer TokenInformation,
+ UInt32 TokenInformationLength,
+ out UInt32 ReturnLength);
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool LookupAccountSid(
+ string lpSystemName,
+ IntPtr Sid,
+ StringBuilder lpName,
+ ref UInt32 cchName,
+ StringBuilder ReferencedDomainName,
+ ref UInt32 cchReferencedDomainName,
+ out UInt32 peUse);
+
+ [DllImport("secur32.dll", SetLastError = true)]
+ public static extern UInt32 LsaEnumerateLogonSessions(
+ out UInt32 LogonSessionCount,
+ out SafeLsaMemoryBuffer LogonSessionList);
+
+ [DllImport("secur32.dll", SetLastError = true)]
+ public static extern UInt32 LsaFreeReturnBuffer(
+ IntPtr Buffer);
+
+ [DllImport("secur32.dll", SetLastError = true)]
+ public static extern UInt32 LsaGetLogonSessionData(
+ IntPtr LogonId,
+ out SafeLsaMemoryBuffer ppLogonSessionData);
+
+ [DllImport("advapi32.dll")]
+ public static extern UInt32 LsaNtStatusToWinError(
+ UInt32 Status);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ public static extern bool OpenProcessToken(
+ SafeNativeHandle ProcessHandle,
+ TokenAccessLevels DesiredAccess,
+ out SafeNativeHandle TokenHandle);
+ }
+
+ internal class SafeLsaMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeLsaMemoryBuffer() : base(true) { }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ UInt32 res = NativeMethods.LsaFreeReturnBuffer(handle);
+ return res == 0;
+ }
+ }
+
+ internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeMemoryBuffer() : base(true) { }
+ public SafeMemoryBuffer(int cb) : base(true)
+ {
+ base.SetHandle(Marshal.AllocHGlobal(cb));
+ }
+ public SafeMemoryBuffer(IntPtr handle) : base(true)
+ {
+ base.SetHandle(handle);
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ Marshal.FreeHGlobal(handle);
+ return true;
+ }
+ }
+
+ internal class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeNativeHandle() : base(true) { }
+ public SafeNativeHandle(IntPtr handle) : base(true) { this.handle = handle; }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ return NativeMethods.CloseHandle(handle);
+ }
+ }
+
+ public class Win32Exception : System.ComponentModel.Win32Exception
+ {
+ private string _msg;
+
+ public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
+ public Win32Exception(int errorCode, string message) : base(errorCode)
+ {
+ _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
+ }
+
+ public override string Message { get { return _msg; } }
+ public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
+ }
+
+ public class Logon
+ {
+ public string AuthenticationPackage { get; internal set; }
+ public string LogonType { get; internal set; }
+ public string MandatoryLabelName { get; internal set; }
+ public SecurityIdentifier MandatoryLabelSid { get; internal set; }
+ public bool ProfileLoaded { get; internal set; }
+ public string SourceName { get; internal set; }
+ public string UserName { get; internal set; }
+ public SecurityIdentifier UserSid { get; internal set; }
+
+ public Logon()
+ {
+ using (SafeNativeHandle process = NativeMethods.GetCurrentProcess())
+ {
+ TokenAccessLevels dwAccess = TokenAccessLevels.Query | TokenAccessLevels.QuerySource;
+
+ SafeNativeHandle hToken;
+ NativeMethods.OpenProcessToken(process, dwAccess, out hToken);
+ using (hToken)
+ {
+ SetLogonSessionData(hToken);
+ SetTokenMandatoryLabel(hToken);
+ SetTokenSource(hToken);
+ SetTokenUser(hToken);
+ }
+ }
+ SetProfileLoaded();
+ }
+
+ private void SetLogonSessionData(SafeNativeHandle hToken)
+ {
+ NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenStatistics;
+ UInt32 returnLength;
+ NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength);
+
+ UInt64 tokenLuidId;
+ using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength))
+ {
+ if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength))
+ throw new Win32Exception("GetTokenInformation(TokenStatistics) failed");
+
+ NativeHelpers.TOKEN_STATISTICS stats = (NativeHelpers.TOKEN_STATISTICS)Marshal.PtrToStructure(
+ infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_STATISTICS));
+ tokenLuidId = (UInt64)stats.AuthenticationId;
+ }
+
+ UInt32 sessionCount;
+ SafeLsaMemoryBuffer sessionPtr;
+ UInt32 res = NativeMethods.LsaEnumerateLogonSessions(out sessionCount, out sessionPtr);
+ if (res != 0)
+ throw new Win32Exception((int)NativeMethods.LsaNtStatusToWinError(res), "LsaEnumerateLogonSession() failed");
+ using (sessionPtr)
+ {
+ IntPtr currentSession = sessionPtr.DangerousGetHandle();
+ for (UInt32 i = 0; i < sessionCount; i++)
+ {
+ SafeLsaMemoryBuffer sessionDataPtr;
+ res = NativeMethods.LsaGetLogonSessionData(currentSession, out sessionDataPtr);
+ if (res != 0)
+ {
+ currentSession = IntPtr.Add(currentSession, Marshal.SizeOf(typeof(NativeHelpers.LUID)));
+ continue;
+ }
+ using (sessionDataPtr)
+ {
+ NativeHelpers.SECURITY_LOGON_SESSION_DATA sessionData = (NativeHelpers.SECURITY_LOGON_SESSION_DATA)Marshal.PtrToStructure(
+ sessionDataPtr.DangerousGetHandle(), typeof(NativeHelpers.SECURITY_LOGON_SESSION_DATA));
+ UInt64 sessionId = (UInt64)sessionData.LogonId;
+ if (sessionId == tokenLuidId)
+ {
+ AuthenticationPackage = sessionData.AuthenticationPackage.ToString();
+ LogonType = sessionData.LogonType.ToString();
+ break;
+ }
+ }
+
+ currentSession = IntPtr.Add(currentSession, Marshal.SizeOf(typeof(NativeHelpers.LUID)));
+ }
+ }
+ }
+
+ private void SetTokenMandatoryLabel(SafeNativeHandle hToken)
+ {
+ NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenIntegrityLevel;
+ UInt32 returnLength;
+ NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength);
+ using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength))
+ {
+ if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength))
+ throw new Win32Exception("GetTokenInformation(TokenIntegrityLevel) failed");
+ NativeHelpers.TOKEN_MANDATORY_LABEL label = (NativeHelpers.TOKEN_MANDATORY_LABEL)Marshal.PtrToStructure(
+ infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_MANDATORY_LABEL));
+ MandatoryLabelName = LookupSidName(label.Label.Sid);
+ MandatoryLabelSid = new SecurityIdentifier(label.Label.Sid);
+ }
+ }
+
+ private void SetTokenSource(SafeNativeHandle hToken)
+ {
+ NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenSource;
+ UInt32 returnLength;
+ NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength);
+ using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength))
+ {
+ if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength))
+ throw new Win32Exception("GetTokenInformation(TokenSource) failed");
+ NativeHelpers.TOKEN_SOURCE source = (NativeHelpers.TOKEN_SOURCE)Marshal.PtrToStructure(
+ infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_SOURCE));
+ SourceName = new string(source.SourceName).Replace('\0', ' ').TrimEnd();
+ }
+ }
+
+ private void SetTokenUser(SafeNativeHandle hToken)
+ {
+ NativeHelpers.TokenInformationClass tokenClass = NativeHelpers.TokenInformationClass.TokenUser;
+ UInt32 returnLength;
+ NativeMethods.GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnLength);
+ using (SafeMemoryBuffer infoPtr = new SafeMemoryBuffer((int)returnLength))
+ {
+ if (!NativeMethods.GetTokenInformation(hToken, tokenClass, infoPtr, returnLength, out returnLength))
+ throw new Win32Exception("GetTokenInformation(TokenSource) failed");
+ NativeHelpers.TOKEN_USER user = (NativeHelpers.TOKEN_USER)Marshal.PtrToStructure(
+ infoPtr.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_USER));
+ UserName = LookupSidName(user.User.Sid);
+ UserSid = new SecurityIdentifier(user.User.Sid);
+ }
+ }
+
+ private void SetProfileLoaded()
+ {
+ UInt32 flags;
+ ProfileLoaded = NativeMethods.GetProfileType(out flags);
+ }
+
+ private static string LookupSidName(IntPtr pSid)
+ {
+ StringBuilder name = new StringBuilder(0);
+ StringBuilder domain = new StringBuilder(0);
+ UInt32 nameLength = 0;
+ UInt32 domainLength = 0;
+ UInt32 peUse;
+ NativeMethods.LookupAccountSid(null, pSid, name, ref nameLength, domain, ref domainLength, out peUse);
+ name.EnsureCapacity((int)nameLength);
+ domain.EnsureCapacity((int)domainLength);
+
+ if (!NativeMethods.LookupAccountSid(null, pSid, name, ref nameLength, domain, ref domainLength, out peUse))
+ throw new Win32Exception("LookupAccountSid() failed");
+
+ return String.Format("{0}\\{1}", domain.ToString(), name.ToString());
+ }
+ }
+}
+'@
+ $logon = New-Object -TypeName Ansible.Logon
+ ConvertTo-Json -InputObject $logon
+}.ToString()
+
+$current_user_raw = [Ansible.Process.ProcessUtil]::CreateProcess($null, "powershell.exe -NoProfile -", $null, $null, $test_whoami + "`r`n")
+$current_user = ConvertFrom-Json -InputObject $current_user_raw.StandardOut
+
+$adsi = [ADSI]"WinNT://$env:COMPUTERNAME"
+
+$standard_user = "become_standard"
+$admin_user = "become_admin"
+$become_pass = "password123!$([System.IO.Path]::GetRandomFileName())"
+$medium_integrity_sid = "S-1-16-8192"
+$high_integrity_sid = "S-1-16-12288"
+$system_integrity_sid = "S-1-16-16384"
+
+$tests = @{
+ "Runas standard user" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ }
+
+ "Runas admin user" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ }
+
+ "Runas SYSTEM" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "System"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected "S-1-5-18"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $system_integrity_sid
+
+ $with_domain = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NT AUTHORITY\System", $null, "whoami.exe")
+ $with_domain.StandardOut | Assert-Equal -Expected "nt authority\system`r`n"
+ }
+
+ "Runas LocalService" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("LocalService", $null,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Service"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected "S-1-5-19"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $system_integrity_sid
+
+ $with_domain = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NT AUTHORITY\LocalService", $null, "whoami.exe")
+ $with_domain.StandardOut | Assert-Equal -Expected "nt authority\local service`r`n"
+ }
+
+ "Runas NetworkService" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NetworkService", $null,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Service"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected "S-1-5-20"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $system_integrity_sid
+
+ $with_domain = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("NT AUTHORITY\NetworkService", $null, "whoami.exe")
+ $with_domain.StandardOut | Assert-Equal -Expected "nt authority\network service`r`n"
+ }
+
+ "Runas without working dir set" = {
+ $expected = "$env:SystemRoot\system32`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe $pwd.Path', $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas with working dir set" = {
+ $expected = "$env:SystemRoot`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe $pwd.Path', $env:SystemRoot, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas without environment set" = {
+ $expected = "Windows_NT`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe $env:TEST; $env:OS', $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas with environment set" = {
+ $env_vars = @{
+ TEST = "tesTing"
+ TEST2 = "Testing 2"
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "Interactive", $null,
+ 'cmd.exe /c set', $null, $env_vars, "")
+ ("TEST=tesTing" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ ("TEST2=Testing 2" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ ("OS=Windows_NT" -cnotin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas with string stdin" = {
+ $expected = "input value`r`n`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe [System.Console]::In.ReadToEnd()', $null, $null, "input value")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas with string stdin and newline" = {
+ $expected = "input value`r`n`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe [System.Console]::In.ReadToEnd()', $null, $null, "input value`r`n")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Runas with byte stdin" = {
+ $expected = "input value`r`n"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0, "Interactive", $null,
+ 'powershell.exe [System.Console]::In.ReadToEnd()', $null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value"))
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "Missing executable" = {
+ $failed = $false
+ try {
+ [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, "fake.exe")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "Ansible.Process.Win32Exception"
+ $expected = 'Exception calling "CreateProcessAsUser" with "3" argument(s): "CreateProcessWithTokenW() failed '
+ $expected += '(The system cannot find the file specified, Win32ErrorCode 2)"'
+ $_.Exception.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "CreateProcessAsUser with lpApplicationName" = {
+ $expected = "abc`r`n"
+ $full_path = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe"
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "Interactive", $full_path,
+ "Write-Output 'abc'", $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "Interactive", $full_path,
+ "powershell.exe Write-Output 'abc'", $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcessAsUser with stderr" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "Interactive", $null,
+ "powershell.exe [System.Console]::Error.WriteLine('hi')", $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected ""
+ $actual.StandardError | Assert-Equal -Expected "hi`r`n"
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcessAsUser with exit code" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("SYSTEM", $null, 0, "Interactive", $null,
+ "powershell.exe exit 10", $null, $null, "")
+ $actual.StandardOut | Assert-Equal -Expected ""
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 10
+ }
+
+ "Local account with computer name" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("$env:COMPUTERNAME\$standard_user", $become_pass,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ }
+
+ "Local account with computer as period" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser(".\$standard_user", $become_pass,
+ "powershell.exe -NoProfile -ExecutionPolicy ByPass -File $tmp_script")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ }
+
+ "Local account with invalid password" = {
+ $failed = $false
+ try {
+ [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, "incorrect", "powershell.exe Write-Output abc")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "Ansible.AccessToken.Win32Exception"
+ # Server 2008 has a slightly different error msg, just assert we get the error 1326
+ ($_.Exception.Message.Contains("Win32ErrorCode 1326")) | Assert-Equal -Expected $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Invalid account" = {
+ $failed = $false
+ try {
+ [Ansible.Become.BecomeUtil]::CreateProcessAsUser("incorrect", "incorrect", "powershell.exe Write-Output abc")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "System.Security.Principal.IdentityNotMappedException"
+ $expected = 'Exception calling "CreateProcessAsUser" with "3" argument(s): "Some or all '
+ $expected += 'identity references could not be translated."'
+ $_.Exception.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Interactive logon with standard" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "WithProfile",
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Batch logon with standard" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "WithProfile",
+ "Batch", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Batch"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Network logon with standard" = {
+ # Server 2008 will not work with become to Network or Network Credentials
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "WithProfile",
+ "Network", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Network"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Network with cleartext logon with standard" = {
+ # Server 2008 will not work with become to Network or Network Cleartext
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, $become_pass, "WithProfile",
+ "NetworkCleartext", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "NetworkCleartext"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Logon without password with standard" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, [NullString]::Value, "WithProfile",
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ # Too unstable, there might be another process still lingering which causes become to steal instead of using
+ # S4U. Just don't check the type and source to verify we can become without a password
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ # $stdout.LogonType | Assert-Equal -Expected "Batch"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ # $stdout.SourceName | Assert-Equal -Expected "ansible"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Logon without password and network type with standard" = {
+ # Server 2008 will not work with become to Network or Network Cleartext
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($standard_user, [NullString]::Value, "WithProfile",
+ "Network", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ # Too unstable, there might be another process still lingering which causes become to steal instead of using
+ # S4U. Just don't check the type and source to verify we can become without a password
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ # $stdout.LogonType | Assert-Equal -Expected "Network"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $medium_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ # $stdout.SourceName | Assert-Equal -Expected "ansible"
+ $stdout.UserSid.Value | Assert-Equal -Expected $standard_user_sid
+ }
+
+ "Interactive logon with admin" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "WithProfile",
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Batch logon with admin" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "WithProfile",
+ "Batch", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Batch"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Network logon with admin" = {
+ # Server 2008 will not work with become to Network or Network Credentials
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "WithProfile",
+ "Network", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Network"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Network with cleartext logon with admin" = {
+ # Server 2008 will not work with become to Network or Network Credentials
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, "WithProfile",
+ "NetworkCleartext", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "NetworkCleartext"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Fail to logon with null or empty password" = {
+ $failed = $false
+ try {
+ # Having $null or an empty string means we are trying to become a user with a blank password and not
+ # become without setting the password. This is confusing as $null gets converted to "" and we need to
+ # use [NullString]::Value instead if we want that behaviour. This just tests to see that an empty
+ # string won't go the S4U route.
+ [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $null, "WithProfile",
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "Ansible.AccessToken.Win32Exception"
+ # Server 2008 has a slightly different error msg, just assert we get the error 1326
+ ($_.Exception.Message.Contains("Win32ErrorCode 1326")) | Assert-Equal -Expected $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Logon without password with admin" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, [NullString]::Value, "WithProfile",
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ # Too unstable, there might be another process still lingering which causes become to steal instead of using
+ # S4U. Just don't check the type and source to verify we can become without a password
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ # $stdout.LogonType | Assert-Equal -Expected "Batch"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ # $stdout.SourceName | Assert-Equal -Expected "ansible"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Logon without password and network type with admin" = {
+ # become network doesn't work on Server 2008
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.1") {
+ continue
+ }
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, [NullString]::Value, "WithProfile",
+ "Network", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ # Too unstable, there might be another process still lingering which causes become to steal instead of using
+ # S4U. Just don't check the type and source to verify we can become without a password
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ # $stdout.LogonType | Assert-Equal -Expected "Network"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $true
+ # $stdout.SourceName | Assert-Equal -Expected "ansible"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Logon without profile with admin" = {
+ # Server 2008 and 2008 R2 does not support running without the profile being set
+ if ([System.Environment]::OSVersion.Version -lt [Version]"6.2") {
+ continue
+ }
+
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($admin_user, $become_pass, 0,
+ "Interactive", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "Interactive"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $high_integrity_sid
+ $stdout.ProfileLoaded | Assert-Equal -Expected $false
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $admin_user_sid
+ }
+
+ "Logon with network credentials and no profile" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("fakeuser", "fakepassword", "NetcredentialsOnly",
+ "NewCredentials", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "NewCredentials"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $current_user.MandatoryLabelSid.Value
+
+ # while we didn't set WithProfile, the new process is based on the current process
+ $stdout.ProfileLoaded | Assert-Equal -Expected $current_user.ProfileLoaded
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $current_user.UserSid.Value
+ }
+
+ "Logon with network credentials and with profile" = {
+ $actual = [Ansible.Become.BecomeUtil]::CreateProcessAsUser("fakeuser", "fakepassword", "NetcredentialsOnly, WithProfile",
+ "NewCredentials", $null, "powershell.exe -NoProfile -", $tmp_dir, $null, $test_whoami + "`r`n")
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $stdout = ConvertFrom-Json -InputObject $actual.StandardOut
+ $stdout.LogonType | Assert-Equal -Expected "NewCredentials"
+ $stdout.MandatoryLabelSid.Value | Assert-Equal -Expected $current_user.MandatoryLabelSid.Value
+ $stdout.ProfileLoaded | Assert-Equal -Expected $current_user.ProfileLoaded
+ $stdout.SourceName | Assert-Equal -Expected "Advapi"
+ $stdout.UserSid.Value | Assert-Equal -Expected $current_user.UserSid.Value
+ }
+}
+
+try {
+ $tmp_dir = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName())
+ New-Item -Path $tmp_dir -ItemType Directory > $null
+ $acl = Get-Acl -Path $tmp_dir
+ $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
+ New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList ([System.Security.Principal.WellKnownSidType]::WorldSid, $null)
+ [System.Security.AccessControl.FileSystemRights]::FullControl,
+ [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
+ [System.Security.AccessControl.PropagationFlags]::None,
+ [System.Security.AccessControl.AccessControlType]::Allow
+ )
+ $acl.AddAccessRule($ace)
+ Set-Acl -Path $tmp_dir -AclObject $acl
+
+ $tmp_script = Join-Path -Path $tmp_dir -ChildPath "whoami.ps1"
+ Set-Content -LiteralPath $tmp_script -Value $test_whoami
+
+ foreach ($user in $standard_user, $admin_user) {
+ $user_obj = $adsi.Children | Where-Object { $_.SchemaClassName -eq "User" -and $_.Name -eq $user }
+ if ($null -eq $user_obj) {
+ $user_obj = $adsi.Create("User", $user)
+ $user_obj.SetPassword($become_pass)
+ $user_obj.SetInfo()
+ }
+ else {
+ $user_obj.SetPassword($become_pass)
+ }
+ $user_obj.RefreshCache()
+
+ if ($user -eq $standard_user) {
+ $standard_user_sid = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($user_obj.ObjectSid.Value, 0)).Value
+ $group = [System.Security.Principal.WellKnownSidType]::BuiltinUsersSid
+ }
+ else {
+ $admin_user_sid = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($user_obj.ObjectSid.Value, 0)).Value
+ $group = [System.Security.Principal.WellKnownSidType]::BuiltinAdministratorsSid
+ }
+ $group = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $group, $null).Value
+ [string[]]$current_groups = $user_obj.Groups() | ForEach-Object {
+ New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @(
+ $_.GetType().InvokeMember("objectSID", "GetProperty", $null, $_, $null),
+ 0
+ )
+ }
+ if ($current_groups -notcontains $group) {
+ $group_obj = $adsi.Children | Where-Object {
+ if ($_.SchemaClassName -eq "Group") {
+ $group_sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($_.objectSID.Value, 0)
+ $group_sid -eq $group
+ }
+ }
+ $group_obj.Add($user_obj.Path)
+ }
+ }
+ foreach ($test_impl in $tests.GetEnumerator()) {
+ $test = $test_impl.Key
+ &$test_impl.Value
+ }
+}
+finally {
+ Remove-Item -LiteralPath $tmp_dir -Force -Recurse
+ foreach ($user in $standard_user, $admin_user) {
+ $user_obj = $adsi.Children | Where-Object { $_.SchemaClassName -eq "User" -and $_.Name -eq $user }
+ $adsi.Delete("User", $user_obj.Name.Value)
+ }
+}
+
+
+$module.Result.data = "success"
+$module.ExitJson()
+
diff --git a/test/integration/targets/module_utils_Ansible.Become/tasks/main.yml b/test/integration/targets/module_utils_Ansible.Become/tasks/main.yml
new file mode 100644
index 0000000..deb228b
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Become/tasks/main.yml
@@ -0,0 +1,28 @@
+---
+# Users by default don't have this right, temporarily enable it
+- name: ensure the Users group have the SeBatchLogonRight
+ win_user_right:
+ name: SeBatchLogonRight
+ users:
+ - Users
+ action: add
+ register: batch_user_add
+
+- block:
+ - name: test Ansible.Become.cs
+ ansible_become_tests:
+ register: ansible_become_tests
+
+ always:
+ - name: remove SeBatchLogonRight from users if added in test
+ win_user_right:
+ name: SeBatchLogonRight
+ users:
+ - Users
+ action: remove
+ when: batch_user_add is changed
+
+- name: assert test Ansible.Become.cs
+ assert:
+ that:
+ - ansible_become_tests.data == "success"
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1
new file mode 100644
index 0000000..d18c42d
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1
@@ -0,0 +1,332 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.AddType
+
+$ErrorActionPreference = "Stop"
+
+$result = @{
+ changed = $false
+}
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ $call_stack = (Get-PSCallStack)[1]
+ $error_msg = -join @(
+ "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: "
+ "$($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
+ )
+ Fail-Json -obj $result -message $error_msg
+ }
+}
+
+$code = @'
+using System;
+
+namespace Namespace1
+{
+ public class Class1
+ {
+ public static string GetString(bool error)
+ {
+ if (error)
+ throw new Exception("error");
+ return "Hello World";
+ }
+ }
+}
+'@
+$res = Add-CSharpType -References $code
+Assert-Equal -actual $res -expected $null
+
+$actual = [Namespace1.Class1]::GetString($false)
+Assert-Equal $actual -expected "Hello World"
+
+try {
+ [Namespace1.Class1]::GetString($true)
+}
+catch {
+ Assert-Equal ($_.Exception.ToString().Contains("at Namespace1.Class1.GetString(Boolean error)`r`n")) -expected $true
+}
+
+$code_debug = @'
+using System;
+
+namespace Namespace2
+{
+ public class Class2
+ {
+ public static string GetString(bool error)
+ {
+ if (error)
+ throw new Exception("error");
+ return "Hello World";
+ }
+ }
+}
+'@
+$res = Add-CSharpType -References $code_debug -IncludeDebugInfo
+Assert-Equal -actual $res -expected $null
+
+$actual = [Namespace2.Class2]::GetString($false)
+Assert-Equal $actual -expected "Hello World"
+
+try {
+ [Namespace2.Class2]::GetString($true)
+}
+catch {
+ $tmp_path = [System.IO.Path]::GetFullPath($env:TMP).ToLower()
+ Assert-Equal ($_.Exception.ToString().ToLower().Contains("at namespace2.class2.getstring(boolean error) in $tmp_path")) -expected $true
+ Assert-Equal ($_.Exception.ToString().Contains(".cs:line 10")) -expected $true
+}
+
+$code_tmp = @'
+using System;
+
+namespace Namespace3
+{
+ public class Class3
+ {
+ public static string GetString(bool error)
+ {
+ if (error)
+ throw new Exception("error");
+ return "Hello World";
+ }
+ }
+}
+'@
+$tmp_path = $env:USERPROFILE
+$res = Add-CSharpType -References $code_tmp -IncludeDebugInfo -TempPath $tmp_path -PassThru
+Assert-Equal -actual $res.GetType().Name -expected "RuntimeAssembly"
+Assert-Equal -actual $res.Location -expected ""
+Assert-Equal -actual $res.GetTypes().Length -expected 1
+Assert-Equal -actual $res.GetTypes()[0].Name -expected "Class3"
+
+$actual = [Namespace3.Class3]::GetString($false)
+Assert-Equal $actual -expected "Hello World"
+
+try {
+ [Namespace3.Class3]::GetString($true)
+}
+catch {
+ $actual = $_.Exception.ToString().ToLower().Contains("at namespace3.class3.getstring(boolean error) in $($tmp_path.ToLower())")
+ Assert-Equal $actual -expected $true
+ Assert-Equal ($_.Exception.ToString().Contains(".cs:line 10")) -expected $true
+}
+
+$warning_code = @'
+using System;
+
+namespace Namespace4
+{
+ public class Class4
+ {
+ public static string GetString(bool test)
+ {
+ if (test)
+ {
+ string a = "";
+ }
+
+ return "Hello World";
+ }
+ }
+}
+'@
+$failed = $false
+try {
+ Add-CSharpType -References $warning_code
+}
+catch {
+ $failed = $true
+ $actual = $_.Exception.Message.Contains("error CS0219: Warning as Error: The variable 'a' is assigned but its value is never used")
+ Assert-Equal -actual $actual -expected $true
+}
+Assert-Equal -actual $failed -expected $true
+
+Add-CSharpType -References $warning_code -IgnoreWarnings
+$actual = [Namespace4.Class4]::GetString($true)
+Assert-Equal -actual $actual -expected "Hello World"
+
+$reference_1 = @'
+using System;
+using System.Web.Script.Serialization;
+
+//AssemblyReference -Name System.Web.Extensions.dll
+
+namespace Namespace5
+{
+ public class Class5
+ {
+ public static string GetString()
+ {
+ return "Hello World";
+ }
+ }
+}
+'@
+
+$reference_2 = @'
+using System;
+using Namespace5;
+using System.Management.Automation;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Namespace6
+{
+ public class Class6
+ {
+ public static string GetString()
+ {
+ Hashtable hash = new Hashtable();
+ hash["test"] = "abc";
+ return Class5.GetString();
+ }
+ }
+}
+'@
+
+Add-CSharpType -References $reference_1, $reference_2
+$actual = [Namespace6.Class6]::GetString()
+Assert-Equal -actual $actual -expected "Hello World"
+
+$ignored_warning = @'
+using System;
+
+//NoWarn -Name CS0219
+
+namespace Namespace7
+{
+ public class Class7
+ {
+ public static string GetString()
+ {
+ string a = "";
+ return "abc";
+ }
+ }
+}
+'@
+Add-CSharpType -References $ignored_warning
+$actual = [Namespace7.Class7]::GetString()
+Assert-Equal -actual $actual -expected "abc"
+
+$defined_symbol = @'
+using System;
+
+namespace Namespace8
+{
+ public class Class8
+ {
+ public static string GetString()
+ {
+#if SYMBOL1
+ string a = "symbol";
+#else
+ string a = "no symbol";
+#endif
+ return a;
+ }
+ }
+}
+'@
+Add-CSharpType -References $defined_symbol -CompileSymbols "SYMBOL1"
+$actual = [Namespace8.Class8]::GetString()
+Assert-Equal -actual $actual -expected "symbol"
+
+$type_accelerator = @'
+using System;
+
+//TypeAccelerator -Name AnsibleType -TypeName Class9
+
+namespace Namespace9
+{
+ public class Class9
+ {
+ public static string GetString()
+ {
+ return "a";
+ }
+ }
+}
+'@
+Add-CSharpType -Reference $type_accelerator
+$actual = [AnsibleType]::GetString()
+Assert-Equal -actual $actual -expected "a"
+
+$missing_type_class = @'
+using System;
+
+//TypeAccelerator -Name AnsibleTypeMissing -TypeName MissingClass
+
+namespace Namespace10
+{
+ public class Class10
+ {
+ public static string GetString()
+ {
+ return "b";
+ }
+ }
+}
+'@
+$failed = $false
+try {
+ Add-CSharpType -Reference $missing_type_class
+}
+catch {
+ $failed = $true
+ Assert-Equal -actual $_.Exception.Message -expected "Failed to find compiled class 'MissingClass' for custom TypeAccelerator."
+}
+Assert-Equal -actual $failed -expected $true
+
+$arch_class = @'
+using System;
+
+namespace Namespace11
+{
+ public class Class11
+ {
+ public static int GetIntPtrSize()
+ {
+#if X86
+ return 4;
+#elif AMD64
+ return 8;
+#else
+ return 0;
+#endif
+ }
+ }
+}
+'@
+Add-CSharpType -Reference $arch_class
+Assert-Equal -actual ([Namespace11.Class11]::GetIntPtrSize()) -expected ([System.IntPtr]::Size)
+
+$lib_set = @'
+using System;
+
+namespace Namespace12
+{
+ public class Class12
+ {
+ public static string GetString()
+ {
+ return "b";
+ }
+ }
+}
+'@
+$env:LIB = "C:\fake\folder\path"
+try {
+ Add-CSharpType -Reference $lib_set
+}
+finally {
+ Remove-Item -LiteralPath env:\LIB
+}
+Assert-Equal -actual ([Namespace12.Class12]::GetString()) -expected "b"
+
+$result.res = "success"
+Exit-Json -obj $result
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/tasks/main.yml
new file mode 100644
index 0000000..4c4810b
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: call module with AddType tests
+ add_type_test:
+ register: add_type_test
+
+- name: assert call module with AddType tests
+ assert:
+ that:
+ - not add_type_test is failed
+ - add_type_test.res == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/library/argv_parser_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/library/argv_parser_test.ps1
new file mode 100644
index 0000000..d7bd4bb
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/library/argv_parser_test.ps1
@@ -0,0 +1,93 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.ArgvParser
+
+$ErrorActionPreference = 'Continue'
+
+$params = Parse-Args $args
+$exe = Get-AnsibleParam -obj $params -name "exe" -type "path" -failifempty $true
+
+Add-Type -TypeDefinition @'
+using System.IO;
+using System.Threading;
+
+namespace Ansible.Command
+{
+ public static class NativeUtil
+ {
+ public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
+ {
+ var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
+ var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
+ string so = null, se = null;
+ ThreadPool.QueueUserWorkItem((s)=>
+ {
+ so = stdoutStream.ReadToEnd();
+ sowait.Set();
+ });
+ ThreadPool.QueueUserWorkItem((s) =>
+ {
+ se = stderrStream.ReadToEnd();
+ sewait.Set();
+ });
+ foreach(var wh in new WaitHandle[] { sowait, sewait })
+ wh.WaitOne();
+ stdout = so;
+ stderr = se;
+ }
+ }
+}
+'@
+
+Function Invoke-Process($executable, $arguments) {
+ $proc = New-Object System.Diagnostics.Process
+ $psi = $proc.StartInfo
+ $psi.FileName = $executable
+ $psi.Arguments = $arguments
+ $psi.RedirectStandardOutput = $true
+ $psi.RedirectStandardError = $true
+ $psi.UseShellExecute = $false
+
+ $proc.Start() > $null # will always return $true for non shell-exec cases
+ $stdout = $stderr = [string] $null
+
+ [Ansible.Command.NativeUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) > $null
+ $proc.WaitForExit() > $null
+ $actual_args = $stdout.Substring(0, $stdout.Length - 2) -split "`r`n"
+
+ return $actual_args
+}
+
+$tests = @(
+ @('abc', 'd', 'e'),
+ @('a\\b', 'de fg', 'h'),
+ @('a\"b', 'c', 'd'),
+ @('a\\b c', 'd', 'e'),
+ @('C:\Program Files\file\', 'arg with " quote'),
+ @('ADDLOCAL="a,b,c"', '/s', 'C:\\Double\\Backslash')
+)
+
+foreach ($expected in $tests) {
+ $joined_string = Argv-ToString -arguments $expected
+ # We can't used CommandLineToArgvW to test this out as it seems to mangle
+ # \, might be something to do with unicode but not sure...
+ $actual = Invoke-Process -executable $exe -arguments $joined_string
+
+ if ($expected.Count -ne $actual.Count) {
+ $result.actual = $actual -join "`n"
+ $result.expected = $expected -join "`n"
+ Fail-Json -obj $result -message "Actual arg count: $($actual.Count) != Expected arg count: $($expected.Count)"
+ }
+ for ($i = 0; $i -lt $expected.Count; $i++) {
+ $expected_arg = $expected[$i]
+ $actual_arg = $actual[$i]
+ if ($expected_arg -cne $actual_arg) {
+ $result.actual = $actual -join "`n"
+ $result.expected = $expected -join "`n"
+ Fail-Json -obj $result -message "Actual arg: '$actual_arg' != Expected arg: '$expected_arg'"
+ }
+ }
+}
+
+Exit-Json @{ data = 'success' }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/meta/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/meta/main.yml
new file mode 100644
index 0000000..fd0dc54
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+- setup_win_printargv
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/tasks/main.yml
new file mode 100644
index 0000000..b39155e
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: call module with ArgvParser tests
+ argv_parser_test:
+ exe: '{{ win_printargv_path }}'
+ register: argv_test
+
+- assert:
+ that:
+ - argv_test.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/library/backup_file_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/library/backup_file_test.ps1
new file mode 100644
index 0000000..39beab7
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/library/backup_file_test.ps1
@@ -0,0 +1,92 @@
+#!powershell
+
+# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.Backup
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+$tmp_dir = $module.Tmpdir
+
+$tests = @{
+ "Test backup file with missing file" = {
+ $actual = Backup-File -path (Join-Path -Path $tmp_dir -ChildPath "missing")
+ $actual | Assert-Equal -Expected $null
+ }
+
+ "Test backup file in check mode" = {
+ $orig_file = Join-Path -Path $tmp_dir -ChildPath "file-check.txt"
+ Set-Content -LiteralPath $orig_file -Value "abc"
+ $actual = Backup-File -path $orig_file -WhatIf
+
+ (Test-Path -LiteralPath $actual) | Assert-Equal -Expected $false
+
+ $parent_dir = Split-Path -LiteralPath $actual
+ $backup_file = Split-Path -Path $actual -Leaf
+ $parent_dir | Assert-Equal -Expected $tmp_dir
+ ($backup_file -match "^file-check\.txt\.$pid\.\d{8}-\d{6}\.bak$") | Assert-Equal -Expected $true
+ }
+
+ "Test backup file" = {
+ $content = "abc"
+ $orig_file = Join-Path -Path $tmp_dir -ChildPath "file.txt"
+ Set-Content -LiteralPath $orig_file -Value $content
+ $actual = Backup-File -path $orig_file
+
+ (Test-Path -LiteralPath $actual) | Assert-Equal -Expected $true
+
+ $parent_dir = Split-Path -LiteralPath $actual
+ $backup_file = Split-Path -Path $actual -Leaf
+ $parent_dir | Assert-Equal -Expected $tmp_dir
+ ($backup_file -match "^file\.txt\.$pid\.\d{8}-\d{6}\.bak$") | Assert-Equal -Expected $true
+ (Get-Content -LiteralPath $actual -Raw) | Assert-Equal -Expected "$content`r`n"
+ }
+}
+
+foreach ($test_impl in $tests.GetEnumerator()) {
+ $test = $test_impl.Key
+ &$test_impl.Value
+}
+
+$module.Result.res = 'success'
+
+$module.ExitJson()
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/tasks/main.yml
new file mode 100644
index 0000000..cb979eb
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: call module with BackupFile tests
+ backup_file_test:
+ register: backup_file_test
+
+- name: assert call module with BackupFile tests
+ assert:
+ that:
+ - not backup_file_test is failed
+ - backup_file_test.res == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/library/camel_conversion_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/library/camel_conversion_test.ps1
new file mode 100644
index 0000000..bcb9558
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/library/camel_conversion_test.ps1
@@ -0,0 +1,81 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.CamelConversion
+
+$ErrorActionPreference = 'Stop'
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ Fail-Json @{} "actual != expected`nActual: $actual`nExpected: $expected"
+ }
+}
+
+$input_dict = @{
+ alllower = 'alllower'
+ ALLUPPER = 'allupper'
+ camelCase = 'camel_case'
+ mixedCase_withCamel = 'mixed_case_with_camel'
+ TwoWords = 'two_words'
+ AllUpperAtEND = 'all_upper_at_end'
+ AllUpperButPLURALs = 'all_upper_but_plurals'
+ TargetGroupARNs = 'target_group_arns'
+ HTTPEndpoints = 'http_endpoints'
+ PLURALs = 'plurals'
+ listDict = @(
+ @{ entry1 = 'entry1'; entryTwo = 'entry_two' },
+ 'stringTwo',
+ 0
+ )
+ INNERHashTable = @{
+ ID = 'id'
+ IEnumerable = 'i_enumerable'
+ }
+ emptyList = @()
+ singleList = @("a")
+}
+
+$output_dict = Convert-DictToSnakeCase -dict $input_dict
+foreach ($entry in $output_dict.GetEnumerator()) {
+ $key = $entry.Name
+ $value = $entry.Value
+
+ if ($value -is [Hashtable]) {
+ Assert-Equal -actual $key -expected "inner_hash_table"
+ foreach ($inner_hash in $value.GetEnumerator()) {
+ Assert-Equal -actual $inner_hash.Name -expected $inner_hash.Value
+ }
+ }
+ elseif ($value -is [Array] -or $value -is [System.Collections.ArrayList]) {
+ if ($key -eq "list_dict") {
+ foreach ($inner_list in $value) {
+ if ($inner_list -is [Hashtable]) {
+ foreach ($inner_list_hash in $inner_list.GetEnumerator()) {
+ Assert-Equal -actual $inner_list_hash.Name -expected $inner_list_hash.Value
+ }
+ }
+ elseif ($inner_list -is [String]) {
+ # this is not a string key so we need to keep it the same
+ Assert-Equal -actual $inner_list -expected "stringTwo"
+ }
+ else {
+ Assert-Equal -actual $inner_list -expected 0
+ }
+ }
+ }
+ elseif ($key -eq "empty_list") {
+ Assert-Equal -actual $value.Count -expected 0
+ }
+ elseif ($key -eq "single_list") {
+ Assert-Equal -actual $value.Count -expected 1
+ }
+ else {
+ Fail-Json -obj $result -message "invalid key found for list $key"
+ }
+ }
+ else {
+ Assert-Equal -actual $key -expected $value
+ }
+}
+
+Exit-Json @{ data = 'success' }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/tasks/main.yml
new file mode 100644
index 0000000..f28ea30
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+- name: call module with camel conversion tests
+ camel_conversion_test:
+ register: camel_conversion
+
+- assert:
+ that:
+ - camel_conversion.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/library/command_util_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/library/command_util_test.ps1
new file mode 100644
index 0000000..ebffae7
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/library/command_util_test.ps1
@@ -0,0 +1,139 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+$ErrorActionPreference = 'Stop'
+
+$params = Parse-Args $args
+$exe = Get-AnsibleParam -obj $params -name "exe" -type "path" -failifempty $true
+
+$result = @{
+ changed = $false
+}
+
+$exe_directory = Split-Path -Path $exe -Parent
+$exe_filename = Split-Path -Path $exe -Leaf
+$test_name = $null
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ Fail-Json -obj $result -message "Test $test_name failed`nActual: '$actual' != Expected: '$expected'"
+ }
+}
+
+$test_name = "full exe path"
+$actual = Run-Command -command "`"$exe`" arg1 arg2 `"arg 3`""
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "arg1`r`narg2`r`narg 3`r`n"
+Assert-Equal -actual $actual.stderr -expected ""
+Assert-Equal -actual $actual.executable -expected $exe
+
+$test_name = "exe in special char dir"
+$tmp_dir = Join-Path -Path $env:TEMP -ChildPath "ansible .ÅÑŚÌβŁÈ [$!@^&test(;)]"
+try {
+ New-Item -Path $tmp_dir -ItemType Directory > $null
+ $exe_special = Join-Path $tmp_dir -ChildPath "PrintArgv.exe"
+ Copy-Item -LiteralPath $exe -Destination $exe_special
+ $actual = Run-Command -command "`"$exe_special`" arg1 arg2 `"arg 3`""
+}
+finally {
+ Remove-Item -LiteralPath $tmp_dir -Force -Recurse
+}
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "arg1`r`narg2`r`narg 3`r`n"
+Assert-Equal -actual $actual.stderr -expected ""
+Assert-Equal -actual $actual.executable -expected $exe_special
+
+$test_name = "invalid exe path"
+try {
+ $actual = Run-Command -command "C:\fakepath\$exe_filename arg1"
+ Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception"
+}
+catch {
+ $expected = "Exception calling `"SearchPath`" with `"1`" argument(s): `"Could not find file 'C:\fakepath\$exe_filename'.`""
+ Assert-Equal -actual $_.Exception.Message -expected $expected
+}
+
+$test_name = "exe in current folder"
+$actual = Run-Command -command "$exe_filename arg1" -working_directory $exe_directory
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "arg1`r`n"
+Assert-Equal -actual $actual.stderr -expected ""
+Assert-Equal -actual $actual.executable -expected $exe
+
+$test_name = "no working directory set"
+$actual = Run-Command -command "cmd.exe /c cd"
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "$($pwd.Path)`r`n"
+Assert-Equal -actual $actual.stderr -expected ""
+Assert-Equal -actual $actual.executable.ToUpper() -expected "$env:SystemRoot\System32\cmd.exe".ToUpper()
+
+$test_name = "working directory override"
+$actual = Run-Command -command "cmd.exe /c cd" -working_directory $env:SystemRoot
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "$env:SystemRoot`r`n"
+Assert-Equal -actual $actual.stderr -expected ""
+Assert-Equal -actual $actual.executable.ToUpper() -expected "$env:SystemRoot\System32\cmd.exe".ToUpper()
+
+$test_name = "working directory invalid path"
+try {
+ $actual = Run-Command -command "doesn't matter" -working_directory "invalid path here"
+ Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception"
+}
+catch {
+ Assert-Equal -actual $_.Exception.Message -expected "invalid working directory path 'invalid path here'"
+}
+
+$test_name = "invalid arguments"
+$actual = Run-Command -command "ipconfig.exe /asdf"
+Assert-Equal -actual $actual.rc -expected 1
+
+$test_name = "test stdout and stderr streams"
+$actual = Run-Command -command "cmd.exe /c echo stdout && echo stderr 1>&2"
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "stdout `r`n"
+Assert-Equal -actual $actual.stderr -expected "stderr `r`n"
+
+$test_name = "Test UTF8 output from stdout stream"
+$actual = Run-Command -command "powershell.exe -ExecutionPolicy ByPass -Command `"Write-Host '💩'`""
+Assert-Equal -actual $actual.rc -expected 0
+Assert-Equal -actual $actual.stdout -expected "💩`n"
+Assert-Equal -actual $actual.stderr -expected ""
+
+$test_name = "test default environment variable"
+Set-Item -LiteralPath env:TESTENV -Value "test"
+$actual = Run-Command -command "cmd.exe /c set"
+$env_present = $actual.stdout -split "`r`n" | Where-Object { $_ -eq "TESTENV=test" }
+if ($null -eq $env_present) {
+ Fail-Json -obj $result -message "Test $test_name failed`nenvironment variable TESTENV not found in stdout`n$($actual.stdout)"
+}
+
+$test_name = "test custom environment variable1"
+$actual = Run-Command -command "cmd.exe /c set" -environment @{ TESTENV2 = "testing" }
+$env_not_present = $actual.stdout -split "`r`n" | Where-Object { $_ -eq "TESTENV=test" }
+$env_present = $actual.stdout -split "`r`n" | Where-Object { $_ -eq "TESTENV2=testing" }
+if ($null -ne $env_not_present) {
+ Fail-Json -obj $result -message "Test $test_name failed`nenvironment variabel TESTENV found in stdout when it should be`n$($actual.stdout)"
+}
+if ($null -eq $env_present) {
+ Fail-json -obj $result -message "Test $test_name failed`nenvironment variable TESTENV2 not found in stdout`n$($actual.stdout)"
+}
+
+$test_name = "input test"
+$wrapper = @"
+begin {
+ `$string = ""
+} process {
+ `$current_input = [string]`$input
+ `$string += `$current_input
+} end {
+ Write-Host `$string
+}
+"@
+$encoded_wrapper = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($wrapper))
+$actual = Run-Command -command "powershell.exe -ExecutionPolicy ByPass -EncodedCommand $encoded_wrapper" -stdin "Ansible"
+Assert-Equal -actual $actual.stdout -expected "Ansible`n"
+
+$result.data = "success"
+Exit-Json -obj $result
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/meta/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/meta/main.yml
new file mode 100644
index 0000000..fd0dc54
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+- setup_win_printargv
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/tasks/main.yml
new file mode 100644
index 0000000..3001518
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: call module with CommandUtil tests
+ command_util_test:
+ exe: '{{ win_printargv_path }}'
+ register: command_util
+
+- assert:
+ that:
+ - command_util.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/library/file_util_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/library/file_util_test.ps1
new file mode 100644
index 0000000..c38f4e6
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/library/file_util_test.ps1
@@ -0,0 +1,114 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.FileUtil
+
+$ErrorActionPreference = "Stop"
+
+$result = @{
+ changed = $false
+}
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ $call_stack = (Get-PSCallStack)[1]
+ $error_msg = -join @(
+ "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: "
+ "$($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
+ )
+ Fail-Json -obj $result -message $error_msg
+ }
+}
+
+Function Get-PagefilePath() {
+ $pagefile = $null
+ $cs = Get-CimInstance -ClassName Win32_ComputerSystem
+ if ($cs.AutomaticManagedPagefile) {
+ $pagefile = "$($env:SystemRoot.Substring(0, 1)):\pagefile.sys"
+ }
+ else {
+ $pf = Get-CimInstance -ClassName Win32_PageFileSetting
+ if ($null -ne $pf) {
+ $pagefile = $pf[0].Name
+ }
+ }
+ return $pagefile
+}
+
+$pagefile = Get-PagefilePath
+if ($pagefile) {
+ # Test-AnsiblePath Hidden system file
+ $actual = Test-AnsiblePath -Path $pagefile
+ Assert-Equal -actual $actual -expected $true
+
+ # Get-AnsibleItem file
+ $actual = Get-AnsibleItem -Path $pagefile
+ Assert-Equal -actual $actual.FullName -expected $pagefile
+ Assert-Equal -actual $actual.Attributes.HasFlag([System.IO.FileAttributes]::Directory) -expected $false
+ Assert-Equal -actual $actual.Exists -expected $true
+}
+
+# Test-AnsiblePath File that doesn't exist
+$actual = Test-AnsiblePath -Path C:\fakefile
+Assert-Equal -actual $actual -expected $false
+
+# Test-AnsiblePath Directory that doesn't exist
+$actual = Test-AnsiblePath -Path C:\fakedirectory
+Assert-Equal -actual $actual -expected $false
+
+# Test-AnsiblePath file in non-existant directory
+$actual = Test-AnsiblePath -Path C:\fakedirectory\fakefile.txt
+Assert-Equal -actual $actual -expected $false
+
+# Test-AnsiblePath Normal directory
+$actual = Test-AnsiblePath -Path C:\Windows
+Assert-Equal -actual $actual -expected $true
+
+# Test-AnsiblePath Normal file
+$actual = Test-AnsiblePath -Path C:\Windows\System32\kernel32.dll
+Assert-Equal -actual $actual -expected $true
+
+# Test-AnsiblePath fails with wildcard
+$failed = $false
+try {
+ Test-AnsiblePath -Path C:\Windows\*.exe
+}
+catch {
+ $failed = $true
+ Assert-Equal -actual $_.Exception.Message -expected "Exception calling `"GetAttributes`" with `"1`" argument(s): `"Illegal characters in path.`""
+}
+Assert-Equal -actual $failed -expected $true
+
+# Test-AnsiblePath on non file PS Provider object
+$actual = Test-AnsiblePath -Path Cert:\LocalMachine\My
+Assert-Equal -actual $actual -expected $true
+
+# Test-AnsiblePath on environment variable
+$actual = Test-AnsiblePath -Path env:SystemDrive
+Assert-Equal -actual $actual -expected $true
+
+# Test-AnsiblePath on environment variable that does not exist
+$actual = Test-AnsiblePath -Path env:FakeEnvValue
+Assert-Equal -actual $actual -expected $false
+
+# Get-AnsibleItem doesn't exist with -ErrorAction SilentlyContinue param
+$actual = Get-AnsibleItem -Path C:\fakefile -ErrorAction SilentlyContinue
+Assert-Equal -actual $actual -expected $null
+
+# Get-AnsibleItem directory
+$actual = Get-AnsibleItem -Path C:\Windows
+Assert-Equal -actual $actual.FullName -expected C:\Windows
+Assert-Equal -actual $actual.Attributes.HasFlag([System.IO.FileAttributes]::Directory) -expected $true
+Assert-Equal -actual $actual.Exists -expected $true
+
+# ensure Get-AnsibleItem doesn't fail in a try/catch and -ErrorAction SilentlyContinue - stop's a trap from trapping it
+try {
+ $actual = Get-AnsibleItem -Path C:\fakepath -ErrorAction SilentlyContinue
+}
+catch {
+ Fail-Json -obj $result -message "this should not fire"
+}
+Assert-Equal -actual $actual -expected $null
+
+$result.data = "success"
+Exit-Json -obj $result
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/tasks/main.yml
new file mode 100644
index 0000000..a636d32
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+- name: call module with FileUtil tests
+ file_util_test:
+ register: file_util_test
+
+- assert:
+ that:
+ - file_util_test.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testlist.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testlist.ps1
new file mode 100644
index 0000000..06ef17b
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testlist.ps1
@@ -0,0 +1,12 @@
+#powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$params = Parse-Args $args
+$value = Get-AnsibleParam -Obj $params -Name value -Type list
+
+if ($value -isnot [array]) {
+ Fail-Json -obj @{} -message "value was not a list but was $($value.GetType().FullName)"
+}
+
+Exit-Json @{ count = $value.Count }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testpath.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testpath.ps1
new file mode 100644
index 0000000..7a6ba0b
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testpath.ps1
@@ -0,0 +1,9 @@
+#powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$params = Parse-Args $args
+
+$path = Get-AnsibleParam -Obj $params -Name path -Type path
+
+Exit-Json @{ path = $path }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/tasks/main.yml
new file mode 100644
index 0000000..0bd1055
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/tasks/main.yml
@@ -0,0 +1,41 @@
+# NB: these tests are just a placeholder until we have pester unit tests.
+# They are being run as part of the Windows smoke tests. Please do not significantly
+# increase the size of these tests, as the smoke tests need to remain fast.
+# Any significant additions should be made to the (as yet nonexistent) PS module_utils unit tests.
+---
+- name: find a nonexistent drive letter
+ raw: foreach($c in [char[]]([char]'D'..[char]'Z')) { If (-not $(Get-PSDrive $c -ErrorAction SilentlyContinue)) { return $c } }
+ register: bogus_driveletter
+
+- assert:
+ that: bogus_driveletter.stdout_lines[0] | length == 1
+
+- name: test path shape validation
+ testpath:
+ path: "{{ item.path }}"
+ failed_when: path_shapes is failed != (item.should_fail | default(false))
+ register: path_shapes
+ with_items:
+ - path: C:\Windows
+ - path: HKLM:\Software
+ - path: '{{ bogus_driveletter.stdout_lines[0] }}:\goodpath'
+ - path: '{{ bogus_driveletter.stdout_lines[0] }}:\badpath*%@:\blar'
+ should_fail: true
+
+- name: test list parameters
+ testlist:
+ value: '{{item.value}}'
+ register: list_tests
+ failed_when: list_tests is failed or list_tests.count != item.count
+ with_items:
+ - value: []
+ count: 0
+ - value:
+ - 1
+ - 2
+ count: 2
+ - value:
+ - 1
+ count: 1
+ - value: "1, 2"
+ count: 2
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/library/symbolic_link_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/library/symbolic_link_test.ps1
new file mode 100644
index 0000000..de0bb8b
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/library/symbolic_link_test.ps1
@@ -0,0 +1,174 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.LinkUtil
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+$ErrorActionPreference = 'Stop'
+
+$path = Join-Path -Path ([System.IO.Path]::GetFullPath($env:TEMP)) -ChildPath '.ansible .ÅÑŚÌβŁÈ [$!@^&test(;)]'
+
+$folder_target = "$path\folder"
+$file_target = "$path\file"
+$symlink_file_path = "$path\file-symlink"
+$symlink_folder_path = "$path\folder-symlink"
+$hardlink_path = "$path\hardlink"
+$hardlink_path_2 = "$path\hardlink2"
+$junction_point_path = "$path\junction"
+
+if (Test-Path -LiteralPath $path) {
+ # Remove-Item struggles with broken symlinks, rely on trusty rmdir instead
+ Run-Command -command "cmd.exe /c rmdir /S /Q `"$path`"" > $null
+}
+New-Item -Path $path -ItemType Directory | Out-Null
+New-Item -Path $folder_target -ItemType Directory | Out-Null
+New-Item -Path $file_target -ItemType File | Out-Null
+Set-Content -LiteralPath $file_target -Value "a"
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -ne $expected) {
+ Fail-Json @{} "actual != expected`nActual: $actual`nExpected: $expected"
+ }
+}
+
+Function Assert-True($expression, $message) {
+ if ($expression -ne $true) {
+ Fail-Json @{} $message
+ }
+}
+
+# need to manually set this
+Load-LinkUtils
+
+# path is not a link
+$no_link_result = Get-Link -link_path $path
+Assert-True -expression ($null -eq $no_link_result) -message "did not return null result for a non link"
+
+# fail to create hard link pointed to a directory
+try {
+ New-Link -link_path "$path\folder-hard" -link_target $folder_target -link_type "hard"
+ Assert-True -expression $false -message "creation of hard link should have failed if target was a directory"
+}
+catch {
+ Assert-Equal -actual $_.Exception.Message -expected "cannot set the target for a hard link to a directory"
+}
+
+# fail to create a junction point pointed to a file
+try {
+ New-Link -link_path "$path\junction-fail" -link_target $file_target -link_type "junction"
+ Assert-True -expression $false -message "creation of junction point should have failed if target was a file"
+}
+catch {
+ Assert-Equal -actual $_.Exception.Message -expected "cannot set the target for a junction point to a file"
+}
+
+# fail to create a symbolic link with non-existent target
+try {
+ New-Link -link_path "$path\symlink-fail" -link_target "$path\fake-folder" -link_type "link"
+ Assert-True -expression $false -message "creation of symbolic link should have failed if target did not exist"
+}
+catch {
+ Assert-Equal -actual $_.Exception.Message -expected "link_target '$path\fake-folder' does not exist, cannot create link"
+}
+
+# create recursive symlink
+Run-Command -command "cmd.exe /c mklink /D symlink-rel folder" -working_directory $path | Out-Null
+$rel_link_result = Get-Link -link_path "$path\symlink-rel"
+Assert-Equal -actual $rel_link_result.Type -expected "SymbolicLink"
+Assert-Equal -actual $rel_link_result.SubstituteName -expected "folder"
+Assert-Equal -actual $rel_link_result.PrintName -expected "folder"
+Assert-Equal -actual $rel_link_result.TargetPath -expected "folder"
+Assert-Equal -actual $rel_link_result.AbsolutePath -expected $folder_target
+Assert-Equal -actual $rel_link_result.HardTargets -expected $null
+
+# create a symbolic file test
+New-Link -link_path $symlink_file_path -link_target $file_target -link_type "link"
+$file_link_result = Get-Link -link_path $symlink_file_path
+Assert-Equal -actual $file_link_result.Type -expected "SymbolicLink"
+Assert-Equal -actual $file_link_result.SubstituteName -expected "\??\$file_target"
+Assert-Equal -actual $file_link_result.PrintName -expected $file_target
+Assert-Equal -actual $file_link_result.TargetPath -expected $file_target
+Assert-Equal -actual $file_link_result.AbsolutePath -expected $file_target
+Assert-Equal -actual $file_link_result.HardTargets -expected $null
+
+# create a symbolic link folder test
+New-Link -link_path $symlink_folder_path -link_target $folder_target -link_type "link"
+$folder_link_result = Get-Link -link_path $symlink_folder_path
+Assert-Equal -actual $folder_link_result.Type -expected "SymbolicLink"
+Assert-Equal -actual $folder_link_result.SubstituteName -expected "\??\$folder_target"
+Assert-Equal -actual $folder_link_result.PrintName -expected $folder_target
+Assert-Equal -actual $folder_link_result.TargetPath -expected $folder_target
+Assert-Equal -actual $folder_link_result.AbsolutePath -expected $folder_target
+Assert-Equal -actual $folder_link_result.HardTargets -expected $null
+
+# create a junction point test
+New-Link -link_path $junction_point_path -link_target $folder_target -link_type "junction"
+$junction_point_result = Get-Link -link_path $junction_point_path
+Assert-Equal -actual $junction_point_result.Type -expected "JunctionPoint"
+Assert-Equal -actual $junction_point_result.SubstituteName -expected "\??\$folder_target"
+Assert-Equal -actual $junction_point_result.PrintName -expected $folder_target
+Assert-Equal -actual $junction_point_result.TargetPath -expected $folder_target
+Assert-Equal -actual $junction_point_result.AbsolutePath -expected $folder_target
+Assert-Equal -actual $junction_point_result.HardTargets -expected $null
+
+# create a hard link test
+New-Link -link_path $hardlink_path -link_target $file_target -link_type "hard"
+$hardlink_result = Get-Link -link_path $hardlink_path
+Assert-Equal -actual $hardlink_result.Type -expected "HardLink"
+Assert-Equal -actual $hardlink_result.SubstituteName -expected $null
+Assert-Equal -actual $hardlink_result.PrintName -expected $null
+Assert-Equal -actual $hardlink_result.TargetPath -expected $null
+Assert-Equal -actual $hardlink_result.AbsolutePath -expected $null
+if ($hardlink_result.HardTargets[0] -ne $hardlink_path -and $hardlink_result.HardTargets[1] -ne $hardlink_path) {
+ Assert-True -expression $false -message "file $hardlink_path is not a target of the hard link"
+}
+if ($hardlink_result.HardTargets[0] -ne $file_target -and $hardlink_result.HardTargets[1] -ne $file_target) {
+ Assert-True -expression $false -message "file $file_target is not a target of the hard link"
+}
+Assert-Equal -actual (Get-Content -LiteralPath $hardlink_path -Raw) -expected (Get-Content -LiteralPath $file_target -Raw)
+
+# create a new hard link and verify targets go to 3
+New-Link -link_path $hardlink_path_2 -link_target $file_target -link_type "hard"
+$hardlink_result_2 = Get-Link -link_path $hardlink_path
+$expected = "did not return 3 targets for the hard link, actual $($hardlink_result_2.Targets.Count)"
+Assert-True -expression ($hardlink_result_2.HardTargets.Count -eq 3) -message $expected
+
+# check if broken symbolic link still works
+Remove-Item -LiteralPath $folder_target -Force | Out-Null
+$broken_link_result = Get-Link -link_path $symlink_folder_path
+Assert-Equal -actual $broken_link_result.Type -expected "SymbolicLink"
+Assert-Equal -actual $broken_link_result.SubstituteName -expected "\??\$folder_target"
+Assert-Equal -actual $broken_link_result.PrintName -expected $folder_target
+Assert-Equal -actual $broken_link_result.TargetPath -expected $folder_target
+Assert-Equal -actual $broken_link_result.AbsolutePath -expected $folder_target
+Assert-Equal -actual $broken_link_result.HardTargets -expected $null
+
+# check if broken junction point still works
+$broken_junction_result = Get-Link -link_path $junction_point_path
+Assert-Equal -actual $broken_junction_result.Type -expected "JunctionPoint"
+Assert-Equal -actual $broken_junction_result.SubstituteName -expected "\??\$folder_target"
+Assert-Equal -actual $broken_junction_result.PrintName -expected $folder_target
+Assert-Equal -actual $broken_junction_result.TargetPath -expected $folder_target
+Assert-Equal -actual $broken_junction_result.AbsolutePath -expected $folder_target
+Assert-Equal -actual $broken_junction_result.HardTargets -expected $null
+
+# delete file symbolic link
+Remove-Link -link_path $symlink_file_path
+Assert-True -expression (-not (Test-Path -LiteralPath $symlink_file_path)) -message "failed to delete file symbolic link"
+
+# delete folder symbolic link
+Remove-Link -link_path $symlink_folder_path
+Assert-True -expression (-not (Test-Path -LiteralPath $symlink_folder_path)) -message "failed to delete folder symbolic link"
+
+# delete junction point
+Remove-Link -link_path $junction_point_path
+Assert-True -expression (-not (Test-Path -LiteralPath $junction_point_path)) -message "failed to delete junction point"
+
+# delete hard link
+Remove-Link -link_path $hardlink_path
+Assert-True -expression (-not (Test-Path -LiteralPath $hardlink_path)) -message "failed to delete hard link"
+
+# cleanup after tests
+Run-Command -command "cmd.exe /c rmdir /S /Q `"$path`"" > $null
+
+Exit-Json @{ data = "success" }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/tasks/main.yml
new file mode 100644
index 0000000..f121ad4
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+- name: call module with symbolic link tests
+ symbolic_link_test:
+ register: symbolic_link
+
+- assert:
+ that:
+ - symbolic_link.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/library/privilege_util_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/library/privilege_util_test.ps1
new file mode 100644
index 0000000..414b80a
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/library/privilege_util_test.ps1
@@ -0,0 +1,113 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.PrivilegeUtil
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -cne $expected) {
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.actual = $actual
+ $module.Result.expected = $expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.FailJson("AssertionError: actual != expected")
+ }
+}
+
+# taken from https://docs.microsoft.com/en-us/windows/desktop/SecAuthZ/privilege-constants
+$total_privileges = @(
+ "SeAssignPrimaryTokenPrivilege",
+ "SeAuditPrivilege",
+ "SeBackupPrivilege",
+ "SeChangeNotifyPrivilege",
+ "SeCreateGlobalPrivilege",
+ "SeCreatePagefilePrivilege",
+ "SeCreatePermanentPrivilege",
+ "SeCreateSymbolicLinkPrivilege",
+ "SeCreateTokenPrivilege",
+ "SeDebugPrivilege",
+ "SeEnableDelegationPrivilege",
+ "SeImpersonatePrivilege",
+ "SeIncreaseBasePriorityPrivilege",
+ "SeIncreaseQuotaPrivilege",
+ "SeIncreaseWorkingSetPrivilege",
+ "SeLoadDriverPrivilege",
+ "SeLockMemoryPrivilege",
+ "SeMachineAccountPrivilege",
+ "SeManageVolumePrivilege",
+ "SeProfileSingleProcessPrivilege",
+ "SeRelabelPrivilege",
+ "SeRemoteShutdownPrivilege",
+ "SeRestorePrivilege",
+ "SeSecurityPrivilege",
+ "SeShutdownPrivilege",
+ "SeSyncAgentPrivilege",
+ "SeSystemEnvironmentPrivilege",
+ "SeSystemProfilePrivilege",
+ "SeSystemtimePrivilege",
+ "SeTakeOwnershipPrivilege",
+ "SeTcbPrivilege",
+ "SeTimeZonePrivilege",
+ "SeTrustedCredManAccessPrivilege",
+ "SeUndockPrivilege"
+)
+
+$raw_privilege_output = &whoami /priv | Where-Object { $_.StartsWith("Se") }
+$actual_privileges = @{}
+foreach ($raw_privilege in $raw_privilege_output) {
+ $split = $raw_privilege.TrimEnd() -split " "
+ $actual_privileges."$($split[0])" = ($split[-1] -eq "Enabled")
+}
+$process = [Ansible.Privilege.PrivilegeUtil]::GetCurrentProcess()
+
+### Test PS cmdlets ###
+# test ps Get-AnsiblePrivilege
+foreach ($privilege in $total_privileges) {
+ $expected = $null
+ if ($actual_privileges.ContainsKey($privilege)) {
+ $expected = $actual_privileges.$privilege
+ }
+ $actual = Get-AnsiblePrivilege -Name $privilege
+ Assert-Equal -actual $actual -expected $expected
+}
+
+# test c# GetAllPrivilegeInfo
+$actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+Assert-Equal -actual $actual.GetType().Name -expected 'Dictionary`2'
+Assert-Equal -actual $actual.Count -expected $actual_privileges.Count
+foreach ($privilege in $total_privileges) {
+ if ($actual_privileges.ContainsKey($privilege)) {
+ $actual_value = $actual.$privilege
+ if ($actual_privileges.$privilege) {
+ Assert-Equal -actual $actual_value.HasFlag([Ansible.Privilege.PrivilegeAttributes]::Enabled) -expected $true
+ }
+ else {
+ Assert-Equal -actual $actual_value.HasFlag([Ansible.Privilege.PrivilegeAttributes]::Enabled) -expected $false
+ }
+ }
+}
+
+# test Set-AnsiblePrivilege
+Set-AnsiblePrivilege -Name SeUndockPrivilege -Value $false # ensure we start with a disabled privilege
+
+Set-AnsiblePrivilege -Name SeUndockPrivilege -Value $true -WhatIf
+$actual = Get-AnsiblePrivilege -Name SeUndockPrivilege
+Assert-Equal -actual $actual -expected $false
+
+Set-AnsiblePrivilege -Name SeUndockPrivilege -Value $true
+$actual = Get-AnsiblePrivilege -Name SeUndockPrivilege
+Assert-Equal -actual $actual -expected $true
+
+Set-AnsiblePrivilege -Name SeUndockPrivilege -Value $false -WhatIf
+$actual = Get-AnsiblePrivilege -Name SeUndockPrivilege
+Assert-Equal -actual $actual -expected $true
+
+Set-AnsiblePrivilege -Name SeUndockPrivilege -Value $false
+$actual = Get-AnsiblePrivilege -Name SeUndockPrivilege
+Assert-Equal -actual $actual -expected $false
+
+$module.Result.data = "success"
+$module.ExitJson()
+
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/tasks/main.yml
new file mode 100644
index 0000000..5f54480
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+- name: call module with PrivilegeUtil tests
+ privilege_util_test:
+ register: privilege_util_test
+
+- assert:
+ that:
+ - privilege_util_test.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/library/sid_utils_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/library/sid_utils_test.ps1
new file mode 100644
index 0000000..85bfbe1
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/library/sid_utils_test.ps1
@@ -0,0 +1,101 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.SID
+
+$params = Parse-Args $args
+$sid_account = Get-AnsibleParam -obj $params -name "sid_account" -type "str" -failifempty $true
+
+Function Assert-Equal($actual, $expected) {
+ if ($actual -ne $expected) {
+ Fail-Json @{} "actual != expected`nActual: $actual`nExpected: $expected"
+ }
+}
+
+Function Get-ComputerSID() {
+ # find any local user and trim off the final UID
+ $luser_sid = (Get-CimInstance Win32_UserAccount -Filter "Domain='$env:COMPUTERNAME'")[0].SID
+
+ return $luser_sid -replace '(S-1-5-21-\d+-\d+-\d+)-\d+', '$1'
+}
+
+$local_sid = Get-ComputerSID
+
+# most machines should have a -500 Administrator account, but it may have been renamed. Look it up by SID
+$default_admin = Get-CimInstance Win32_UserAccount -Filter "SID='$local_sid-500'"
+
+# this group is called Administrators by default on English Windows, but could named something else. Look it up by SID
+$default_admin_group = Get-CimInstance Win32_Group -Filter "SID='S-1-5-32-544'"
+
+if (@($default_admin).Length -ne 1) {
+ Fail-Json @{} "could not find a local admin account with SID ending in -500"
+}
+
+### Set this to the NETBIOS name of the domain you wish to test, not set for shippable ###
+$test_domain = $null
+
+$tests = @(
+ # Local Users
+ @{ sid = "S-1-1-0"; full_name = "Everyone"; names = @("Everyone") },
+ @{ sid = "S-1-5-18"; full_name = "NT AUTHORITY\SYSTEM"; names = @("NT AUTHORITY\SYSTEM", "SYSTEM") },
+ @{ sid = "S-1-5-20"; full_name = "NT AUTHORITY\NETWORK SERVICE"; names = @("NT AUTHORITY\NETWORK SERVICE", "NETWORK SERVICE") },
+ @{
+ sid = "$($default_admin.SID)"
+ full_name = "$($default_admin.FullName)"
+ names = @("$env:COMPUTERNAME\$($default_admin.Name)", "$($default_admin.Name)", ".\$($default_admin.Name)")
+ },
+
+ # Local Groups
+ @{
+ sid = "$($default_admin_group.SID)"
+ full_name = "BUILTIN\$($default_admin_group.Name)"
+ names = @("BUILTIN\$($default_admin_group.Name)", "$($default_admin_group.Name)", ".\$($default_admin_group.Name)")
+ }
+)
+
+# Add domain tests if the domain name has been set
+if ($null -ne $test_domain) {
+ Import-Module ActiveDirectory
+ $domain_info = Get-ADDomain -Identity $test_domain
+ $domain_sid = $domain_info.DomainSID
+ $domain_netbios = $domain_info.NetBIOSName
+ $domain_upn = $domain_info.Forest
+
+ $tests += @{
+ sid = "$domain_sid-512"
+ full_name = "$domain_netbios\Domain Admins"
+ names = @("$domain_netbios\Domain Admins", "Domain Admins@$domain_upn", "Domain Admins")
+ }
+
+ $tests += @{
+ sid = "$domain_sid-500"
+ full_name = "$domain_netbios\Administrator"
+ names = @("$domain_netbios\Administrator", "Administrator@$domain_upn")
+ }
+}
+
+foreach ($test in $tests) {
+ $actual_account_name = Convert-FromSID -sid $test.sid
+ # renamed admins may have an empty FullName; skip comparison in that case
+ if ($test.full_name) {
+ Assert-Equal -actual $actual_account_name -expected $test.full_name
+ }
+
+ foreach ($test_name in $test.names) {
+ $actual_sid = Convert-ToSID -account_name $test_name
+ Assert-Equal -actual $actual_sid -expected $test.sid
+ }
+}
+
+# the account to SID test is run outside of the normal run as we can't test it
+# in the normal test suite
+# Calling Convert-ToSID with a string like a SID should return that SID back
+$actual = Convert-ToSID -account_name $sid_account
+Assert-Equal -actual $actual -expected $sid_account
+
+# Calling COnvert-ToSID with a string prefixed with .\ should return the SID
+# for a user that is called that SID and not the SID passed in
+$actual = Convert-ToSID -account_name ".\$sid_account"
+Assert-Equal -actual ($actual -ne $sid_account) -expected $true
+
+Exit-Json @{ data = "success" }
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/tasks/main.yml
new file mode 100644
index 0000000..acbae50
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.SID/tasks/main.yml
@@ -0,0 +1,22 @@
+---
+- block:
+ - name: create test user with well know SID as the name
+ win_user:
+ name: S-1-0-0
+ password: AbcDef123!@#
+ state: present
+
+ - name: call module with SID tests
+ sid_utils_test:
+ sid_account: S-1-0-0
+ register: sid_test
+
+ always:
+ - name: remove test SID user
+ win_user:
+ name: S-1-0-0
+ state: absent
+
+- assert:
+ that:
+ - sid_test.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/aliases b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/aliases
new file mode 100644
index 0000000..b5ad7ca
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/aliases
@@ -0,0 +1,4 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
+needs/httptester
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/library/web_request_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/library/web_request_test.ps1
new file mode 100644
index 0000000..c168b92
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/library/web_request_test.ps1
@@ -0,0 +1,473 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.WebRequest
+
+$spec = @{
+ options = @{
+ httpbin_host = @{ type = 'str'; required = $true }
+ }
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$httpbin_host = $module.Params.httpbin_host
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array] -or $Actual -is [System.Collections.IList]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actualValue = $Actual[$i]
+ $expectedValue = $Expected[$i]
+ Assert-Equal -Actual $actualValue -Expected $expectedValue
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+Function Convert-StreamToString {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.IO.Stream]
+ $Stream
+ )
+
+ $ms = New-Object -TypeName System.IO.MemoryStream
+ try {
+ $Stream.CopyTo($ms)
+ [System.Text.Encoding]::UTF8.GetString($ms.ToArray())
+ }
+ finally {
+ $ms.Dispose()
+ }
+}
+
+$tests = [Ordered]@{
+ 'GET request over http' = {
+ $r = Get-AnsibleWebRequest -Uri "http://$httpbin_host/get"
+
+ $r.Method | Assert-Equal -Expected 'GET'
+ $r.Timeout | Assert-Equal -Expected 30000
+ $r.UseDefaultCredentials | Assert-Equal -Expected $false
+ $r.Credentials | Assert-Equal -Expected $null
+ $r.ClientCertificates.Count | Assert-Equal -Expected 0
+ $r.Proxy.Credentials | Assert-Equal -Expected $null
+ $r.UserAgent | Assert-Equal -Expected 'ansible-httpget'
+
+ $actual = Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ Convert-StreamToString -Stream $Stream
+ } | ConvertFrom-Json
+
+ $actual.headers.'User-Agent' | Assert-Equal -Expected 'ansible-httpget'
+ $actual.headers.'Host' | Assert-Equal -Expected $httpbin_host
+
+ $module.Result.msg | Assert-Equal -Expected 'OK'
+ $module.Result.status_code | Assert-Equal -Expected 200
+ $module.Result.ContainsKey('elapsed') | Assert-Equal -Expected $true
+ }
+
+ 'GET request over https' = {
+ # url is an alias for the -Uri parameter.
+ $r = Get-AnsibleWebRequest -url "https://$httpbin_host/get"
+
+ $r.Method | Assert-Equal -Expected 'GET'
+ $r.Timeout | Assert-Equal -Expected 30000
+ $r.UseDefaultCredentials | Assert-Equal -Expected $false
+ $r.Credentials | Assert-Equal -Expected $null
+ $r.ClientCertificates.Count | Assert-Equal -Expected 0
+ $r.Proxy.Credentials | Assert-Equal -Expected $null
+ $r.UserAgent | Assert-Equal -Expected 'ansible-httpget'
+
+ $actual = Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ Convert-StreamToString -Stream $Stream
+ } | ConvertFrom-Json
+
+ $actual.headers.'User-Agent' | Assert-Equal -Expected 'ansible-httpget'
+ $actual.headers.'Host' | Assert-Equal -Expected $httpbin_host
+ }
+
+ 'POST request' = {
+ $getParams = @{
+ Headers = @{
+ 'Content-Type' = 'application/json'
+ }
+ Method = 'POST'
+ Uri = "https://$httpbin_host/post"
+ }
+ $r = Get-AnsibleWebRequest @getParams
+
+ $r.Method | Assert-Equal -Expected 'POST'
+ $r.Timeout | Assert-Equal -Expected 30000
+ $r.UseDefaultCredentials | Assert-Equal -Expected $false
+ $r.Credentials | Assert-Equal -Expected $null
+ $r.ClientCertificates.Count | Assert-Equal -Expected 0
+ $r.Proxy.Credentials | Assert-Equal -Expected $null
+ $r.ContentType | Assert-Equal -Expected 'application/json'
+ $r.UserAgent | Assert-Equal -Expected 'ansible-httpget'
+
+ $body = New-Object -TypeName System.IO.MemoryStream -ArgumentList @(,
+ ([System.Text.Encoding]::UTF8.GetBytes('{"foo":"bar"}'))
+ )
+ $actual = Invoke-WithWebRequest -Module $module -Request $r -Body $body -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ Convert-StreamToString -Stream $Stream
+ } | ConvertFrom-Json
+
+ $actual.headers.'User-Agent' | Assert-Equal -Expected 'ansible-httpget'
+ $actual.headers.'Host' | Assert-Equal -Expected $httpbin_host
+ $actual.data | Assert-Equal -Expected '{"foo":"bar"}'
+ }
+
+ 'Safe redirection of GET' = {
+ $r = Get-AnsibleWebRequest -Uri "http://$httpbin_host/redirect/2"
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "http://$httpbin_host/get"
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'Safe redirection of HEAD' = {
+ $r = Get-AnsibleWebRequest -Uri "http://$httpbin_host/redirect/2" -Method HEAD
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "http://$httpbin_host/get"
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'Safe redirection of PUT' = {
+ $params = @{
+ Method = 'PUT'
+ Uri = "http://$httpbin_host/redirect-to?url=https://$httpbin_host/put"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected $r.RequestUri
+ $Response.StatusCode | Assert-Equal -Expected 302
+ }
+ }
+
+ 'None redirection of GET' = {
+ $params = @{
+ FollowRedirects = 'None'
+ Uri = "http://$httpbin_host/redirect/2"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected $r.RequestUri
+ $Response.StatusCode | Assert-Equal -Expected 302
+ }
+ }
+
+ 'None redirection of HEAD' = {
+ $params = @{
+ follow_redirects = 'None'
+ method = 'HEAD'
+ Uri = "http://$httpbin_host/redirect/2"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected $r.RequestUri
+ $Response.StatusCode | Assert-Equal -Expected 302
+ }
+ }
+
+ 'None redirection of PUT' = {
+ $params = @{
+ FollowRedirects = 'None'
+ Method = 'PUT'
+ Uri = "http://$httpbin_host/redirect-to?url=https://$httpbin_host/put"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected $r.RequestUri
+ $Response.StatusCode | Assert-Equal -Expected 302
+ }
+ }
+
+ 'All redirection of GET' = {
+ $params = @{
+ FollowRedirects = 'All'
+ Uri = "http://$httpbin_host/redirect/2"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "http://$httpbin_host/get"
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'All redirection of HEAD' = {
+ $params = @{
+ follow_redirects = 'All'
+ method = 'HEAD'
+ Uri = "http://$httpbin_host/redirect/2"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "http://$httpbin_host/get"
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'All redirection of PUT' = {
+ $params = @{
+ FollowRedirects = 'All'
+ Method = 'PUT'
+ Uri = "http://$httpbin_host/redirect-to?url=https://$httpbin_host/put"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "https://$httpbin_host/put"
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'Exceeds maximum redirection - ignored' = {
+ $params = @{
+ MaximumRedirection = 4
+ Uri = "https://$httpbin_host/redirect/5"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -IgnoreBadResponse -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "https://$httpbin_host/relative-redirect/1"
+ $Response.StatusCode | Assert-Equal -Expected 302
+ }
+ }
+
+ 'Exceeds maximum redirection - exception' = {
+ $params = @{
+ MaximumRedirection = 1
+ Uri = "https://$httpbin_host/redirect/2"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ $failed = $false
+ try {
+ $null = Invoke-WithWebRequest -Module $module -Request $r -Script {}
+ }
+ catch {
+ $_.Exception.GetType().Name | Assert-Equal -Expected 'WebException'
+ $_.Exception.Message | Assert-Equal -Expected 'Too many automatic redirections were attempted.'
+ $failed = $true
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ 'Basic auth as Credential' = {
+ $params = @{
+ Url = "http://$httpbin_host/basic-auth/username/password"
+ UrlUsername = 'username'
+ UrlPassword = 'password'
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -IgnoreBadResponse -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'Basic auth as Header' = {
+ $params = @{
+ Url = "http://$httpbin_host/basic-auth/username/password"
+ url_username = 'username'
+ url_password = 'password'
+ ForceBasicAuth = $true
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ Invoke-WithWebRequest -Module $module -Request $r -IgnoreBadResponse -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ }
+ }
+
+ 'Send request with headers' = {
+ $params = @{
+ Headers = @{
+ 'Content-Length' = 0
+ testingheader = 'testing_header'
+ TestHeader = 'test-header'
+ 'User-Agent' = 'test-agent'
+ }
+ Url = "https://$httpbin_host/get"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ $actual = Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.StatusCode | Assert-Equal -Expected 200
+ Convert-StreamToString -Stream $Stream
+ } | ConvertFrom-Json
+
+ $actual.headers.'Testheader' | Assert-Equal -Expected 'test-header'
+ $actual.headers.'testingheader' | Assert-Equal -Expected 'testing_header'
+ $actual.Headers.'User-Agent' | Assert-Equal -Expected 'test-agent'
+ }
+
+ 'Request with timeout' = {
+ $params = @{
+ Uri = "https://$httpbin_host/delay/5"
+ Timeout = 1
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ $failed = $false
+ try {
+ $null = Invoke-WithWebRequest -Module $module -Request $r -Script {}
+ }
+ catch {
+ $failed = $true
+ $_.Exception.GetType().Name | Assert-Equal -Expected WebException
+ $_.Exception.Message | Assert-Equal -Expected 'The operation has timed out'
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ 'Request with file URI' = {
+ $filePath = Join-Path $module.Tmpdir -ChildPath 'test.txt'
+ Set-Content -LiteralPath $filePath -Value 'test'
+
+ $r = Get-AnsibleWebRequest -Uri $filePath
+
+ $actual = Invoke-WithWebRequest -Module $module -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ContentLength | Assert-Equal -Expected 6
+ Convert-StreamToString -Stream $Stream
+ }
+ $actual | Assert-Equal -Expected "test`r`n"
+ $module.Result.msg | Assert-Equal -Expected "OK"
+ $module.Result.status_code | Assert-Equal -Expected 200
+ }
+
+ 'Web request based on module options' = {
+ Set-Variable complex_args -Scope Global -Value @{
+ url = "https://$httpbin_host/redirect/2"
+ method = 'GET'
+ follow_redirects = 'safe'
+ headers = @{
+ 'User-Agent' = 'other-agent'
+ }
+ http_agent = 'actual-agent'
+ maximum_redirection = 2
+ timeout = 10
+ validate_certs = $false
+ }
+ $spec = @{
+ options = @{
+ url = @{ type = 'str'; required = $true }
+ test = @{ type = 'str'; choices = 'abc', 'def' }
+ }
+ mutually_exclusive = @(, @('url', 'test'))
+ }
+
+ $testModule = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @(Get-AnsibleWebRequestSpec))
+ $r = Get-AnsibleWebRequest -Url $testModule.Params.url -Module $testModule
+
+ $actual = Invoke-WithWebRequest -Module $testModule -Request $r -Script {
+ Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream)
+
+ $Response.ResponseUri | Assert-Equal -Expected "https://$httpbin_host/get"
+ Convert-StreamToString -Stream $Stream
+ } | ConvertFrom-Json
+ $actual.headers.'User-Agent' | Assert-Equal -Expected 'actual-agent'
+ }
+
+ 'Web request with default proxy' = {
+ $params = @{
+ Uri = "https://$httpbin_host/get"
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ $null -ne $r.Proxy | Assert-Equal -Expected $true
+ }
+
+ 'Web request with no proxy' = {
+ $params = @{
+ Uri = "https://$httpbin_host/get"
+ UseProxy = $false
+ }
+ $r = Get-AnsibleWebRequest @params
+
+ $null -eq $r.Proxy | Assert-Equal -Expected $true
+ }
+}
+
+# setup and teardown should favour native tools to create and delete the service and not the util we are testing.
+foreach ($testImpl in $tests.GetEnumerator()) {
+ Set-Variable -Name complex_args -Scope Global -Value @{}
+ $test = $testImpl.Key
+ &$testImpl.Value
+}
+
+$module.Result.data = "success"
+$module.ExitJson()
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/meta/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/meta/main.yml
new file mode 100644
index 0000000..829d0a7
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+- prepare_http_tests
diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/tasks/main.yml b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/tasks/main.yml
new file mode 100644
index 0000000..57d8138
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: test Ansible.ModuleUtils.WebRequest
+ web_request_test:
+ httpbin_host: '{{ httpbin_host }}'
+ register: web_request
+
+- name: assert test Ansible.ModuleUtils.WebRequest succeeded
+ assert:
+ that:
+ - web_request.data == 'success'
diff --git a/test/integration/targets/module_utils_Ansible.Privilege/aliases b/test/integration/targets/module_utils_Ansible.Privilege/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Privilege/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.Privilege/library/ansible_privilege_tests.ps1 b/test/integration/targets/module_utils_Ansible.Privilege/library/ansible_privilege_tests.ps1
new file mode 100644
index 0000000..58ee9c1
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Privilege/library/ansible_privilege_tests.ps1
@@ -0,0 +1,278 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Ansiblerequires -CSharpUtil Ansible.Privilege
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+Function Assert-DictionaryEqual {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $actual_keys = $Actual.Keys
+ $expected_keys = $Expected.Keys
+
+ $actual_keys.Count | Assert-Equal -Expected $expected_keys.Count
+ foreach ($actual_entry in $Actual.GetEnumerator()) {
+ $actual_key = $actual_entry.Key
+ ($actual_key -cin $expected_keys) | Assert-Equal -Expected $true
+ $actual_value = $actual_entry.Value
+ $expected_value = $Expected.$actual_key
+
+ if ($actual_value -is [System.Collections.IDictionary]) {
+ $actual_value | Assert-DictionaryEqual -Expected $expected_value
+ }
+ elseif ($actual_value -is [System.Collections.ArrayList]) {
+ for ($i = 0; $i -lt $actual_value.Count; $i++) {
+ $actual_entry = $actual_value[$i]
+ $expected_entry = $expected_value[$i]
+ if ($actual_entry -is [System.Collections.IDictionary]) {
+ $actual_entry | Assert-DictionaryEqual -Expected $expected_entry
+ }
+ else {
+ Assert-Equal -Actual $actual_entry -Expected $expected_entry
+ }
+ }
+ }
+ else {
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ }
+ foreach ($expected_key in $expected_keys) {
+ ($expected_key -cin $actual_keys) | Assert-Equal -Expected $true
+ }
+ }
+}
+
+$process = [Ansible.Privilege.PrivilegeUtil]::GetCurrentProcess()
+
+$tests = @{
+ "Check valid privilege name" = {
+ $actual = [Ansible.Privilege.PrivilegeUtil]::CheckPrivilegeName("SeTcbPrivilege")
+ $actual | Assert-Equal -Expected $true
+ }
+
+ "Check invalid privilege name" = {
+ $actual = [Ansible.Privilege.PrivilegeUtil]::CheckPrivilegeName("SeFake")
+ $actual | Assert-Equal -Expected $false
+ }
+
+ "Disable a privilege" = {
+ # Ensure the privilege is enabled at the start
+ [Ansible.Privilege.PrivilegeUtil]::EnablePrivilege($process, "SeTimeZonePrivilege") > $null
+
+ $actual = [Ansible.Privilege.PrivilegeUtil]::DisablePrivilege($process, "SeTimeZonePrivilege")
+ $actual.GetType().Name | Assert-Equal -Expected 'Dictionary`2'
+ $actual.Count | Assert-Equal -Expected 1
+ $actual.SeTimeZonePrivilege | Assert-Equal -Expected $true
+
+ # Disable again
+ $actual = [Ansible.Privilege.PrivilegeUtil]::DisablePrivilege($process, "SeTimeZonePrivilege")
+ $actual.GetType().Name | Assert-Equal -Expected 'Dictionary`2'
+ $actual.Count | Assert-Equal -Expected 0
+ }
+
+ "Enable a privilege" = {
+ # Ensure the privilege is disabled at the start
+ [Ansible.Privilege.PrivilegeUtil]::DisablePrivilege($process, "SeTimeZonePrivilege") > $null
+
+ $actual = [Ansible.Privilege.PrivilegeUtil]::EnablePrivilege($process, "SeTimeZonePrivilege")
+ $actual.GetType().Name | Assert-Equal -Expected 'Dictionary`2'
+ $actual.Count | Assert-Equal -Expected 1
+ $actual.SeTimeZonePrivilege | Assert-Equal -Expected $false
+
+ # Disable again
+ $actual = [Ansible.Privilege.PrivilegeUtil]::EnablePrivilege($process, "SeTimeZonePrivilege")
+ $actual.GetType().Name | Assert-Equal -Expected 'Dictionary`2'
+ $actual.Count | Assert-Equal -Expected 0
+ }
+
+ "Disable and revert privileges" = {
+ $current_state = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+
+ $previous_state = [Ansible.Privilege.PrivilegeUtil]::DisableAllPrivileges($process)
+ $previous_state.GetType().Name | Assert-Equal -Expected 'Dictionary`2'
+ foreach ($previous_state_entry in $previous_state.GetEnumerator()) {
+ $previous_state_entry.Value | Assert-Equal -Expected $true
+ }
+
+ # Disable again
+ $previous_state2 = [Ansible.Privilege.PrivilegeUtil]::DisableAllPrivileges($process)
+ $previous_state2.Count | Assert-Equal -Expected 0
+
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ foreach ($actual_entry in $actual.GetEnumerator()) {
+ $actual_entry.Value -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ }
+
+ [Ansible.Privilege.PrivilegeUtil]::SetTokenPrivileges($process, $previous_state) > $null
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual | Assert-DictionaryEqual -Expected $current_state
+ }
+
+ "Remove a privilege" = {
+ [Ansible.Privilege.PrivilegeUtil]::RemovePrivilege($process, "SeUndockPrivilege") > $null
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.ContainsKey("SeUndockPrivilege") | Assert-Equal -Expected $false
+ }
+
+ "Test Enabler" = {
+ # Disable privilege at the start
+ $new_state = @{
+ SeTimeZonePrivilege = $false
+ SeShutdownPrivilege = $false
+ SeIncreaseWorkingSetPrivilege = $false
+ }
+ [Ansible.Privilege.PrivilegeUtil]::SetTokenPrivileges($process, $new_state) > $null
+ $check_state = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $check_state.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $check_state.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $check_state.SeIncreaseWorkingSetPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+
+ # Check that strict = false won't validate privileges not held but activates the ones we want
+ $enabler = New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $false, "SeTimeZonePrivilege", "SeShutdownPrivilege", "SeTcbPrivilege"
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeIncreaseWorkingSetPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $actual.ContainsKey("SeTcbPrivilege") | Assert-Equal -Expected $false
+
+ # Now verify a no-op enabler will not rever back to disabled
+ $enabler2 = New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $false, "SeTimeZonePrivilege", "SeShutdownPrivilege", "SeTcbPrivilege"
+ $enabler2.Dispose()
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+
+ # Verify that when disposing the object the privileges are reverted
+ $enabler.Dispose()
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ }
+
+ "Test Enabler strict" = {
+ # Disable privilege at the start
+ $new_state = @{
+ SeTimeZonePrivilege = $false
+ SeShutdownPrivilege = $false
+ SeIncreaseWorkingSetPrivilege = $false
+ }
+ [Ansible.Privilege.PrivilegeUtil]::SetTokenPrivileges($process, $new_state) > $null
+ $check_state = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $check_state.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $check_state.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $check_state.SeIncreaseWorkingSetPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+
+ # Check that strict = false won't validate privileges not held but activates the ones we want
+ $enabler = New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $true, "SeTimeZonePrivilege", "SeShutdownPrivilege"
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeIncreaseWorkingSetPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+
+ # Now verify a no-op enabler will not rever back to disabled
+ $enabler2 = New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $true, "SeTimeZonePrivilege", "SeShutdownPrivilege"
+ $enabler2.Dispose()
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled |
+ Assert-Equal -Expected ([Ansible.Privilege.PrivilegeAttributes]::Enabled)
+
+ # Verify that when disposing the object the privileges are reverted
+ $enabler.Dispose()
+ $actual = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $actual.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ $actual.SeShutdownPrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+ }
+
+ "Test Enabler invalid privilege" = {
+ $failed = $false
+ try {
+ New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $false, "SeTimeZonePrivilege", "SeFake"
+ }
+ catch {
+ $failed = $true
+ $expected = "Failed to enable privilege(s) SeTimeZonePrivilege, SeFake (A specified privilege does not exist, Win32ErrorCode 1313)"
+ $_.Exception.InnerException.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Test Enabler strict failure" = {
+ # Start disabled
+ [Ansible.Privilege.PrivilegeUtil]::DisablePrivilege($process, "SeTimeZonePrivilege") > $null
+ $check_state = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process)
+ $check_state.SeTimeZonePrivilege -band [Ansible.Privilege.PrivilegeAttributes]::Enabled | Assert-Equal -Expected 0
+
+ $failed = $false
+ try {
+ New-Object -TypeName Ansible.Privilege.PrivilegeEnabler -ArgumentList $true, "SeTimeZonePrivilege", "SeTcbPrivilege"
+ }
+ catch {
+ $failed = $true
+ $expected = -join @(
+ "Failed to enable privilege(s) SeTimeZonePrivilege, SeTcbPrivilege "
+ "(Not all privileges or groups referenced are assigned to the caller, Win32ErrorCode 1300)"
+ )
+ $_.Exception.InnerException.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+}
+
+foreach ($test_impl in $tests.GetEnumerator()) {
+ $test = $test_impl.Key
+ &$test_impl.Value
+}
+
+$module.Result.data = "success"
+$module.ExitJson()
+
diff --git a/test/integration/targets/module_utils_Ansible.Privilege/tasks/main.yml b/test/integration/targets/module_utils_Ansible.Privilege/tasks/main.yml
new file mode 100644
index 0000000..888394d
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Privilege/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: test Ansible.Privilege.cs
+ ansible_privilege_tests:
+ register: ansible_privilege_test
+
+- name: assert test Ansible.Privilege.cs
+ assert:
+ that:
+ - ansible_privilege_test.data == "success"
diff --git a/test/integration/targets/module_utils_Ansible.Process/aliases b/test/integration/targets/module_utils_Ansible.Process/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Process/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.Process/library/ansible_process_tests.ps1 b/test/integration/targets/module_utils_Ansible.Process/library/ansible_process_tests.ps1
new file mode 100644
index 0000000..bca7eb1
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Process/library/ansible_process_tests.ps1
@@ -0,0 +1,242 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -CSharpUtil Ansible.Process
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actual_value = $Actual[$i]
+ $expected_value = $Expected[$i]
+ Assert-Equal -Actual $actual_value -Expected $expected_value
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+$tests = @{
+ "ParseCommandLine empty string" = {
+ $expected = @((Get-Process -Id $pid).Path)
+ $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("")
+ Assert-Equal -Actual $actual -Expected $expected
+ }
+
+ "ParseCommandLine single argument" = {
+ $expected = @("powershell.exe")
+ $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("powershell.exe")
+ Assert-Equal -Actual $actual -Expected $expected
+ }
+
+ "ParseCommandLine multiple arguments" = {
+ $expected = @("powershell.exe", "-File", "C:\temp\script.ps1")
+ $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("powershell.exe -File C:\temp\script.ps1")
+ Assert-Equal -Actual $actual -Expected $expected
+ }
+
+ "ParseCommandLine comples arguments" = {
+ $expected = @('abc', 'd', 'ef gh', 'i\j', 'k"l', 'm\n op', 'ADDLOCAL=qr, s', 'tuv\', 'w''x', 'yz')
+ $actual = [Ansible.Process.ProcessUtil]::ParseCommandLine('abc d "ef gh" i\j k\"l m\\"n op" ADDLOCAL="qr, s" tuv\ w''x yz')
+ Assert-Equal -Actual $actual -Expected $expected
+ }
+
+ "SearchPath normal" = {
+ $expected = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe"
+ $actual = [Ansible.Process.ProcessUtil]::SearchPath("powershell.exe")
+ $actual | Assert-Equal -Expected $expected
+ }
+
+ "SearchPath missing" = {
+ $failed = $false
+ try {
+ [Ansible.Process.ProcessUtil]::SearchPath("fake.exe")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "System.IO.FileNotFoundException"
+ $expected = 'Exception calling "SearchPath" with "1" argument(s): "Could not find file ''fake.exe''."'
+ $_.Exception.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "CreateProcess basic" = {
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess("whoami.exe")
+ $actual.GetType().FullName | Assert-Equal -Expected "Ansible.Process.Result"
+ $actual.StandardOut | Assert-Equal -Expected "$(&whoami.exe)`r`n"
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess stderr" = {
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess("powershell.exe [System.Console]::Error.WriteLine('hi')")
+ $actual.StandardOut | Assert-Equal -Expected ""
+ $actual.StandardError | Assert-Equal -Expected "hi`r`n"
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess exit code" = {
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess("powershell.exe exit 10")
+ $actual.StandardOut | Assert-Equal -Expected ""
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 10
+ }
+
+ "CreateProcess bad executable" = {
+ $failed = $false
+ try {
+ [Ansible.Process.ProcessUtil]::CreateProcess("fake.exe")
+ }
+ catch {
+ $failed = $true
+ $_.Exception.InnerException.GetType().FullName | Assert-Equal -Expected "Ansible.Process.Win32Exception"
+ $expected = 'Exception calling "CreateProcess" with "1" argument(s): "CreateProcessW() failed '
+ $expected += '(The system cannot find the file specified, Win32ErrorCode 2)"'
+ $_.Exception.Message | Assert-Equal -Expected $expected
+ }
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "CreateProcess with unicode" = {
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess("cmd.exe /c echo 💩 café")
+ $actual.StandardOut | Assert-Equal -Expected "💩 café`r`n"
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, "cmd.exe /c echo 💩 café", $null, $null)
+ $actual.StandardOut | Assert-Equal -Expected "💩 café`r`n"
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess without working dir" = {
+ $expected = $pwd.Path + "`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $pwd.Path', $null, $null)
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with working dir" = {
+ $expected = "C:\Windows`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $pwd.Path', "C:\Windows", $null)
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess without environment" = {
+ $expected = "$($env:USERNAME)`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $env:TEST; $env:USERNAME', $null, $null)
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with environment" = {
+ $env_vars = @{
+ TEST = "tesTing"
+ TEST2 = "Testing 2"
+ }
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'cmd.exe /c set', $null, $env_vars)
+ ("TEST=tesTing" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ ("TEST2=Testing 2" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ ("USERNAME=$($env:USERNAME)" -cnotin $actual.StandardOut.Split("`r`n")) | Assert-Equal -Expected $true
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with string stdin" = {
+ $expected = "input value`r`n`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
+ $null, $null, "input value")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with string stdin and newline" = {
+ $expected = "input value`r`n`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
+ $null, $null, "input value`r`n")
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with byte stdin" = {
+ $expected = "input value`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
+ $null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value"))
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with byte stdin and newline" = {
+ $expected = "input value`r`n`r`n"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
+ $null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value`r`n"))
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with lpApplicationName" = {
+ $expected = "abc`r`n"
+ $full_path = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe"
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($full_path, "Write-Output 'abc'", $null, $null)
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($full_path, "powershell.exe Write-Output 'abc'", $null, $null)
+ $actual.StandardOut | Assert-Equal -Expected $expected
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+
+ "CreateProcess with unicode and us-ascii encoding" = {
+ # Coverage breaks due to script parsing encoding issues with unicode chars, just use the code point instead
+ $poop = [System.Char]::ConvertFromUtf32(0xE05A)
+ $actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, "cmd.exe /c echo $poop café", $null, $null, '', 'us-ascii')
+ $actual.StandardOut | Assert-Equal -Expected "??? caf??`r`n"
+ $actual.StandardError | Assert-Equal -Expected ""
+ $actual.ExitCode | Assert-Equal -Expected 0
+ }
+}
+
+foreach ($test_impl in $tests.GetEnumerator()) {
+ $test = $test_impl.Key
+ &$test_impl.Value
+}
+
+$module.Result.data = "success"
+$module.ExitJson()
diff --git a/test/integration/targets/module_utils_Ansible.Process/tasks/main.yml b/test/integration/targets/module_utils_Ansible.Process/tasks/main.yml
new file mode 100644
index 0000000..13a5c16
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Process/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: test Ansible.Process.cs
+ ansible_process_tests:
+ register: ansible_process_tests
+
+- name: assert test Ansible.Process.cs
+ assert:
+ that:
+ - ansible_process_tests.data == "success"
diff --git a/test/integration/targets/module_utils_Ansible.Service/aliases b/test/integration/targets/module_utils_Ansible.Service/aliases
new file mode 100644
index 0000000..cf71478
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Service/aliases
@@ -0,0 +1,3 @@
+windows
+shippable/windows/group1
+shippable/windows/smoketest
diff --git a/test/integration/targets/module_utils_Ansible.Service/library/ansible_service_tests.ps1 b/test/integration/targets/module_utils_Ansible.Service/library/ansible_service_tests.ps1
new file mode 100644
index 0000000..dab42d4
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Service/library/ansible_service_tests.ps1
@@ -0,0 +1,953 @@
+#!powershell
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -CSharpUtil Ansible.Service
+#Requires -Module Ansible.ModuleUtils.ArgvParser
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
+
+$path = "$env:SystemRoot\System32\svchost.exe"
+
+Function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)][AllowNull()]$Actual,
+ [Parameter(Mandatory = $true, Position = 0)][AllowNull()]$Expected
+ )
+
+ process {
+ $matched = $false
+ if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array] -or $Actual -is [System.Collections.IList]) {
+ $Actual.Count | Assert-Equal -Expected $Expected.Count
+ for ($i = 0; $i -lt $Actual.Count; $i++) {
+ $actualValue = $Actual[$i]
+ $expectedValue = $Expected[$i]
+ Assert-Equal -Actual $actualValue -Expected $expectedValue
+ }
+ $matched = $true
+ }
+ else {
+ $matched = $Actual -ceq $Expected
+ }
+
+ if (-not $matched) {
+ if ($Actual -is [PSObject]) {
+ $Actual = $Actual.ToString()
+ }
+
+ $call_stack = (Get-PSCallStack)[1]
+ $module.Result.test = $test
+ $module.Result.actual = $Actual
+ $module.Result.expected = $Expected
+ $module.Result.line = $call_stack.ScriptLineNumber
+ $module.Result.method = $call_stack.Position.Text
+
+ $module.FailJson("AssertionError: actual != expected")
+ }
+ }
+}
+
+Function Invoke-Sc {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true)]
+ [String]
+ $Action,
+
+ [Parameter(Mandatory = $true)]
+ [String]
+ $Name,
+
+ [Object]
+ $Arguments
+ )
+
+ $commandArgs = [System.Collections.Generic.List[String]]@("sc.exe", $Action, $Name)
+ if ($null -ne $Arguments) {
+ if ($Arguments -is [System.Collections.IDictionary]) {
+ foreach ($arg in $Arguments.GetEnumerator()) {
+ $commandArgs.Add("$($arg.Key)=")
+ $commandArgs.Add($arg.Value)
+ }
+ }
+ else {
+ foreach ($arg in $Arguments) {
+ $commandArgs.Add($arg)
+ }
+ }
+ }
+
+ $command = Argv-ToString -arguments $commandArgs
+
+ $res = Run-Command -command $command
+ if ($res.rc -ne 0) {
+ $module.Result.rc = $res.rc
+ $module.Result.stdout = $res.stdout
+ $module.Result.stderr = $res.stderr
+ $module.FailJson("Failed to invoke sc with: $command")
+ }
+
+ $info = @{ Name = $Name }
+
+ if ($Action -eq 'qtriggerinfo') {
+ # qtriggerinfo is in a different format which requires some manual parsing from the norm.
+ $info.Triggers = [System.Collections.Generic.List[PSObject]]@()
+ }
+
+ $currentKey = $null
+ $qtriggerSection = @{}
+ $res.stdout -split "`r`n" | Foreach-Object -Process {
+ $line = $_.Trim()
+
+ if ($Action -eq 'qtriggerinfo' -and $line -in @('START SERVICE', 'STOP SERVICE')) {
+ if ($qtriggerSection.Count -gt 0) {
+ $info.Triggers.Add([PSCustomObject]$qtriggerSection)
+ $qtriggerSection = @{}
+ }
+
+ $qtriggerSection = @{
+ Action = $line
+ }
+ }
+
+ if (-not $line -or (-not $line.Contains(':') -and $null -eq $currentKey)) {
+ return
+ }
+
+ $lineSplit = $line.Split(':', 2)
+ if ($lineSplit.Length -eq 2) {
+ $k = $lineSplit[0].Trim()
+ if (-not $k) {
+ $k = $currentKey
+ }
+
+ $v = $lineSplit[1].Trim()
+ }
+ else {
+ $k = $currentKey
+ $v = $line
+ }
+
+ if ($qtriggerSection.Count -gt 0) {
+ if ($k -eq 'DATA') {
+ $qtriggerSection.Data.Add($v)
+ }
+ else {
+ $qtriggerSection.Type = $k
+ $qtriggerSection.SubType = $v
+ $qtriggerSection.Data = [System.Collections.Generic.List[String]]@()
+ }
+ }
+ else {
+ if ($info.ContainsKey($k)) {
+ if ($info[$k] -isnot [System.Collections.Generic.List[String]]) {
+ $info[$k] = [System.Collections.Generic.List[String]]@($info[$k])
+ }
+ $info[$k].Add($v)
+ }
+ else {
+ $currentKey = $k
+ $info[$k] = $v
+ }
+ }
+ }
+
+ if ($qtriggerSection.Count -gt 0) {
+ $info.Triggers.Add([PSCustomObject]$qtriggerSection)
+ }
+
+ [PSCustomObject]$info
+}
+
+$tests = [Ordered]@{
+ "Props on service created by New-Service" = {
+ $actual = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+
+ $actual.ServiceName | Assert-Equal -Expected $serviceName
+ $actual.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
+ $actual.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
+ $actual.ErrorControl | Assert-Equal -Expected ([Ansible.Service.ErrorControl]::Normal)
+ $actual.Path | Assert-Equal -Expected ('"{0}"' -f $path)
+ $actual.LoadOrderGroup | Assert-Equal -Expected ""
+ $actual.DependentOn.Count | Assert-Equal -Expected 0
+ $actual.Account | Assert-Equal -Expected (
+ [System.Security.Principal.SecurityIdentifier]'S-1-5-18').Translate([System.Security.Principal.NTAccount]
+ )
+ $actual.DisplayName | Assert-Equal -Expected $serviceName
+ $actual.Description | Assert-Equal -Expected $null
+ $actual.FailureActions.ResetPeriod | Assert-Equal -Expected 0
+ $actual.FailureActions.RebootMsg | Assert-Equal -Expected $null
+ $actual.FailureActions.Command | Assert-Equal -Expected $null
+ $actual.FailureActions.Actions.Count | Assert-Equal -Expected 0
+ $actual.FailureActionsOnNonCrashFailures | Assert-Equal -Expected $false
+ $actual.ServiceSidInfo | Assert-Equal -Expected ([Ansible.Service.ServiceSidInfo]::None)
+ $actual.RequiredPrivileges.Count | Assert-Equal -Expected 0
+ # Cannot test default values as it differs per OS version
+ $null -ne $actual.PreShutdownTimeout | Assert-Equal -Expected $true
+ $actual.Triggers.Count | Assert-Equal -Expected 0
+ $actual.PreferredNode | Assert-Equal -Expected $null
+ if ([Environment]::OSVersion.Version -ge [Version]'6.3') {
+ $actual.LaunchProtection | Assert-Equal -Expected ([Ansible.Service.LaunchProtection]::None)
+ }
+ else {
+ $actual.LaunchProtection | Assert-Equal -Expected $null
+ }
+ $actual.State | Assert-Equal -Expected ([Ansible.Service.ServiceStatus]::Stopped)
+ $actual.Win32ExitCode | Assert-Equal -Expected 1077 # ERROR_SERVICE_NEVER_STARTED
+ $actual.ServiceExitCode | Assert-Equal -Expected 0
+ $actual.Checkpoint | Assert-Equal -Expected 0
+ $actual.WaitHint | Assert-Equal -Expected 0
+ $actual.ProcessId | Assert-Equal -Expected 0
+ $actual.ServiceFlags | Assert-Equal -Expected ([Ansible.Service.ServiceFlags]::None)
+ $actual.DependedBy.Count | Assert-Equal 0
+ }
+
+ "Service creation through util" = {
+ $testName = "$($serviceName)_2"
+ $actual = [Ansible.Service.Service]::Create($testName, '"{0}"' -f $path)
+
+ try {
+ $cmdletService = Get-Service -Name $testName -ErrorAction SilentlyContinue
+ $null -ne $cmdletService | Assert-Equal -Expected $true
+
+ $actual.ServiceName | Assert-Equal -Expected $testName
+ $actual.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
+ $actual.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
+ $actual.ErrorControl | Assert-Equal -Expected ([Ansible.Service.ErrorControl]::Normal)
+ $actual.Path | Assert-Equal -Expected ('"{0}"' -f $path)
+ $actual.LoadOrderGroup | Assert-Equal -Expected ""
+ $actual.DependentOn.Count | Assert-Equal -Expected 0
+ $actual.Account | Assert-Equal -Expected (
+ [System.Security.Principal.SecurityIdentifier]'S-1-5-18').Translate([System.Security.Principal.NTAccount]
+ )
+ $actual.DisplayName | Assert-Equal -Expected $testName
+ $actual.Description | Assert-Equal -Expected $null
+ $actual.FailureActions.ResetPeriod | Assert-Equal -Expected 0
+ $actual.FailureActions.RebootMsg | Assert-Equal -Expected $null
+ $actual.FailureActions.Command | Assert-Equal -Expected $null
+ $actual.FailureActions.Actions.Count | Assert-Equal -Expected 0
+ $actual.FailureActionsOnNonCrashFailures | Assert-Equal -Expected $false
+ $actual.ServiceSidInfo | Assert-Equal -Expected ([Ansible.Service.ServiceSidInfo]::None)
+ $actual.RequiredPrivileges.Count | Assert-Equal -Expected 0
+ $null -ne $actual.PreShutdownTimeout | Assert-Equal -Expected $true
+ $actual.Triggers.Count | Assert-Equal -Expected 0
+ $actual.PreferredNode | Assert-Equal -Expected $null
+ if ([Environment]::OSVersion.Version -ge [Version]'6.3') {
+ $actual.LaunchProtection | Assert-Equal -Expected ([Ansible.Service.LaunchProtection]::None)
+ }
+ else {
+ $actual.LaunchProtection | Assert-Equal -Expected $null
+ }
+ $actual.State | Assert-Equal -Expected ([Ansible.Service.ServiceStatus]::Stopped)
+ $actual.Win32ExitCode | Assert-Equal -Expected 1077 # ERROR_SERVICE_NEVER_STARTED
+ $actual.ServiceExitCode | Assert-Equal -Expected 0
+ $actual.Checkpoint | Assert-Equal -Expected 0
+ $actual.WaitHint | Assert-Equal -Expected 0
+ $actual.ProcessId | Assert-Equal -Expected 0
+ $actual.ServiceFlags | Assert-Equal -Expected ([Ansible.Service.ServiceFlags]::None)
+ $actual.DependedBy.Count | Assert-Equal 0
+ }
+ finally {
+ $actual.Delete()
+ }
+ }
+
+ "Fail to open non-existing service" = {
+ $failed = $false
+ try {
+ $null = New-Object -TypeName Ansible.Service.Service -ArgumentList 'fake_service'
+ }
+ catch [Ansible.Service.ServiceManagerException] {
+ # 1060 == ERROR_SERVICE_DOES_NOT_EXIST
+ $_.Exception.Message -like '*Win32ErrorCode 1060 - 0x00000424*' | Assert-Equal -Expected $true
+ $failed = $true
+ }
+
+ $failed | Assert-Equal -Expected $true
+ }
+
+ "Open with specific access rights" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList @(
+ $serviceName, [Ansible.Service.ServiceRights]'QueryConfig, QueryStatus'
+ )
+
+ # QueryStatus can get the status
+ $service.State | Assert-Equal -Expected ([Ansible.Service.ServiceStatus]::Stopped)
+
+ # Should fail to get the config because we did not request that right
+ $failed = $false
+ try {
+ $service.Path = 'fail'
+ }
+ catch [Ansible.Service.ServiceManagerException] {
+ # 5 == ERROR_ACCESS_DENIED
+ $_.Exception.Message -like '*Win32ErrorCode 5 - 0x00000005*' | Assert-Equal -Expected $true
+ $failed = $true
+ }
+
+ $failed | Assert-Equal -Expected $true
+
+ }
+
+ "Modfiy ServiceType" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.ServiceType = [Ansible.Service.ServiceType]::Win32ShareProcess
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]::Win32ShareProcess)
+ $actual.TYPE | Assert-Equal -Expected "20 WIN32_SHARE_PROCESS"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{type = "own" }
+ $service.Refresh()
+ $service.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
+ }
+
+ "Create desktop interactive service" = {
+ $service = New-Object -Typename Ansible.Service.Service -ArgumentList $serviceName
+ $service.ServiceType = [Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess'
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $actual.TYPE | Assert-Equal -Expected "110 WIN32_OWN_PROCESS (interactive)"
+ $service.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess')
+
+ # Change back from interactive process
+ $service.ServiceType = [Ansible.Service.ServiceType]::Win32OwnProcess
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $actual.TYPE | Assert-Equal -Expected "10 WIN32_OWN_PROCESS"
+ $service.ServiceType | Assert-Equal -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
+
+ $service.Account = [System.Security.Principal.SecurityIdentifier]'S-1-5-20'
+
+ $failed = $false
+ try {
+ $service.ServiceType = [Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess'
+ }
+ catch [Ansible.Service.ServiceManagerException] {
+ $failed = $true
+ $_.Exception.NativeErrorCode | Assert-Equal -Expected 87 # ERROR_INVALID_PARAMETER
+ }
+ $failed | Assert-Equal -Expected $true
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $actual.TYPE | Assert-Equal -Expected "10 WIN32_OWN_PROCESS"
+ }
+
+ "Modify StartType" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.StartType = [Ansible.Service.ServiceStartType]::Disabled
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::Disabled)
+ $actual.START_TYPE | Assert-Equal -Expected "4 DISABLED"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{start = "demand" }
+ $service.Refresh()
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
+ }
+
+ "Modify StartType auto delayed" = {
+ # Delayed start type is a modifier of the AutoStart type. It uses a separate config entry to define and this
+ # makes sure the util does that correctly from various types and back.
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.StartType = [Ansible.Service.ServiceStartType]::Disabled # Start from Disabled
+
+ # Disabled -> Auto Start Delayed
+ $service.StartType = [Ansible.Service.ServiceStartType]::AutoStartDelayed
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::AutoStartDelayed)
+ $actual.START_TYPE | Assert-Equal -Expected "2 AUTO_START (DELAYED)"
+
+ # Auto Start Delayed -> Auto Start
+ $service.StartType = [Ansible.Service.ServiceStartType]::AutoStart
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::AutoStart)
+ $actual.START_TYPE | Assert-Equal -Expected "2 AUTO_START"
+
+ # Auto Start -> Auto Start Delayed
+ $service.StartType = [Ansible.Service.ServiceStartType]::AutoStartDelayed
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::AutoStartDelayed)
+ $actual.START_TYPE | Assert-Equal -Expected "2 AUTO_START (DELAYED)"
+
+ # Auto Start Delayed -> Manual
+ $service.StartType = [Ansible.Service.ServiceStartType]::DemandStart
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.StartType | Assert-Equal -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
+ $actual.START_TYPE | Assert-Equal -Expected "3 DEMAND_START"
+ }
+
+ "Modify ErrorControl" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.ErrorControl = [Ansible.Service.ErrorControl]::Severe
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.ErrorControl | Assert-Equal -Expected ([Ansible.Service.ErrorControl]::Severe)
+ $actual.ERROR_CONTROL | Assert-Equal -Expected "2 SEVERE"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{error = "ignore" }
+ $service.Refresh()
+ $service.ErrorControl | Assert-Equal -Expected ([Ansible.Service.ErrorControl]::Ignore)
+ }
+
+ "Modify Path" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Path = "Fake path"
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Path | Assert-Equal -Expected "Fake path"
+ $actual.BINARY_PATH_NAME | Assert-Equal -Expected "Fake path"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{binpath = "other fake path" }
+ $service.Refresh()
+ $service.Path | Assert-Equal -Expected "other fake path"
+ }
+
+ "Modify LoadOrderGroup" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.LoadOrderGroup = "my group"
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.LoadOrderGroup | Assert-Equal -Expected "my group"
+ $actual.LOAD_ORDER_GROUP | Assert-Equal -Expected "my group"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{group = "" }
+ $service.Refresh()
+ $service.LoadOrderGroup | Assert-Equal -Expected ""
+ }
+
+ "Modify DependentOn" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.DependentOn = @("HTTP", "WinRM")
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ @(, $service.DependentOn) | Assert-Equal -Expected @("HTTP", "WinRM")
+ @(, $actual.DEPENDENCIES) | Assert-Equal -Expected @("HTTP", "WinRM")
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{depend = "" }
+ $service.Refresh()
+ $service.DependentOn.Count | Assert-Equal -Expected 0
+ }
+
+ "Modify Account - service account" = {
+ $systemSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-18'
+ $systemName = $systemSid.Translate([System.Security.Principal.NTAccount])
+ $localSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-19'
+ $localName = $localSid.Translate([System.Security.Principal.NTAccount])
+ $networkSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-20'
+ $networkName = $networkSid.Translate([System.Security.Principal.NTAccount])
+
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Account = $networkSid
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $networkName
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected $networkName.Value
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{obj = $localName.Value }
+ $service.Refresh()
+ $service.Account | Assert-Equal -Expected $localName
+
+ $service.Account = $systemSid
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $systemName
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected "LocalSystem"
+ }
+
+ "Modify Account - user" = {
+ $currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Account = $currentSid
+ $service.Password = 'password'
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+
+ # When running tests in CI this seems to become .\Administrator
+ if ($service.Account.Value.StartsWith('.\')) {
+ $username = $service.Account.Value.Substring(2, $service.Account.Value.Length - 2)
+ $actualSid = ([System.Security.Principal.NTAccount]"$env:COMPUTERNAME\$username").Translate(
+ [System.Security.Principal.SecurityIdentifier]
+ )
+ }
+ else {
+ $actualSid = $service.Account.Translate([System.Security.Principal.SecurityIdentifier])
+ }
+ $actualSid.Value | Assert-Equal -Expected $currentSid.Value
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected $service.Account.Value
+
+ # Go back to SYSTEM from account
+ $systemSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-18'
+ $service.Account = $systemSid
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $systemSid.Translate([System.Security.Principal.NTAccount])
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected "LocalSystem"
+ }
+
+ "Modify Account - virtual account" = {
+ $account = [System.Security.Principal.NTAccount]"NT SERVICE\$serviceName"
+
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Account = $account
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $account
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected $account.Value
+ }
+
+ "Modify Account - gMSA" = {
+ # This cannot be tested through CI, only done on manual tests.
+ return
+
+ $gmsaName = [System.Security.Principal.NTAccount]'gMSA$@DOMAIN.LOCAL' # Make sure this is UPN.
+ $gmsaSid = $gmsaName.Translate([System.Security.Principal.SecurityIdentifier])
+ $gmsaNetlogon = $gmsaSid.Translate([System.Security.Principal.NTAccount])
+
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Account = $gmsaName
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $gmsaName
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected $gmsaName
+
+ # Go from gMSA to account and back to verify the Password doesn't matter.
+ $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
+ $service.Account = $currentUser
+ $service.Password = 'fake password'
+ $service.Password = 'fake password2'
+
+ # Now test in the Netlogon format.
+ $service.Account = $gmsaSid
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.Account | Assert-Equal -Expected $gmsaNetlogon
+ $actual.SERVICE_START_NAME | Assert-Equal -Expected $gmsaNetlogon.Value
+ }
+
+ "Modify DisplayName" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.DisplayName = "Custom Service Name"
+
+ $actual = Invoke-Sc -Action qc -Name $serviceName
+ $service.DisplayName | Assert-Equal -Expected "Custom Service Name"
+ $actual.DISPLAY_NAME | Assert-Equal -Expected "Custom Service Name"
+
+ $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{displayname = "New Service Name" }
+ $service.Refresh()
+ $service.DisplayName | Assert-Equal -Expected "New Service Name"
+ }
+
+ "Modify Description" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.Description = "My custom service description"
+
+ $actual = Invoke-Sc -Action qdescription -Name $serviceName
+ $service.Description | Assert-Equal -Expected "My custom service description"
+ $actual.DESCRIPTION | Assert-Equal -Expected "My custom service description"
+
+ $null = Invoke-Sc -Action description -Name $serviceName -Arguments @(, "new description")
+ $service.Description | Assert-Equal -Expected "new description"
+
+ $service.Description = $null
+
+ $actual = Invoke-Sc -Action qdescription -Name $serviceName
+ $service.Description | Assert-Equal -Expected $null
+ $actual.DESCRIPTION | Assert-Equal -Expected ""
+ }
+
+ "Modify FailureActions" = {
+ $newAction = [Ansible.Service.FailureActions]@{
+ ResetPeriod = 86400
+ RebootMsg = 'Reboot msg'
+ Command = 'Command line'
+ Actions = @(
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 1000 },
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 2000 },
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::Restart; Delay = 1000 },
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::Reboot; Delay = 1000 }
+ )
+ }
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.FailureActions = $newAction
+
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 86400
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'Reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'Command line'
+ $actual.FAILURE_ACTIONS.Count | Assert-Equal -Expected 4
+ $actual.FAILURE_ACTIONS[0] | Assert-Equal -Expected "RUN PROCESS -- Delay = 1000 milliseconds."
+ $actual.FAILURE_ACTIONS[1] | Assert-Equal -Expected "RUN PROCESS -- Delay = 2000 milliseconds."
+ $actual.FAILURE_ACTIONS[2] | Assert-Equal -Expected "RESTART -- Delay = 1000 milliseconds."
+ $actual.FAILURE_ACTIONS[3] | Assert-Equal -Expected "REBOOT -- Delay = 1000 milliseconds."
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 4
+
+ # Test that we can change individual settings and it doesn't change all
+ $service.FailureActions = [Ansible.Service.FailureActions]@{ResetPeriod = 172800 }
+
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 172800
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'Reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'Command line'
+ $actual.FAILURE_ACTIONS.Count | Assert-Equal -Expected 4
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 4
+
+ $service.FailureActions = [Ansible.Service.FailureActions]@{RebootMsg = "New reboot msg" }
+
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 172800
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'New reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'Command line'
+ $actual.FAILURE_ACTIONS.Count | Assert-Equal -Expected 4
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 4
+
+ $service.FailureActions = [Ansible.Service.FailureActions]@{Command = "New command line" }
+
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 172800
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'New reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'New command line'
+ $actual.FAILURE_ACTIONS.Count | Assert-Equal -Expected 4
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 4
+
+ # Test setting both ResetPeriod and Actions together
+ $service.FailureActions = [Ansible.Service.FailureActions]@{
+ ResetPeriod = 86400
+ Actions = @(
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 5000 },
+ [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::None; Delay = 0 }
+ )
+ }
+
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 86400
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'New reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'New command line'
+ # sc.exe does not show the None action it just ends the list, so we verify from get_FailureActions
+ $actual.FAILURE_ACTIONS | Assert-Equal -Expected "RUN PROCESS -- Delay = 5000 milliseconds."
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 2
+ $service.FailureActions.Actions[1].Type | Assert-Equal -Expected ([Ansible.Service.FailureAction]::None)
+
+ # Test setting just Actions without ResetPeriod
+ $service.FailureActions = [Ansible.Service.FailureActions]@{
+ Actions = [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 10000 }
+ }
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 86400
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'New reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'New command line'
+ $actual.FAILURE_ACTIONS | Assert-Equal -Expected "RUN PROCESS -- Delay = 10000 milliseconds."
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 1
+
+ # Test removing all actions
+ $service.FailureActions = [Ansible.Service.FailureActions]@{
+ Actions = @()
+ }
+ $actual = Invoke-Sc -Action qfailure -Name $serviceName
+ $actual.'RESET_PERIOD (in seconds)' | Assert-Equal -Expected 0 # ChangeServiceConfig2W resets this back to 0.
+ $actual.REBOOT_MESSAGE | Assert-Equal -Expected 'New reboot msg'
+ $actual.COMMAND_LINE | Assert-Equal -Expected 'New command line'
+ $actual.PSObject.Properties.Name.Contains('FAILURE_ACTIONS') | Assert-Equal -Expected $false
+ $service.FailureActions.Actions.Count | Assert-Equal -Expected 0
+
+ # Test that we are reading the right values
+ $null = Invoke-Sc -Action failure -Name $serviceName -Arguments @{
+ reset = 172800
+ reboot = "sc reboot msg"
+ command = "sc command line"
+ actions = "run/5000/reboot/800"
+ }
+
+ $actual = $service.FailureActions
+ $actual.ResetPeriod | Assert-Equal -Expected 172800
+ $actual.RebootMsg | Assert-Equal -Expected "sc reboot msg"
+ $actual.Command | Assert-Equal -Expected "sc command line"
+ $actual.Actions.Count | Assert-Equal -Expected 2
+ $actual.Actions[0].Type | Assert-Equal -Expected ([Ansible.Service.FailureAction]::RunCommand)
+ $actual.Actions[0].Delay | Assert-Equal -Expected 5000
+ $actual.Actions[1].Type | Assert-Equal -Expected ([Ansible.Service.FailureAction]::Reboot)
+ $actual.Actions[1].Delay | Assert-Equal -Expected 800
+ }
+
+ "Modify FailureActionsOnNonCrashFailures" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.FailureActionsOnNonCrashFailures = $true
+
+ $actual = Invoke-Sc -Action qfailureflag -Name $serviceName
+ $service.FailureActionsOnNonCrashFailures | Assert-Equal -Expected $true
+ $actual.FAILURE_ACTIONS_ON_NONCRASH_FAILURES | Assert-Equal -Expected "TRUE"
+
+ $null = Invoke-Sc -Action failureflag -Name $serviceName -Arguments @(, 0)
+ $service.FailureActionsOnNonCrashFailures | Assert-Equal -Expected $false
+ }
+
+ "Modify ServiceSidInfo" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.ServiceSidInfo = [Ansible.Service.ServiceSidInfo]::None
+
+ $actual = Invoke-Sc -Action qsidtype -Name $serviceName
+ $service.ServiceSidInfo | Assert-Equal -Expected ([Ansible.Service.ServiceSidInfo]::None)
+ $actual.SERVICE_SID_TYPE | Assert-Equal -Expected 'NONE'
+
+ $null = Invoke-Sc -Action sidtype -Name $serviceName -Arguments @(, 'unrestricted')
+ $service.ServiceSidInfo | Assert-Equal -Expected ([Ansible.Service.ServiceSidInfo]::Unrestricted)
+
+ $service.ServiceSidInfo = [Ansible.Service.ServiceSidInfo]::Restricted
+
+ $actual = Invoke-Sc -Action qsidtype -Name $serviceName
+ $service.ServiceSidInfo | Assert-Equal -Expected ([Ansible.Service.ServiceSidInfo]::Restricted)
+ $actual.SERVICE_SID_TYPE | Assert-Equal -Expected 'RESTRICTED'
+ }
+
+ "Modify RequiredPrivileges" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.RequiredPrivileges = @("SeBackupPrivilege", "SeTcbPrivilege")
+
+ $actual = Invoke-Sc -Action qprivs -Name $serviceName
+ , $service.RequiredPrivileges | Assert-Equal -Expected @("SeBackupPrivilege", "SeTcbPrivilege")
+ , $actual.PRIVILEGES | Assert-Equal -Expected @("SeBackupPrivilege", "SeTcbPrivilege")
+
+ # Ensure setting to $null is the same as an empty array
+ $service.RequiredPrivileges = $null
+
+ $actual = Invoke-Sc -Action qprivs -Name $serviceName
+ , $service.RequiredPrivileges | Assert-Equal -Expected @()
+ , $actual.PRIVILEGES | Assert-Equal -Expected @()
+
+ $service.RequiredPrivileges = @("SeBackupPrivilege", "SeTcbPrivilege")
+ $service.RequiredPrivileges = @()
+
+ $actual = Invoke-Sc -Action qprivs -Name $serviceName
+ , $service.RequiredPrivileges | Assert-Equal -Expected @()
+ , $actual.PRIVILEGES | Assert-Equal -Expected @()
+
+ $null = Invoke-Sc -Action privs -Name $serviceName -Arguments @(, "SeCreateTokenPrivilege/SeRestorePrivilege")
+ , $service.RequiredPrivileges | Assert-Equal -Expected @("SeCreateTokenPrivilege", "SeRestorePrivilege")
+ }
+
+ "Modify PreShutdownTimeout" = {
+ $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
+ $service.PreShutdownTimeout = 60000
+
+ # sc.exe doesn't seem to have a query argument for this, just get it from the registry
+ $actual = (
+ Get-ItemProperty -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName" -Name PreshutdownTimeout
+ ).PreshutdownTimeout
+ $actual | Assert-Equal -Expected 60000
+ }
+
+ "Modify Triggers" = {
+ $service = [Ansible.Service.Service]$serviceName
+ $service.Triggers = @(
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::DomainJoin
+ Action = [Ansible.Service.TriggerAction]::ServiceStop
+ SubType = [Guid][Ansible.Service.Trigger]::DOMAIN_JOIN_GUID
+ },
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::NetworkEndpoint
+ Action = [Ansible.Service.TriggerAction]::ServiceStart
+ SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
+ DataItems = [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::String
+ Data = 'my named pipe'
+ }
+ },
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::NetworkEndpoint
+ Action = [Ansible.Service.TriggerAction]::ServiceStart
+ SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
+ DataItems = [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::String
+ Data = 'my named pipe 2'
+ }
+ },
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::Custom
+ Action = [Ansible.Service.TriggerAction]::ServiceStart
+ SubType = [Guid]'9bf04e57-05dc-4914-9ed9-84bf992db88c'
+ DataItems = @(
+ [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::Binary
+ Data = [byte[]]@(1, 2, 3, 4)
+ },
+ [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::Binary
+ Data = [byte[]]@(5, 6, 7, 8, 9)
+ }
+ )
+ }
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::Custom
+ Action = [Ansible.Service.TriggerAction]::ServiceStart
+ SubType = [Guid]'9fbcfc7e-7581-4d46-913b-53bb15c80c51'
+ DataItems = @(
+ [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::String
+ Data = 'entry 1'
+ },
+ [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::String
+ Data = 'entry 2'
+ }
+ )
+ },
+ [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::FirewallPortEvent
+ Action = [Ansible.Service.TriggerAction]::ServiceStop
+ SubType = [Guid][Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID
+ DataItems = [Ansible.Service.TriggerItem]@{
+ Type = [Ansible.Service.TriggerDataType]::String
+ Data = [System.Collections.Generic.List[String]]@("1234", "tcp", "imagepath", "servicename")
+ }
+ }
+ )
+
+ $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
+
+ $actual.Triggers.Count | Assert-Equal -Expected 6
+ $actual.Triggers[0].Type | Assert-Equal -Expected 'DOMAIN JOINED STATUS'
+ $actual.Triggers[0].Action | Assert-Equal -Expected 'STOP SERVICE'
+ $actual.Triggers[0].SubType | Assert-Equal -Expected "$([Ansible.Service.Trigger]::DOMAIN_JOIN_GUID) [DOMAIN JOINED]"
+ $actual.Triggers[0].Data.Count | Assert-Equal -Expected 0
+
+ $actual.Triggers[1].Type | Assert-Equal -Expected 'NETWORK EVENT'
+ $actual.Triggers[1].Action | Assert-Equal -Expected 'START SERVICE'
+ $actual.Triggers[1].SubType | Assert-Equal -Expected "$([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) [NAMED PIPE EVENT]"
+ $actual.Triggers[1].Data.Count | Assert-Equal -Expected 1
+ $actual.Triggers[1].Data[0] | Assert-Equal -Expected 'my named pipe'
+
+ $actual.Triggers[2].Type | Assert-Equal -Expected 'NETWORK EVENT'
+ $actual.Triggers[2].Action | Assert-Equal -Expected 'START SERVICE'
+ $actual.Triggers[2].SubType | Assert-Equal -Expected "$([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) [NAMED PIPE EVENT]"
+ $actual.Triggers[2].Data.Count | Assert-Equal -Expected 1
+ $actual.Triggers[2].Data[0] | Assert-Equal -Expected 'my named pipe 2'
+
+ $actual.Triggers[3].Type | Assert-Equal -Expected 'CUSTOM'
+ $actual.Triggers[3].Action | Assert-Equal -Expected 'START SERVICE'
+ $actual.Triggers[3].SubType | Assert-Equal -Expected '9bf04e57-05dc-4914-9ed9-84bf992db88c [ETW PROVIDER UUID]'
+ $actual.Triggers[3].Data.Count | Assert-Equal -Expected 2
+ $actual.Triggers[3].Data[0] | Assert-Equal -Expected '01 02 03 04'
+ $actual.Triggers[3].Data[1] | Assert-Equal -Expected '05 06 07 08 09'
+
+ $actual.Triggers[4].Type | Assert-Equal -Expected 'CUSTOM'
+ $actual.Triggers[4].Action | Assert-Equal -Expected 'START SERVICE'
+ $actual.Triggers[4].SubType | Assert-Equal -Expected '9fbcfc7e-7581-4d46-913b-53bb15c80c51 [ETW PROVIDER UUID]'
+ $actual.Triggers[4].Data.Count | Assert-Equal -Expected 2
+ $actual.Triggers[4].Data[0] | Assert-Equal -Expected "entry 1"
+ $actual.Triggers[4].Data[1] | Assert-Equal -Expected "entry 2"
+
+ $actual.Triggers[5].Type | Assert-Equal -Expected 'FIREWALL PORT EVENT'
+ $actual.Triggers[5].Action | Assert-Equal -Expected 'STOP SERVICE'
+ $actual.Triggers[5].SubType | Assert-Equal -Expected "$([Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID) [PORT CLOSE]"
+ $actual.Triggers[5].Data.Count | Assert-Equal -Expected 1
+ $actual.Triggers[5].Data[0] | Assert-Equal -Expected '1234;tcp;imagepath;servicename'
+
+ # Remove trigger with $null
+ $service.Triggers = $null
+
+ $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
+ $actual.Triggers.Count | Assert-Equal -Expected 0
+
+ # Add a single trigger
+ $service.Triggers = [Ansible.Service.Trigger]@{
+ Type = [Ansible.Service.TriggerType]::GroupPolicy
+ Action = [Ansible.Service.TriggerAction]::ServiceStart
+ SubType = [Guid][Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID
+ }
+
+ $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
+ $actual.Triggers.Count | Assert-Equal -Expected 1
+ $actual.Triggers[0].Type | Assert-Equal -Expected 'GROUP POLICY'
+ $actual.Triggers[0].Action | Assert-Equal -Expected 'START SERVICE'
+ $actual.Triggers[0].SubType | Assert-Equal -Expected "$([Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID) [MACHINE POLICY PRESENT]"
+ $actual.Triggers[0].Data.Count | Assert-Equal -Expected 0
+
+ # Remove trigger with empty list
+ $service.Triggers = @()
+
+ $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
+ $actual.Triggers.Count | Assert-Equal -Expected 0
+
+ # Add triggers through sc and check we get the values correctly
+ $null = Invoke-Sc -Action triggerinfo -Name $serviceName -Arguments @(
+ 'start/namedpipe/abc',
+ 'start/namedpipe/def',
+ 'start/custom/d4497e12-ac36-4823-af61-92db0dbd4a76/11223344/aabbccdd',
+ 'start/strcustom/435a1742-22c5-4234-9db3-e32dafde695c/11223344/aabbccdd',
+ 'stop/portclose/1234;tcp;imagepath;servicename',
+ 'stop/networkoff'
+ )
+
+ $actual = $service.Triggers
+ $actual.Count | Assert-Equal -Expected 6
+
+ $actual[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::NetworkEndpoint)
+ $actual[0].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
+ $actual[0].SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
+ $actual[0].DataItems.Count | Assert-Equal -Expected 1
+ $actual[0].DataItems[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::String)
+ $actual[0].DataItems[0].Data | Assert-Equal -Expected 'abc'
+
+ $actual[1].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::NetworkEndpoint)
+ $actual[1].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
+ $actual[1].SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
+ $actual[1].DataItems.Count | Assert-Equal -Expected 1
+ $actual[1].DataItems[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::String)
+ $actual[1].DataItems[0].Data | Assert-Equal -Expected 'def'
+
+ $actual[2].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::Custom)
+ $actual[2].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
+ $actual[2].SubType = [Guid]'d4497e12-ac36-4823-af61-92db0dbd4a76'
+ $actual[2].DataItems.Count | Assert-Equal -Expected 2
+ $actual[2].DataItems[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::Binary)
+ , $actual[2].DataItems[0].Data | Assert-Equal -Expected ([byte[]]@(17, 34, 51, 68))
+ $actual[2].DataItems[1].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::Binary)
+ , $actual[2].DataItems[1].Data | Assert-Equal -Expected ([byte[]]@(170, 187, 204, 221))
+
+ $actual[3].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::Custom)
+ $actual[3].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
+ $actual[3].SubType = [Guid]'435a1742-22c5-4234-9db3-e32dafde695c'
+ $actual[3].DataItems.Count | Assert-Equal -Expected 2
+ $actual[3].DataItems[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::String)
+ $actual[3].DataItems[0].Data | Assert-Equal -Expected '11223344'
+ $actual[3].DataItems[1].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::String)
+ $actual[3].DataItems[1].Data | Assert-Equal -Expected 'aabbccdd'
+
+ $actual[4].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::FirewallPortEvent)
+ $actual[4].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStop)
+ $actual[4].SubType = [Guid][Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID
+ $actual[4].DataItems.Count | Assert-Equal -Expected 1
+ $actual[4].DataItems[0].Type | Assert-Equal -Expected ([Ansible.Service.TriggerDataType]::String)
+ , $actual[4].DataItems[0].Data | Assert-Equal -Expected @('1234', 'tcp', 'imagepath', 'servicename')
+
+ $actual[5].Type | Assert-Equal -Expected ([Ansible.Service.TriggerType]::IpAddressAvailability)
+ $actual[5].Action | Assert-Equal -Expected ([Ansible.Service.TriggerAction]::ServiceStop)
+ $actual[5].SubType = [Guid][Ansible.Service.Trigger]::NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID
+ $actual[5].DataItems.Count | Assert-Equal -Expected 0
+ }
+
+ # Cannot test PreferredNode as we can't guarantee CI is set up with NUMA support.
+ # Cannot test LaunchProtection as once set we cannot remove unless rebooting
+}
+
+# setup and teardown should favour native tools to create and delete the service and not the util we are testing.
+foreach ($testImpl in $tests.GetEnumerator()) {
+ $serviceName = "ansible_$([System.IO.Path]::GetRandomFileName())"
+ $null = New-Service -Name $serviceName -BinaryPathName ('"{0}"' -f $path) -StartupType Manual
+
+ try {
+ $test = $testImpl.Key
+ &$testImpl.Value
+ }
+ finally {
+ $null = Invoke-Sc -Action delete -Name $serviceName
+ }
+}
+
+$module.Result.data = "success"
+$module.ExitJson()
diff --git a/test/integration/targets/module_utils_Ansible.Service/tasks/main.yml b/test/integration/targets/module_utils_Ansible.Service/tasks/main.yml
new file mode 100644
index 0000000..78f91e1
--- /dev/null
+++ b/test/integration/targets/module_utils_Ansible.Service/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: test Ansible.Service.cs
+ ansible_service_tests:
+ register: ansible_service_test
+
+- name: assert test Ansible.Service.cs
+ assert:
+ that:
+ - ansible_service_test.data == "success"
diff --git a/test/integration/targets/module_utils_ansible_release/aliases b/test/integration/targets/module_utils_ansible_release/aliases
new file mode 100644
index 0000000..7ae73ab
--- /dev/null
+++ b/test/integration/targets/module_utils_ansible_release/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+context/target
diff --git a/test/integration/targets/module_utils_ansible_release/library/ansible_release.py b/test/integration/targets/module_utils_ansible_release/library/ansible_release.py
new file mode 100644
index 0000000..528465d
--- /dev/null
+++ b/test/integration/targets/module_utils_ansible_release/library/ansible_release.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2021, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: ansible_release
+short_description: Get ansible_release info from module_utils
+description: Get ansible_release info from module_utils
+author:
+- Ansible Project
+'''
+
+EXAMPLES = r'''
+#
+'''
+
+RETURN = r'''
+#
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.ansible_release import __version__, __author__, __codename__
+
+
+def main():
+ module = AnsibleModule(argument_spec={})
+ result = {
+ 'version': __version__,
+ 'author': __author__,
+ 'codename': __codename__,
+ }
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils_ansible_release/tasks/main.yml b/test/integration/targets/module_utils_ansible_release/tasks/main.yml
new file mode 100644
index 0000000..4d20b84
--- /dev/null
+++ b/test/integration/targets/module_utils_ansible_release/tasks/main.yml
@@ -0,0 +1,9 @@
+- name: Get module_utils ansible_release vars
+ ansible_release:
+ register: ansible_release
+
+- assert:
+ that:
+ - ansible_release['version'][0]|int != 0
+ - ansible_release['author'] == 'Ansible, Inc.'
+ - ansible_release['codename']|length > 0
diff --git a/test/integration/targets/module_utils_common.respawn/aliases b/test/integration/targets/module_utils_common.respawn/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/module_utils_common.respawn/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/module_utils_common.respawn/library/respawnme.py b/test/integration/targets/module_utils_common.respawn/library/respawnme.py
new file mode 100644
index 0000000..6471dba
--- /dev/null
+++ b/test/integration/targets/module_utils_common.respawn/library/respawnme.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2021, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import sys
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.respawn import respawn_module, has_respawned
+
+
+def main():
+ mod = AnsibleModule(argument_spec=dict(
+ mode=dict(required=True, choices=['multi_respawn', 'no_respawn', 'respawn'])
+ ))
+
+ # just return info about what interpreter we're currently running under
+ if mod.params['mode'] == 'no_respawn':
+ mod.exit_json(interpreter_path=sys.executable)
+ elif mod.params['mode'] == 'respawn':
+ if not has_respawned():
+ new_interpreter = os.path.join(mod.tmpdir, 'anotherpython')
+ os.symlink(sys.executable, new_interpreter)
+ respawn_module(interpreter_path=new_interpreter)
+
+ # respawn should always exit internally- if we continue executing here, it's a bug
+ raise Exception('FAIL, should never reach this line')
+ else:
+ # return the current interpreter, as well as a signal that we created a different one
+ mod.exit_json(created_interpreter=sys.executable, interpreter_path=sys.executable)
+ elif mod.params['mode'] == 'multi_respawn':
+ # blindly respawn with the current interpreter, the second time should bomb
+ respawn_module(sys.executable)
+
+ # shouldn't be any way for us to fall through, but just in case, that's also a bug
+ raise Exception('FAIL, should never reach this code')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils_common.respawn/tasks/main.yml b/test/integration/targets/module_utils_common.respawn/tasks/main.yml
new file mode 100644
index 0000000..50178df
--- /dev/null
+++ b/test/integration/targets/module_utils_common.respawn/tasks/main.yml
@@ -0,0 +1,24 @@
+- name: collect the default interpreter
+ respawnme:
+ mode: no_respawn
+ register: default_python
+
+- name: cause a respawn
+ respawnme:
+ mode: respawn
+ register: respawned_python
+
+- name: cause multiple respawns (should fail)
+ respawnme:
+ mode: multi_respawn
+ ignore_errors: true
+ register: multi_respawn
+
+- assert:
+ that:
+ # the respawn task should report the path of the interpreter it created, and that it's running under the new interpreter
+ - respawned_python.created_interpreter == respawned_python.interpreter_path
+ # ensure that the respawned interpreter is not the same as the default
+ - default_python.interpreter_path != respawned_python.interpreter_path
+ # multiple nested respawns should fail
+ - multi_respawn is failed and (multi_respawn.module_stdout + multi_respawn.module_stderr) is search('has already been respawned')
diff --git a/test/integration/targets/module_utils_distro/aliases b/test/integration/targets/module_utils_distro/aliases
new file mode 100644
index 0000000..1d28bdb
--- /dev/null
+++ b/test/integration/targets/module_utils_distro/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group5
+context/controller
diff --git a/test/integration/targets/module_utils_distro/meta/main.yml b/test/integration/targets/module_utils_distro/meta/main.yml
new file mode 100644
index 0000000..1810d4b
--- /dev/null
+++ b/test/integration/targets/module_utils_distro/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/module_utils_distro/runme.sh b/test/integration/targets/module_utils_distro/runme.sh
new file mode 100755
index 0000000..e5d3d05
--- /dev/null
+++ b/test/integration/targets/module_utils_distro/runme.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# Ensure that when a non-distro 'distro' package is in PYTHONPATH, we fallback
+# to our bundled one.
+new_pythonpath="$OUTPUT_DIR/pythonpath"
+mkdir -p "$new_pythonpath/distro"
+touch "$new_pythonpath/distro/__init__.py"
+
+export PYTHONPATH="$new_pythonpath:$PYTHONPATH"
+
+# Sanity test to make sure the above worked
+set +e
+distro_id_fail="$(python -c 'import distro; distro.id' 2>&1)"
+set -e
+grep -q "AttributeError:.*has no attribute 'id'" <<< "$distro_id_fail"
+
+# ansible.module_utils.common.sys_info imports distro, and itself gets imported
+# in DataLoader, so all we have to do to test the fallback is run `ansible`.
+ansirun="$(ansible -i ../../inventory -a "echo \$PYTHONPATH" localhost)"
+grep -q "$new_pythonpath" <<< "$ansirun"
+
+rm -rf "$new_pythonpath"
diff --git a/test/integration/targets/module_utils_facts.system.selinux/aliases b/test/integration/targets/module_utils_facts.system.selinux/aliases
new file mode 100644
index 0000000..a6dafcf
--- /dev/null
+++ b/test/integration/targets/module_utils_facts.system.selinux/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/module_utils_facts.system.selinux/tasks/main.yml b/test/integration/targets/module_utils_facts.system.selinux/tasks/main.yml
new file mode 100644
index 0000000..1717239
--- /dev/null
+++ b/test/integration/targets/module_utils_facts.system.selinux/tasks/main.yml
@@ -0,0 +1,38 @@
+- name: check selinux config
+ shell: |
+ command -v getenforce &&
+ getenforce | grep -E 'Enforcing|Permissive'
+ ignore_errors: yes
+ register: selinux_state
+
+- name: explicitly collect selinux facts
+ setup:
+ gather_subset:
+ - '!all'
+ - '!any'
+ - selinux
+ register: selinux_facts
+
+- set_fact:
+ selinux_policytype: "unknown"
+
+- name: check selinux policy type
+ shell: grep '^SELINUXTYPE=' /etc/selinux/config | cut -d'=' -f2
+ ignore_errors: yes
+ register: r
+
+- set_fact:
+ selinux_policytype: "{{ r.stdout_lines[0] }}"
+ when: r is success and r.stdout_lines
+
+- assert:
+ that:
+ - selinux_facts is success and selinux_facts.ansible_facts.ansible_selinux is defined
+ - (selinux_facts.ansible_facts.ansible_selinux.status in ['disabled', 'Missing selinux Python library'] if selinux_state is not success else True)
+ - (selinux_facts.ansible_facts.ansible_selinux.status == 'enabled' if selinux_state is success else True)
+ - (selinux_facts.ansible_facts.ansible_selinux.mode in ['enforcing', 'permissive'] if selinux_state is success else True)
+ - (selinux_facts.ansible_facts.ansible_selinux.type == selinux_policytype if selinux_state is success else True)
+
+- name: run selinux tests
+ include_tasks: selinux.yml
+ when: selinux_state is success
diff --git a/test/integration/targets/module_utils_facts.system.selinux/tasks/selinux.yml b/test/integration/targets/module_utils_facts.system.selinux/tasks/selinux.yml
new file mode 100644
index 0000000..6a2b159
--- /dev/null
+++ b/test/integration/targets/module_utils_facts.system.selinux/tasks/selinux.yml
@@ -0,0 +1,93 @@
+- name: collect selinux facts
+ setup:
+ gather_subset: ['!all', '!min', selinux]
+ register: fact_output
+
+- debug:
+ var: fact_output
+
+- name: create tempdir container in home
+ file:
+ path: ~/.selinux_tmp
+ state: directory
+
+- name: create tempdir
+ tempfile:
+ path: ~/.selinux_tmp
+ prefix: selinux_test
+ state: directory
+ register: tempdir
+
+- name: ls -1Zd tempdir to capture context from FS
+ shell: ls -1Zd '{{ tempdir.path }}'
+ register: tempdir_context_output
+
+- name: create a file under the tempdir with no context info specified (it should inherit parent context)
+ file:
+ path: '{{ tempdir.path }}/file_inherited_context'
+ state: touch
+ register: file_inherited_context
+
+- name: ls -1Z inherited file to capture context from FS
+ shell: ls -1Z '{{ tempdir.path }}/file_inherited_context'
+ register: inherited_context_output
+
+- name: copy the file with explicit overrides on all context values
+ copy:
+ remote_src: yes
+ src: '{{ tempdir.path }}/file_inherited_context'
+ dest: '{{ tempdir.path }}/file_explicit_context'
+ seuser: system_u
+ serole: system_r
+ setype: user_tmp_t
+ # default configs don't have MLS levels defined, so we can't test that yet
+ # selevel: s1
+ register: file_explicit_context
+
+- name: ls -1Z explicit file to capture context from FS
+ shell: ls -1Z '{{ tempdir.path }}/file_explicit_context'
+ register: explicit_context_output
+
+- name: alter the tempdir context
+ file:
+ path: '{{ tempdir.path }}'
+ seuser: system_u
+ serole: system_r
+ setype: user_tmp_t
+ # default configs don't have MLS levels defined, so we can't test that yet
+ # selevel: s1
+ register: tempdir_altered
+
+- name: ls -1Z tempdir to capture context from FS
+ shell: ls -1Z '{{ tempdir.path }}/file_explicit_context'
+ register: tempdir_altered_context_output
+
+- name: copy the explicit context file with default overrides on all context values
+ copy:
+ remote_src: yes
+ src: '{{ tempdir.path }}/file_explicit_context'
+ dest: '{{ tempdir.path }}/file_default_context'
+ seuser: _default
+ serole: _default
+ setype: _default
+ selevel: _default
+ register: file_default_context
+
+- name: see what matchpathcon thinks the context of default_file_context should be
+ shell: matchpathcon {{ file_default_context.dest }} | awk '{ print $2 }'
+ register: expected_default_context
+
+- assert:
+ that:
+ - fact_output.ansible_facts.ansible_selinux.config_mode in ['enforcing','permissive']
+ - fact_output.ansible_facts.ansible_selinux.mode in ['enforcing','permissive']
+ - fact_output.ansible_facts.ansible_selinux.status == 'enabled'
+ - fact_output.ansible_facts.ansible_selinux_python_present == true
+ # assert that secontext is set on the file results (injected by basic.py, for better or worse)
+ - tempdir.secontext is match('.+:.+:.+') and tempdir.secontext in tempdir_context_output.stdout
+ - file_inherited_context.secontext is match('.+:.+:.+') and file_inherited_context.secontext in inherited_context_output.stdout
+ - file_inherited_context.secontext == tempdir.secontext # should've been inherited from the parent dir since not set explicitly
+ - file_explicit_context.secontext == 'system_u:system_r:user_tmp_t:s0' and file_explicit_context.secontext in explicit_context_output.stdout
+ - tempdir_altered.secontext == 'system_u:system_r:user_tmp_t:s0' and tempdir_altered.secontext in tempdir_altered_context_output.stdout
+ # the one with reset defaults should match the original tempdir context, not the altered one (ie, it was set by the original policy context, not inherited from the parent dir)
+ - file_default_context.secontext == expected_default_context.stdout_lines[0]
diff --git a/test/integration/targets/module_utils_urls/aliases b/test/integration/targets/module_utils_urls/aliases
new file mode 100644
index 0000000..3c4491b
--- /dev/null
+++ b/test/integration/targets/module_utils_urls/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group1
+needs/httptester
diff --git a/test/integration/targets/module_utils_urls/library/test_peercert.py b/test/integration/targets/module_utils_urls/library/test_peercert.py
new file mode 100644
index 0000000..ecb7d20
--- /dev/null
+++ b/test/integration/targets/module_utils_urls/library/test_peercert.py
@@ -0,0 +1,98 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2020, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: test_perrcert
+short_description: Test getting the peer certificate of a HTTP response
+description: Test getting the peer certificate of a HTTP response.
+options:
+ url:
+ description: The endpoint to get the peer cert for
+ required: true
+ type: str
+author:
+- Ansible Project
+'''
+
+EXAMPLES = r'''
+#
+'''
+
+RETURN = r'''
+#
+'''
+
+import base64
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_text
+from ansible.module_utils.urls import getpeercert, Request
+
+
+def get_x509_shorthand(name, value):
+ prefix = {
+ 'countryName': 'C',
+ 'stateOrProvinceName': 'ST',
+ 'localityName': 'L',
+ 'organizationName': 'O',
+ 'commonName': 'CN',
+ 'organizationalUnitName': 'OU',
+ }[name]
+
+ return '%s=%s' % (prefix, value)
+
+
+def main():
+ module_args = dict(
+ url=dict(type='str', required=True),
+ )
+ module = AnsibleModule(
+ argument_spec=module_args,
+ supports_check_mode=True,
+ )
+ result = {
+ 'changed': False,
+ 'cert': None,
+ 'raw_cert': None,
+ }
+
+ req = Request().get(module.params['url'])
+ try:
+ cert = getpeercert(req)
+ b_cert = getpeercert(req, binary_form=True)
+
+ finally:
+ req.close()
+
+ if cert:
+ processed_cert = {
+ 'issuer': '',
+ 'not_after': cert.get('notAfter', None),
+ 'not_before': cert.get('notBefore', None),
+ 'serial_number': cert.get('serialNumber', None),
+ 'subject': '',
+ 'version': cert.get('version', None),
+ }
+
+ for field in ['issuer', 'subject']:
+ field_values = []
+ for x509_part in cert.get(field, []):
+ field_values.append(get_x509_shorthand(x509_part[0][0], x509_part[0][1]))
+
+ processed_cert[field] = ",".join(field_values)
+
+ result['cert'] = processed_cert
+
+ if b_cert:
+ result['raw_cert'] = to_text(base64.b64encode(b_cert))
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_utils_urls/meta/main.yml b/test/integration/targets/module_utils_urls/meta/main.yml
new file mode 100644
index 0000000..f3a332d
--- /dev/null
+++ b/test/integration/targets/module_utils_urls/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+- prepare_http_tests
+- setup_remote_tmp_dir
diff --git a/test/integration/targets/module_utils_urls/tasks/main.yml b/test/integration/targets/module_utils_urls/tasks/main.yml
new file mode 100644
index 0000000..ca76a7d
--- /dev/null
+++ b/test/integration/targets/module_utils_urls/tasks/main.yml
@@ -0,0 +1,32 @@
+- name: get peercert for HTTP connection
+ test_peercert:
+ url: http://{{ httpbin_host }}/get
+ register: cert_http
+
+- name: assert get peercert for HTTP connection
+ assert:
+ that:
+ - cert_http.raw_cert == None
+
+- name: get peercert for HTTPS connection
+ test_peercert:
+ url: https://{{ httpbin_host }}/get
+ register: cert_https
+
+# Alpine does not have openssl, just make sure the text was actually set instead
+- name: check if openssl is installed
+ command: which openssl
+ ignore_errors: yes
+ register: openssl
+
+- name: get actual certificate from endpoint
+ shell: echo | openssl s_client -connect {{ httpbin_host }}:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'
+ register: cert_https_actual
+ changed_when: no
+ when: openssl is successful
+
+- name: assert get peercert for HTTPS connection
+ assert:
+ that:
+ - cert_https.raw_cert != None
+ - openssl is failed or cert_https.raw_cert == cert_https_actual.stdout_lines[1:-1] | join("")