#!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