1 #!powershell
2 
3 #AnsibleRequires -CSharpUtil Ansible.Basic
4 
5 $module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
6 
Assert-Equals()7 Function Assert-Equals {
8     param(
9         [Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual,
10         [Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected
11     )
12 
13     $matched = $false
14     if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
15         $Actual.Count | Assert-Equals -Expected $Expected.Count
16         for ($i = 0; $i -lt $Actual.Count; $i++) {
17             $actual_value = $Actual[$i]
18             $expected_value = $Expected[$i]
19             Assert-Equals -Actual $actual_value -Expected $expected_value
20         }
21         $matched = $true
22     } else {
23         $matched = $Actual -ceq $Expected
24     }
25 
26     if (-not $matched) {
27         if ($Actual -is [PSObject]) {
28             $Actual = $Actual.ToString()
29         }
30 
31         $call_stack = (Get-PSCallStack)[1]
32         $module.Result.failed = $true
33         $module.Result.test = $test
34         $module.Result.actual = $Actual
35         $module.Result.expected = $Expected
36         $module.Result.line = $call_stack.ScriptLineNumber
37         $module.Result.method = $call_stack.Position.Text
38         $module.Result.msg = "AssertionError: actual != expected"
39 
40         Exit-Module
41     }
42 }
43 
Assert-DictionaryEqualsnull44 Function Assert-DictionaryEquals {
45     param(
46         [Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual,
47         [Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected
48     )
49     $actual_keys = $Actual.Keys
50     $expected_keys = $Expected.Keys
51 
52     $actual_keys.Count | Assert-Equals -Expected $expected_keys.Count
53     foreach ($actual_entry in $Actual.GetEnumerator()) {
54         $actual_key = $actual_entry.Key
55         ($actual_key -cin $expected_keys) | Assert-Equals -Expected $true
56         $actual_value = $actual_entry.Value
57         $expected_value = $Expected.$actual_key
58 
59         if ($actual_value -is [System.Collections.IDictionary]) {
60             $actual_value | Assert-DictionaryEquals -Expected $expected_value
61         } elseif ($actual_value -is [System.Collections.ArrayList] -or $actual_value -is [Array]) {
62             for ($i = 0; $i -lt $actual_value.Count; $i++) {
63                 $actual_entry = $actual_value[$i]
64                 $expected_entry = $expected_value[$i]
65                 if ($actual_entry -is [System.Collections.IDictionary]) {
66                     $actual_entry | Assert-DictionaryEquals -Expected $expected_entry
67                 } else {
68                     Assert-Equals -Actual $actual_entry -Expected $expected_entry
69                 }
70             }
71         } else {
72             Assert-Equals -Actual $actual_value -Expected $expected_value
73         }
74     }
75     foreach ($expected_key in $expected_keys) {
76         ($expected_key -cin $actual_keys) | Assert-Equals -Expected $true
77     }
78 }
79 
Exit-Modulenull80 Function Exit-Module {
81     # Make sure Exit actually calls exit and not our overriden test behaviour
82     [Ansible.Basic.AnsibleModule]::Exit = { param([Int32]$rc) exit $rc }
83     Write-Output -InputObject (ConvertTo-Json -InputObject $module.Result -Compress -Depth 99)
84     $module.ExitJson()
85 }
86 
87 $tmpdir = $module.Tmpdir
88 
89 # Override the Exit and WriteLine behaviour to throw an exception instead of exiting the module
90 [Ansible.Basic.AnsibleModule]::Exit = {
91     param([Int32]$rc)
92     $exp = New-Object -TypeName System.Exception -ArgumentList "exit: $rc"
93     $exp | Add-Member -Type NoteProperty -Name Output -Value $_test_out
94     throw $exp
95 }
96 [Ansible.Basic.AnsibleModule]::WriteLine = {
97     param([String]$line)
98     Set-Variable -Name _test_out -Scope Global -Value $line
99 }
100 
101 $tests = @{
102     "Empty spec and no options - args file" = {
103         $args_file = Join-Path -Path $tmpdir -ChildPath "args-$(Get-Random).json"
104         [System.IO.File]::WriteAllText($args_file, '{ "ANSIBLE_MODULE_ARGS": {} }')
105         $m = [Ansible.Basic.AnsibleModule]::Create(@($args_file), @{})
106 
107         $m.CheckMode | Assert-Equals -Expected $false
108         $m.DebugMode | Assert-Equals -Expected $false
109         $m.DiffMode | Assert-Equals -Expected $false
110         $m.KeepRemoteFiles | Assert-Equals -Expected $false
111         $m.ModuleName | Assert-Equals -Expected "undefined win module"
112         $m.NoLog | Assert-Equals -Expected $false
113         $m.Verbosity | Assert-Equals -Expected 0
114         $m.AnsibleVersion | Assert-Equals -Expected $null
115     }
116 
117     "Empty spec and no options - complex_args" = {
118         Set-Variable -Name complex_args -Scope Global -Value @{}
119         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
120 
121         $m.CheckMode | Assert-Equals -Expected $false
122         $m.DebugMode | Assert-Equals -Expected $false
123         $m.DiffMode | Assert-Equals -Expected $false
124         $m.KeepRemoteFiles | Assert-Equals -Expected $false
125         $m.ModuleName | Assert-Equals -Expected "undefined win module"
126         $m.NoLog | Assert-Equals -Expected $false
127         $m.Verbosity | Assert-Equals -Expected 0
128         $m.AnsibleVersion | Assert-Equals -Expected $null
129     }
130 
131     "Internal param changes - args file" = {
132         $m_tmpdir = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
133         New-Item -Path $m_tmpdir -ItemType Directory > $null
134         $args_file = Join-Path -Path $tmpdir -ChildPath "args-$(Get-Random).json"
135         [System.IO.File]::WriteAllText($args_file, @"
136 {
137     "ANSIBLE_MODULE_ARGS": {
138         "_ansible_check_mode": true,
139         "_ansible_debug": true,
140         "_ansible_diff": true,
141         "_ansible_keep_remote_files": true,
142         "_ansible_module_name": "ansible_basic_tests",
143         "_ansible_no_log": true,
144         "_ansible_remote_tmp": "%TEMP%",
145         "_ansible_selinux_special_fs": "ignored",
146         "_ansible_shell_executable": "ignored",
147         "_ansible_socket": "ignored",
148         "_ansible_syslog_facility": "ignored",
149         "_ansible_tmpdir": "$($m_tmpdir -replace "\\", "\\")",
150         "_ansible_verbosity": 3,
151         "_ansible_version": "2.8.0"
152     }
153 }
154 "@)
155         $m = [Ansible.Basic.AnsibleModule]::Create(@($args_file), @{supports_check_mode=$true})
156         $m.CheckMode | Assert-Equals -Expected $true
157         $m.DebugMode | Assert-Equals -Expected $true
158         $m.DiffMode | Assert-Equals -Expected $true
159         $m.KeepRemoteFiles | Assert-Equals -Expected $true
160         $m.ModuleName | Assert-Equals -Expected "ansible_basic_tests"
161         $m.NoLog | Assert-Equals -Expected $true
162         $m.Verbosity | Assert-Equals -Expected 3
163         $m.AnsibleVersion | Assert-Equals -Expected "2.8.0"
164         $m.Tmpdir | Assert-Equals -Expected $m_tmpdir
165     }
166 
167     "Internal param changes - complex_args" = {
168         $m_tmpdir = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
169         New-Item -Path $m_tmpdir -ItemType Directory > $null
170         Set-Variable -Name complex_args -Scope Global -Value @{
171             _ansible_check_mode = $true
172             _ansible_debug = $true
173             _ansible_diff = $true
174             _ansible_keep_remote_files = $true
175             _ansible_module_name = "ansible_basic_tests"
176             _ansible_no_log = $true
177             _ansible_remote_tmp = "%TEMP%"
178             _ansible_selinux_special_fs = "ignored"
179             _ansible_shell_executable = "ignored"
180             _ansible_socket = "ignored"
181             _ansible_syslog_facility = "ignored"
182             _ansible_tmpdir = $m_tmpdir.ToString()
183             _ansible_verbosity = 3
184             _ansible_version = "2.8.0"
185         }
186         $spec = @{
187             supports_check_mode = $true
188         }
189         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
190         $m.CheckMode | Assert-Equals -Expected $true
191         $m.DebugMode | Assert-Equals -Expected $true
192         $m.DiffMode | Assert-Equals -Expected $true
193         $m.KeepRemoteFiles | Assert-Equals -Expected $true
194         $m.ModuleName | Assert-Equals -Expected "ansible_basic_tests"
195         $m.NoLog | Assert-Equals -Expected $true
196         $m.Verbosity | Assert-Equals -Expected 3
197         $m.AnsibleVersion | Assert-Equals -Expected "2.8.0"
198         $m.Tmpdir | Assert-Equals -Expected $m_tmpdir
199     }
200 
201     "Parse complex module options" = {
202         $spec = @{
203             options = @{
204                 option_default = @{}
205                 missing_option_default = @{}
206                 string_option = @{type = "str"}
207                 required_option = @{required = $true}
208                 missing_choices = @{choices = "a", "b"}
209                 choices = @{choices = "a", "b"}
210                 one_choice = @{choices = ,"b"}
211                 choice_with_default = @{choices = "a", "b"; default = "b"}
212                 alias_direct = @{aliases = ,"alias_direct1"}
213                 alias_as_alias = @{aliases = "alias_as_alias1", "alias_as_alias2"}
214                 bool_type = @{type = "bool"}
215                 bool_from_str = @{type = "bool"}
216                 dict_type = @{
217                     type = "dict"
218                     options = @{
219                         int_type = @{type = "int"}
220                         str_type = @{type = "str"; default = "str_sub_type"}
221                     }
222                 }
223                 dict_type_missing = @{
224                     type = "dict"
225                     options = @{
226                         int_type = @{type = "int"}
227                         str_type = @{type = "str"; default = "str_sub_type"}
228                     }
229                 }
230                 dict_type_defaults = @{
231                     type = "dict"
232                     apply_defaults = $true
233                     options = @{
234                         int_type = @{type = "int"}
235                         str_type = @{type = "str"; default = "str_sub_type"}
236                     }
237                 }
238                 dict_type_json = @{type = "dict"}
239                 dict_type_str = @{type = "dict"}
240                 float_type = @{type = "float"}
241                 int_type = @{type = "int"}
242                 json_type = @{type = "json"}
243                 json_type_dict = @{type = "json"}
244                 list_type = @{type = "list"}
245                 list_type_str = @{type = "list"}
246                 list_with_int = @{type = "list"; elements = "int"}
247                 list_type_single = @{type = "list"}
248                 list_with_dict = @{
249                     type = "list"
250                     elements = "dict"
251                     options = @{
252                         int_type = @{type = "int"}
253                         str_type = @{type = "str"; default = "str_sub_type"}
254                     }
255                 }
256                 path_type = @{type = "path"}
257                 path_type_nt = @{type = "path"}
258                 path_type_missing = @{type = "path"}
259                 raw_type_str = @{type = "raw"}
260                 raw_type_int = @{type = "raw"}
261                 sid_type = @{type = "sid"}
262                 sid_from_name = @{type = "sid"}
263                 str_type = @{type = "str"}
264                 delegate_type = @{type = [Func[[Object], [UInt64]]]{ [System.UInt64]::Parse($args[0]) }}
265             }
266         }
267         Set-Variable -Name complex_args -Scope Global -Value @{
268             option_default = 1
269             string_option = 1
270             required_option = "required"
271             choices = "a"
272             one_choice = "b"
273             alias_direct = "a"
274             alias_as_alias2 = "a"
275             bool_type = $true
276             bool_from_str = "false"
277             dict_type = @{
278                 int_type = "10"
279             }
280             dict_type_json = '{"a":"a","b":1,"c":["a","b"]}'
281             dict_type_str = 'a=a b="b 2" c=c'
282             float_type = "3.14159"
283             int_type = 0
284             json_type = '{"a":"a","b":1,"c":["a","b"]}'
285             json_type_dict = @{
286                 a = "a"
287                 b = 1
288                 c = @("a", "b")
289             }
290             list_type = @("a", "b", 1, 2)
291             list_type_str = "a, b,1,2 "
292             list_with_int = @("1", 2)
293             list_type_single = "single"
294             list_with_dict = @(
295                 @{
296                     int_type = 2
297                     str_type = "dict entry"
298                 },
299                 @{ int_type = 1 },
300                 @{}
301             )
302             path_type = "%SystemRoot%\System32"
303             path_type_nt = "\\?\%SystemRoot%\System32"
304             path_type_missing = "T:\missing\path"
305             raw_type_str = "str"
306             raw_type_int = 1
307             sid_type = "S-1-5-18"
308             sid_from_name = "SYSTEM"
309             str_type = "str"
310             delegate_type = "1234"
311         }
312         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
313 
314         $m.Params.option_default | Assert-Equals -Expected "1"
315         $m.Params.option_default.GetType().ToString() | Assert-Equals -Expected "System.String"
316         $m.Params.missing_option_default | Assert-Equals -Expected $null
317         $m.Params.string_option | Assert-Equals -Expected "1"
318         $m.Params.string_option.GetType().ToString() | Assert-Equals -Expected "System.String"
319         $m.Params.required_option | Assert-Equals -Expected "required"
320         $m.Params.required_option.GetType().ToString() | Assert-Equals -Expected "System.String"
321         $m.Params.missing_choices | Assert-Equals -Expected $null
322         $m.Params.choices | Assert-Equals -Expected "a"
323         $m.Params.choices.GetType().ToString() | Assert-Equals -Expected "System.String"
324         $m.Params.one_choice | Assert-Equals -Expected "b"
325         $m.Params.one_choice.GetType().ToString() | Assert-Equals -Expected "System.String"
326         $m.Params.choice_with_default | Assert-Equals -Expected "b"
327         $m.Params.choice_with_default.GetType().ToString() | Assert-Equals -Expected "System.String"
328         $m.Params.alias_direct | Assert-Equals -Expected "a"
329         $m.Params.alias_direct.GetType().ToString() | Assert-Equals -Expected "System.String"
330         $m.Params.alias_as_alias | Assert-Equals -Expected "a"
331         $m.Params.alias_as_alias.GetType().ToString() | Assert-Equals -Expected "System.String"
332         $m.Params.bool_type | Assert-Equals -Expected $true
333         $m.Params.bool_type.GetType().ToString() | Assert-Equals -Expected "System.Boolean"
334         $m.Params.bool_from_str | Assert-Equals -Expected $false
335         $m.Params.bool_from_str.GetType().ToString() | Assert-Equals -Expected "System.Boolean"
336         $m.Params.dict_type | Assert-DictionaryEquals -Expected @{int_type = 10; str_type = "str_sub_type"}
337         $m.Params.dict_type.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
338         $m.Params.dict_type.int_type.GetType().ToString() | Assert-Equals -Expected "System.Int32"
339         $m.Params.dict_type.str_type.GetType().ToString() | Assert-Equals -Expected "System.String"
340         $m.Params.dict_type_missing | Assert-Equals -Expected $null
341         $m.Params.dict_type_defaults | Assert-DictionaryEquals -Expected @{int_type = $null; str_type = "str_sub_type"}
342         $m.Params.dict_type_defaults.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
343         $m.Params.dict_type_defaults.str_type.GetType().ToString() | Assert-Equals -Expected "System.String"
344         $m.Params.dict_type_json | Assert-DictionaryEquals -Expected @{
345             a = "a"
346             b = 1
347             c = @("a", "b")
348         }
349         $m.Params.dict_type_json.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
350         $m.Params.dict_type_json.a.GetType().ToString() | Assert-Equals -Expected "System.String"
351         $m.Params.dict_type_json.b.GetType().ToString() | Assert-Equals -Expected "System.Int32"
352         $m.Params.dict_type_json.c.GetType().ToString() | Assert-Equals -Expected "System.Collections.ArrayList"
353         $m.Params.dict_type_str | Assert-DictionaryEquals -Expected @{a = "a"; b = "b 2"; c = "c"}
354         $m.Params.dict_type_str.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]"
355         $m.Params.dict_type_str.a.GetType().ToString() | Assert-Equals -Expected "System.String"
356         $m.Params.dict_type_str.b.GetType().ToString() | Assert-Equals -Expected "System.String"
357         $m.Params.dict_type_str.c.GetType().ToString() | Assert-Equals -Expected "System.String"
358         $m.Params.float_type | Assert-Equals -Expected ([System.Single]3.14159)
359         $m.Params.float_type.GetType().ToString() | Assert-Equals -Expected "System.Single"
360         $m.Params.int_type | Assert-Equals -Expected 0
361         $m.Params.int_type.GetType().ToString() | Assert-Equals -Expected "System.Int32"
362         $m.Params.json_type | Assert-Equals -Expected '{"a":"a","b":1,"c":["a","b"]}'
363         $m.Params.json_type.GetType().ToString() | Assert-Equals -Expected "System.String"
364         [Ansible.Basic.AnsibleModule]::FromJson($m.Params.json_type_dict) | Assert-DictionaryEquals -Expected ([Ansible.Basic.AnsibleModule]::FromJson('{"a":"a","b":1,"c":["a","b"]}'))
365         $m.Params.json_type_dict.GetType().ToString() | Assert-Equals -Expected "System.String"
366         $m.Params.list_type.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.List``1[System.Object]"
367         $m.Params.list_type.Count | Assert-Equals -Expected 4
368         $m.Params.list_type[0] | Assert-Equals -Expected "a"
369         $m.Params.list_type[0].GetType().FullName | Assert-Equals -Expected "System.String"
370         $m.Params.list_type[1] | Assert-Equals -Expected "b"
371         $m.Params.list_type[1].GetType().FullName | Assert-Equals -Expected "System.String"
372         $m.Params.list_type[2] | Assert-Equals -Expected 1
373         $m.Params.list_type[2].GetType().FullName | Assert-Equals -Expected "System.Int32"
374         $m.Params.list_type[3] | Assert-Equals -Expected 2
375         $m.Params.list_type[3].GetType().FullName | Assert-Equals -Expected "System.Int32"
376         $m.Params.list_type_str.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.List``1[System.Object]"
377         $m.Params.list_type_str.Count | Assert-Equals -Expected 4
378         $m.Params.list_type_str[0] | Assert-Equals -Expected "a"
379         $m.Params.list_type_str[0].GetType().FullName | Assert-Equals -Expected "System.String"
380         $m.Params.list_type_str[1] | Assert-Equals -Expected "b"
381         $m.Params.list_type_str[1].GetType().FullName | Assert-Equals -Expected "System.String"
382         $m.Params.list_type_str[2] | Assert-Equals -Expected "1"
383         $m.Params.list_type_str[2].GetType().FullName | Assert-Equals -Expected "System.String"
384         $m.Params.list_type_str[3] | Assert-Equals -Expected "2"
385         $m.Params.list_type_str[3].GetType().FullName | Assert-Equals -Expected "System.String"
386         $m.Params.list_with_int.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.List``1[System.Object]"
387         $m.Params.list_with_int.Count | Assert-Equals -Expected 2
388         $m.Params.list_with_int[0] | Assert-Equals -Expected 1
389         $m.Params.list_with_int[0].GetType().FullName | Assert-Equals -Expected "System.Int32"
390         $m.Params.list_with_int[1] | Assert-Equals -Expected 2
391         $m.Params.list_with_int[1].GetType().FullName | Assert-Equals -Expected "System.Int32"
392         $m.Params.list_type_single.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.List``1[System.Object]"
393         $m.Params.list_type_single.Count | Assert-Equals -Expected 1
394         $m.Params.list_type_single[0] | Assert-Equals -Expected "single"
395         $m.Params.list_type_single[0].GetType().FullName | Assert-Equals -Expected "System.String"
396         $m.Params.list_with_dict.GetType().FullName.StartsWith("System.Collections.Generic.List``1[[System.Object") | Assert-Equals -Expected $true
397         $m.Params.list_with_dict.Count | Assert-Equals -Expected 3
398         $m.Params.list_with_dict[0].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equals -Expected $true
399         $m.Params.list_with_dict[0] | Assert-DictionaryEquals -Expected @{int_type = 2; str_type = "dict entry"}
400         $m.Params.list_with_dict[0].int_type.GetType().FullName.ToString() | Assert-Equals -Expected "System.Int32"
401         $m.Params.list_with_dict[0].str_type.GetType().FullName.ToString() | Assert-Equals -Expected "System.String"
402         $m.Params.list_with_dict[1].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equals -Expected $true
403         $m.Params.list_with_dict[1] | Assert-DictionaryEquals -Expected @{int_type = 1; str_type = "str_sub_type"}
404         $m.Params.list_with_dict[1].int_type.GetType().FullName.ToString() | Assert-Equals -Expected "System.Int32"
405         $m.Params.list_with_dict[1].str_type.GetType().FullName.ToString() | Assert-Equals -Expected "System.String"
406         $m.Params.list_with_dict[2].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equals -Expected $true
407         $m.Params.list_with_dict[2] | Assert-DictionaryEquals -Expected @{int_type = $null; str_type = "str_sub_type"}
408         $m.Params.list_with_dict[2].str_type.GetType().FullName.ToString() | Assert-Equals -Expected "System.String"
409         $m.Params.path_type | Assert-Equals -Expected "$($env:SystemRoot)\System32"
410         $m.Params.path_type.GetType().ToString() | Assert-Equals -Expected "System.String"
411         $m.Params.path_type_nt | Assert-Equals -Expected "\\?\%SystemRoot%\System32"
412         $m.Params.path_type_nt.GetType().ToString() | Assert-Equals -Expected "System.String"
413         $m.Params.path_type_missing | Assert-Equals -Expected "T:\missing\path"
414         $m.Params.path_type_missing.GetType().ToString() | Assert-Equals -Expected "System.String"
415         $m.Params.raw_type_str | Assert-Equals -Expected "str"
416         $m.Params.raw_type_str.GetType().FullName | Assert-Equals -Expected "System.String"
417         $m.Params.raw_type_int | Assert-Equals -Expected 1
418         $m.Params.raw_type_int.GetType().FullName | Assert-Equals -Expected "System.Int32"
419         $m.Params.sid_type | Assert-Equals -Expected (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList "S-1-5-18")
420         $m.Params.sid_type.GetType().ToString() | Assert-Equals -Expected "System.Security.Principal.SecurityIdentifier"
421         $m.Params.sid_from_name | Assert-Equals -Expected (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList "S-1-5-18")
422         $m.Params.sid_from_name.GetType().ToString() | Assert-Equals -Expected "System.Security.Principal.SecurityIdentifier"
423         $m.Params.str_type | Assert-Equals -Expected "str"
424         $m.Params.str_type.GetType().ToString() | Assert-Equals -Expected "System.String"
425         $m.Params.delegate_type | Assert-Equals -Expected 1234
426         $m.Params.delegate_type.GetType().ToString() | Assert-Equals -Expected "System.UInt64"
427 
428         $failed = $false
429         try {
430             $m.ExitJson()
431         } catch [System.Management.Automation.RuntimeException] {
432             $failed = $true
433             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
434             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
435         }
436         $failed | Assert-Equals -Expected $true
437 
438         $expected_module_args = @{
439             option_default = "1"
440             missing_option_default = $null
441             string_option = "1"
442             required_option = "required"
443             missing_choices = $null
444             choices = "a"
445             one_choice = "b"
446             choice_with_default = "b"
447             alias_direct = "a"
448             alias_as_alias = "a"
449             alias_as_alias2 = "a"
450             bool_type = $true
451             bool_from_str = $false
452             dict_type = @{
453                 int_type = 10
454                 str_type = "str_sub_type"
455             }
456             dict_type_missing = $null
457             dict_type_defaults = @{
458                 int_type = $null
459                 str_type = "str_sub_type"
460             }
461             dict_type_json = @{
462                 a = "a"
463                 b = 1
464                 c = @("a", "b")
465             }
466             dict_type_str = @{
467                 a = "a"
468                 b = "b 2"
469                 c = "c"
470             }
471             float_type = 3.14159
472             int_type = 0
473             json_type = $m.Params.json_type.ToString()
474             json_type_dict = $m.Params.json_type_dict.ToString()
475             list_type = @("a", "b", 1, 2)
476             list_type_str = @("a", "b", "1", "2")
477             list_with_int = @(1, 2)
478             list_type_single = @("single")
479             list_with_dict = @(
480                 @{
481                     int_type = 2
482                     str_type = "dict entry"
483                 },
484                 @{
485                     int_type = 1
486                     str_type = "str_sub_type"
487                 },
488                 @{
489                     int_type = $null
490                     str_type = "str_sub_type"
491                 }
492             )
493             path_type = "$($env:SystemRoot)\System32"
494             path_type_nt = "\\?\%SystemRoot%\System32"
495             path_type_missing = "T:\missing\path"
496             raw_type_str = "str"
497             raw_type_int = 1
498             sid_type = "S-1-5-18"
499             sid_from_name = "S-1-5-18"
500             str_type = "str"
501             delegate_type = 1234
502         }
503         $actual.Keys.Count | Assert-Equals -Expected 2
504         $actual.changed | Assert-Equals -Expected $false
505         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $expected_module_args}
506     }
507 
508     "Parse module args with list elements and delegate type" = {
509         $spec = @{
510             options = @{
511                 list_delegate_type = @{
512                     type = "list"
513                     elements = [Func[[Object], [UInt16]]]{ [System.UInt16]::Parse($args[0]) }
514                 }
515             }
516         }
517         Set-Variable -Name complex_args -Scope Global -Value @{
518             list_delegate_type = @(
519                 "1234",
520                 4321
521             )
522         }
523         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
524         $m.Params.list_delegate_type.GetType().Name | Assert-Equals -Expected 'List`1'
525         $m.Params.list_delegate_type[0].GetType().FullName | Assert-Equals -Expected "System.UInt16"
526         $m.Params.list_delegate_Type[1].GetType().FullName | Assert-Equals -Expected "System.UInt16"
527 
528         $failed = $false
529         try {
530             $m.ExitJson()
531         } catch [System.Management.Automation.RuntimeException] {
532             $failed = $true
533             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
534             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
535         }
536         $failed | Assert-Equals -Expected $true
537 
538         $expected_module_args = @{
539             list_delegate_type = @(
540                 1234,
541                 4321
542             )
543         }
544         $actual.Keys.Count | Assert-Equals -Expected 2
545         $actual.changed | Assert-Equals -Expected $false
546         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $expected_module_args}
547     }
548 
549     "Parse module args with case insensitive input" = {
550         $spec = @{
551             options = @{
552                 option1 = @{ type = "int"; required = $true }
553             }
554         }
555         Set-Variable -Name complex_args -Scope Global -Value @{
556             _ansible_module_name = "win_test"
557             Option1 = "1"
558         }
559 
560         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
561         # Verifies the case of the params key is set to the module spec not actual input
562         $m.Params.Keys | Assert-Equals -Expected @("option1")
563         $m.Params.option1 | Assert-Equals -Expected 1
564 
565         # Verifies the type conversion happens even on a case insensitive match
566         $m.Params.option1.GetType().FullName | Assert-Equals -Expected "System.Int32"
567 
568         $failed = $false
569         try {
570             $m.ExitJson()
571         } catch [System.Management.Automation.RuntimeException] {
572             $failed = $true
573             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
574             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
575         }
576         $failed | Assert-Equals -Expected $true
577 
578         $expected_warnings = "Parameters for (win_test) was a case insensitive match: Option1. "
579         $expected_warnings += "Module options will become case sensitive in a future Ansible release. "
580         $expected_warnings += "Supported parameters include: option1"
581 
582         $expected = @{
583             changed = $false
584             invocation = @{
585                 module_args = @{
586                     option1 = 1
587                 }
588             }
589             # We have disabled the warning for now
590             #warnings = @($expected_warnings)
591         }
592         $actual | Assert-DictionaryEquals -Expected $expected
593     }
594 
595     "No log values" = {
596         $spec = @{
597             options = @{
598                 username = @{type = "str"}
599                 password = @{type = "str"; no_log = $true}
600                 password2 = @{type = "int"; no_log = $true}
601                 dict = @{type = "dict"}
602             }
603         }
604         Set-Variable -Name complex_args -Scope Global -Value @{
605             _ansible_module_name = "test_no_log"
606             username = "user - pass - name"
607             password = "pass"
608             password2 = 1234
609             dict = @{
610                 data = "Oops this is secret: pass"
611                 dict = @{
612                     pass = "plain"
613                     hide = "pass"
614                     sub_hide = "password"
615                     int_hide = 123456
616                 }
617                 list = @(
618                     "pass",
619                     "password",
620                     1234567,
621                     "pa ss",
622                     @{
623                         pass = "plain"
624                         hide = "pass"
625                         sub_hide = "password"
626                         int_hide = 123456
627                     }
628                 )
629                 custom = "pass"
630             }
631         }
632 
633         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
634         $m.Result.data = $complex_args.dict
635 
636         # verify params internally aren't masked
637         $m.Params.username | Assert-Equals -Expected "user - pass - name"
638         $m.Params.password | Assert-Equals -Expected "pass"
639         $m.Params.password2 | Assert-Equals -Expected 1234
640         $m.Params.dict.custom | Assert-Equals -Expected "pass"
641 
642         $failed = $false
643         try {
644             $m.ExitJson()
645         } catch [System.Management.Automation.RuntimeException] {
646             $failed = $true
647             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
648             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
649         }
650         $failed | Assert-Equals -Expected $true
651 
652         # verify no_log params are masked in invocation
653         $expected = @{
654             invocation = @{
655                 module_args = @{
656                     password2 = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
657                     dict = @{
658                         dict = @{
659                             pass = "plain"
660                             hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
661                             sub_hide = "********word"
662                             int_hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
663                         }
664                         custom = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
665                         list = @(
666                             "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
667                             "********word",
668                             "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
669                             "pa ss",
670                             @{
671                                 pass = "plain"
672                                 hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
673                                 sub_hide = "********word"
674                                 int_hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
675                             }
676                         )
677                         data = "Oops this is secret: ********"
678                     }
679                     username = "user - ******** - name"
680                     password = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
681                 }
682             }
683             changed = $false
684             data = $complex_args.dict
685         }
686         $actual | Assert-DictionaryEquals -Expected $expected
687 
688         $expected_event = @'
689 test_no_log - Invoked with:
690   username: user - ******** - name
691   dict: dict: sub_hide: ****word
692       pass: plain
693       int_hide: ********56
694       hide: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
695       data: Oops this is secret: ********
696       custom: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
697       list:
698       - VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
699       - ********word
700       - ********567
701       - pa ss
702       - sub_hide: ********word
703           pass: plain
704           int_hide: ********56
705           hide: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
706   password2: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
707   password: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
708 '@
709         $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message
710         $actual_event | Assert-DictionaryEquals -Expected $expected_event
711     }
712 
713     "No log value with an empty string" = {
714         $spec = @{
715             options = @{
716                 password1 = @{type = "str"; no_log = $true}
717                 password2 = @{type = "str"; no_log = $true}
718             }
719         }
720         Set-Variable -Name complex_args -Scope Global -Value @{
721             _ansible_module_name = "test_no_log"
722             password1 = ""
723         }
724 
725         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
726         $m.Result.data = $complex_args.dict
727 
728         # verify params internally aren't masked
729         $m.Params.password1 | Assert-Equals -Expected ""
730         $m.Params.password2 | Assert-Equals -Expected $null
731 
732         $failed = $false
733         try {
734             $m.ExitJson()
735         } catch [System.Management.Automation.RuntimeException] {
736             $failed = $true
737             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
738             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
739         }
740         $failed | Assert-Equals -Expected $true
741 
742         $expected = @{
743             invocation = @{
744                 module_args = @{
745                     password1 = ""
746                     password2 = $null
747                 }
748             }
749             changed = $false
750             data = $complex_args.dict
751         }
752         $actual | Assert-DictionaryEquals -Expected $expected
753     }
754 
755     "Removed in version" = {
756         $spec = @{
757             options = @{
758                 removed1 = @{removed_in_version = "2.1"}
759                 removed2 = @{removed_in_version = "2.2"}
760                 removed3 = @{removed_in_version = "2.3"; removed_from_collection = "ansible.builtin"}
761             }
762         }
763         Set-Variable -Name complex_args -Scope Global -Value @{
764             removed1 = "value"
765             removed3 = "value"
766         }
767 
768         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
769 
770         $failed = $false
771         try {
772             $m.ExitJson()
773         } catch [System.Management.Automation.RuntimeException] {
774             $failed = $true
775             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
776             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
777         }
778         $failed | Assert-Equals -Expected $true
779 
780         $expected = @{
781             changed = $false
782             invocation = @{
783                 module_args = @{
784                     removed1 = "value"
785                     removed2 = $null
786                     removed3 = "value"
787                 }
788             }
789             deprecations = @(
790                 @{
791                     msg = "Param 'removed3' is deprecated. See the module docs for more information"
792                     version = "2.3"
793                     collection_name = "ansible.builtin"
794                 },
795                 @{
796                     msg = "Param 'removed1' is deprecated. See the module docs for more information"
797                     version = "2.1"
798                     collection_name = $null
799                 }
800             )
801         }
802         $actual | Assert-DictionaryEquals -Expected $expected
803     }
804 
805     "Removed at date" = {
806         $spec = @{
807             options = @{
808                 removed1 = @{removed_at_date = [DateTime]"2020-03-10"}
809                 removed2 = @{removed_at_date = [DateTime]"2020-03-11"}
810                 removed3 = @{removed_at_date = [DateTime]"2020-06-07"; removed_from_collection = "ansible.builtin"}
811             }
812         }
813         Set-Variable -Name complex_args -Scope Global -Value @{
814             removed1 = "value"
815             removed3 = "value"
816         }
817 
818         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
819 
820         $failed = $false
821         try {
822             $m.ExitJson()
823         } catch [System.Management.Automation.RuntimeException] {
824             $failed = $true
825             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
826             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
827         }
828         $failed | Assert-Equals -Expected $true
829 
830         $expected = @{
831             changed = $false
832             invocation = @{
833                 module_args = @{
834                     removed1 = "value"
835                     removed2 = $null
836                     removed3 = "value"
837                 }
838             }
839             deprecations = @(
840                 @{
841                     msg = "Param 'removed3' is deprecated. See the module docs for more information"
842                     date = "2020-06-07"
843                     collection_name = "ansible.builtin"
844                 },
845                 @{
846                     msg = "Param 'removed1' is deprecated. See the module docs for more information"
847                     date = "2020-03-10"
848                     collection_name = $null
849                 }
850             )
851         }
852         $actual | Assert-DictionaryEquals -Expected $expected
853     }
854 
855     "Deprecated aliases" = {
856         $spec = @{
857             options = @{
858                 option1 = @{ type = "str"; aliases = "alias1"; deprecated_aliases = @(@{name = "alias1"; version = "2.10"}) }
859                 option2 = @{ type = "str"; aliases = "alias2"; deprecated_aliases = @(@{name = "alias2"; version = "2.11"}) }
860                 option3 = @{
861                     type = "dict"
862                     options = @{
863                         option1 = @{ type = "str"; aliases = "alias1"; deprecated_aliases = @(@{name = "alias1"; version = "2.10"}) }
864                         option2 = @{ type = "str"; aliases = "alias2"; deprecated_aliases = @(@{name = "alias2"; version = "2.11"}) }
865                         option3 = @{ type = "str"; aliases = "alias3"; deprecated_aliases = @(@{name = "alias3"; version = "2.12"; collection_name = "ansible.builtin"}) }
866                         option4 = @{ type = "str"; aliases = "alias4"; deprecated_aliases = @(@{name = "alias4"; date = [DateTime]"2020-03-11"}) }
867                         option5 = @{ type = "str"; aliases = "alias5"; deprecated_aliases = @(@{name = "alias5"; date = [DateTime]"2020-03-09"}) }
868                         option6 = @{ type = "str"; aliases = "alias6"; deprecated_aliases = @(@{name = "alias6"; date = [DateTime]"2020-06-01"; collection_name = "ansible.builtin"}) }
869                     }
870                 }
871                 option4 = @{ type = "str"; aliases = "alias4"; deprecated_aliases = @(@{name = "alias4"; date = [DateTime]"2020-03-10"}) }
872                 option5 = @{ type = "str"; aliases = "alias5"; deprecated_aliases = @(@{name = "alias5"; date = [DateTime]"2020-03-12"}) }
873                 option6 = @{ type = "str"; aliases = "alias6"; deprecated_aliases = @(@{name = "alias6"; version = "2.12"; collection_name = "ansible.builtin"}) }
874                 option7 = @{ type = "str"; aliases = "alias7"; deprecated_aliases = @(@{name = "alias7"; date = [DateTime]"2020-06-07"; collection_name = "ansible.builtin"}) }
875             }
876         }
877 
878         Set-Variable -Name complex_args -Scope Global -Value @{
879             alias1 = "alias1"
880             option2 = "option2"
881             option3 = @{
882                 option1 = "option1"
883                 alias2 = "alias2"
884                 alias3 = "alias3"
885                 option4 = "option4"
886                 alias5 = "alias5"
887                 alias6 = "alias6"
888             }
889             option4 = "option4"
890             alias5 = "alias5"
891             alias6 = "alias6"
892             alias7 = "alias7"
893         }
894 
895         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
896         $failed = $false
897         try {
898             $m.ExitJson()
899         } catch [System.Management.Automation.RuntimeException] {
900             $failed = $true
901             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
902             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
903         }
904         $failed | Assert-Equals -Expected $true
905 
906         $expected = @{
907             changed = $false
908             invocation = @{
909                 module_args = @{
910                     alias1 = "alias1"
911                     option1 = "alias1"
912                     option2 = "option2"
913                     option3 = @{
914                         option1 = "option1"
915                         option2 = "alias2"
916                         alias2 = "alias2"
917                         option3 = "alias3"
918                         alias3 = "alias3"
919                         option4 = "option4"
920                         option5 = "alias5"
921                         alias5 = "alias5"
922                         option6 = "alias6"
923                         alias6 = "alias6"
924                     }
925                     option4 = "option4"
926                     option5 = "alias5"
927                     alias5 = "alias5"
928                     option6 = "alias6"
929                     alias6 = "alias6"
930                     option7 = "alias7"
931                     alias7 = "alias7"
932                 }
933             }
934             deprecations = @(
935                 @{
936                     msg = "Alias 'alias7' is deprecated. See the module docs for more information"
937                     date = "2020-06-07"
938                     collection_name = "ansible.builtin"
939                 },
940                 @{
941                     msg = "Alias 'alias1' is deprecated. See the module docs for more information"
942                     version = "2.10"
943                     collection_name = $null
944                 },
945                 @{
946                     msg = "Alias 'alias5' is deprecated. See the module docs for more information"
947                     date = "2020-03-12"
948                     collection_name = $null
949                 },
950                 @{
951                     msg = "Alias 'alias6' is deprecated. See the module docs for more information"
952                     version = "2.12"
953                     collection_name = "ansible.builtin"
954                 },
955                 @{
956                     msg = "Alias 'alias2' is deprecated. See the module docs for more information - found in option3"
957                     version = "2.11"
958                     collection_name = $null
959                 },
960                 @{
961                     msg = "Alias 'alias5' is deprecated. See the module docs for more information - found in option3"
962                     date = "2020-03-09"
963                     collection_name = $null
964                 },
965                 @{
966                     msg = "Alias 'alias3' is deprecated. See the module docs for more information - found in option3"
967                     version = "2.12"
968                     collection_name = "ansible.builtin"
969                 },
970                 @{
971                     msg = "Alias 'alias6' is deprecated. See the module docs for more information - found in option3"
972                     date = "2020-06-01"
973                     collection_name = "ansible.builtin"
974                 }
975             )
976         }
977         $actual | Assert-DictionaryEquals -Expected $expected
978     }
979 
980     "Required by - single value" = {
981         $spec = @{
982             options = @{
983                 option1 = @{type = "str"}
984                 option2 = @{type = "str"}
985                 option3 = @{type = "str"}
986             }
987             required_by = @{
988                 option1 = "option2"
989             }
990         }
991         Set-Variable -Name complex_args -Scope Global -Value @{
992             option1 = "option1"
993             option2 = "option2"
994         }
995 
996         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
997 
998         $failed = $false
999         try {
1000             $m.ExitJson()
1001         } catch [System.Management.Automation.RuntimeException] {
1002             $failed = $true
1003             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
1004             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1005         }
1006         $failed | Assert-Equals -Expected $true
1007 
1008         $expected = @{
1009             changed = $false
1010             invocation = @{
1011                 module_args = @{
1012                     option1 = "option1"
1013                     option2 = "option2"
1014                     option3 = $null
1015                 }
1016             }
1017         }
1018         $actual | Assert-DictionaryEquals -Expected $expected
1019     }
1020 
1021     "Required by - multiple values" = {
1022         $spec = @{
1023             options = @{
1024                 option1 = @{type = "str"}
1025                 option2 = @{type = "str"}
1026                 option3 = @{type = "str"}
1027             }
1028             required_by = @{
1029                 option1 = "option2", "option3"
1030             }
1031         }
1032         Set-Variable -Name complex_args -Scope Global -Value @{
1033             option1 = "option1"
1034             option2 = "option2"
1035             option3 = "option3"
1036         }
1037 
1038         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1039 
1040         $failed = $false
1041         try {
1042             $m.ExitJson()
1043         } catch [System.Management.Automation.RuntimeException] {
1044             $failed = $true
1045             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
1046             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1047         }
1048         $failed | Assert-Equals -Expected $true
1049 
1050         $expected = @{
1051             changed = $false
1052             invocation = @{
1053                 module_args = @{
1054                     option1 = "option1"
1055                     option2 = "option2"
1056                     option3 = "option3"
1057                 }
1058             }
1059         }
1060         $actual | Assert-DictionaryEquals -Expected $expected
1061     }
1062 
1063     "Required by explicit null" = {
1064         $spec = @{
1065             options = @{
1066                 option1 = @{type = "str"}
1067                 option2 = @{type = "str"}
1068                 option3 = @{type = "str"}
1069             }
1070             required_by = @{
1071                 option1 = "option2"
1072             }
1073         }
1074         Set-Variable -Name complex_args -Scope Global -Value @{
1075             option1 = "option1"
1076             option2 = $null
1077         }
1078 
1079         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1080 
1081         $failed = $false
1082         try {
1083             $m.ExitJson()
1084         } catch [System.Management.Automation.RuntimeException] {
1085             $failed = $true
1086             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
1087             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1088         }
1089         $failed | Assert-Equals -Expected $true
1090 
1091         $expected = @{
1092             changed = $false
1093             invocation = @{
1094                 module_args = @{
1095                     option1 = "option1"
1096                     option2 = $null
1097                     option3 = $null
1098                 }
1099             }
1100         }
1101         $actual | Assert-DictionaryEquals -Expected $expected
1102     }
1103 
1104     "Required by failed - single value" = {
1105         $spec = @{
1106             options = @{
1107                 option1 = @{type = "str"}
1108                 option2 = @{type = "str"}
1109                 option3 = @{type = "str"}
1110             }
1111             required_by = @{
1112                 option1 = "option2"
1113             }
1114         }
1115         Set-Variable -Name complex_args -Scope Global -Value @{
1116             option1 = "option1"
1117         }
1118 
1119         $failed = $false
1120         try {
1121             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1122         } catch [System.Management.Automation.RuntimeException] {
1123             $failed = $true
1124             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1125             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1126         }
1127         $failed | Assert-Equals -Expected $true
1128 
1129         $expected = @{
1130             changed = $false
1131             failed = $true
1132             invocation = @{
1133                 module_args = @{
1134                     option1 = "option1"
1135                 }
1136             }
1137             msg = "missing parameter(s) required by 'option1': option2"
1138         }
1139         $actual | Assert-DictionaryEquals -Expected $expected
1140     }
1141 
1142     "Required by failed - multiple values" = {
1143         $spec = @{
1144             options = @{
1145                 option1 = @{type = "str"}
1146                 option2 = @{type = "str"}
1147                 option3 = @{type = "str"}
1148             }
1149             required_by = @{
1150                 option1 = "option2", "option3"
1151             }
1152         }
1153         Set-Variable -Name complex_args -Scope Global -Value @{
1154             option1 = "option1"
1155         }
1156 
1157         $failed = $false
1158         try {
1159             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1160         } catch [System.Management.Automation.RuntimeException] {
1161             $failed = $true
1162             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1163             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1164         }
1165         $failed | Assert-Equals -Expected $true
1166 
1167         $expected = @{
1168             changed = $false
1169             failed = $true
1170             invocation = @{
1171                 module_args = @{
1172                     option1 = "option1"
1173                 }
1174             }
1175             msg = "missing parameter(s) required by 'option1': option2, option3"
1176         }
1177         $actual | Assert-DictionaryEquals -Expected $expected
1178     }
1179 
1180     "Debug without debug set" = {
1181         Set-Variable -Name complex_args -Scope Global -Value @{
1182             _ansible_debug = $false
1183         }
1184         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1185         $m.Debug("debug message")
1186         $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message
1187         $actual_event | Assert-Equals -Expected "undefined win module - Invoked with:`r`n  "
1188     }
1189 
1190     "Debug with debug set" = {
1191         Set-Variable -Name complex_args -Scope Global -Value @{
1192             _ansible_debug = $true
1193         }
1194         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1195         $m.Debug("debug message")
1196         $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message
1197         $actual_event | Assert-Equals -Expected "undefined win module - [DEBUG] debug message"
1198     }
1199 
1200     "Deprecate and warn with version" = {
1201         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1202         $m.Deprecate("message", "2.7")
1203         $actual_deprecate_event_1 = Get-EventLog -LogName Application -Source Ansible -Newest 1
1204         $m.Deprecate("message w collection", "2.8", "ansible.builtin")
1205         $actual_deprecate_event_2 = Get-EventLog -LogName Application -Source Ansible -Newest 1
1206         $m.Warn("warning")
1207         $actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1
1208 
1209         $actual_deprecate_event_1.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message 2.7"
1210         $actual_deprecate_event_2.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message w collection 2.8"
1211         $actual_warn_event.EntryType | Assert-Equals -Expected "Warning"
1212         $actual_warn_event.Message | Assert-Equals -Expected "undefined win module - [WARNING] warning"
1213 
1214         $failed = $false
1215         try {
1216             $m.ExitJson()
1217         } catch [System.Management.Automation.RuntimeException] {
1218             $failed = $true
1219             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
1220             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1221         }
1222         $failed | Assert-Equals -Expected $true
1223 
1224         $expected = @{
1225             changed = $false
1226             invocation = @{
1227                 module_args = @{}
1228             }
1229             warnings = @("warning")
1230             deprecations = @(
1231                 @{msg = "message"; version = "2.7"; collection_name = $null},
1232                 @{msg = "message w collection"; version = "2.8"; collection_name = "ansible.builtin"}
1233             )
1234         }
1235         $actual | Assert-DictionaryEquals -Expected $expected
1236     }
1237 
1238     "Deprecate and warn with date" = {
1239         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1240         $m.Deprecate("message", [DateTime]"2020-01-01")
1241         $actual_deprecate_event_1 = Get-EventLog -LogName Application -Source Ansible -Newest 1
1242         $m.Deprecate("message w collection", [DateTime]"2020-01-02", "ansible.builtin")
1243         $actual_deprecate_event_2 = Get-EventLog -LogName Application -Source Ansible -Newest 1
1244         $m.Warn("warning")
1245         $actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1
1246 
1247         $actual_deprecate_event_1.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message 2020-01-01"
1248         $actual_deprecate_event_2.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message w collection 2020-01-02"
1249         $actual_warn_event.EntryType | Assert-Equals -Expected "Warning"
1250         $actual_warn_event.Message | Assert-Equals -Expected "undefined win module - [WARNING] warning"
1251 
1252         $failed = $false
1253         try {
1254             $m.ExitJson()
1255         } catch [System.Management.Automation.RuntimeException] {
1256             $failed = $true
1257             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
1258             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1259         }
1260         $failed | Assert-Equals -Expected $true
1261 
1262         $expected = @{
1263             changed = $false
1264             invocation = @{
1265                 module_args = @{}
1266             }
1267             warnings = @("warning")
1268             deprecations = @(
1269                 @{msg = "message"; date = "2020-01-01"; collection_name = $null},
1270                 @{msg = "message w collection"; date = "2020-01-02"; collection_name = "ansible.builtin"}
1271             )
1272         }
1273         $actual | Assert-DictionaryEquals -Expected $expected
1274     }
1275 
1276     "FailJson with message" = {
1277         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1278 
1279         $failed = $false
1280         try {
1281             $m.FailJson("fail message")
1282         } catch [System.Management.Automation.RuntimeException] {
1283             $failed = $true
1284             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1285             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1286         }
1287         $failed | Assert-Equals -Expected $failed
1288 
1289         $expected = @{
1290             changed = $false
1291             invocation = @{
1292                 module_args = @{}
1293             }
1294             failed = $true
1295             msg = "fail message"
1296         }
1297         $actual | Assert-DictionaryEquals -Expected $expected
1298     }
1299 
1300     "FailJson with Exception" = {
1301         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1302 
1303         try {
1304             [System.IO.Path]::GetFullPath($null)
1305         } catch {
1306             $excp = $_.Exception
1307         }
1308 
1309         $failed = $false
1310         try {
1311             $m.FailJson("fail message", $excp)
1312         } catch [System.Management.Automation.RuntimeException] {
1313             $failed = $true
1314             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1315             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1316         }
1317         $failed | Assert-Equals -Expected $failed
1318 
1319         $expected = @{
1320             changed = $false
1321             invocation = @{
1322                 module_args = @{}
1323             }
1324             failed = $true
1325             msg = "fail message"
1326         }
1327         $actual | Assert-DictionaryEquals -Expected $expected
1328     }
1329 
1330     "FailJson with ErrorRecord" = {
1331         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1332 
1333         try {
1334             Get-Item -LiteralPath $null
1335         } catch {
1336             $error_record = $_
1337         }
1338 
1339         $failed = $false
1340         try {
1341             $m.FailJson("fail message", $error_record)
1342         } catch [System.Management.Automation.RuntimeException] {
1343             $failed = $true
1344             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1345             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1346         }
1347         $failed | Assert-Equals -Expected $failed
1348 
1349         $expected = @{
1350             changed = $false
1351             invocation = @{
1352                 module_args = @{}
1353             }
1354             failed = $true
1355             msg = "fail message"
1356         }
1357         $actual | Assert-DictionaryEquals -Expected $expected
1358     }
1359 
1360     "FailJson with Exception and verbosity 3" = {
1361         Set-Variable -Name complex_args -Scope Global -Value @{
1362             _ansible_verbosity = 3
1363         }
1364         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1365 
1366         try {
1367             [System.IO.Path]::GetFullPath($null)
1368         } catch {
1369             $excp = $_.Exception
1370         }
1371 
1372         $failed = $false
1373         try {
1374             $m.FailJson("fail message", $excp)
1375         } catch [System.Management.Automation.RuntimeException] {
1376             $failed = $true
1377             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1378             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1379         }
1380         $failed | Assert-Equals -Expected $failed
1381 
1382         $actual.changed | Assert-Equals -Expected $false
1383         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = @{}}
1384         $actual.failed | Assert-Equals -Expected $true
1385         $actual.msg | Assert-Equals -Expected "fail message"
1386         $actual.exception.Contains('System.Management.Automation.MethodInvocationException: Exception calling "GetFullPath" with "1" argument(s)') | Assert-Equals -Expected $true
1387     }
1388 
1389     "FailJson with ErrorRecord and verbosity 3" = {
1390         Set-Variable -Name complex_args -Scope Global -Value @{
1391             _ansible_verbosity = 3
1392         }
1393         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1394 
1395         try {
1396             Get-Item -LiteralPath $null
1397         } catch {
1398             $error_record = $_
1399         }
1400 
1401         $failed = $false
1402         try {
1403             $m.FailJson("fail message", $error_record)
1404         } catch [System.Management.Automation.RuntimeException] {
1405             $failed = $true
1406             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1407             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1408         }
1409         $failed | Assert-Equals -Expected $failed
1410 
1411         $actual.changed | Assert-Equals -Expected $false
1412         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = @{}}
1413         $actual.failed | Assert-Equals -Expected $true
1414         $actual.msg | Assert-Equals -Expected "fail message"
1415         $actual.exception.Contains("Cannot bind argument to parameter 'LiteralPath' because it is null") | Assert-Equals -Expected $true
1416         $actual.exception.Contains("+             Get-Item -LiteralPath `$null") | Assert-Equals -Expected $true
1417         $actual.exception.Contains("ScriptStackTrace:") | Assert-Equals -Expected $true
1418     }
1419 
1420     "Diff entry without diff set" = {
1421         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1422         $m.Diff.before = @{a = "a"}
1423         $m.Diff.after = @{b = "b"}
1424 
1425         $failed = $false
1426         try {
1427             $m.ExitJson()
1428         } catch [System.Management.Automation.RuntimeException] {
1429             $failed = $true
1430             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
1431             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1432         }
1433         $failed | Assert-Equals -Expected $failed
1434 
1435         $expected = @{
1436             changed = $false
1437             invocation = @{
1438                 module_args = @{}
1439             }
1440         }
1441         $actual | Assert-DictionaryEquals -Expected $expected
1442     }
1443 
1444     "Diff entry with diff set" = {
1445         Set-Variable -Name complex_args -Scope Global -Value @{
1446             _ansible_diff = $true
1447         }
1448         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1449         $m.Diff.before = @{a = "a"}
1450         $m.Diff.after = @{b = "b"}
1451 
1452         $failed = $false
1453         try {
1454             $m.ExitJson()
1455         } catch [System.Management.Automation.RuntimeException] {
1456             $failed = $true
1457             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
1458             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1459         }
1460         $failed | Assert-Equals -Expected $failed
1461 
1462         $expected = @{
1463             changed = $false
1464             invocation = @{
1465                 module_args = @{}
1466             }
1467             diff = @{
1468                 before = @{a = "a"}
1469                 after = @{b = "b"}
1470             }
1471         }
1472         $actual | Assert-DictionaryEquals -Expected $expected
1473     }
1474 
1475     "ParseBool tests" = {
1476         $mapping = New-Object -TypeName 'System.Collections.Generic.Dictionary`2[[Object], [Bool]]'
1477         $mapping.Add("y", $true)
1478         $mapping.Add("Y", $true)
1479         $mapping.Add("yes", $true)
1480         $mapping.Add("Yes", $true)
1481         $mapping.Add("on", $true)
1482         $mapping.Add("On", $true)
1483         $mapping.Add("1", $true)
1484         $mapping.Add(1, $true)
1485         $mapping.Add("true", $true)
1486         $mapping.Add("True", $true)
1487         $mapping.Add("t", $true)
1488         $mapping.Add("T", $true)
1489         $mapping.Add("1.0", $true)
1490         $mapping.Add(1.0, $true)
1491         $mapping.Add($true, $true)
1492         $mapping.Add("n", $false)
1493         $mapping.Add("N", $false)
1494         $mapping.Add("no", $false)
1495         $mapping.Add("No", $false)
1496         $mapping.Add("off", $false)
1497         $mapping.Add("Off", $false)
1498         $mapping.Add("0", $false)
1499         $mapping.Add(0, $false)
1500         $mapping.Add("false", $false)
1501         $mapping.Add("False", $false)
1502         $mapping.Add("f", $false)
1503         $mapping.Add("F", $false)
1504         $mapping.Add("0.0", $false)
1505         $mapping.Add(0.0, $false)
1506         $mapping.Add($false, $false)
1507 
1508         foreach ($map in $mapping.GetEnumerator()) {
1509             $expected = $map.Value
1510             $actual = [Ansible.Basic.AnsibleModule]::ParseBool($map.Key)
1511             $actual | Assert-Equals -Expected $expected
1512             $actual.GetType().FullName | Assert-Equals -Expected "System.Boolean"
1513         }
1514 
1515         $fail_bools = @(
1516             "falsey",
1517             "abc",
1518             2,
1519             "2",
1520             -1
1521         )
1522         foreach ($fail_bool in $fail_bools) {
1523             $failed = $false
1524             try {
1525                 [Ansible.Basic.AnsibleModule]::ParseBool($fail_bool)
1526             } catch {
1527                 $failed = $true
1528                 $_.Exception.Message.Contains("The value '$fail_bool' is not a valid boolean") | Assert-Equals -Expected $true
1529             }
1530             $failed | Assert-Equals -Expected $true
1531         }
1532     }
1533 
1534     "Unknown internal key" = {
1535         Set-Variable -Name complex_args -Scope Global -Value @{
1536             _ansible_invalid = "invalid"
1537         }
1538         $failed = $false
1539         try {
1540             $null = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1541         } catch [System.Management.Automation.RuntimeException] {
1542             $failed = $true
1543             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1544 
1545             $expected = @{
1546                 invocation = @{
1547                     module_args = @{
1548                         _ansible_invalid = "invalid"
1549                     }
1550                 }
1551                 changed = $false
1552                 failed = $true
1553                 msg = "Unsupported parameters for (undefined win module) module: _ansible_invalid. Supported parameters include: "
1554             }
1555             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1556             $actual | Assert-DictionaryEquals -Expected $expected
1557         }
1558         $failed | Assert-Equals -Expected $true
1559     }
1560 
1561     "Module tmpdir with present remote tmp" = {
1562         $current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
1563         $dir_security = New-Object -TypeName System.Security.AccessControl.DirectorySecurity
1564         $dir_security.SetOwner($current_user)
1565         $dir_security.SetAccessRuleProtection($true, $false)
1566         $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
1567             $current_user, [System.Security.AccessControl.FileSystemRights]::FullControl,
1568             [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
1569             [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow
1570         )
1571         $dir_security.AddAccessRule($ace)
1572         $expected_sd = $dir_security.GetSecurityDescriptorSddlForm("Access, Owner")
1573 
1574         $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
1575         New-Item -Path $remote_tmp -ItemType Directory > $null
1576         Set-Variable -Name complex_args -Scope Global -Value @{
1577             _ansible_remote_tmp = $remote_tmp.ToString()
1578         }
1579         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1580         (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equals -Expected $true
1581 
1582         $actual_tmpdir = $m.Tmpdir
1583         $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent
1584         $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf
1585 
1586         $parent_tmpdir | Assert-Equals -Expected $remote_tmp
1587         $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equals -Expected $true
1588         (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equals -Expected $true
1589         (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equals -Expected $true
1590         $children = [System.IO.Directory]::EnumerateDirectories($remote_tmp)
1591         $children.Count | Assert-Equals -Expected 1
1592         $actual_tmpdir_sd = (Get-Acl -Path $actual_tmpdir).GetSecurityDescriptorSddlForm("Access, Owner")
1593         $actual_tmpdir_sd | Assert-Equals -Expected $expected_sd
1594 
1595         try {
1596             $m.ExitJson()
1597         } catch [System.Management.Automation.RuntimeException] {
1598             $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1599         }
1600         (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equals -Expected $false
1601         (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equals -Expected $true
1602         $output.warnings.Count | Assert-Equals -Expected 0
1603     }
1604 
1605     "Module tmpdir with missing remote_tmp" = {
1606         $current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
1607         $dir_security = New-Object -TypeName System.Security.AccessControl.DirectorySecurity
1608         $dir_security.SetOwner($current_user)
1609         $dir_security.SetAccessRuleProtection($true, $false)
1610         $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
1611             $current_user, [System.Security.AccessControl.FileSystemRights]::FullControl,
1612             [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
1613             [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow
1614         )
1615         $dir_security.AddAccessRule($ace)
1616         $expected_sd = $dir_security.GetSecurityDescriptorSddlForm("Access, Owner")
1617 
1618         $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
1619         Set-Variable -Name complex_args -Scope Global -Value @{
1620             _ansible_remote_tmp = $remote_tmp.ToString()
1621         }
1622         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1623         (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equals -Expected $false
1624 
1625         $actual_tmpdir = $m.Tmpdir
1626         $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent
1627         $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf
1628 
1629         $parent_tmpdir | Assert-Equals -Expected $remote_tmp
1630         $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equals -Expected $true
1631         (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equals -Expected $true
1632         (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equals -Expected $true
1633         $children = [System.IO.Directory]::EnumerateDirectories($remote_tmp)
1634         $children.Count | Assert-Equals -Expected 1
1635         $actual_remote_sd = (Get-Acl -Path $remote_tmp).GetSecurityDescriptorSddlForm("Access, Owner")
1636         $actual_tmpdir_sd = (Get-Acl -Path $actual_tmpdir).GetSecurityDescriptorSddlForm("Access, Owner")
1637         $actual_remote_sd | Assert-Equals -Expected $expected_sd
1638         $actual_tmpdir_sd | Assert-Equals -Expected $expected_sd
1639 
1640         try {
1641             $m.ExitJson()
1642         } catch [System.Management.Automation.RuntimeException] {
1643             $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1644         }
1645         (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equals -Expected $false
1646         (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equals -Expected $true
1647         $output.warnings.Count | Assert-Equals -Expected 1
1648         $nt_account = $current_user.Translate([System.Security.Principal.NTAccount])
1649         $actual_warning = "Module remote_tmp $remote_tmp did not exist and was created with FullControl to $nt_account, "
1650         $actual_warning += "this may cause issues when running as another user. To avoid this, "
1651         $actual_warning += "create the remote_tmp dir with the correct permissions manually"
1652         $actual_warning | Assert-Equals -Expected $output.warnings[0]
1653     }
1654 
1655     "Module tmp, keep remote files" = {
1656         $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)"
1657         New-Item -Path $remote_tmp -ItemType Directory > $null
1658         Set-Variable -Name complex_args -Scope Global -Value @{
1659             _ansible_remote_tmp = $remote_tmp.ToString()
1660             _ansible_keep_remote_files = $true
1661         }
1662         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
1663 
1664         $actual_tmpdir = $m.Tmpdir
1665         $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent
1666         $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf
1667 
1668         $parent_tmpdir | Assert-Equals -Expected $remote_tmp
1669         $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equals -Expected $true
1670         (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equals -Expected $true
1671         (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equals -Expected $true
1672 
1673         try {
1674             $m.ExitJson()
1675         } catch [System.Management.Automation.RuntimeException] {
1676             $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1677         }
1678         (Test-Path -LiteralPath $actual_tmpdir -PathType Container) | Assert-Equals -Expected $true
1679         (Test-Path -LiteralPath $remote_tmp -PathType Container) | Assert-Equals -Expected $true
1680         $output.warnings.Count | Assert-Equals -Expected 0
1681         Remove-Item -LiteralPath $actual_tmpdir -Force -Recurse
1682     }
1683 
1684     "Invalid argument spec key" = {
1685         $spec = @{
1686             invalid = $true
1687         }
1688         $failed = $false
1689         try {
1690             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1691         } catch [System.Management.Automation.RuntimeException] {
1692             $failed = $true
1693             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1694             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1695         }
1696         $failed | Assert-Equals -Expected $true
1697 
1698         $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, "
1699         $expected_msg += "aliases, choices, default, deprecated_aliases, elements, mutually_exclusive, no_log, options, "
1700         $expected_msg += "removed_in_version, removed_at_date, removed_from_collection, required, required_by, required_if, "
1701         $expected_msg += "required_one_of, required_together, supports_check_mode, type"
1702 
1703         $actual.Keys.Count | Assert-Equals -Expected 3
1704         $actual.failed | Assert-Equals -Expected $true
1705         $actual.msg | Assert-Equals -Expected $expected_msg
1706         ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true
1707     }
1708 
1709     "Invalid argument spec key - nested" = {
1710         $spec = @{
1711             options = @{
1712                 option_key = @{
1713                     options = @{
1714                         sub_option_key = @{
1715                             invalid = $true
1716                         }
1717                     }
1718                 }
1719             }
1720         }
1721         $failed = $false
1722         try {
1723             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1724         } catch [System.Management.Automation.RuntimeException] {
1725             $failed = $true
1726             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1727             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1728         }
1729         $failed | Assert-Equals -Expected $true
1730 
1731         $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, "
1732         $expected_msg += "aliases, choices, default, deprecated_aliases, elements, mutually_exclusive, no_log, options, "
1733         $expected_msg += "removed_in_version, removed_at_date, removed_from_collection, required, required_by, required_if, "
1734         $expected_msg += "required_one_of, required_together, supports_check_mode, type - found in option_key -> sub_option_key"
1735 
1736         $actual.Keys.Count | Assert-Equals -Expected 3
1737         $actual.failed | Assert-Equals -Expected $true
1738         $actual.msg | Assert-Equals -Expected $expected_msg
1739         ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true
1740     }
1741 
1742     "Invalid argument spec value type" = {
1743         $spec = @{
1744             apply_defaults = "abc"
1745         }
1746         $failed = $false
1747         try {
1748             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1749         } catch [System.Management.Automation.RuntimeException] {
1750             $failed = $true
1751             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1752             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1753         }
1754         $failed | Assert-Equals -Expected $true
1755 
1756         $expected_msg = "internal error: argument spec for 'apply_defaults' did not match expected "
1757         $expected_msg += "type System.Boolean: actual type System.String"
1758 
1759         $actual.Keys.Count | Assert-Equals -Expected 3
1760         $actual.failed | Assert-Equals -Expected $true
1761         $actual.msg | Assert-Equals -Expected $expected_msg
1762         ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true
1763     }
1764 
1765     "Invalid argument spec option type" = {
1766         $spec = @{
1767             options = @{
1768                 option_key = @{
1769                     type = "invalid type"
1770                 }
1771             }
1772         }
1773         $failed = $false
1774         try {
1775             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1776         } catch [System.Management.Automation.RuntimeException] {
1777             $failed = $true
1778             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1779             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1780         }
1781         $failed | Assert-Equals -Expected $true
1782 
1783         $expected_msg = "internal error: type 'invalid type' is unsupported - found in option_key. "
1784         $expected_msg += "Valid types are: bool, dict, float, int, json, list, path, raw, sid, str"
1785 
1786         $actual.Keys.Count | Assert-Equals -Expected 3
1787         $actual.failed | Assert-Equals -Expected $true
1788         $actual.msg | Assert-Equals -Expected $expected_msg
1789         ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true
1790     }
1791 
1792     "Invalid argument spec option element type" = {
1793         $spec = @{
1794             options = @{
1795                 option_key = @{
1796                     type = "list"
1797                     elements = "invalid type"
1798                 }
1799             }
1800         }
1801         $failed = $false
1802         try {
1803             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1804         } catch [System.Management.Automation.RuntimeException] {
1805             $failed = $true
1806             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1807             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1808         }
1809         $failed | Assert-Equals -Expected $true
1810 
1811         $expected_msg = "internal error: elements 'invalid type' is unsupported - found in option_key. "
1812         $expected_msg += "Valid types are: bool, dict, float, int, json, list, path, raw, sid, str"
1813 
1814         $actual.Keys.Count | Assert-Equals -Expected 3
1815         $actual.failed | Assert-Equals -Expected $true
1816         $actual.msg | Assert-Equals -Expected $expected_msg
1817         ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true
1818     }
1819 
1820     "Invalid deprecated aliases entry - no version and date" = {
1821         $spec = @{
1822             options = @{
1823                 option_key = @{
1824                     type = "str"
1825                     aliases = ,"alias_name"
1826                     deprecated_aliases = @(
1827                         @{name = "alias_name"}
1828                     )
1829                 }
1830             }
1831         }
1832 
1833         $failed = $false
1834         try {
1835             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1836         } catch [System.Management.Automation.RuntimeException] {
1837             $failed = $true
1838             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1839             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1840         }
1841         $failed | Assert-Equals -Expected $true
1842 
1843         $expected_msg = "internal error: One of version or date is required in a deprecated_aliases entry"
1844 
1845         $actual.Keys.Count | Assert-Equals -Expected 3
1846         $actual.failed | Assert-Equals -Expected $true
1847         $actual.msg | Assert-Equals -Expected $expected_msg
1848         ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true
1849     }
1850 
1851     "Invalid deprecated aliases entry - no name (nested)" = {
1852         $spec = @{
1853             options = @{
1854                 option_key = @{
1855                     type = "dict"
1856                     options = @{
1857                         sub_option_key = @{
1858                             type = "str"
1859                             aliases = ,"alias_name"
1860                             deprecated_aliases = @(
1861                                 @{version = "2.10"}
1862                             )
1863                         }
1864                     }
1865                 }
1866             }
1867         }
1868 
1869         Set-Variable -Name complex_args -Scope Global -Value @{
1870             option_key = @{
1871                 sub_option_key = "a"
1872             }
1873         }
1874 
1875         $failed = $false
1876         try {
1877             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1878         } catch [System.ArgumentException] {
1879             $failed = $true
1880             $expected_msg = "name is required in a deprecated_aliases entry - found in option_key"
1881             $_.Exception.Message | Assert-Equals -Expected $expected_msg
1882         }
1883         $failed | Assert-Equals -Expected $true
1884     }
1885 
1886     "Invalid deprecated aliases entry - both version and date" = {
1887         $spec = @{
1888             options = @{
1889                 option_key = @{
1890                     type = "str"
1891                     aliases = ,"alias_name"
1892                     deprecated_aliases = @(
1893                         @{
1894                             name = "alias_name"
1895                             date = [DateTime]"2020-03-10"
1896                             version = "2.11"
1897                         }
1898                     )
1899                 }
1900             }
1901         }
1902 
1903         $failed = $false
1904         try {
1905             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1906         } catch [System.Management.Automation.RuntimeException] {
1907             $failed = $true
1908             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1909             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1910         }
1911         $failed | Assert-Equals -Expected $true
1912 
1913         $expected_msg = "internal error: Only one of version or date is allowed in a deprecated_aliases entry"
1914 
1915         $actual.Keys.Count | Assert-Equals -Expected 3
1916         $actual.failed | Assert-Equals -Expected $true
1917         $actual.msg | Assert-Equals -Expected $expected_msg
1918         ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true
1919     }
1920 
1921     "Invalid deprecated aliases entry - wrong date type" = {
1922         $spec = @{
1923             options = @{
1924                 option_key = @{
1925                     type = "str"
1926                     aliases = ,"alias_name"
1927                     deprecated_aliases = @(
1928                         @{
1929                             name = "alias_name"
1930                             date = "2020-03-10"
1931                         }
1932                     )
1933                 }
1934             }
1935         }
1936 
1937         $failed = $false
1938         try {
1939             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1940         } catch [System.Management.Automation.RuntimeException] {
1941             $failed = $true
1942             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1943             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1944         }
1945         $failed | Assert-Equals -Expected $true
1946 
1947         $expected_msg = "internal error: A deprecated_aliases date must be a DateTime object"
1948 
1949         $actual.Keys.Count | Assert-Equals -Expected 3
1950         $actual.failed | Assert-Equals -Expected $true
1951         $actual.msg | Assert-Equals -Expected $expected_msg
1952         ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true
1953     }
1954 
1955     "Spec required and default set at the same time" = {
1956         $spec = @{
1957             options = @{
1958                 option_key = @{
1959                     required = $true
1960                     default = "default value"
1961                 }
1962             }
1963         }
1964 
1965         $failed = $false
1966         try {
1967             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
1968         } catch [System.Management.Automation.RuntimeException] {
1969             $failed = $true
1970             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
1971             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
1972         }
1973         $failed | Assert-Equals -Expected $true
1974 
1975         $expected_msg = "internal error: required and default are mutually exclusive for option_key"
1976 
1977         $actual.Keys.Count | Assert-Equals -Expected 3
1978         $actual.failed | Assert-Equals -Expected $true
1979         $actual.msg | Assert-Equals -Expected $expected_msg
1980         ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true
1981     }
1982 
1983     "Unsupported options" = {
1984         $spec = @{
1985             options = @{
1986                 option_key = @{
1987                     type = "str"
1988                 }
1989             }
1990         }
1991         Set-Variable -Name complex_args -Scope Global -Value @{
1992             option_key = "abc"
1993             invalid_key = "def"
1994             another_key = "ghi"
1995         }
1996 
1997         $failed = $false
1998         try {
1999             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2000         } catch [System.Management.Automation.RuntimeException] {
2001             $failed = $true
2002             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2003             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2004         }
2005         $failed | Assert-Equals -Expected $true
2006 
2007         $expected_msg = "Unsupported parameters for (undefined win module) module: another_key, invalid_key. "
2008         $expected_msg += "Supported parameters include: option_key"
2009 
2010         $actual.Keys.Count | Assert-Equals -Expected 4
2011         $actual.changed | Assert-Equals -Expected $false
2012         $actual.failed | Assert-Equals -Expected $true
2013         $actual.msg | Assert-Equals -Expected $expected_msg
2014         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2015     }
2016 
2017     "Check mode and module doesn't support check mode" = {
2018         $spec = @{
2019             options = @{
2020                 option_key = @{
2021                     type = "str"
2022                 }
2023             }
2024         }
2025         Set-Variable -Name complex_args -Scope Global -Value @{
2026             _ansible_check_mode = $true
2027             option_key = "abc"
2028         }
2029 
2030         $failed = $false
2031         try {
2032             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2033         } catch [System.Management.Automation.RuntimeException] {
2034             $failed = $true
2035             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
2036             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2037         }
2038         $failed | Assert-Equals -Expected $true
2039 
2040         $expected_msg = "remote module (undefined win module) does not support check mode"
2041 
2042         $actual.Keys.Count | Assert-Equals -Expected 4
2043         $actual.changed | Assert-Equals -Expected $false
2044         $actual.skipped | Assert-Equals -Expected $true
2045         $actual.msg | Assert-Equals -Expected $expected_msg
2046         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = @{option_key = "abc"}}
2047     }
2048 
2049     "Check mode with suboption without supports_check_mode" = {
2050         $spec = @{
2051             options = @{
2052                 sub_options = @{
2053                     # This tests the situation where a sub key doesn't set supports_check_mode, the logic in
2054                     # Ansible.Basic automatically sets that to $false and we want it to ignore it for a nested check
2055                     type = "dict"
2056                     options = @{
2057                         sub_option = @{ type = "str"; default = "value" }
2058                     }
2059                 }
2060             }
2061             supports_check_mode = $true
2062         }
2063         Set-Variable -Name complex_args -Scope Global -Value @{
2064             _ansible_check_mode = $true
2065         }
2066 
2067         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2068         $m.CheckMode | Assert-Equals -Expected $true
2069     }
2070 
2071     "Type conversion error" = {
2072         $spec = @{
2073             options = @{
2074                 option_key = @{
2075                     type = "int"
2076                 }
2077             }
2078         }
2079         Set-Variable -Name complex_args -Scope Global -Value @{
2080             option_key = "a"
2081         }
2082 
2083         $failed = $false
2084         try {
2085             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2086         } catch [System.Management.Automation.RuntimeException] {
2087             $failed = $true
2088             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2089             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2090         }
2091         $failed | Assert-Equals -Expected $true
2092 
2093         $expected_msg = "argument for option_key is of type System.String and we were unable to convert to int: "
2094         $expected_msg += "Input string was not in a correct format."
2095 
2096         $actual.Keys.Count | Assert-Equals -Expected 4
2097         $actual.changed | Assert-Equals -Expected $false
2098         $actual.failed | Assert-Equals -Expected $true
2099         $actual.msg | Assert-Equals -Expected $expected_msg
2100         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2101     }
2102 
2103     "Type conversion error - delegate" = {
2104         $spec = @{
2105             options = @{
2106                 option_key = @{
2107                     type = "dict"
2108                     options = @{
2109                         sub_option_key = @{
2110                             type = [Func[[Object], [UInt64]]]{ [System.UInt64]::Parse($args[0]) }
2111                         }
2112                     }
2113                 }
2114             }
2115         }
2116         Set-Variable -Name complex_args -Scope Global -Value @{
2117             option_key = @{
2118                 sub_option_key = "a"
2119             }
2120         }
2121 
2122         $failed = $false
2123         try {
2124             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2125         } catch [System.Management.Automation.RuntimeException] {
2126             $failed = $true
2127             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2128             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2129         }
2130         $failed | Assert-Equals -Expected $true
2131 
2132         $expected_msg = "argument for sub_option_key is of type System.String and we were unable to convert to delegate: "
2133         $expected_msg += "Exception calling `"Parse`" with `"1`" argument(s): `"Input string was not in a correct format.`" "
2134         $expected_msg += "found in option_key"
2135 
2136         $actual.Keys.Count | Assert-Equals -Expected 4
2137         $actual.changed | Assert-Equals -Expected $false
2138         $actual.failed | Assert-Equals -Expected $true
2139         $actual.msg | Assert-Equals -Expected $expected_msg
2140         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2141     }
2142 
2143     "Numeric choices" = {
2144         $spec = @{
2145             options = @{
2146                 option_key = @{
2147                     choices = 1, 2, 3
2148                     type = "int"
2149                 }
2150             }
2151         }
2152         Set-Variable -Name complex_args -Scope Global -Value @{
2153             option_key = "2"
2154         }
2155 
2156         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2157         try {
2158             $m.ExitJson()
2159         } catch [System.Management.Automation.RuntimeException] {
2160             $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2161         }
2162         $output.Keys.Count | Assert-Equals -Expected 2
2163         $output.changed | Assert-Equals -Expected $false
2164         $output.invocation | Assert-DictionaryEquals -Expected @{module_args = @{option_key = 2}}
2165     }
2166 
2167     "Case insensitive choice" = {
2168         $spec = @{
2169             options = @{
2170                 option_key = @{
2171                     choices = "abc", "def"
2172                 }
2173             }
2174         }
2175         Set-Variable -Name complex_args -Scope Global -Value @{
2176             option_key = "ABC"
2177         }
2178 
2179         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2180         try {
2181             $m.ExitJson()
2182         } catch [System.Management.Automation.RuntimeException] {
2183             $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2184         }
2185         $expected_warning = "value of option_key was a case insensitive match of one of: abc, def. "
2186         $expected_warning += "Checking of choices will be case sensitive in a future Ansible release. "
2187         $expected_warning += "Case insensitive matches were: ABC"
2188 
2189         $output.invocation | Assert-DictionaryEquals -Expected @{module_args = @{option_key = "ABC"}}
2190         # We have disabled the warnings for now
2191         #$output.warnings.Count | Assert-Equals -Expected 1
2192         #$output.warnings[0] | Assert-Equals -Expected $expected_warning
2193     }
2194 
2195     "Case insensitive choice no_log" = {
2196         $spec = @{
2197             options = @{
2198                 option_key = @{
2199                     choices = "abc", "def"
2200                     no_log = $true
2201                 }
2202             }
2203         }
2204         Set-Variable -Name complex_args -Scope Global -Value @{
2205             option_key = "ABC"
2206         }
2207 
2208         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2209         try {
2210             $m.ExitJson()
2211         } catch [System.Management.Automation.RuntimeException] {
2212             $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2213         }
2214         $expected_warning = "value of option_key was a case insensitive match of one of: abc, def. "
2215         $expected_warning += "Checking of choices will be case sensitive in a future Ansible release. "
2216         $expected_warning += "Case insensitive matches were: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
2217 
2218         $output.invocation | Assert-DictionaryEquals -Expected @{module_args = @{option_key = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"}}
2219         # We have disabled the warnings for now
2220         #$output.warnings.Count | Assert-Equals -Expected 1
2221         #$output.warnings[0] | Assert-Equals -Expected $expected_warning
2222     }
2223 
2224     "Case insentitive choice as list" = {
2225         $spec = @{
2226             options = @{
2227                 option_key = @{
2228                     choices = "abc", "def", "ghi", "JKL"
2229                     type = "list"
2230                     elements = "str"
2231                 }
2232             }
2233         }
2234         Set-Variable -Name complex_args -Scope Global -Value @{
2235             option_key = "AbC", "ghi", "jkl"
2236         }
2237 
2238         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2239         try {
2240             $m.ExitJson()
2241         } catch [System.Management.Automation.RuntimeException] {
2242             $output = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2243         }
2244         $expected_warning = "value of option_key was a case insensitive match of one or more of: abc, def, ghi, JKL. "
2245         $expected_warning += "Checking of choices will be case sensitive in a future Ansible release. "
2246         $expected_warning += "Case insensitive matches were: AbC, jkl"
2247 
2248         $output.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2249         # We have disabled the warnings for now
2250         #$output.warnings.Count | Assert-Equals -Expected 1
2251         #$output.warnings[0] | Assert-Equals -Expected $expected_warning
2252     }
2253 
2254     "Invalid choice" = {
2255         $spec = @{
2256             options = @{
2257                 option_key = @{
2258                     choices = "a", "b"
2259                 }
2260             }
2261         }
2262         Set-Variable -Name complex_args -Scope Global -Value @{
2263             option_key = "c"
2264         }
2265 
2266         $failed = $false
2267         try {
2268             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2269         } catch [System.Management.Automation.RuntimeException] {
2270             $failed = $true
2271             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2272             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2273         }
2274         $failed | Assert-Equals -Expected $true
2275 
2276         $expected_msg = "value of option_key must be one of: a, b. Got no match for: c"
2277 
2278         $actual.Keys.Count | Assert-Equals -Expected 4
2279         $actual.changed | Assert-Equals -Expected $false
2280         $actual.failed | Assert-Equals -Expected $true
2281         $actual.msg | Assert-Equals -Expected $expected_msg
2282         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2283     }
2284 
2285     "Invalid choice with no_log" = {
2286         $spec = @{
2287             options = @{
2288                 option_key = @{
2289                     choices = "a", "b"
2290                     no_log = $true
2291                 }
2292             }
2293         }
2294         Set-Variable -Name complex_args -Scope Global -Value @{
2295             option_key = "abc"
2296         }
2297 
2298         $failed = $false
2299         try {
2300             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2301         } catch [System.Management.Automation.RuntimeException] {
2302             $failed = $true
2303             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2304             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2305         }
2306         $failed | Assert-Equals -Expected $true
2307 
2308         $expected_msg = "value of option_key must be one of: a, b. Got no match for: ********"
2309 
2310         $actual.Keys.Count | Assert-Equals -Expected 4
2311         $actual.changed | Assert-Equals -Expected $false
2312         $actual.failed | Assert-Equals -Expected $true
2313         $actual.msg | Assert-Equals -Expected $expected_msg
2314         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = @{option_key = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"}}
2315     }
2316 
2317     "Invalid choice in list" = {
2318         $spec = @{
2319             options = @{
2320                 option_key = @{
2321                     choices = "a", "b"
2322                     type = "list"
2323                 }
2324             }
2325         }
2326         Set-Variable -Name complex_args -Scope Global -Value @{
2327             option_key = "a", "c"
2328         }
2329 
2330         $failed = $false
2331         try {
2332             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2333         } catch [System.Management.Automation.RuntimeException] {
2334             $failed = $true
2335             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2336             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2337         }
2338         $failed | Assert-Equals -Expected $true
2339 
2340         $expected_msg = "value of option_key must be one or more of: a, b. Got no match for: c"
2341 
2342         $actual.Keys.Count | Assert-Equals -Expected 4
2343         $actual.changed | Assert-Equals -Expected $false
2344         $actual.failed | Assert-Equals -Expected $true
2345         $actual.msg | Assert-Equals -Expected $expected_msg
2346         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2347     }
2348 
2349     "Mutually exclusive options" = {
2350         $spec = @{
2351             options = @{
2352                 option1 = @{}
2353                 option2 = @{}
2354             }
2355             mutually_exclusive = @(,@("option1", "option2"))
2356         }
2357         Set-Variable -Name complex_args -Scope Global -Value @{
2358             option1 = "a"
2359             option2 = "b"
2360         }
2361 
2362         $failed = $false
2363         try {
2364             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2365         } catch [System.Management.Automation.RuntimeException] {
2366             $failed = $true
2367             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2368             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2369         }
2370         $failed | Assert-Equals -Expected $true
2371 
2372         $expected_msg = "parameters are mutually exclusive: option1, option2"
2373 
2374         $actual.Keys.Count | Assert-Equals -Expected 4
2375         $actual.changed | Assert-Equals -Expected $false
2376         $actual.failed | Assert-Equals -Expected $true
2377         $actual.msg | Assert-Equals -Expected $expected_msg
2378         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2379     }
2380 
2381     "Missing required argument" = {
2382         $spec = @{
2383             options = @{
2384                 option1 = @{}
2385                 option2 = @{required = $true}
2386             }
2387         }
2388         Set-Variable -Name complex_args -Scope Global -Value @{
2389             option1 = "a"
2390         }
2391 
2392         $failed = $false
2393         try {
2394             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2395         } catch [System.Management.Automation.RuntimeException] {
2396             $failed = $true
2397             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2398             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2399         }
2400         $failed | Assert-Equals -Expected $true
2401 
2402         $expected_msg = "missing required arguments: option2"
2403 
2404         $actual.Keys.Count | Assert-Equals -Expected 4
2405         $actual.changed | Assert-Equals -Expected $false
2406         $actual.failed | Assert-Equals -Expected $true
2407         $actual.msg | Assert-Equals -Expected $expected_msg
2408         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2409     }
2410 
2411     "Missing required argument subspec - no value defined" = {
2412         $spec = @{
2413             options = @{
2414                 option_key = @{
2415                     type = "dict"
2416                     options = @{
2417                         sub_option_key = @{
2418                             required = $true
2419                         }
2420                     }
2421                 }
2422             }
2423         }
2424 
2425         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2426         $failed = $false
2427         try {
2428             $m.ExitJson()
2429         } catch [System.Management.Automation.RuntimeException] {
2430             $failed = $true
2431             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
2432             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2433         }
2434         $failed | Assert-Equals -Expected $true
2435 
2436         $actual.Keys.Count | Assert-Equals -Expected 2
2437         $actual.changed | Assert-Equals -Expected $false
2438         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2439     }
2440 
2441     "Missing required argument subspec" = {
2442         $spec = @{
2443             options = @{
2444                 option_key = @{
2445                     type = "dict"
2446                     options = @{
2447                         sub_option_key = @{
2448                             required = $true
2449                         }
2450                         another_key = @{}
2451                     }
2452                 }
2453             }
2454         }
2455         Set-Variable -Name complex_args -Scope Global -Value @{
2456             option_key = @{
2457                 another_key = "abc"
2458             }
2459         }
2460 
2461         $failed = $false
2462         try {
2463             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2464         } catch [System.Management.Automation.RuntimeException] {
2465             $failed = $true
2466             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2467             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2468         }
2469         $failed | Assert-Equals -Expected $true
2470 
2471         $expected_msg = "missing required arguments: sub_option_key found in option_key"
2472 
2473         $actual.Keys.Count | Assert-Equals -Expected 4
2474         $actual.changed | Assert-Equals -Expected $false
2475         $actual.failed | Assert-Equals -Expected $true
2476         $actual.msg | Assert-Equals -Expected $expected_msg
2477         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2478     }
2479 
2480     "Required together not set" = {
2481         $spec = @{
2482             options = @{
2483                 option1 = @{}
2484                 option2 = @{}
2485             }
2486             required_together = @(,@("option1", "option2"))
2487         }
2488         Set-Variable -Name complex_args -Scope Global -Value @{
2489             option1 = "abc"
2490         }
2491 
2492         $failed = $false
2493         try {
2494             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2495         } catch [System.Management.Automation.RuntimeException] {
2496             $failed = $true
2497             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2498             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2499         }
2500         $failed | Assert-Equals -Expected $true
2501 
2502         $expected_msg = "parameters are required together: option1, option2"
2503 
2504         $actual.Keys.Count | Assert-Equals -Expected 4
2505         $actual.changed | Assert-Equals -Expected $false
2506         $actual.failed | Assert-Equals -Expected $true
2507         $actual.msg | Assert-Equals -Expected $expected_msg
2508         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2509     }
2510 
2511     "Required together not set - subspec" = {
2512         $spec = @{
2513             options = @{
2514                 option_key = @{
2515                     type = "dict"
2516                     options = @{
2517                         option1 = @{}
2518                         option2 = @{}
2519                     }
2520                     required_together = @(,@("option1", "option2"))
2521                 }
2522                 another_option = @{}
2523             }
2524             required_together = @(,@("option_key", "another_option"))
2525         }
2526         Set-Variable -Name complex_args -Scope Global -Value @{
2527             option_key = @{
2528                 option1 = "abc"
2529             }
2530             another_option = "def"
2531         }
2532 
2533         $failed = $false
2534         try {
2535             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2536         } catch [System.Management.Automation.RuntimeException] {
2537             $failed = $true
2538             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2539             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2540         }
2541         $failed | Assert-Equals -Expected $true
2542 
2543         $expected_msg = "parameters are required together: option1, option2 found in option_key"
2544 
2545         $actual.Keys.Count | Assert-Equals -Expected 4
2546         $actual.changed | Assert-Equals -Expected $false
2547         $actual.failed | Assert-Equals -Expected $true
2548         $actual.msg | Assert-Equals -Expected $expected_msg
2549         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2550     }
2551 
2552     "Required one of not set" = {
2553         $spec = @{
2554             options = @{
2555                 option1 = @{}
2556                 option2 = @{}
2557                 option3 = @{}
2558             }
2559             required_one_of = @(@("option1", "option2"), @("option2", "option3"))
2560         }
2561         Set-Variable -Name complex_args -Scope Global -Value @{
2562             option1 = "abc"
2563         }
2564 
2565         $failed = $false
2566         try {
2567             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2568         } catch [System.Management.Automation.RuntimeException] {
2569             $failed = $true
2570             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2571             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2572         }
2573         $failed | Assert-Equals -Expected $true
2574 
2575         $expected_msg = "one of the following is required: option2, option3"
2576 
2577         $actual.Keys.Count | Assert-Equals -Expected 4
2578         $actual.changed | Assert-Equals -Expected $false
2579         $actual.failed | Assert-Equals -Expected $true
2580         $actual.msg | Assert-Equals -Expected $expected_msg
2581         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2582     }
2583 
2584     "Required if invalid entries" = {
2585         $spec = @{
2586             options = @{
2587                 state = @{choices = "absent", "present"; default = "present"}
2588                 path = @{type = "path"}
2589             }
2590             required_if = @(,@("state", "absent"))
2591         }
2592 
2593         $failed = $false
2594         try {
2595             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2596         } catch [System.Management.Automation.RuntimeException] {
2597             $failed = $true
2598             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2599             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2600         }
2601         $failed | Assert-Equals -Expected $true
2602 
2603         $expected_msg = "internal error: invalid required_if value count of 2, expecting 3 or 4 entries"
2604 
2605         $actual.Keys.Count | Assert-Equals -Expected 4
2606         $actual.changed | Assert-Equals -Expected $false
2607         $actual.failed | Assert-Equals -Expected $true
2608         $actual.msg | Assert-Equals -Expected $expected_msg
2609         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2610     }
2611 
2612     "Required if no missing option" = {
2613         $spec = @{
2614             options = @{
2615                 state = @{choices = "absent", "present"; default = "present"}
2616                 name = @{}
2617                 path = @{type = "path"}
2618             }
2619             required_if = @(,@("state", "absent", @("name", "path")))
2620         }
2621         Set-Variable -Name complex_args -Scope Global -Value @{
2622             name = "abc"
2623         }
2624         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2625 
2626         $failed = $false
2627         try {
2628             $m.ExitJson()
2629         } catch [System.Management.Automation.RuntimeException] {
2630             $failed = $true
2631             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
2632             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2633         }
2634         $failed | Assert-Equals -Expected $true
2635 
2636         $actual.Keys.Count | Assert-Equals -Expected 2
2637         $actual.changed | Assert-Equals -Expected $false
2638         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2639     }
2640 
2641     "Required if missing option" = {
2642         $spec = @{
2643             options = @{
2644                 state = @{choices = "absent", "present"; default = "present"}
2645                 name = @{}
2646                 path = @{type = "path"}
2647             }
2648             required_if = @(,@("state", "absent", @("name", "path")))
2649         }
2650         Set-Variable -Name complex_args -Scope Global -Value @{
2651             state = "absent"
2652             name = "abc"
2653         }
2654 
2655         $failed = $false
2656         try {
2657             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2658         } catch [System.Management.Automation.RuntimeException] {
2659             $failed = $true
2660             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2661             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2662         }
2663         $failed | Assert-Equals -Expected $true
2664 
2665         $expected_msg = "state is absent but all of the following are missing: path"
2666 
2667         $actual.Keys.Count | Assert-Equals -Expected 4
2668         $actual.changed | Assert-Equals -Expected $false
2669         $actual.failed | Assert-Equals -Expected $true
2670         $actual.msg | Assert-Equals -Expected $expected_msg
2671         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2672     }
2673 
2674     "Required if missing option and required one is set" = {
2675         $spec = @{
2676             options = @{
2677                 state = @{choices = "absent", "present"; default = "present"}
2678                 name = @{}
2679                 path = @{type = "path"}
2680             }
2681             required_if = @(,@("state", "absent", @("name", "path"), $true))
2682         }
2683         Set-Variable -Name complex_args -Scope Global -Value @{
2684             state = "absent"
2685         }
2686 
2687         $failed = $false
2688         try {
2689             $null = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2690         } catch [System.Management.Automation.RuntimeException] {
2691             $failed = $true
2692             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2693             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2694         }
2695         $failed | Assert-Equals -Expected $true
2696 
2697         $expected_msg = "state is absent but any of the following are missing: name, path"
2698 
2699         $actual.Keys.Count | Assert-Equals -Expected 4
2700         $actual.changed | Assert-Equals -Expected $false
2701         $actual.failed | Assert-Equals -Expected $true
2702         $actual.msg | Assert-Equals -Expected $expected_msg
2703         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2704     }
2705 
2706     "Required if missing option but one required set" = {
2707         $spec = @{
2708             options = @{
2709                 state = @{choices = "absent", "present"; default = "present"}
2710                 name = @{}
2711                 path = @{type = "path"}
2712             }
2713             required_if = @(,@("state", "absent", @("name", "path"), $true))
2714         }
2715         Set-Variable -Name complex_args -Scope Global -Value @{
2716             state = "absent"
2717             name = "abc"
2718         }
2719         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
2720 
2721         $failed = $false
2722         try {
2723             $m.ExitJson()
2724         } catch [System.Management.Automation.RuntimeException] {
2725             $failed = $true
2726             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
2727             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2728         }
2729         $failed | Assert-Equals -Expected $true
2730 
2731         $actual.Keys.Count | Assert-Equals -Expected 2
2732         $actual.changed | Assert-Equals -Expected $false
2733         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2734     }
2735 
2736     "PS Object in return result" = {
2737         $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{})
2738 
2739         # JavaScriptSerializer struggles with PS Object like PSCustomObject due to circular references, this test makes
2740         # sure we can handle these types of objects without bombing
2741         $m.Result.output = [PSCustomObject]@{a = "a"; b = "b"}
2742         $failed = $true
2743         try {
2744             $m.ExitJson()
2745         } catch [System.Management.Automation.RuntimeException] {
2746             $failed = $true
2747             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
2748             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2749         }
2750         $failed | Assert-Equals -Expected $true
2751 
2752         $actual.Keys.Count | Assert-Equals -Expected 3
2753         $actual.changed | Assert-Equals -Expected $false
2754         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = @{}}
2755         $actual.output | Assert-DictionaryEquals -Expected @{a = "a"; b = "b"}
2756     }
2757 
2758     "String json array to object" = {
2759         $input_json = '["abc", "def"]'
2760         $actual = [Ansible.Basic.AnsibleModule]::FromJson($input_json)
2761         $actual -is [Array] | Assert-Equals -Expected $true
2762         $actual.Length | Assert-Equals -Expected 2
2763         $actual[0] | Assert-Equals -Expected "abc"
2764         $actual[1] | Assert-Equals -Expected "def"
2765     }
2766 
2767     "String json array of dictionaries to object" = {
2768         $input_json = '[{"abc":"def"}]'
2769         $actual = [Ansible.Basic.AnsibleModule]::FromJson($input_json)
2770         $actual -is [Array] | Assert-Equals -Expected $true
2771         $actual.Length | Assert-Equals -Expected 1
2772         $actual[0] | Assert-DictionaryEquals -Expected @{"abc" = "def"}
2773     }
2774 
2775     "Spec with fragments" = {
2776         $spec = @{
2777             options = @{
2778                 option1 = @{ type = "str" }
2779             }
2780         }
2781         $fragment1 = @{
2782             options = @{
2783                 option2 = @{ type = "str" }
2784             }
2785         }
2786 
2787         Set-Variable -Name complex_args -Scope Global -Value @{
2788             option1 = "option1"
2789             option2 = "option2"
2790         }
2791         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1))
2792 
2793         $failed = $false
2794         try {
2795             $m.ExitJson()
2796         } catch [System.Management.Automation.RuntimeException] {
2797             $failed = $true
2798             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
2799             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2800         }
2801         $failed | Assert-Equals -Expected $true
2802 
2803         $actual.changed | Assert-Equals -Expected $false
2804         $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args}
2805     }
2806 
2807     "Fragment spec that with a deprecated alias" = {
2808         $spec = @{
2809             options = @{
2810                 option1 = @{
2811                     aliases = @("alias1_spec")
2812                     type = "str"
2813                     deprecated_aliases = @(
2814                         @{name = "alias1_spec"; version = "2.0"}
2815                     )
2816                 }
2817                 option2 = @{
2818                     aliases = @("alias2_spec")
2819                     deprecated_aliases = @(
2820                         @{name = "alias2_spec"; version = "2.0"; collection_name = "ansible.builtin"}
2821                     )
2822                 }
2823             }
2824         }
2825         $fragment1 = @{
2826             options = @{
2827                 option1 = @{
2828                     aliases = @("alias1")
2829                     deprecated_aliases = @()  # Makes sure it doesn't overwrite the spec, just adds to it.
2830                 }
2831                 option2 = @{
2832                     aliases = @("alias2")
2833                     deprecated_aliases = @(
2834                         @{name = "alias2"; version = "2.0"; collection_name = "foo.bar"}
2835                     )
2836                     type = "str"
2837                 }
2838             }
2839         }
2840 
2841         Set-Variable -Name complex_args -Scope Global -Value @{
2842             alias1_spec = "option1"
2843             alias2 = "option2"
2844         }
2845         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1))
2846 
2847         $failed = $false
2848         try {
2849             $m.ExitJson()
2850         } catch [System.Management.Automation.RuntimeException] {
2851             $failed = $true
2852             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
2853             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2854         }
2855         $failed | Assert-Equals -Expected $true
2856 
2857         $actual.deprecations.Count | Assert-Equals -Expected 2
2858         $actual.deprecations[0] | Assert-DictionaryEquals -Expected @{
2859             msg = "Alias 'alias1_spec' is deprecated. See the module docs for more information"; version = "2.0"; collection_name = $null
2860         }
2861         $actual.deprecations[1] | Assert-DictionaryEquals -Expected @{
2862             msg = "Alias 'alias2' is deprecated. See the module docs for more information"; version = "2.0"; collection_name = "foo.bar"
2863         }
2864         $actual.changed | Assert-Equals -Expected $false
2865         $actual.invocation | Assert-DictionaryEquals -Expected @{
2866             module_args = @{
2867                 option1 = "option1"
2868                 alias1_spec = "option1"
2869                 option2 = "option2"
2870                 alias2 = "option2"
2871             }
2872         }
2873     }
2874 
2875     "Fragment spec with mutual args" = {
2876         $spec = @{
2877             options = @{
2878                 option1 = @{ type = "str" }
2879                 option2 = @{ type = "str" }
2880             }
2881             mutually_exclusive = @(
2882                 ,@('option1', 'option2')
2883             )
2884         }
2885         $fragment1 = @{
2886             options = @{
2887                 fragment1_1 = @{ type = "str" }
2888                 fragment1_2 = @{ type = "str" }
2889             }
2890             mutually_exclusive = @(
2891                 ,@('fragment1_1', 'fragment1_2')
2892             )
2893         }
2894         $fragment2 = @{
2895             options = @{
2896                 fragment2 = @{ type = "str" }
2897             }
2898         }
2899 
2900         Set-Variable -Name complex_args -Scope Global -Value @{
2901             option1 = "option1"
2902             fragment1_1 = "fragment1_1"
2903             fragment1_2 = "fragment1_2"
2904             fragment2 = "fragment2"
2905         }
2906 
2907         $failed = $false
2908         try {
2909             [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1, $fragment2))
2910         } catch [System.Management.Automation.RuntimeException] {
2911             $failed = $true
2912             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2913             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2914         }
2915         $failed | Assert-Equals -Expected $true
2916 
2917         $actual.changed | Assert-Equals -Expected $false
2918         $actual.failed | Assert-Equals -Expected $true
2919         $actual.msg | Assert-Equals -Expected "parameters are mutually exclusive: fragment1_1, fragment1_2"
2920         $actual.invocation | Assert-DictionaryEquals -Expected @{ module_args = $complex_args }
2921     }
2922 
2923     "Fragment spec with no_log" = {
2924         $spec = @{
2925             options = @{
2926                 option1 = @{
2927                     aliases = @("alias")
2928                 }
2929             }
2930         }
2931         $fragment1 = @{
2932             options = @{
2933                 option1 = @{
2934                     no_log = $true  # Makes sure that a value set in the fragment but not in the spec is respected.
2935                     type = "str"
2936                 }
2937             }
2938         }
2939 
2940         Set-Variable -Name complex_args -Scope Global -Value @{
2941             alias = "option1"
2942         }
2943         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment1))
2944 
2945         $failed = $false
2946         try {
2947             $m.ExitJson()
2948         } catch [System.Management.Automation.RuntimeException] {
2949             $failed = $true
2950             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
2951             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2952         }
2953         $failed | Assert-Equals -Expected $true
2954 
2955         $actual.changed | Assert-Equals -Expected $false
2956         $actual.invocation | Assert-DictionaryEquals -Expected @{
2957             module_args = @{
2958                 option1 = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
2959                 alias = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
2960             }
2961         }
2962     }
2963 
2964     "Catch invalid fragment spec format" = {
2965         $spec = @{
2966             options = @{
2967                 option1 = @{ type = "str" }
2968             }
2969         }
2970         $fragment = @{
2971             options = @{}
2972             invalid = "will fail"
2973         }
2974 
2975         Set-Variable -Name complex_args -Scope Global -Value @{
2976             option1 = "option1"
2977         }
2978 
2979         $failed = $false
2980         try {
2981             [Ansible.Basic.AnsibleModule]::Create(@(), $spec, @($fragment))
2982         } catch [System.Management.Automation.RuntimeException] {
2983             $failed = $true
2984             $_.Exception.Message | Assert-Equals -Expected "exit: 1"
2985             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
2986         }
2987         $failed | Assert-Equals -Expected $true
2988 
2989         $actual.failed | Assert-Equals -Expected $true
2990         $actual.msg.StartsWith("internal error: argument spec entry contains an invalid key 'invalid', valid keys: ") | Assert-Equals -Expected $true
2991     }
2992 
2993     "Spec with different list types" = {
2994         $spec = @{
2995             options = @{
2996                 # Single element of the same list type not in a list
2997                 option1 = @{
2998                     aliases = "alias1"
2999                     deprecated_aliases = @{name="alias1";version="2.0";collection_name="foo.bar"}
3000                 }
3001 
3002                 # Arrays
3003                 option2 = @{
3004                     aliases = ,"alias2"
3005                     deprecated_aliases = ,@{name="alias2";version="2.0";collection_name="foo.bar"}
3006                 }
3007 
3008                 # ArrayList
3009                 option3 = @{
3010                     aliases = [System.Collections.ArrayList]@("alias3")
3011                     deprecated_aliases = [System.Collections.ArrayList]@(@{name="alias3";version="2.0";collection_name="foo.bar"})
3012                 }
3013 
3014                 # Generic.List[Object]
3015                 option4 = @{
3016                     aliases = [System.Collections.Generic.List[Object]]@("alias4")
3017                     deprecated_aliases = [System.Collections.Generic.List[Object]]@(@{name="alias4";version="2.0";collection_name="foo.bar"})
3018                 }
3019 
3020                 # Generic.List[T]
3021                 option5 = @{
3022                     aliases = [System.Collections.Generic.List[String]]@("alias5")
3023                     deprecated_aliases = [System.Collections.Generic.List[Hashtable]]@()
3024                 }
3025             }
3026         }
3027         $spec.options.option5.deprecated_aliases.Add(@{name="alias5";version="2.0";collection_name="foo.bar"})
3028 
3029         Set-Variable -Name complex_args -Scope Global -Value @{
3030             alias1 = "option1"
3031             alias2 = "option2"
3032             alias3 = "option3"
3033             alias4 = "option4"
3034             alias5 = "option5"
3035         }
3036         $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec)
3037 
3038         $failed = $false
3039         try {
3040             $m.ExitJson()
3041         } catch [System.Management.Automation.RuntimeException] {
3042             $failed = $true
3043             $_.Exception.Message | Assert-Equals -Expected "exit: 0"
3044             $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output)
3045         }
3046         $failed | Assert-Equals -Expected $true
3047 
3048         $actual.changed | Assert-Equals -Expected $false
3049         $actual.deprecations.Count | Assert-Equals -Expected 5
3050         foreach ($dep in $actual.deprecations) {
3051             $dep.msg -like "Alias 'alias?' is deprecated. See the module docs for more information" | Assert-Equals -Expected $true
3052             $dep.version | Assert-Equals -Expected '2.0'
3053             $dep.collection_name | Assert-Equals -Expected 'foo.bar'
3054         }
3055         $actual.invocation | Assert-DictionaryEquals -Expected @{
3056             module_args = @{
3057                 alias1 = "option1"
3058                 option1 = "option1"
3059                 alias2 = "option2"
3060                 option2 = "option2"
3061                 alias3 = "option3"
3062                 option3 = "option3"
3063                 alias4 = "option4"
3064                 option4 = "option4"
3065                 alias5 = "option5"
3066                 option5 = "option5"
3067             }
3068         }
3069     }
3070 }
3071 
3072 try {
3073     foreach ($test_impl in $tests.GetEnumerator()) {
3074         # Reset the variables before each test
3075         Set-Variable -Name complex_args -Value @{} -Scope Global
3076 
3077         $test = $test_impl.Key
3078         &$test_impl.Value
3079     }
3080     $module.Result.data = "success"
3081 } catch [System.Management.Automation.RuntimeException] {
3082     $module.Result.failed = $true
3083     $module.Result.test = $test
3084     $module.Result.line = $_.InvocationInfo.ScriptLineNumber
3085     $module.Result.method = $_.InvocationInfo.Line.Trim()
3086 
3087     if ($_.Exception.Message.StartSwith("exit: ")) {
3088         # The exception was caused by an unexpected Exit call, log that on the output
3089         $module.Result.output = (ConvertFrom-Json -InputObject $_.Exception.InnerException.Output)
3090         $module.Result.msg = "Uncaught AnsibleModule exit in tests, see output"
3091     } else {
3092         # Unrelated exception
3093         $module.Result.exception = $_.Exception.ToString()
3094         $module.Result.msg = "Uncaught exception: $(($_ | Out-String).ToString())"
3095     }
3096 }
3097 
3098 Exit-Module
3099