diff options
Diffstat (limited to '')
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("") |