ruby-pwsh 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +29 -25
- data/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb +3 -0
- data/lib/pwsh/version.rb +1 -1
- data/lib/pwsh.rb +1 -1
- data/lib/templates/init.ps1 +11 -1
- data/spec/acceptance/dsc/basic.rb +205 -0
- data/spec/acceptance/dsc/cim_instances.rb +82 -0
- data/spec/acceptance/dsc/class.rb +129 -0
- data/spec/acceptance/dsc/complex.rb +139 -0
- data/spec/exit-27.ps1 +1 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/unit/puppet/provider/dsc_base_provider/dsc_base_provider_spec.rb +2026 -0
- data/spec/unit/pwsh/util_spec.rb +286 -0
- data/spec/unit/pwsh/version_spec.rb +10 -0
- data/spec/unit/pwsh/windows_powershell_spec.rb +116 -0
- data/spec/unit/pwsh_spec.rb +823 -0
- metadata +17 -22
- data/.gitattributes +0 -2
- data/.github/workflows/ci.yml +0 -109
- data/.gitignore +0 -23
- data/.pmtignore +0 -21
- data/.rspec +0 -3
- data/CHANGELOG.md +0 -196
- data/CODEOWNERS +0 -2
- data/CONTRIBUTING.md +0 -155
- data/DESIGN.md +0 -70
- data/Gemfile +0 -54
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -188
- data/design-comms.png +0 -0
- data/metadata.json +0 -82
- data/pwshlib.md +0 -92
- data/ruby-pwsh.gemspec +0 -39
@@ -0,0 +1,2026 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'puppet/type'
|
5
|
+
require 'puppet/provider/dsc_base_provider/dsc_base_provider'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
RSpec.describe Puppet::Provider::DscBaseProvider do
|
9
|
+
subject(:provider) { described_class.new }
|
10
|
+
|
11
|
+
let(:context) { instance_double('Puppet::ResourceApi::PuppetContext') }
|
12
|
+
let(:type) { instance_double('Puppet::ResourceApi::TypeDefinition') }
|
13
|
+
let(:ps_manager) { instance_double('Pwsh::Manager') }
|
14
|
+
let(:execute_response) { { stdout: nil, stderr: nil, exitcode: 0 } }
|
15
|
+
|
16
|
+
# Reset the caches after each run
|
17
|
+
after(:each) do
|
18
|
+
described_class.class_variable_set(:@@cached_canonicalized_resource, nil) # rubocop:disable Style/ClassVars
|
19
|
+
described_class.class_variable_set(:@@cached_query_results, nil) # rubocop:disable Style/ClassVars
|
20
|
+
described_class.class_variable_set(:@@cached_test_results, nil) # rubocop:disable Style/ClassVars
|
21
|
+
described_class.class_variable_set(:@@logon_failures, nil) # rubocop:disable Style/ClassVars
|
22
|
+
end
|
23
|
+
|
24
|
+
context '.initialize' do
|
25
|
+
before(:each) do
|
26
|
+
# Need to initialize the provider to load the class variables
|
27
|
+
provider
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'initializes the cached_canonicalized_resource class variable' do
|
31
|
+
expect(described_class.class_variable_get(:@@cached_canonicalized_resource)).to eq([])
|
32
|
+
end
|
33
|
+
it 'initializes the cached_query_results class variable' do
|
34
|
+
expect(described_class.class_variable_get(:@@cached_query_results)).to eq([])
|
35
|
+
end
|
36
|
+
it 'initializes the cached_test_results class variable' do
|
37
|
+
expect(described_class.class_variable_get(:@@cached_test_results)).to eq([])
|
38
|
+
end
|
39
|
+
it 'initializes the logon_failures class variable' do
|
40
|
+
expect(described_class.class_variable_get(:@@logon_failures)).to eq([])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context '.cached_test_results' do
|
45
|
+
let(:cache_value) { %w[foo bar] }
|
46
|
+
|
47
|
+
it 'returns the value of the @@cached_test_results class variable' do
|
48
|
+
described_class.class_variable_set(:@@cached_test_results, cache_value) # rubocop:disable Style/ClassVars
|
49
|
+
expect(provider.cached_test_results).to eq(cache_value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context '.fetch_cached_hashes' do
|
54
|
+
let(:cached_hashes) { [{ foo: 1, bar: 2, baz: 3 }, { foo: 4, bar: 5, baz: 6 }] }
|
55
|
+
let(:findable_full_hash) { { foo: 1, bar: 2, baz: 3 } }
|
56
|
+
let(:findable_sub_hash) { { foo: 1 } }
|
57
|
+
let(:undiscoverable_hash) { { foo: 7, bar: 8, baz: 9 } }
|
58
|
+
|
59
|
+
it 'finds a hash that exactly matches one in the cache' do
|
60
|
+
expect(provider.fetch_cached_hashes(cached_hashes, [findable_full_hash])).to eq([findable_full_hash])
|
61
|
+
end
|
62
|
+
it 'finds a hash that is wholly contained by a hash in the cache' do
|
63
|
+
expect(provider.fetch_cached_hashes(cached_hashes, [findable_sub_hash])).to eq([findable_full_hash])
|
64
|
+
end
|
65
|
+
it 'returns an empty array if there is no match' do
|
66
|
+
expect(provider.fetch_cached_hashes(cached_hashes, [undiscoverable_hash])).to eq([])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context '.canonicalize' do
|
71
|
+
subject(:canonicalized_resource) { provider.canonicalize(context, [manifest_resource]) }
|
72
|
+
|
73
|
+
let(:resource_name_hash) { { name: 'foo', dsc_name: 'foo' } }
|
74
|
+
let(:namevar_keys) { %i[name dsc_name] }
|
75
|
+
let(:parameter_keys) { %i[dsc_parameter dsc_psdscrunascredential] }
|
76
|
+
let(:credential_hash) { { 'username' => 'foo', 'password' => 'bar' } }
|
77
|
+
let(:base_resource) { resource_name_hash.dup }
|
78
|
+
|
79
|
+
before(:each) do
|
80
|
+
allow(context).to receive(:debug)
|
81
|
+
allow(provider).to receive(:namevar_attributes).and_return(namevar_keys)
|
82
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return(cached_canonicalized_resource)
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'when a manifest resource has meta parameters' do
|
86
|
+
let(:manifest_resource) { base_resource.merge({ dsc_ensure: 'present', noop: true }) }
|
87
|
+
let(:expected_resource) { base_resource.merge({ dsc_property: 'foobar' }) }
|
88
|
+
let(:cached_canonicalized_resource) { expected_resource.dup }
|
89
|
+
|
90
|
+
it 'does not get removed as part of the canonicalization' do
|
91
|
+
expect(canonicalized_resource.first[:noop]).to eq(true)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'when a manifest resource is in the canonicalized resource cache' do
|
96
|
+
let(:manifest_resource) { base_resource.merge({ dsc_property: 'FooBar' }) }
|
97
|
+
let(:expected_resource) { base_resource.merge({ dsc_property: 'foobar' }) }
|
98
|
+
let(:cached_canonicalized_resource) { expected_resource.dup }
|
99
|
+
|
100
|
+
it 'returns the manifest resource' do
|
101
|
+
expect(canonicalized_resource).to eq([manifest_resource])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context 'when a manifest resource not in the canonicalized resource cache' do
|
106
|
+
let(:cached_canonicalized_resource) { [] }
|
107
|
+
|
108
|
+
before(:each) do
|
109
|
+
allow(provider).to receive(:invoke_get_method).and_return(actual_resource)
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'when invoke_get_method returns nil for the manifest resource' do
|
113
|
+
let(:manifest_resource) { base_resource.merge({ dsc_property: 'FooBar' }) }
|
114
|
+
let(:actual_resource) { nil }
|
115
|
+
|
116
|
+
it 'treats the manifest as canonical' do
|
117
|
+
expect(canonicalized_resource).to eq([manifest_resource])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'when invoke_get_method returns a resource' do
|
122
|
+
before(:each) do
|
123
|
+
allow(provider).to receive(:parameter_attributes).and_return(parameter_keys)
|
124
|
+
allow(provider).to receive(:enum_attributes).and_return([])
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'when canonicalizing property values' do
|
128
|
+
let(:manifest_resource) { base_resource.merge({ dsc_property: 'bar' }) }
|
129
|
+
|
130
|
+
context 'when the value is a downcased match' do
|
131
|
+
let(:actual_resource) { base_resource.merge({ dsc_property: 'Bar' }) }
|
132
|
+
|
133
|
+
it 'assigns the value of the discovered resource for that property' do
|
134
|
+
expect(canonicalized_resource.first[:dsc_property]).to eq('Bar')
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'when the value is not a downcased match' do
|
139
|
+
let(:actual_resource) { base_resource.merge({ dsc_property: 'Baz' }) }
|
140
|
+
|
141
|
+
it 'assigns the value of the manifest resource for that property' do
|
142
|
+
expect(canonicalized_resource.first[:dsc_property]).to eq('bar')
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'when the value should be nil and the actual state is not' do
|
147
|
+
let(:manifest_resource) { base_resource.merge({ dsc_property: nil }) }
|
148
|
+
let(:actual_resource) { base_resource.merge({ dsc_property: 'Bar' }) }
|
149
|
+
|
150
|
+
it 'treats the manifest value as canonical' do
|
151
|
+
expect(canonicalized_resource.first[:dsc_property]).to eq(nil)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'when the value should not be nil and the actual state is nil' do
|
156
|
+
let(:manifest_resource) { base_resource.merge({ dsc_property: 'bar' }) }
|
157
|
+
let(:actual_resource) { base_resource.merge({ dsc_property: nil }) }
|
158
|
+
|
159
|
+
it 'treats the manifest value as canonical' do
|
160
|
+
expect(canonicalized_resource.first[:dsc_property]).to eq('bar')
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
context 'when the property is an enum and the casing differs' do
|
165
|
+
let(:manifest_resource) { base_resource.merge({ dsc_property: 'Dword' }) }
|
166
|
+
let(:actual_resource) { base_resource.merge({ dsc_property: 'DWord' }) }
|
167
|
+
|
168
|
+
before(:each) do
|
169
|
+
allow(provider).to receive(:enum_attributes).and_return([:dsc_property])
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'treats the manifest value as canonical' do
|
173
|
+
expect(canonicalized_resource.first[:dsc_property]).to eq('Dword')
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
context 'when handling dsc_psdscrunascredential' do
|
179
|
+
let(:actual_resource) { base_resource.merge({ dsc_psdscrunascredential: nil }) }
|
180
|
+
|
181
|
+
context 'when it is specified in the resource' do
|
182
|
+
let(:manifest_resource) { base_resource.merge({ dsc_psdscrunascredential: credential_hash }) }
|
183
|
+
|
184
|
+
it 'is included from the manifest resource' do
|
185
|
+
expect(canonicalized_resource.first[:dsc_psdscrunascredential]).not_to be_nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
context 'when it is not specified in the resource' do
|
190
|
+
let(:manifest_resource) { base_resource.dup }
|
191
|
+
|
192
|
+
it 'is not included in the canonicalized resource' do
|
193
|
+
expect(canonicalized_resource.first[:dsc_psdscrunascredential]).to be_nil
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context 'when an ensurable resource is specified' do
|
199
|
+
context 'when it should be present' do
|
200
|
+
let(:manifest_resource) { base_resource.merge({ dsc_ensure: 'present', dsc_property: 'bar' }) }
|
201
|
+
|
202
|
+
context 'when the actual state is set to absent' do
|
203
|
+
let(:actual_resource) { base_resource.merge({ dsc_ensure: 'absent', dsc_property: nil }) }
|
204
|
+
|
205
|
+
it 'treats the manifest as canonical' do
|
206
|
+
expect(canonicalized_resource).to eq([manifest_resource])
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context 'when it is returned from invoke_get_method with ensure set to present' do
|
211
|
+
let(:actual_resource) { base_resource.merge({ dsc_ensure: 'present', dsc_property: 'Bar' }) }
|
212
|
+
|
213
|
+
it 'is case insensitive but case preserving' do
|
214
|
+
expect(canonicalized_resource.first[:dsc_property]).to eq('Bar')
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
context 'when it should be absent' do
|
220
|
+
let(:manifest_resource) { base_resource.merge({ dsc_ensure: 'absent' }) }
|
221
|
+
let(:actual_resource) { base_resource.merge({ dsc_ensure: 'present', dsc_property: 'Bar' }) }
|
222
|
+
|
223
|
+
it 'treats the manifest as canonical' do
|
224
|
+
expect(provider).not_to receive(:invoke_get_method)
|
225
|
+
expect(canonicalized_resource).to eq([manifest_resource])
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
context '.get' do
|
234
|
+
after(:each) do
|
235
|
+
described_class.class_variable_set(:@@cached_canonicalized_resource, []) # rubocop:disable Style/ClassVars
|
236
|
+
end
|
237
|
+
|
238
|
+
it 'checks the cached results, returning if one exists for the specified names' do
|
239
|
+
described_class.class_variable_set(:@@cached_canonicalized_resource, []) # rubocop:disable Style/ClassVars
|
240
|
+
allow(context).to receive(:debug)
|
241
|
+
expect(provider).to receive(:fetch_cached_hashes).with([], [{ name: 'foo' }]).and_return([{ name: 'foo', property: 'bar' }])
|
242
|
+
expect(provider).not_to receive(:invoke_get_method)
|
243
|
+
expect(provider.get(context, [{ name: 'foo' }])).to eq([{ name: 'foo', property: 'bar' }])
|
244
|
+
end
|
245
|
+
it 'adds mandatory properties to the name hash when calling invoke_get_method' do
|
246
|
+
described_class.class_variable_set(:@@cached_canonicalized_resource, [{ name: 'foo', property: 'bar', dsc_some_parameter: 'baz' }]) # rubocop:disable Style/ClassVars
|
247
|
+
allow(context).to receive(:debug)
|
248
|
+
expect(provider).to receive(:fetch_cached_hashes).with([], [{ name: 'foo' }]).and_return([])
|
249
|
+
expect(provider).to receive(:namevar_attributes).and_return([:name]).exactly(3).times
|
250
|
+
expect(provider).to receive(:mandatory_get_attributes).and_return([:dsc_some_parameter]).exactly(3).times
|
251
|
+
expect(provider).to receive(:invoke_get_method).with(context, { name: 'foo', dsc_some_parameter: 'baz' }).and_return({ name: 'foo', property: 'bar' })
|
252
|
+
expect(provider.get(context, [{ name: 'foo' }])).to eq([{ name: 'foo', property: 'bar' }])
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
context '.set' do
|
257
|
+
subject(:result) { provider.set(context, change_set) }
|
258
|
+
|
259
|
+
let(:name_hash) { { name: 'foo', dsc_name: 'foo' } }
|
260
|
+
let(:change_set) { { name_hash => { is: actual_state, should: should_state } } }
|
261
|
+
# Empty because we can mock everything but calling .keys on the hash
|
262
|
+
let(:attributes) { { name: {}, dsc_name: {}, dsc_setting: {} } }
|
263
|
+
|
264
|
+
before(:each) do
|
265
|
+
allow(context).to receive(:type).and_return(type)
|
266
|
+
allow(type).to receive(:namevars).and_return(%i[name dsc_name])
|
267
|
+
allow(type).to receive(:attributes).and_return(attributes)
|
268
|
+
end
|
269
|
+
|
270
|
+
context 'when the resource is not ensurable' do
|
271
|
+
let(:actual_state) { name_hash.merge(dsc_setting: 'Bar') }
|
272
|
+
let(:should_state) { name_hash.merge(dsc_setting: 'Foo') }
|
273
|
+
|
274
|
+
it 'calls context.updating and provider.update' do
|
275
|
+
expect(context).to receive(:updating).with(name_hash).and_yield
|
276
|
+
expect(provider).to receive(:update).with(context, name_hash, should_state)
|
277
|
+
expect { result }.not_to raise_error
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
context 'when the resource is ensurable' do
|
282
|
+
let(:attributes) { { name: {}, dsc_name: {}, dsc_setting: {}, dsc_ensure: {} } }
|
283
|
+
|
284
|
+
context 'when the resource should be present' do
|
285
|
+
let(:should_state) { name_hash.merge({ dsc_setting: 'Foo', dsc_ensure: 'Present' }) }
|
286
|
+
|
287
|
+
context 'when the resource exists but is out of sync' do
|
288
|
+
let(:actual_state) { name_hash.merge({ dsc_setting: 'Bar', dsc_ensure: 'Present' }) }
|
289
|
+
|
290
|
+
it 'calls context.updating and provider.update' do
|
291
|
+
expect(context).to receive(:updating).with(name_hash).and_yield
|
292
|
+
expect(provider).to receive(:update).with(context, name_hash, should_state)
|
293
|
+
expect { result }.not_to raise_error
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
context 'when the resource does not exist' do
|
298
|
+
let(:actual_state) { name_hash.merge({ dsc_name: 'Foo', dsc_ensure: 'Absent' }) }
|
299
|
+
|
300
|
+
it 'calls context.creating and provider.create' do
|
301
|
+
expect(context).to receive(:creating).with(name_hash).and_yield
|
302
|
+
expect(provider).to receive(:create).with(context, name_hash, should_state)
|
303
|
+
expect { result }.not_to raise_error
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
context 'when the resource should be absent' do
|
309
|
+
let(:should_state) { name_hash.merge({ dsc_setting: 'Foo', dsc_ensure: 'Absent' }) }
|
310
|
+
let(:actual_state) { name_hash.merge({ dsc_name: 'Foo', dsc_ensure: 'Present' }) }
|
311
|
+
|
312
|
+
it 'calls context.deleting and provider.delete' do
|
313
|
+
expect(context).to receive(:deleting).with(name_hash).and_yield
|
314
|
+
expect(provider).to receive(:delete).with(context, name_hash)
|
315
|
+
expect { result }.not_to raise_error
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
context 'when ensure is not passed to should' do
|
320
|
+
let(:should_state) { name_hash.merge({ dsc_setting: 'Foo' }) }
|
321
|
+
let(:actual_state) { name_hash.merge({ dsc_name: 'Foo', dsc_ensure: 'Present' }) }
|
322
|
+
|
323
|
+
it 'assumes dsc_ensure should be `Present` and acts accordingly' do
|
324
|
+
expect(context).to receive(:updating).with(name_hash).and_yield
|
325
|
+
expect(provider).to receive(:update).with(context, name_hash, should_state)
|
326
|
+
expect { result }.not_to raise_error
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
context 'when ensure is not passed to is' do
|
331
|
+
let(:should_state) { name_hash.merge({ dsc_setting: 'Foo', dsc_ensure: 'Present' }) }
|
332
|
+
let(:actual_state) { name_hash.merge({ dsc_name: 'Foo', dsc_ensure: 'Absent' }) }
|
333
|
+
|
334
|
+
it 'assumes dsc_ensure should be `Present` and acts accordingly' do
|
335
|
+
expect(context).to receive(:creating).with(name_hash).and_yield
|
336
|
+
expect(provider).to receive(:create).with(context, name_hash, should_state)
|
337
|
+
expect { result }.not_to raise_error
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
context 'when `is` is nil' do
|
343
|
+
let(:change_set) { { name_hash => { should: should_state } } }
|
344
|
+
let(:should_state) { name_hash.merge(dsc_setting: 'Foo') }
|
345
|
+
it 'attempts to retrieve the resource from the machine to populate `is` value' do
|
346
|
+
pending('Implementation only works for when `get` returns an array, but `get` returns one resource as a hash')
|
347
|
+
expect(provider).to receive(:get).with(context, [name_hash]).and_return(name_hash.merge(dsc_setting: 'Bar'))
|
348
|
+
expect(type).to receive(:check_schema)
|
349
|
+
expect(context).to receive(:updating).with(name_hash)
|
350
|
+
expect(provider).to receive(:update).with(context, name_hash, should_state)
|
351
|
+
expect { result }.not_to raise_error
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
context '.create' do
|
357
|
+
it 'calls invoke_set_method' do
|
358
|
+
allow(context).to receive(:debug)
|
359
|
+
expect(provider).to receive(:invoke_set_method)
|
360
|
+
expect { provider.create(context, 'foo', { foo: 1 }) }.not_to raise_error
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
context '.update' do
|
365
|
+
it 'calls invoke_set_method' do
|
366
|
+
allow(context).to receive(:debug)
|
367
|
+
expect(provider).to receive(:invoke_set_method)
|
368
|
+
expect { provider.update(context, 'foo', { foo: 1 }) }.not_to raise_error
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
context '.delete' do
|
373
|
+
it 'calls invoke_set_method' do
|
374
|
+
allow(context).to receive(:debug)
|
375
|
+
expect(provider).to receive(:invoke_set_method)
|
376
|
+
expect { provider.delete(context, { foo: 1 }) }.not_to raise_error
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
context '.insync?' do
|
381
|
+
let(:name) { { name: 'foo' } }
|
382
|
+
let(:attribute_name) { :foo }
|
383
|
+
let(:is_hash) { { name: 'foo', foo: 1 } }
|
384
|
+
let(:cached_test_result) { [{ name: 'foo', in_desired_state: true }] }
|
385
|
+
let(:should_hash_validate_by_property) { { name: 'foo', foo: 1, validation_mode: 'property' } }
|
386
|
+
let(:should_hash_validate_by_resource) { { name: 'foo', foo: 1, validation_mode: 'resource' } }
|
387
|
+
|
388
|
+
context 'when the validation_mode is "resource"' do
|
389
|
+
it 'calls invoke_test_method if the result of a test is not already cached' do
|
390
|
+
expect(provider).to receive(:fetch_cached_hashes).and_return([])
|
391
|
+
expect(provider).to receive(:invoke_test_method).and_return(true)
|
392
|
+
expect(provider.send(:insync?, context, name, attribute_name, is_hash, should_hash_validate_by_resource)).to be true
|
393
|
+
end
|
394
|
+
it 'does not call invoke_test_method if the result of a test is already cached' do
|
395
|
+
expect(provider).to receive(:fetch_cached_hashes).and_return(cached_test_result)
|
396
|
+
expect(provider).not_to receive(:invoke_test_method)
|
397
|
+
expect(provider.send(:insync?, context, name, attribute_name, is_hash, should_hash_validate_by_resource)).to be true
|
398
|
+
end
|
399
|
+
end
|
400
|
+
context 'when the validation_mode is "property"' do
|
401
|
+
it 'does not call invoke_test_method and returns nil' do
|
402
|
+
expect(provider).not_to receive(:fetch_cached_hashes)
|
403
|
+
expect(provider).not_to receive(:invoke_test_method)
|
404
|
+
expect(provider.send(:insync?, context, name, attribute_name, is_hash, should_hash_validate_by_property)).to be nil
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
context '.invoke_get_method' do
|
410
|
+
subject(:result) { provider.invoke_get_method(context, name_hash) }
|
411
|
+
|
412
|
+
let(:attributes) do
|
413
|
+
{
|
414
|
+
name: {
|
415
|
+
type: 'String',
|
416
|
+
behaviour: :namevar
|
417
|
+
},
|
418
|
+
dsc_name: {
|
419
|
+
type: 'String',
|
420
|
+
behaviour: :namevar,
|
421
|
+
mandatory_for_get: true,
|
422
|
+
mandatory_for_set: true,
|
423
|
+
mof_type: 'String',
|
424
|
+
mof_is_embedded: false
|
425
|
+
},
|
426
|
+
dsc_psdscrunascredential: {
|
427
|
+
type: 'Optional[Struct[{ user => String[1], password => Sensitive[String[1]] }]]',
|
428
|
+
behaviour: :parameter,
|
429
|
+
mandatory_for_get: false,
|
430
|
+
mandatory_for_set: false,
|
431
|
+
mof_type: 'PSCredential',
|
432
|
+
mof_is_embedded: true
|
433
|
+
},
|
434
|
+
dsc_ensure: {
|
435
|
+
type: "Optional[Enum['Present', 'Absent']]",
|
436
|
+
mandatory_for_get: false,
|
437
|
+
mandatory_for_set: false,
|
438
|
+
mof_type: 'String',
|
439
|
+
mof_is_embedded: false
|
440
|
+
},
|
441
|
+
dsc_time: {
|
442
|
+
type: 'Optional[Timestamp]',
|
443
|
+
mandatory_for_get: false,
|
444
|
+
mandatory_for_set: false,
|
445
|
+
mof_type: 'DateTime',
|
446
|
+
mof_is_embedded: false
|
447
|
+
},
|
448
|
+
dsc_ciminstance: {
|
449
|
+
type: "Optional[Struct[{
|
450
|
+
foo => Optional[Boolean],
|
451
|
+
bar => Optional[String],
|
452
|
+
}]]",
|
453
|
+
mandatory_for_get: false,
|
454
|
+
mandatory_for_set: false,
|
455
|
+
mof_type: 'FooBarCimInstance',
|
456
|
+
mof_is_embedded: true
|
457
|
+
},
|
458
|
+
dsc_nestedciminstance: {
|
459
|
+
type: "Optional[Struct[{
|
460
|
+
baz => Optional[Boolean],
|
461
|
+
nestedProperty => Struct[{
|
462
|
+
nestedFoo => Optional[Enum['Yay', 'Boo']],
|
463
|
+
nestedBar => Optional[String],
|
464
|
+
cim_instance_type => 'FooBarNestedCimInstance'
|
465
|
+
}],
|
466
|
+
}]]",
|
467
|
+
mandatory_for_get: false,
|
468
|
+
mandatory_for_set: false,
|
469
|
+
mof_type: 'BazCimInstance',
|
470
|
+
mof_is_embedded: true
|
471
|
+
},
|
472
|
+
dsc_array: {
|
473
|
+
type: 'Optional[Array[String]]',
|
474
|
+
mandatory_for_get: false,
|
475
|
+
mandatory_for_set: false,
|
476
|
+
mof_type: 'String[]',
|
477
|
+
mof_is_embedded: false
|
478
|
+
},
|
479
|
+
dsc_param: {
|
480
|
+
type: 'Optional[String]',
|
481
|
+
behaviour: :parameter,
|
482
|
+
mandatory_for_get: false,
|
483
|
+
mandatory_for_set: false,
|
484
|
+
mof_type: 'String',
|
485
|
+
mof_is_embedded: false
|
486
|
+
}
|
487
|
+
}
|
488
|
+
end
|
489
|
+
let(:name_hash) { { name: 'foo', dsc_name: 'foo', dsc_time: '2100-01-01' } }
|
490
|
+
let(:mandatory_get_attributes) { %i[dsc_name] }
|
491
|
+
let(:query_props) { { dsc_name: 'foo' } }
|
492
|
+
let(:resource) { "Resource: #{query_props}" }
|
493
|
+
let(:script) { "Script: #{query_props}" }
|
494
|
+
let(:parsed_invocation_data) do
|
495
|
+
{
|
496
|
+
'Name' => 'foo',
|
497
|
+
'Ensure' => 'Present',
|
498
|
+
'Time' => '2100-01-01',
|
499
|
+
'CimInstance' => { 'Foo' => true, 'Bar' => 'Ope' },
|
500
|
+
'NestedCimInstance' => {
|
501
|
+
'Baz' => true,
|
502
|
+
'NestedProperty' => { 'NestedFoo' => 'yay', 'NestedBar' => 'Ope', 'cim_instance_type' => 'FooBarNestedCimINstance' }
|
503
|
+
},
|
504
|
+
'Array' => %w[foo bar],
|
505
|
+
'EmptyArray' => nil,
|
506
|
+
'Param' => 'Value',
|
507
|
+
'UnusedProperty' => 'foo'
|
508
|
+
}
|
509
|
+
end
|
510
|
+
|
511
|
+
before(:each) do
|
512
|
+
allow(context).to receive(:debug)
|
513
|
+
allow(provider).to receive(:mandatory_get_attributes).and_return(mandatory_get_attributes)
|
514
|
+
allow(provider).to receive(:invocable_resource).with(query_props, context, 'get').and_return(resource)
|
515
|
+
allow(provider).to receive(:ps_script_content).with(resource).and_return(script)
|
516
|
+
allow(provider).to receive(:redact_secrets).with(script)
|
517
|
+
allow(provider).to receive(:remove_secret_identifiers).with(script).and_return(script)
|
518
|
+
allow(provider).to receive(:ps_manager).and_return(ps_manager)
|
519
|
+
allow(context).to receive(:type).and_return(type)
|
520
|
+
allow(type).to receive(:attributes).and_return(attributes)
|
521
|
+
end
|
522
|
+
|
523
|
+
after(:each) do
|
524
|
+
described_class.class_variable_set(:@@cached_query_results, nil) # rubocop:disable Style/ClassVars
|
525
|
+
end
|
526
|
+
|
527
|
+
context 'when the invocation script returns data without errors' do
|
528
|
+
before(:each) do
|
529
|
+
allow(ps_manager).to receive(:execute).with(script).and_return({ stdout: 'DSC Data' })
|
530
|
+
allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data)
|
531
|
+
allow(Puppet::Pops::Time::Timestamp).to receive(:parse).with('2100-01-01').and_return('TimeStamp:2100-01-01')
|
532
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return([])
|
533
|
+
end
|
534
|
+
|
535
|
+
it 'does not check for logon failures as no PSDscRunAsCredential was passed' do
|
536
|
+
expect(provider).not_to receive(:logon_failed_already?)
|
537
|
+
expect { result }.not_to raise_error
|
538
|
+
end
|
539
|
+
it 'writes no errors to the context' do
|
540
|
+
expect(context).not_to receive(:err)
|
541
|
+
expect { result }.not_to raise_error
|
542
|
+
end
|
543
|
+
it 're-adds the puppet name to the resource' do
|
544
|
+
expect(result[:name]).to eq('foo')
|
545
|
+
end
|
546
|
+
it 'caches the result' do
|
547
|
+
expect { result }.not_to raise_error
|
548
|
+
expect(described_class.class_variable_get(:@@cached_query_results)).to eq([result])
|
549
|
+
end
|
550
|
+
it 'removes unrelated properties from the result' do
|
551
|
+
expect(result.keys).not_to include('UnusedProperty')
|
552
|
+
expect(result.keys).not_to include('unusedproperty')
|
553
|
+
expect(result.keys).not_to include(:unusedproperty)
|
554
|
+
end
|
555
|
+
it 'removes parameters from the result' do
|
556
|
+
expect(result[:dsc_param]).to be_nil
|
557
|
+
end
|
558
|
+
it 'handles timestamps' do
|
559
|
+
expect(result[:dsc_time]).to eq('TimeStamp:2100-01-01')
|
560
|
+
end
|
561
|
+
it 'downcases keys in cim instance properties' do
|
562
|
+
expect(result[:dsc_nestedciminstance].keys).to eq(%w[baz nestedproperty])
|
563
|
+
expect(result[:dsc_nestedciminstance]['nestedproperty'].keys).to eq(%w[cim_instance_type nestedbar nestedfoo])
|
564
|
+
end
|
565
|
+
it 'recursively sorts the result for order-insensitive comparisons' do
|
566
|
+
expect(result.keys).to eq(%i[dsc_array dsc_ciminstance dsc_ensure dsc_name dsc_nestedciminstance dsc_time name])
|
567
|
+
expect(result[:dsc_array]).to eq(%w[bar foo])
|
568
|
+
expect(result[:dsc_ciminstance].keys).to eq(%w[bar foo])
|
569
|
+
expect(result[:dsc_nestedciminstance].keys).to eq(%w[baz nestedproperty])
|
570
|
+
expect(result[:dsc_nestedciminstance]['nestedproperty'].keys).to eq(%w[cim_instance_type nestedbar nestedfoo])
|
571
|
+
end
|
572
|
+
|
573
|
+
context 'when a namevar is an array' do
|
574
|
+
let(:name_hash) { { name: 'foo', dsc_name: 'foo', dsc_array: %w[foo bar] } }
|
575
|
+
let(:query_props) { { dsc_name: 'foo', dsc_array: %w[foo bar] } }
|
576
|
+
let(:mandatory_get_attributes) { %i[dsc_name dsc_array] }
|
577
|
+
let(:attributes) do
|
578
|
+
{
|
579
|
+
name: {
|
580
|
+
type: 'String',
|
581
|
+
behaviour: :namevar
|
582
|
+
},
|
583
|
+
dsc_name: {
|
584
|
+
type: 'String',
|
585
|
+
behaviour: :namevar,
|
586
|
+
mandatory_for_get: true,
|
587
|
+
mandatory_for_set: true,
|
588
|
+
mof_type: 'String',
|
589
|
+
mof_is_embedded: false
|
590
|
+
},
|
591
|
+
dsc_array: {
|
592
|
+
type: 'Array[String]',
|
593
|
+
mandatory_for_get: true,
|
594
|
+
mandatory_for_set: true,
|
595
|
+
mof_type: 'String[]',
|
596
|
+
mof_is_embedded: false
|
597
|
+
}
|
598
|
+
}
|
599
|
+
end
|
600
|
+
let(:parsed_invocation_data) do
|
601
|
+
{ 'Name' => 'foo', 'Ensure' => 'Present', 'Array' => %w[foo bar] }
|
602
|
+
end
|
603
|
+
|
604
|
+
it 'behaves like any other namevar when specified as not empty' do
|
605
|
+
expect(result[:dsc_array]).to eq(%w[bar foo])
|
606
|
+
end
|
607
|
+
|
608
|
+
context 'when the namevar array is empty' do
|
609
|
+
# Does this ever happen?
|
610
|
+
let(:name_hash) { { name: 'foo', dsc_name: 'foo', dsc_array: [] } }
|
611
|
+
let(:query_props) { { dsc_name: 'foo', dsc_array: [] } }
|
612
|
+
|
613
|
+
context 'when DSC returns @()' do
|
614
|
+
let(:parsed_invocation_data) do
|
615
|
+
{ 'Name' => 'foo', 'Ensure' => 'Present', 'Array' => [] }
|
616
|
+
end
|
617
|
+
|
618
|
+
it 'returns [] for the array value' do
|
619
|
+
expect(result[:dsc_array]).to eq([])
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
context 'when DSC returns $null' do
|
624
|
+
let(:parsed_invocation_data) do
|
625
|
+
{ 'Name' => 'foo', 'Ensure' => 'Present', 'Array' => nil }
|
626
|
+
end
|
627
|
+
|
628
|
+
it 'returns [] for the array value' do
|
629
|
+
expect(result[:dsc_array]).to eq([])
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
context 'when the DSC invocation errors' do
|
637
|
+
it 'writes an error and returns nil' do
|
638
|
+
expect(provider).not_to receive(:logon_failed_already?)
|
639
|
+
expect(ps_manager).to receive(:execute).with(script).and_return({ stdout: nil })
|
640
|
+
expect(context).to receive(:err).with('Nothing returned')
|
641
|
+
expect(result).to be_nil
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
context 'when handling DateTimes' do
|
646
|
+
before(:each) do
|
647
|
+
allow(ps_manager).to receive(:execute).with(script).and_return({ stdout: 'DSC Data' })
|
648
|
+
allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data)
|
649
|
+
allow(provider).to receive(:fetch_cached_hashes).and_return([])
|
650
|
+
end
|
651
|
+
|
652
|
+
context 'When the DateTime is nil' do
|
653
|
+
let(:name_hash) { { name: 'foo', dsc_name: 'foo', dsc_time: nil } }
|
654
|
+
let(:parsed_invocation_data) do
|
655
|
+
{ 'Name' => 'foo', 'Ensure' => 'Present', 'Time' => nil }
|
656
|
+
end
|
657
|
+
|
658
|
+
it 'returns nil for the value' do
|
659
|
+
expect(context).not_to receive(:err)
|
660
|
+
expect(result[:dsc_time]).to eq(nil)
|
661
|
+
end
|
662
|
+
end
|
663
|
+
|
664
|
+
context 'When the DateTime is an invalid string' do
|
665
|
+
let(:name_hash) { { name: 'foo', dsc_name: 'foo', dsc_time: 'foo' } }
|
666
|
+
let(:parsed_invocation_data) do
|
667
|
+
{ 'Name' => 'foo', 'Ensure' => 'Present', 'Time' => 'foo' }
|
668
|
+
end
|
669
|
+
|
670
|
+
it 'writes an error and sets the value of `dsc_time` to nil' do
|
671
|
+
expect(context).to receive(:err).with(/Value returned for DateTime/)
|
672
|
+
expect(result[:dsc_time]).to eq(nil)
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
context 'When the DateTime is an invalid type (integer, hash, etc)' do
|
677
|
+
let(:name_hash) { { name: 'foo', dsc_name: 'foo', dsc_time: 2100 } }
|
678
|
+
let(:parsed_invocation_data) do
|
679
|
+
{ 'Name' => 'foo', 'Ensure' => 'Present', 'Time' => 2100 }
|
680
|
+
end
|
681
|
+
|
682
|
+
it 'writes an error and sets the value of `dsc_time` to nil' do
|
683
|
+
expect(context).to receive(:err).with(/Value returned for DateTime/)
|
684
|
+
expect(result[:dsc_time]).to eq(nil)
|
685
|
+
end
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
context 'with PSDscCredential' do
|
690
|
+
let(:credential_hash) { { 'user' => 'SomeUser', 'password' => 'FooBarBaz' } }
|
691
|
+
let(:dsc_logon_failure_error) { 'Logon failure: the user has not been granted the requested logon type at this computer' }
|
692
|
+
let(:puppet_logon_failure_error) { 'PSDscRunAsCredential account specified (SomeUser) does not have appropriate logon rights; are they an administrator?' }
|
693
|
+
let(:name_hash) { { name: 'foo', dsc_name: 'foo', dsc_psdscrunascredential: credential_hash } }
|
694
|
+
let(:query_props) { { dsc_name: 'foo', dsc_psdscrunascredential: credential_hash } }
|
695
|
+
|
696
|
+
context 'when the credential is invalid' do
|
697
|
+
before(:each) do
|
698
|
+
allow(provider).to receive(:logon_failed_already?).and_return(false)
|
699
|
+
allow(ps_manager).to receive(:execute).with(script).and_return({ stdout: 'DSC Data' })
|
700
|
+
allow(JSON).to receive(:parse).with('DSC Data').and_return({ 'errormessage' => dsc_logon_failure_error })
|
701
|
+
allow(context).to receive(:err).with(name_hash[:name], puppet_logon_failure_error)
|
702
|
+
end
|
703
|
+
after(:each) do
|
704
|
+
described_class.class_variable_set(:@@logon_failures, nil) # rubocop:disable Style/ClassVars
|
705
|
+
end
|
706
|
+
|
707
|
+
it 'errors specifically for a logon failure and returns nil' do
|
708
|
+
expect(result).to be_nil
|
709
|
+
end
|
710
|
+
it 'caches the logon failure' do
|
711
|
+
expect { result }.not_to raise_error
|
712
|
+
expect(described_class.class_variable_get(:@@logon_failures)).to eq([credential_hash])
|
713
|
+
end
|
714
|
+
it 'caches the query results' do
|
715
|
+
expect { result }.not_to raise_error
|
716
|
+
expect(described_class.class_variable_get(:@@cached_query_results)).to eq([name_hash])
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
context 'with a previously failed logon' do
|
721
|
+
it 'errors and returns nil if the specified account has already failed to logon' do
|
722
|
+
expect(provider).to receive(:logon_failed_already?).and_return(true)
|
723
|
+
expect(context).to receive(:err).with('Logon credentials are invalid')
|
724
|
+
expect(result).to be_nil
|
725
|
+
end
|
726
|
+
end
|
727
|
+
end
|
728
|
+
end
|
729
|
+
|
730
|
+
context '.invoke_set_method' do
|
731
|
+
subject(:result) { provider.invoke_set_method(context, name, should_hash) }
|
732
|
+
|
733
|
+
let(:name) { { name: 'foo', dsc_name: 'foo' } }
|
734
|
+
let(:should_hash) { name.merge(dsc_foo: 'bar') }
|
735
|
+
let(:apply_props) { { dsc_name: 'foo', dsc_foo: 'bar' } }
|
736
|
+
let(:resource) { "Resource: #{apply_props}" }
|
737
|
+
let(:script) { "Script: #{apply_props}" }
|
738
|
+
|
739
|
+
before(:each) do
|
740
|
+
allow(context).to receive(:debug)
|
741
|
+
allow(provider).to receive(:invocable_resource).with(apply_props, context, 'set').and_return(resource)
|
742
|
+
allow(provider).to receive(:ps_script_content).with(resource).and_return(script)
|
743
|
+
allow(provider).to receive(:ps_manager).and_return(ps_manager)
|
744
|
+
allow(provider).to receive(:remove_secret_identifiers).with(script).and_return(script)
|
745
|
+
end
|
746
|
+
|
747
|
+
context 'when the specified account has already failed to logon' do
|
748
|
+
let(:should_hash) { name.merge(dsc_psdscrunascredential: 'bar') }
|
749
|
+
|
750
|
+
it 'returns immediately' do
|
751
|
+
expect(provider).to receive(:logon_failed_already?).and_return(true)
|
752
|
+
expect(context).to receive(:err).with('Logon credentials are invalid')
|
753
|
+
expect(result).to eq(nil)
|
754
|
+
end
|
755
|
+
end
|
756
|
+
|
757
|
+
context 'when the invocation script returns nil' do
|
758
|
+
it 'errors via context but does not raise' do
|
759
|
+
expect(ps_manager).to receive(:execute).and_return({ stdout: nil })
|
760
|
+
expect(context).to receive(:err).with('Nothing returned')
|
761
|
+
expect { result }.not_to raise_error
|
762
|
+
end
|
763
|
+
end
|
764
|
+
|
765
|
+
context 'when the invocation script errors' do
|
766
|
+
it 'writes the error via context but does not raise and returns nil' do
|
767
|
+
expect(ps_manager).to receive(:execute).and_return({ stdout: '{"errormessage": "DSC Error!"}' })
|
768
|
+
expect(context).to receive(:err).with('DSC Error!')
|
769
|
+
expect(result).to eq(nil)
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
773
|
+
context 'when the invocation script returns data without errors' do
|
774
|
+
it 'filters for the correct properties to invoke and returns the results' do
|
775
|
+
expect(ps_manager).to receive(:execute).with("Script: #{apply_props}").and_return({ stdout: '{"in_desired_state": true, "errormessage": null}' })
|
776
|
+
expect(context).not_to receive(:err)
|
777
|
+
expect(result).to eq({ 'in_desired_state' => true, 'errormessage' => nil })
|
778
|
+
end
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
context '.puppetize_name' do
|
783
|
+
it 'downcases the input string' do
|
784
|
+
expect(provider.puppetize_name('FooBar')).to eq('foobar')
|
785
|
+
end
|
786
|
+
|
787
|
+
it 'replaces nonstandard characters with underscores' do
|
788
|
+
expect(provider.puppetize_name('Foo!Bar?Baz Ope')).to eq('foo_bar_baz_ope')
|
789
|
+
end
|
790
|
+
|
791
|
+
it 'prepends "a" if the input string starts with a numeral' do
|
792
|
+
expect(provider.puppetize_name('123bc')).to eq('a123bc')
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
context '.invocable_resource' do
|
797
|
+
subject(:result) { provider.invocable_resource(should_hash, context, 'Get') }
|
798
|
+
|
799
|
+
let(:definition) do
|
800
|
+
{
|
801
|
+
name: 'dsc_foo',
|
802
|
+
dscmeta_resource_friendly_name: 'Foo',
|
803
|
+
dscmeta_resource_name: 'PUPPET_Foo',
|
804
|
+
dscmeta_module_name: 'PuppetDsc',
|
805
|
+
dscmeta_module_version: '1.2.3.4',
|
806
|
+
docs: 'The DSC Foo resource type. Automatically generated from version 1.2.3.4',
|
807
|
+
features: %w[simple_get_filter canonicalize],
|
808
|
+
attributes: {
|
809
|
+
name: {
|
810
|
+
type: 'String',
|
811
|
+
desc: 'Description of the purpose for this resource declaration.',
|
812
|
+
behaviour: :namevar
|
813
|
+
},
|
814
|
+
dsc_name: {
|
815
|
+
type: 'String',
|
816
|
+
desc: 'The unique name of the Foo resource to manage',
|
817
|
+
behaviour: :namevar,
|
818
|
+
mandatory_for_get: true,
|
819
|
+
mandatory_for_set: true,
|
820
|
+
mof_type: 'String',
|
821
|
+
mof_is_embedded: false
|
822
|
+
},
|
823
|
+
dsc_psdscrunascredential: {
|
824
|
+
type: 'Optional[Struct[{ user => String[1], password => Sensitive[String[1]] }]]',
|
825
|
+
desc: 'The Credential to run DSC under',
|
826
|
+
behaviour: :parameter,
|
827
|
+
mandatory_for_get: false,
|
828
|
+
mandatory_for_set: false,
|
829
|
+
mof_type: 'PSCredential',
|
830
|
+
mof_is_embedded: true
|
831
|
+
},
|
832
|
+
dsc_ensure: {
|
833
|
+
type: "Optional[Enum['Present', 'Absent']]",
|
834
|
+
desc: 'Whether Foo should be absent from or present on the system',
|
835
|
+
mandatory_for_get: false,
|
836
|
+
mandatory_for_set: false,
|
837
|
+
mof_type: 'String',
|
838
|
+
mof_is_embedded: false
|
839
|
+
}
|
840
|
+
}
|
841
|
+
}
|
842
|
+
end
|
843
|
+
let(:should_hash) { { dsc_name: 'foo' } }
|
844
|
+
let(:vendored_modules_path) { 'C:/code/puppetlabs/gems/ruby-pwsh/lib/puppet_x/puppetdsc/dsc_resources' }
|
845
|
+
|
846
|
+
before(:each) do
|
847
|
+
allow(context).to receive(:debug)
|
848
|
+
allow(context).to receive(:type).and_return(type)
|
849
|
+
allow(type).to receive(:definition).and_return(definition)
|
850
|
+
allow(provider).to receive(:vendored_modules_path).and_return(vendored_modules_path)
|
851
|
+
end
|
852
|
+
|
853
|
+
it 'retrieves the metadata from the type definition for the resource' do
|
854
|
+
expect(result[:name]).to eq(definition[:name])
|
855
|
+
expect(result[:dscmeta_resource_friendly_name]).to eq(definition[:dscmeta_resource_friendly_name])
|
856
|
+
expect(result[:dscmeta_resource_name]).to eq(definition[:dscmeta_resource_name])
|
857
|
+
expect(result[:dscmeta_module_name]).to eq(definition[:dscmeta_module_name])
|
858
|
+
expect(result[:dscmeta_module_version]).to eq(definition[:dscmeta_module_version])
|
859
|
+
end
|
860
|
+
|
861
|
+
it 'includes the specified parameter and its value' do
|
862
|
+
expect(result[:parameters][:dsc_name][:value]).to eq('foo')
|
863
|
+
end
|
864
|
+
|
865
|
+
it 'adds the mof information to a parameter if required' do
|
866
|
+
expect(result[:parameters][:dsc_name][:mof_type]).to eq('String')
|
867
|
+
expect(result[:parameters][:dsc_name][:mof_is_embedded]).to be false
|
868
|
+
end
|
869
|
+
|
870
|
+
context 'handling dsc_psdscrunascredential' do
|
871
|
+
context 'when it is not specified in the should hash' do
|
872
|
+
it 'is not included in the resource hash' do
|
873
|
+
expect(result[:parameters].keys).not_to include(:dsc_psdscrunascredential)
|
874
|
+
end
|
875
|
+
end
|
876
|
+
|
877
|
+
context 'when it is nil in the should hash' do
|
878
|
+
let(:should_hash) { { dsc_name: 'foo', dsc_psdscrunascredential: nil } }
|
879
|
+
|
880
|
+
it 'is not included in the resource hash' do
|
881
|
+
expect(result[:parameters].keys).not_to include(:dsc_psdscrunascredential)
|
882
|
+
end
|
883
|
+
end
|
884
|
+
|
885
|
+
context 'when it is specified fully in the should hash' do
|
886
|
+
let(:should_hash) { { dsc_name: 'foo', dsc_psdscrunascredential: { 'user' => 'foo', 'password' => 'bar' } } }
|
887
|
+
|
888
|
+
it 'is added to the parameters of the resource hash' do
|
889
|
+
expect(result[:parameters][:dsc_psdscrunascredential][:value]).to eq({ 'user' => 'foo', 'password' => 'bar' })
|
890
|
+
end
|
891
|
+
end
|
892
|
+
end
|
893
|
+
end
|
894
|
+
|
895
|
+
context '.vendored_modules_path' do
|
896
|
+
let(:load_path) { [] }
|
897
|
+
let(:new_path_nil_root_module) { 'C:/code/puppetlabs/gems/ruby-pwsh/lib/puppet_x/puppetdsc/dsc_resources' }
|
898
|
+
|
899
|
+
before(:each) do
|
900
|
+
allow(provider).to receive(:load_path).and_return(load_path)
|
901
|
+
allow(File).to receive(:exist?).and_call_original
|
902
|
+
end
|
903
|
+
|
904
|
+
it 'raises an error when the vendored resources cannot be found' do
|
905
|
+
expect { provider.vendored_modules_path('NeverGonnaFindMe') }.to raise_error(/Unable to find expected vendored DSC Resource/)
|
906
|
+
end
|
907
|
+
|
908
|
+
context 'when the vendored resources are in puppet_x/<module_name>/dsc_resources' do
|
909
|
+
context 'when the root module path can be found' do
|
910
|
+
let(:load_path) { ['/Puppet/modules/puppetdsc/lib'] }
|
911
|
+
let(:vendored_path) { File.expand_path('/Puppet/modules/puppetdsc/lib/puppet_x/puppetdsc/dsc_resources') }
|
912
|
+
|
913
|
+
it 'returns the constructed path' do
|
914
|
+
expect(File).to receive(:exist?).twice.with(vendored_path).and_return(true)
|
915
|
+
expect(provider.vendored_modules_path('PuppetDsc')).to eq(vendored_path)
|
916
|
+
end
|
917
|
+
end
|
918
|
+
|
919
|
+
context 'when the root module path cannot be found' do
|
920
|
+
# This is awkward but necessary to get to /path/to/gem/lib/puppet_x/
|
921
|
+
let(:vendored_path) { File.expand_path(Pathname.new(__FILE__).dirname + '../../../../../' + 'lib/puppet_x/puppetdsc/dsc_resources') } # rubocop:disable Style/StringConcatenation
|
922
|
+
|
923
|
+
it 'returns the relative path' do
|
924
|
+
expect(File).to receive(:exist?).twice.with(vendored_path).and_return(true)
|
925
|
+
expect(provider.vendored_modules_path('PuppetDsc')).to eq(vendored_path)
|
926
|
+
end
|
927
|
+
end
|
928
|
+
end
|
929
|
+
|
930
|
+
context 'when the vendored resources are in puppet_x/dsc_resources' do
|
931
|
+
context 'when the root module path can be found' do
|
932
|
+
let(:load_path) { ['/Puppet/modules/puppetdsc/lib'] }
|
933
|
+
let(:namespaced_vendored_path) { File.expand_path('/Puppet/modules/puppetdsc/lib/puppet_x/puppetdsc/dsc_resources') }
|
934
|
+
let(:legacy_vendored_path) { File.expand_path('/Puppet/modules/puppetdsc/lib/puppet_x/dsc_resources') }
|
935
|
+
|
936
|
+
it 'returns the constructed path' do
|
937
|
+
expect(File).to receive(:exist?).with(namespaced_vendored_path).and_return(false)
|
938
|
+
expect(File).to receive(:exist?).with(legacy_vendored_path).and_return(true)
|
939
|
+
expect(provider.vendored_modules_path('PuppetDsc')).to eq(legacy_vendored_path)
|
940
|
+
end
|
941
|
+
end
|
942
|
+
|
943
|
+
context 'when the root module path cannot be found' do
|
944
|
+
# This is awkward but necessary to get to /path/to/gem/lib/puppet_x/
|
945
|
+
let(:namespaced_vendored_path) { File.expand_path(Pathname.new(__FILE__).dirname + '../../../../../' + 'lib/puppet_x/puppetdsc/dsc_resources') } # rubocop:disable Style/StringConcatenation
|
946
|
+
let(:legacy_vendored_path) { File.expand_path(Pathname.new(__FILE__).dirname + '../../../../../' + 'lib/puppet_x/dsc_resources') } # rubocop:disable Style/StringConcatenation
|
947
|
+
|
948
|
+
it 'returns the constructed path' do
|
949
|
+
expect(File).to receive(:exist?).with(namespaced_vendored_path).and_return(false)
|
950
|
+
expect(File).to receive(:exist?).with(legacy_vendored_path).and_return(true)
|
951
|
+
expect(provider.vendored_modules_path('PuppetDsc')).to eq(legacy_vendored_path)
|
952
|
+
end
|
953
|
+
end
|
954
|
+
end
|
955
|
+
end
|
956
|
+
|
957
|
+
context '.load_path' do
|
958
|
+
it 'returns the ruby LOAD_PATH global variable' do
|
959
|
+
expect(provider.load_path).to eq($LOAD_PATH)
|
960
|
+
end
|
961
|
+
end
|
962
|
+
|
963
|
+
context '.invoke_test_method' do
|
964
|
+
subject(:result) { provider.invoke_test_method(context, name, should) }
|
965
|
+
|
966
|
+
let(:name) { { name: 'foo', dsc_name: 'bar' } }
|
967
|
+
let(:should) { name.merge(dsc_ensure: 'present') }
|
968
|
+
let(:test_properties) { should.reject { |k, _v| k == :name } }
|
969
|
+
let(:invoke_dsc_resource_data) { nil }
|
970
|
+
|
971
|
+
before(:each) do
|
972
|
+
allow(context).to receive(:notice)
|
973
|
+
allow(context).to receive(:debug)
|
974
|
+
allow(provider).to receive(:invoke_dsc_resource).with(context, name, test_properties, 'test').and_return(invoke_dsc_resource_data)
|
975
|
+
end
|
976
|
+
|
977
|
+
after(:each) do
|
978
|
+
described_class.class_variable_set(:@@cached_test_results, []) # rubocop:disable Style/ClassVars
|
979
|
+
end
|
980
|
+
|
981
|
+
context 'when something went wrong calling Invoke-DscResource' do
|
982
|
+
it 'falls back on property-by-property state comparison and does not cache anything' do
|
983
|
+
expect(context).not_to receive(:err)
|
984
|
+
expect(result).to be(nil)
|
985
|
+
expect(provider.cached_test_results).to eq([])
|
986
|
+
end
|
987
|
+
end
|
988
|
+
|
989
|
+
context 'when the DSC Resource is in the desired state' do
|
990
|
+
let(:invoke_dsc_resource_data) { { 'indesiredstate' => true, 'errormessage' => '' } }
|
991
|
+
|
992
|
+
it 'returns true and caches the result' do
|
993
|
+
expect(context).not_to receive(:err)
|
994
|
+
expect(result).to eq(true)
|
995
|
+
expect(provider.cached_test_results).to eq([name.merge(in_desired_state: true)])
|
996
|
+
end
|
997
|
+
end
|
998
|
+
|
999
|
+
context 'when the DSC Resource is not in the desired state' do
|
1000
|
+
let(:invoke_dsc_resource_data) { { 'indesiredstate' => false, 'errormessage' => '' } }
|
1001
|
+
|
1002
|
+
it 'returns false and caches the result' do
|
1003
|
+
expect(context).not_to receive(:err)
|
1004
|
+
# Resource is not in the desired state
|
1005
|
+
expect(result.first).to eq(false)
|
1006
|
+
# Custom out-of-sync message passed
|
1007
|
+
expect(result.last).to match(/not in the desired state/)
|
1008
|
+
expect(provider.cached_test_results).to eq([name.merge(in_desired_state: false)])
|
1009
|
+
end
|
1010
|
+
end
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
context '.random_variable_name' do
|
1014
|
+
it 'creates random variables' do
|
1015
|
+
expect(provider.random_variable_name).not_to be_nil
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
it 'includes underscores instead of hyphens' do
|
1019
|
+
expect(provider.random_variable_name).to match(/_/)
|
1020
|
+
expect(provider.random_variable_name).to_not match(/-/)
|
1021
|
+
end
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
context '.instantiated_variables' do
|
1025
|
+
after(:each) do
|
1026
|
+
described_class.class_variable_set(:@@instantiated_variables, nil) # rubocop:disable Style/ClassVars
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
it 'sets the instantiated_variables class variable to {} if not initialized' do
|
1030
|
+
expect(provider.instantiated_variables).to eq({})
|
1031
|
+
end
|
1032
|
+
it 'returns the instantiated_variables class variable if already initialized' do
|
1033
|
+
described_class.class_variable_set(:@@instantiated_variables, { foo: 'bar' }) # rubocop:disable Style/ClassVars
|
1034
|
+
expect(provider.instantiated_variables).to eq({ foo: 'bar' })
|
1035
|
+
end
|
1036
|
+
end
|
1037
|
+
|
1038
|
+
context '.clear_instantiated_variables!' do
|
1039
|
+
after(:each) do
|
1040
|
+
described_class.class_variable_set(:@@instantiated_variables, nil) # rubocop:disable Style/ClassVars
|
1041
|
+
end
|
1042
|
+
|
1043
|
+
it 'sets the instantiated_variables class variable to {}' do
|
1044
|
+
described_class.class_variable_set(:@@instantiated_variables, { foo: 'bar' }) # rubocop:disable Style/ClassVars
|
1045
|
+
expect { provider.clear_instantiated_variables! }.not_to raise_error
|
1046
|
+
expect(described_class.class_variable_get(:@@instantiated_variables)).to eq({})
|
1047
|
+
end
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
context '.logon_failed_already?' do
|
1051
|
+
let(:good_password) { instance_double('Puppet::Pops::Types::PSensitiveType::Sensitive', 'foo') }
|
1052
|
+
let(:bad_password) { instance_double('Puppet::Pops::Types::PSensitiveType::Sensitive', 'bar') }
|
1053
|
+
let(:good_credential_hash) { { 'user' => 'foo', 'password' => good_password } }
|
1054
|
+
let(:bad_credential_hash) { { 'user' => 'bar', 'password' => bad_password } }
|
1055
|
+
|
1056
|
+
context 'when the logon_failures cache is empty' do
|
1057
|
+
it 'returns false' do
|
1058
|
+
expect(provider.logon_failed_already?(good_credential_hash)).to eq(false)
|
1059
|
+
end
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
context 'when the logon_failures cache has entries' do
|
1063
|
+
before(:each) do
|
1064
|
+
allow(good_password).to receive(:unwrap).and_return('foo')
|
1065
|
+
allow(bad_password).to receive(:unwrap).and_return('bar')
|
1066
|
+
end
|
1067
|
+
|
1068
|
+
after(:each) do
|
1069
|
+
described_class.class_variable_set(:@@logon_failures, nil) # rubocop:disable Style/ClassVars
|
1070
|
+
end
|
1071
|
+
|
1072
|
+
it 'returns false if there have been no failed logons with the username/password combination' do
|
1073
|
+
described_class.class_variable_set(:@@logon_failures, [bad_credential_hash]) # rubocop:disable Style/ClassVars
|
1074
|
+
expect(provider.logon_failed_already?(good_credential_hash)).to eq(false)
|
1075
|
+
end
|
1076
|
+
it 'returns true if the username/password specified are found in the logon_failures class variable' do
|
1077
|
+
described_class.class_variable_set(:@@logon_failures, [good_credential_hash, bad_credential_hash]) # rubocop:disable Style/ClassVars
|
1078
|
+
expect(provider.logon_failed_already?(bad_credential_hash)).to eq(true)
|
1079
|
+
end
|
1080
|
+
end
|
1081
|
+
end
|
1082
|
+
|
1083
|
+
context '.downcase_hash_keys!' do
|
1084
|
+
let(:test_hash) do
|
1085
|
+
{
|
1086
|
+
'SomeKey' => 'value',
|
1087
|
+
'SomeArray' => [
|
1088
|
+
{ 'ArrayKeyOne' => 1, 'ArrayKeyTwo' => 2 },
|
1089
|
+
{ 'ArrayKeyOne' => '1', 'ArrayKeyTwo' => '2' }
|
1090
|
+
],
|
1091
|
+
'SomeHash' => {
|
1092
|
+
'NestedKey' => 'foo',
|
1093
|
+
'NestedArray' => [{ 'NestedArrayKeyOne' => 1, 'NestedArrayKeyTwo' => 2 }],
|
1094
|
+
'NestedHash' => {
|
1095
|
+
'DeeplyNestedKey' => 'foo',
|
1096
|
+
'DeeplyNestedArray' => [{ 'DeeplyNestedArrayKeyOne' => 1, 'DeeplyNestedArrayKeyTwo' => 2 }],
|
1097
|
+
'DeeplyNestedHash' => {
|
1098
|
+
'VeryDeeplyNestedKey' => 'foo'
|
1099
|
+
}
|
1100
|
+
}
|
1101
|
+
}
|
1102
|
+
}
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
it 'converts all the keys in a hash into downcase, even if nested in another hash or array' do
|
1106
|
+
downcased_hash = test_hash.dup
|
1107
|
+
expect { provider.downcase_hash_keys!(downcased_hash) }.not_to raise_error
|
1108
|
+
expect(downcased_hash.keys).to eq(%w[somekey somearray somehash])
|
1109
|
+
expect(downcased_hash['somearray'][0].keys).to eq(%w[arraykeyone arraykeytwo])
|
1110
|
+
expect(downcased_hash['somearray'][1].keys).to eq(%w[arraykeyone arraykeytwo])
|
1111
|
+
expect(downcased_hash['somehash'].keys).to eq(%w[nestedkey nestedarray nestedhash])
|
1112
|
+
expect(downcased_hash['somehash']['nestedarray'].first.keys).to eq(%w[nestedarraykeyone nestedarraykeytwo])
|
1113
|
+
expect(downcased_hash['somehash']['nestedhash'].keys).to eq(%w[deeplynestedkey deeplynestedarray deeplynestedhash])
|
1114
|
+
expect(downcased_hash['somehash']['nestedhash']['deeplynestedarray'].first.keys).to eq(%w[deeplynestedarraykeyone deeplynestedarraykeytwo])
|
1115
|
+
expect(downcased_hash['somehash']['nestedhash']['deeplynestedhash'].keys).to eq(%w[verydeeplynestedkey])
|
1116
|
+
end
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
context '.munge_cim_instances!' do
|
1120
|
+
let(:cim_instance) do
|
1121
|
+
{
|
1122
|
+
'CertificateSubject' => nil,
|
1123
|
+
'SslFlags' => '0',
|
1124
|
+
'CertificateStoreName' => nil,
|
1125
|
+
'CertificateThumbprint' => nil,
|
1126
|
+
'HostName' => nil,
|
1127
|
+
'BindingInformation' => '*:80:',
|
1128
|
+
'cim_instance_type' => 'MSFT_xWebBindingInformation',
|
1129
|
+
'Port' => 80,
|
1130
|
+
'IPAddress' => '*',
|
1131
|
+
'Protocol' => 'http'
|
1132
|
+
}
|
1133
|
+
end
|
1134
|
+
let(:nested_cim_instance) do
|
1135
|
+
{
|
1136
|
+
'AccessControlEntry' => [
|
1137
|
+
{
|
1138
|
+
'AccessControlType' => 'Allow',
|
1139
|
+
'Inheritance' => 'This folder and files',
|
1140
|
+
'Ensure' => 'Present',
|
1141
|
+
'cim_instance_type' => 'NTFSAccessControlEntry',
|
1142
|
+
'FileSystemRights' => ['FullControl']
|
1143
|
+
}
|
1144
|
+
],
|
1145
|
+
'ForcePrincipal' => true,
|
1146
|
+
'Principal' => 'Everyone'
|
1147
|
+
}
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
before(:each) { provider.munge_cim_instances!(value) }
|
1151
|
+
|
1152
|
+
context 'when called against a non-nested cim instance' do
|
1153
|
+
let(:value) { cim_instance.dup }
|
1154
|
+
|
1155
|
+
it 'removes the cim_instance_type key' do
|
1156
|
+
expect(value.keys).not_to include('cim_instance_type')
|
1157
|
+
end
|
1158
|
+
|
1159
|
+
context 'in an array' do
|
1160
|
+
let(:value) { [cim_instance.dup] }
|
1161
|
+
|
1162
|
+
it 'removes the cim_instance_type key' do
|
1163
|
+
expect(value.first.keys).not_to include('cim_instance_type')
|
1164
|
+
end
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
context 'when called against a nested cim instance' do
|
1168
|
+
let(:value) { nested_cim_instance.dup }
|
1169
|
+
|
1170
|
+
it 'does not remove the cim_instance_type key' do
|
1171
|
+
expect(value['AccessControlEntry'].first.keys).to include('cim_instance_type')
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
context 'in an array' do
|
1175
|
+
let(:value) { [nested_cim_instance.dup] }
|
1176
|
+
|
1177
|
+
it 'does not remove the cim_instance_type key' do
|
1178
|
+
expect(value.first['AccessControlEntry'].first.keys).to include('cim_instance_type')
|
1179
|
+
end
|
1180
|
+
end
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
context 'when called against a value which is not a cim_instance' do
|
1184
|
+
let(:original) { %w[foo bar baz] }
|
1185
|
+
let(:value) { original.dup }
|
1186
|
+
|
1187
|
+
it 'does not change the value' do
|
1188
|
+
expect(value).to eq(original)
|
1189
|
+
end
|
1190
|
+
end
|
1191
|
+
end
|
1192
|
+
|
1193
|
+
context '.recursively_downcase' do
|
1194
|
+
let(:test_hash) do
|
1195
|
+
{
|
1196
|
+
SomeKey: 'Value',
|
1197
|
+
SomeArray: [
|
1198
|
+
{ ArrayKeyOne: 1, ArrayKeyTwo: 2 },
|
1199
|
+
{ ArrayKeyOne: 'ONE', ArrayKeyTwo: 2 }
|
1200
|
+
],
|
1201
|
+
SomeHash: {
|
1202
|
+
NestedKey: 'Foo',
|
1203
|
+
NestedArray: [{ NestedArrayKeyOne: 'ONE', NestedArrayKeyTwo: 2 }],
|
1204
|
+
NestedHash: {
|
1205
|
+
DeeplyNestedKey: 'Foo',
|
1206
|
+
DeeplyNestedArray: [{ DeeplyNestedArrayKeyOne: 'One', DeeplyNestedArrayKeyTwo: 2 }],
|
1207
|
+
DeeplyNestedHash: {
|
1208
|
+
VeryDeeplyNestedKey: 'Foo'
|
1209
|
+
}
|
1210
|
+
}
|
1211
|
+
}
|
1212
|
+
}
|
1213
|
+
end
|
1214
|
+
let(:downcased_array) { [{ arraykeyone: 1, arraykeytwo: 2 }, { arraykeyone: 'one', arraykeytwo: 2 }] }
|
1215
|
+
let(:downcased_hash) do
|
1216
|
+
{
|
1217
|
+
nestedkey: 'foo',
|
1218
|
+
nestedarray: [{ nestedarraykeyone: 'one', nestedarraykeytwo: 2 }],
|
1219
|
+
nestedhash: {
|
1220
|
+
deeplynestedkey: 'foo',
|
1221
|
+
deeplynestedarray: [{ deeplynestedarraykeyone: 'one', deeplynestedarraykeytwo: 2 }],
|
1222
|
+
deeplynestedhash: {
|
1223
|
+
verydeeplynestedkey: 'foo'
|
1224
|
+
}
|
1225
|
+
}
|
1226
|
+
}
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
it 'downcases any string passed, whether alone or in a hash or array or nested deeply' do
|
1230
|
+
result = provider.recursively_downcase(test_hash)
|
1231
|
+
expect(result[:somekey]).to eq('value')
|
1232
|
+
expect(result[:somearray]).to eq(downcased_array)
|
1233
|
+
expect(result[:somehash]).to eq(downcased_hash)
|
1234
|
+
end
|
1235
|
+
end
|
1236
|
+
|
1237
|
+
context '.recursively_sort' do
|
1238
|
+
let(:test_hash) do
|
1239
|
+
{
|
1240
|
+
SomeKey: 'Value',
|
1241
|
+
SomeArray: [2, 3, 1],
|
1242
|
+
SomeComplexArray: [2, 3, [2, 1]],
|
1243
|
+
SomeHash: {
|
1244
|
+
NestedKey: 'Foo',
|
1245
|
+
NestedArray: [{ NestedArrayKeyTwo: 2, NestedArrayKeyOne: 'ONE' }, 2, 1],
|
1246
|
+
NestedHash: {
|
1247
|
+
DeeplyNestedKey: 'Foo',
|
1248
|
+
DeeplyNestedArray: [2, 3, 'a', 1, 'c', 'b'],
|
1249
|
+
DeeplyNestedHash: {
|
1250
|
+
VeryDeeplyNestedKey2: 'Foo',
|
1251
|
+
VeryDeeplyNestedKey1: 'Bar'
|
1252
|
+
}
|
1253
|
+
}
|
1254
|
+
}
|
1255
|
+
}
|
1256
|
+
end
|
1257
|
+
let(:sorted_keys) { %i[SomeArray SomeComplexArray SomeHash SomeKey] }
|
1258
|
+
let(:sorted_some_array) { [1, 2, 3] }
|
1259
|
+
let(:sorted_complex_array) { [2, 3, [1, 2]] }
|
1260
|
+
let(:sorted_some_hash_keys) { %i[NestedArray NestedHash NestedKey] }
|
1261
|
+
let(:sorted_nested_array) { [1, 2, { NestedArrayKeyOne: 'ONE', NestedArrayKeyTwo: 2 }] }
|
1262
|
+
let(:sorted_nested_hash_keys) { %i[DeeplyNestedArray DeeplyNestedHash DeeplyNestedKey] }
|
1263
|
+
let(:sorted_deeply_nested_array) { [1, 2, 3, 'a', 'b', 'c'] }
|
1264
|
+
let(:sorted_deeply_nested_hash_keys) { %i[VeryDeeplyNestedKey1 VeryDeeplyNestedKey2] }
|
1265
|
+
|
1266
|
+
it 'downcases any string passed, whether alone or in a hash or array or nested deeply' do
|
1267
|
+
result = provider.recursively_sort(test_hash)
|
1268
|
+
expect(result.keys).to eq(sorted_keys)
|
1269
|
+
expect(result[:SomeArray]).to eq(sorted_some_array)
|
1270
|
+
expect(result[:SomeComplexArray]).to eq(sorted_complex_array)
|
1271
|
+
expect(result[:SomeHash].keys).to eq(sorted_some_hash_keys)
|
1272
|
+
expect(result[:SomeHash][:NestedArray]).to eq(sorted_nested_array)
|
1273
|
+
expect(result[:SomeHash][:NestedHash].keys).to eq(sorted_nested_hash_keys)
|
1274
|
+
expect(result[:SomeHash][:NestedHash][:DeeplyNestedArray]).to eq(sorted_deeply_nested_array)
|
1275
|
+
expect(result[:SomeHash][:NestedHash][:DeeplyNestedHash].keys).to eq(sorted_deeply_nested_hash_keys)
|
1276
|
+
end
|
1277
|
+
end
|
1278
|
+
|
1279
|
+
context '.same?' do
|
1280
|
+
it 'compares hashes regardless of order' do
|
1281
|
+
expect(provider.same?({ foo: 1, bar: 2 }, { bar: 2, foo: 1 })).to be true
|
1282
|
+
end
|
1283
|
+
it 'compares hashes with nested arrays regardless of order' do
|
1284
|
+
expect(provider.same?({ foo: [1, 2], bar: { baz: [1, 2] } }, { foo: [2, 1], bar: { baz: [2, 1] } })).to be true
|
1285
|
+
end
|
1286
|
+
it 'compares arrays regardless of order' do
|
1287
|
+
expect(provider.same?([1, 2], [2, 1])).to be true
|
1288
|
+
end
|
1289
|
+
it 'compares arrays with nested arrays regardless of order' do
|
1290
|
+
expect(provider.same?([1, [1, 2]], [[2, 1], 1])).to be true
|
1291
|
+
end
|
1292
|
+
it 'compares non enumerables directly' do
|
1293
|
+
expect(provider.same?(1, 1)).to be true
|
1294
|
+
expect(provider.same?(1, 2)).to be false
|
1295
|
+
end
|
1296
|
+
end
|
1297
|
+
|
1298
|
+
context '.mandatory_get_attributes' do
|
1299
|
+
let(:attributes) do
|
1300
|
+
{
|
1301
|
+
name: { type: 'String' },
|
1302
|
+
dsc_ensure: { mandatory_for_get: true },
|
1303
|
+
dsc_enum: { mandatory_for_get: false },
|
1304
|
+
dsc_string: { mandatory_for_get: true }
|
1305
|
+
}
|
1306
|
+
end
|
1307
|
+
|
1308
|
+
it 'returns the list of attributes from the type where the mandatory_for_get meta property is true' do
|
1309
|
+
expect(context).to receive(:type).and_return(type)
|
1310
|
+
expect(type).to receive(:attributes).and_return(attributes)
|
1311
|
+
expect(provider.mandatory_get_attributes(context)).to eq(%i[dsc_ensure dsc_string])
|
1312
|
+
end
|
1313
|
+
end
|
1314
|
+
|
1315
|
+
context '.mandatory_set_attributes' do
|
1316
|
+
let(:attributes) do
|
1317
|
+
{
|
1318
|
+
name: { type: 'String' },
|
1319
|
+
dsc_ensure: { mandatory_for_set: true },
|
1320
|
+
dsc_enum: { mandatory_for_set: false },
|
1321
|
+
dsc_string: { mandatory_for_set: true }
|
1322
|
+
}
|
1323
|
+
end
|
1324
|
+
|
1325
|
+
it 'returns the list of attributes from the type where the mandatory_for_set meta property is true' do
|
1326
|
+
expect(context).to receive(:type).and_return(type)
|
1327
|
+
expect(type).to receive(:attributes).and_return(attributes)
|
1328
|
+
expect(provider.mandatory_set_attributes(context)).to eq(%i[dsc_ensure dsc_string])
|
1329
|
+
end
|
1330
|
+
end
|
1331
|
+
|
1332
|
+
context '.namevar_attributes' do
|
1333
|
+
let(:attributes) do
|
1334
|
+
{
|
1335
|
+
name: { type: 'String', behaviour: :namevar },
|
1336
|
+
dsc_name: { type: 'String', behaviour: :namevar },
|
1337
|
+
dsc_ensure: { type: "[Enum['Present', 'Absent']]" },
|
1338
|
+
dsc_enum: { type: "Optional[Enum['Trusted', 'Untrusted']]" },
|
1339
|
+
dsc_string: { type: 'Optional[String]' }
|
1340
|
+
}
|
1341
|
+
end
|
1342
|
+
|
1343
|
+
it 'returns the list of attributes from the type where the attribute has the namevar behavior' do
|
1344
|
+
expect(context).to receive(:type).and_return(type)
|
1345
|
+
expect(type).to receive(:attributes).and_return(attributes)
|
1346
|
+
expect(provider.namevar_attributes(context)).to eq(%i[name dsc_name])
|
1347
|
+
end
|
1348
|
+
end
|
1349
|
+
|
1350
|
+
context '.parameter_attributes' do
|
1351
|
+
let(:attributes) do
|
1352
|
+
{
|
1353
|
+
name: { type: 'String', behaviour: :namevar },
|
1354
|
+
dsc_name: { type: 'String', behaviour: :namevar },
|
1355
|
+
dsc_ensure: { type: "[Enum['Present', 'Absent']]" },
|
1356
|
+
dsc_enum: { type: "Optional[Enum['Trusted', 'Untrusted']]", behaviour: :parameter },
|
1357
|
+
dsc_string: { type: 'Optional[String]', behaviour: :parameter }
|
1358
|
+
}
|
1359
|
+
end
|
1360
|
+
|
1361
|
+
it 'returns the list of attributes from the type where the attribute has the parameter behavior' do
|
1362
|
+
expect(context).to receive(:type).and_return(type)
|
1363
|
+
expect(type).to receive(:attributes).and_return(attributes)
|
1364
|
+
expect(provider.parameter_attributes(context)).to eq(%i[dsc_enum dsc_string])
|
1365
|
+
end
|
1366
|
+
end
|
1367
|
+
|
1368
|
+
context '.enum_attributes' do
|
1369
|
+
let(:enum_test_attributes) do
|
1370
|
+
{
|
1371
|
+
name: { type: 'String' },
|
1372
|
+
dsc_ensure: { type: "[Enum['Present', 'Absent']]" },
|
1373
|
+
dsc_enum: { type: "Optional[Enum['Trusted', 'Untrusted']]" },
|
1374
|
+
dsc_string: { type: 'Optional[String]' }
|
1375
|
+
}
|
1376
|
+
end
|
1377
|
+
|
1378
|
+
it 'returns the list of attributes from the type where the attribute data type is an enum' do
|
1379
|
+
expect(context).to receive(:type).and_return(type)
|
1380
|
+
expect(type).to receive(:attributes).and_return(enum_test_attributes)
|
1381
|
+
expect(provider.enum_attributes(context)).to eq(%i[dsc_ensure dsc_enum])
|
1382
|
+
end
|
1383
|
+
end
|
1384
|
+
|
1385
|
+
context '.interpolate_variables' do
|
1386
|
+
let(:instantiated_variables) do
|
1387
|
+
{
|
1388
|
+
some_variable_name: 'FooBar',
|
1389
|
+
another_variable_name: 'Get-Foo',
|
1390
|
+
third_variable_name: 'Get-Foo "bar"'
|
1391
|
+
}
|
1392
|
+
end
|
1393
|
+
|
1394
|
+
before(:each) do
|
1395
|
+
allow(provider).to receive(:instantiated_variables).and_return(instantiated_variables)
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
it 'replaces all discovered pointers to a variable with the variable' do
|
1399
|
+
expect(provider.interpolate_variables("'FooBar' ; 'FooBar'")).to eq('$some_variable_name ; $some_variable_name')
|
1400
|
+
end
|
1401
|
+
it 'replaces discovered pointers in reverse order they were stored' do
|
1402
|
+
expect(provider.interpolate_variables("'Get-Foo \"bar\"'")).to eq('$third_variable_name')
|
1403
|
+
end
|
1404
|
+
end
|
1405
|
+
|
1406
|
+
context '.munge_psmodulepath' do
|
1407
|
+
subject(:result) { provider.munge_psmodulepath(test_resource) }
|
1408
|
+
|
1409
|
+
context 'when the resource does not have the dscmeta_resource_implementation key' do
|
1410
|
+
let(:test_resource) { {} }
|
1411
|
+
|
1412
|
+
it 'returns nil' do
|
1413
|
+
expect(result).to be(nil)
|
1414
|
+
end
|
1415
|
+
end
|
1416
|
+
|
1417
|
+
context "when the resource's dscmeta_resource_implementation is not 'Class'" do
|
1418
|
+
let(:test_resource) { { dscmeta_resource_implementation: 'MOF' } }
|
1419
|
+
|
1420
|
+
it 'returns nil' do
|
1421
|
+
expect(result).to be(nil)
|
1422
|
+
end
|
1423
|
+
end
|
1424
|
+
|
1425
|
+
context "when the resource's dscmeta_resource_implementation is 'Class'" do
|
1426
|
+
let(:test_resource) { { dscmeta_resource_implementation: 'Class', vendored_modules_path: 'C:/foo/bar' } }
|
1427
|
+
|
1428
|
+
it 'sets $UnmungedPSModulePath to the current PSModulePath' do
|
1429
|
+
expect(result).to match(/\$UnmungedPSModulePath = .+GetEnvironmentVariable.+PSModulePath.+machine/)
|
1430
|
+
end
|
1431
|
+
it 'sets $MungedPSModulePath the vendor path with backslash separators' do
|
1432
|
+
expect(result).to match(/\$MungedPSModulePath = .+;C:\\foo\\bar/)
|
1433
|
+
end
|
1434
|
+
it 'updates the system PSModulePath to $MungedPSModulePath' do
|
1435
|
+
expect(result).to match(/SetEnvironmentVariable\('PSModulePath', \$MungedPSModulePath/)
|
1436
|
+
end
|
1437
|
+
it 'sets the process level PSModulePath to the modified system PSModulePath' do
|
1438
|
+
expect(result).to match(/\$env:PSModulePath = .+GetEnvironmentVariable.+PSModulePath.+machine/)
|
1439
|
+
end
|
1440
|
+
end
|
1441
|
+
end
|
1442
|
+
|
1443
|
+
context '.prepare_credentials' do
|
1444
|
+
subject(:result) { provider.prepare_credentials(test_resource) }
|
1445
|
+
|
1446
|
+
let(:base_resource) do
|
1447
|
+
{
|
1448
|
+
parameters: {
|
1449
|
+
dsc_name: { value: 'foo', mof_type: 'String', mof_is_embedded: false }
|
1450
|
+
}
|
1451
|
+
}
|
1452
|
+
end
|
1453
|
+
|
1454
|
+
context 'when no PSCredentials are passed as parameters' do
|
1455
|
+
let(:test_resource) { base_resource.dup }
|
1456
|
+
|
1457
|
+
it 'returns an empty string' do
|
1458
|
+
expect(result).to eq('')
|
1459
|
+
end
|
1460
|
+
end
|
1461
|
+
|
1462
|
+
context 'when one or more PSCredentials are passed as parameters' do
|
1463
|
+
let(:foo_password) { instance_double('Puppet::Pops::Types::PSensitiveType::Sensitive', 'foo') }
|
1464
|
+
let(:bar_password) { instance_double('Puppet::Pops::Types::PSensitiveType::Sensitive', 'bar') }
|
1465
|
+
let(:additional_parameters) do
|
1466
|
+
{
|
1467
|
+
parameters: {
|
1468
|
+
dsc_psdscrunascredential: { value: nil, mof_type: 'PSCredential' },
|
1469
|
+
dsc_somecredential: { value: { 'user' => 'foo', 'password' => foo_password }, mof_type: 'PSCredential' },
|
1470
|
+
dsc_othercredential: { value: { 'user' => 'bar', 'password' => bar_password }, mof_type: 'PSCredential' }
|
1471
|
+
}
|
1472
|
+
}
|
1473
|
+
end
|
1474
|
+
let(:test_resource) { base_resource.merge(additional_parameters) }
|
1475
|
+
|
1476
|
+
before(:each) do
|
1477
|
+
allow(foo_password).to receive(:unwrap).and_return('foo')
|
1478
|
+
allow(bar_password).to receive(:unwrap).and_return('bar')
|
1479
|
+
end
|
1480
|
+
|
1481
|
+
after(:each) do
|
1482
|
+
described_class.class_variable_set(:@@instantiated_variables, nil) # rubocop:disable Style/ClassVars
|
1483
|
+
end
|
1484
|
+
|
1485
|
+
it 'writes the ruby representation of the credentials as the value of a key named for the new variable into the instantiated_variables cache' do
|
1486
|
+
expect(result.count).to eq(2) # dsc_psdscrunascredential should not get an entry as it is nil
|
1487
|
+
instantiated_variables = provider.instantiated_variables
|
1488
|
+
first_variable_name, first_credential_hash = instantiated_variables.first
|
1489
|
+
instantiated_variables.delete(first_variable_name)
|
1490
|
+
_variable_name, second_credential_hash = instantiated_variables.first
|
1491
|
+
expect(first_credential_hash).to eq({ 'user' => 'foo', 'password' => 'foo' })
|
1492
|
+
expect(second_credential_hash).to eq({ 'user' => 'bar', 'password' => 'bar' })
|
1493
|
+
end
|
1494
|
+
it 'returns an array of strings each containing the instantiation of a PowerShell variable representing the credential hash' do
|
1495
|
+
expect(result[0]).to match(/^\$\w+ = New-PSCredential -User foo -Password 'foo#PuppetSensitive'/)
|
1496
|
+
expect(result[1]).to match(/^\$\w+ = New-PSCredential -User bar -Password 'bar#PuppetSensitive'/)
|
1497
|
+
end
|
1498
|
+
end
|
1499
|
+
end
|
1500
|
+
|
1501
|
+
context '.format_pscredential' do
|
1502
|
+
let(:credential_hash) { { 'user' => 'foo', 'password' => 'bar' } }
|
1503
|
+
|
1504
|
+
it 'returns a string representing the instantiation of a PowerShell variable representing the credential hash' do
|
1505
|
+
expected_powershell_code = "$foo = New-PSCredential -User foo -Password 'bar#PuppetSensitive'"
|
1506
|
+
expect(provider.format_pscredential('foo', credential_hash)).to eq(expected_powershell_code)
|
1507
|
+
end
|
1508
|
+
end
|
1509
|
+
|
1510
|
+
context '.prepare_cim_instances' do
|
1511
|
+
subject(:result) { provider.prepare_cim_instances(test_resource) }
|
1512
|
+
|
1513
|
+
after(:each) do
|
1514
|
+
described_class.class_variable_set(:@@instantiated_variables, nil) # rubocop:disable Style/ClassVars
|
1515
|
+
end
|
1516
|
+
|
1517
|
+
context 'when a cim instance is passed without nested cim instances' do
|
1518
|
+
let(:test_resource) do
|
1519
|
+
{
|
1520
|
+
parameters: {
|
1521
|
+
dsc_someciminstance: {
|
1522
|
+
value: { 'foo' => 1, 'bar' => 'two' },
|
1523
|
+
mof_type: 'SomeCimType',
|
1524
|
+
mof_is_embedded: true
|
1525
|
+
},
|
1526
|
+
dsc_name: { value: 'foo', mof_type: 'String', mof_is_embedded: false }
|
1527
|
+
}
|
1528
|
+
}
|
1529
|
+
end
|
1530
|
+
|
1531
|
+
before(:each) do
|
1532
|
+
allow(provider).to receive(:nested_cim_instances).with(test_resource[:parameters][:dsc_someciminstance][:value]).and_return([nil, nil])
|
1533
|
+
allow(provider).to receive(:random_variable_name).and_return('cim_foo')
|
1534
|
+
end
|
1535
|
+
|
1536
|
+
it 'processes only for properties which have an embedded mof' do
|
1537
|
+
expect(provider).not_to receive(:nested_cim_instances).with(test_resource[:parameters][:dsc_name][:value])
|
1538
|
+
expect { result }.not_to raise_error
|
1539
|
+
end
|
1540
|
+
|
1541
|
+
it 'instantiates a variable for the cim instance' do
|
1542
|
+
expect(result).to eq("$cim_foo = New-CimInstance -ClientOnly -ClassName 'SomeCimType' -Property @{'foo' = 1; 'bar' = 'two'}")
|
1543
|
+
end
|
1544
|
+
end
|
1545
|
+
|
1546
|
+
context 'when a cim instance is passed with nested cim instances' do
|
1547
|
+
let(:test_resource) do
|
1548
|
+
{
|
1549
|
+
parameters: {
|
1550
|
+
dsc_accesscontrollist: {
|
1551
|
+
value: [{
|
1552
|
+
'accesscontrolentry' => [{
|
1553
|
+
'accesscontroltype' => 'Allow',
|
1554
|
+
'ensure' => 'Present',
|
1555
|
+
'cim_instance_type' => 'NTFSAccessControlEntry'
|
1556
|
+
}],
|
1557
|
+
'principal' => 'veryRealUserName'
|
1558
|
+
}],
|
1559
|
+
mof_type: 'NTFSAccessControlList[]',
|
1560
|
+
mof_is_embedded: true
|
1561
|
+
}
|
1562
|
+
}
|
1563
|
+
}
|
1564
|
+
end
|
1565
|
+
let(:nested_cim_instances) do
|
1566
|
+
[[[{ 'accesscontroltype' => 'Allow', 'ensure' => 'Present', 'cim_instance_type' => 'NTFSAccessControlEntry' }], nil]]
|
1567
|
+
end
|
1568
|
+
|
1569
|
+
before(:each) do
|
1570
|
+
allow(provider).to receive(:nested_cim_instances).with(test_resource[:parameters][:dsc_accesscontrollist][:value]).and_return(nested_cim_instances)
|
1571
|
+
allow(provider).to receive(:random_variable_name).and_return('cim_foo', 'cim_bar')
|
1572
|
+
end
|
1573
|
+
|
1574
|
+
it 'processes nested cim instances first' do
|
1575
|
+
cim_instance_declarations = result.split("\n")
|
1576
|
+
expect(cim_instance_declarations.first).to match(/\$cim_foo =/)
|
1577
|
+
expect(cim_instance_declarations.first).to match(/ClassName 'NTFSAccessControlEntry'/)
|
1578
|
+
end
|
1579
|
+
it 'references nested cim instances as variables in the parent cim instance' do
|
1580
|
+
cim_instance_declarations = result.split("\n")
|
1581
|
+
expect(cim_instance_declarations[1]).to match(/\$cim_bar =.+Property @{'accesscontrolentry' = \[CimInstance\[\]\]@\(\$cim_foo\); 'principal' = 'veryRealUserName'}/)
|
1582
|
+
end
|
1583
|
+
end
|
1584
|
+
|
1585
|
+
context 'when there are no cim instances' do
|
1586
|
+
let(:test_resource) do
|
1587
|
+
{
|
1588
|
+
parameters: {
|
1589
|
+
dsc_name: { value: 'foo', mof_type: 'String', mof_is_embedded: false }
|
1590
|
+
}
|
1591
|
+
}
|
1592
|
+
end
|
1593
|
+
|
1594
|
+
it 'returns an empty string' do
|
1595
|
+
expect(result).to eq('')
|
1596
|
+
end
|
1597
|
+
end
|
1598
|
+
end
|
1599
|
+
|
1600
|
+
context '.nested_cim_instances' do
|
1601
|
+
subject(:nested_cim_instances) { provider.nested_cim_instances(enumerable).flatten }
|
1602
|
+
|
1603
|
+
let(:enumerable) do
|
1604
|
+
[{
|
1605
|
+
'accesscontrolentry' => [{
|
1606
|
+
'accesscontroltype' => 'Allow',
|
1607
|
+
'ensure' => 'Present',
|
1608
|
+
'cim_instance_type' => 'NTFSAccessControlEntry'
|
1609
|
+
}],
|
1610
|
+
'principal' => 'veryRealUserName'
|
1611
|
+
}]
|
1612
|
+
end
|
1613
|
+
|
1614
|
+
it 'returns an array with only nested cim instances as non-nil' do
|
1615
|
+
expect(nested_cim_instances.first).to eq(enumerable.first['accesscontrolentry'].first)
|
1616
|
+
expect(nested_cim_instances[1]).to be_nil
|
1617
|
+
end
|
1618
|
+
end
|
1619
|
+
|
1620
|
+
context '.format_ciminstance' do
|
1621
|
+
after(:each) do
|
1622
|
+
described_class.class_variable_set(:@@instantiated_variables, nil) # rubocop:disable Style/ClassVars
|
1623
|
+
end
|
1624
|
+
|
1625
|
+
it 'defines and returns a new cim instance as a PowerShell variable, passing the class name and property hash' do
|
1626
|
+
property_hash = { 'foo' => 1, 'bar' => 'two' }
|
1627
|
+
expected_command = "$foo = New-CimInstance -ClientOnly -ClassName 'SomeClass' -Property @{'foo' = 1; 'bar' = 'two'}"
|
1628
|
+
expect(provider.format_ciminstance('foo', 'SomeClass', property_hash)).to eq(expected_command)
|
1629
|
+
end
|
1630
|
+
it 'handles arrays of cim instances' do
|
1631
|
+
property_hash = [{ 'foo' => 1, 'bar' => 'two' }, { 'foo' => 3, 'bar' => 'four' }]
|
1632
|
+
expected_cim_instance_array_regex = /Property \[CimInstance\[\]\]@\(@\{'foo' = 1; 'bar' = 'two'}, @\{'foo' = 3; 'bar' = 'four'\}\)/
|
1633
|
+
expect(provider.format_ciminstance('foo', 'SomeClass', property_hash)).to match(expected_cim_instance_array_regex)
|
1634
|
+
end
|
1635
|
+
it 'interpolates variables in the case of a cim instance containing a nested instance' do
|
1636
|
+
described_class.class_variable_set(:@@instantiated_variables, { 'SomeVariable' => { 'bar' => 'ope' } }) # rubocop:disable Style/ClassVars
|
1637
|
+
property_hash = { 'foo' => { 'bar' => 'ope' } }
|
1638
|
+
expect(provider.format_ciminstance('foo', 'SomeClass', property_hash)).to match(/@\{'foo' = \$SomeVariable\}/)
|
1639
|
+
end
|
1640
|
+
end
|
1641
|
+
|
1642
|
+
context '.invoke_params' do
|
1643
|
+
subject(:result) { provider.invoke_params(test_resource) }
|
1644
|
+
|
1645
|
+
let(:test_parameter) { { dsc_name: { value: 'foo', mof_type: 'String', mof_is_embedded: false } } }
|
1646
|
+
let(:test_resource) do
|
1647
|
+
{
|
1648
|
+
parameters: test_parameter,
|
1649
|
+
name: 'dsc_foo',
|
1650
|
+
dscmeta_resource_friendly_name: 'Foo',
|
1651
|
+
dscmeta_resource_name: 'PUPPET_Foo',
|
1652
|
+
dscmeta_module_name: 'PuppetDsc',
|
1653
|
+
dsc_invoke_method: 'Get',
|
1654
|
+
vendored_modules_path: 'C:/code/puppetlabs/gems/ruby-pwsh/lib/puppet_x/puppetdsc/dsc_resources',
|
1655
|
+
attributes: nil
|
1656
|
+
}
|
1657
|
+
end
|
1658
|
+
|
1659
|
+
it 'includes the DSC Resource name in the output hash' do
|
1660
|
+
expect(result).to match(/Name = 'Foo'/)
|
1661
|
+
end
|
1662
|
+
it 'includes the specified method in the output hash' do
|
1663
|
+
expect(result).to match(/Method = 'Get'/)
|
1664
|
+
end
|
1665
|
+
it 'includes the properties as a hashtable to pass the DSC Resource in the output hash' do
|
1666
|
+
expect(result).to match(/Property = @\{name = 'foo'\}/)
|
1667
|
+
end
|
1668
|
+
|
1669
|
+
context 'when handling module versioning' do
|
1670
|
+
context 'when the dscmeta_module_version is not specified' do
|
1671
|
+
it 'includes the ModuleName in the output hash as a string' do
|
1672
|
+
expect(result).to match(/ModuleName = 'PuppetDsc'/)
|
1673
|
+
end
|
1674
|
+
end
|
1675
|
+
|
1676
|
+
context 'when the dscmeta_module_version is specified' do
|
1677
|
+
let(:test_resource) do
|
1678
|
+
{
|
1679
|
+
parameters: test_parameter,
|
1680
|
+
dscmeta_resource_friendly_name: 'Foo',
|
1681
|
+
dscmeta_module_name: 'PuppetDsc',
|
1682
|
+
dscmeta_module_version: '1.2.3.4',
|
1683
|
+
dsc_invoke_method: 'Get',
|
1684
|
+
vendored_modules_path: 'C:/path/to/ruby-pwsh/lib/puppet_x/puppetdsc/dsc_resources'
|
1685
|
+
}
|
1686
|
+
end
|
1687
|
+
let(:expected_module_name) do
|
1688
|
+
"ModuleName = @\{ModuleName = 'C:/path/to/ruby-pwsh/lib/puppet_x/puppetdsc/dsc_resources/PuppetDsc/PuppetDsc.psd1'; RequiredVersion = '1.2.3.4'\}"
|
1689
|
+
end
|
1690
|
+
|
1691
|
+
it 'includes the ModuleName in the output hash as a hashtable of name and version' do
|
1692
|
+
expect(result).to match(/#{expected_module_name}/)
|
1693
|
+
end
|
1694
|
+
end
|
1695
|
+
end
|
1696
|
+
|
1697
|
+
context 'parameter handling' do
|
1698
|
+
context 'PSCredential' do
|
1699
|
+
let(:password) { instance_double('Puppet::Pops::Types::PSensitiveType::Sensitive', 'FooPassword') }
|
1700
|
+
let(:test_parameter) do
|
1701
|
+
{ dsc_credential: { value: { 'user' => 'foo', 'password' => password }, mof_type: 'PSCredential', mof_is_embedded: false } }
|
1702
|
+
end
|
1703
|
+
let(:formatted_param_hash) do
|
1704
|
+
"$InvokeParams = @{Name = 'Foo'; Method = 'Get'; Property = @{credential = @{'user' = 'foo'; 'password' = 'FooPassword'}}; ModuleName = 'PuppetDsc'}"
|
1705
|
+
end
|
1706
|
+
let(:variable_interpolated_param_hash) do
|
1707
|
+
"$InvokeParams = @{Name = 'Foo'; Method = 'Get'; Property = @{credential = $SomeCredential}; ModuleName = 'PuppetDsc'}"
|
1708
|
+
end
|
1709
|
+
|
1710
|
+
it 'unwraps the credential hash and interpolates the appropriate variable' do
|
1711
|
+
expect(password).to receive(:unwrap).and_return('FooPassword')
|
1712
|
+
expect(provider).to receive(:interpolate_variables).with(formatted_param_hash).and_return(variable_interpolated_param_hash)
|
1713
|
+
expect(result).to eq(variable_interpolated_param_hash)
|
1714
|
+
end
|
1715
|
+
end
|
1716
|
+
|
1717
|
+
context 'DateTime' do
|
1718
|
+
let(:date_time) { instance_double('Puppet::Pops::Time::Timestamp', '2100-01-01') }
|
1719
|
+
let(:test_parameter) do
|
1720
|
+
{ dsc_datetime: { value: date_time, mof_type: 'DateTime', mof_is_embedded: false } }
|
1721
|
+
end
|
1722
|
+
|
1723
|
+
it 'casts the formatted timestamp string to DateTime in the property hash' do
|
1724
|
+
expect(date_time).to receive(:format).and_return('2100-01-01')
|
1725
|
+
expect(result).to match(/datetime = \[DateTime\]'2100-01-01'/)
|
1726
|
+
end
|
1727
|
+
end
|
1728
|
+
|
1729
|
+
context 'Empty Array' do
|
1730
|
+
let(:test_parameter) do
|
1731
|
+
{ dsc_emptyarray: { value: [], mof_type: 'String[]', mof_is_embedded: false } }
|
1732
|
+
end
|
1733
|
+
|
1734
|
+
it 'casts the empty aray to the mof type in the property hash' do
|
1735
|
+
expect(result).to match(/emptyarray = \[String\[\]\]@\(\)/)
|
1736
|
+
end
|
1737
|
+
end
|
1738
|
+
|
1739
|
+
context 'Nested CIM Instances' do
|
1740
|
+
let(:test_parameter) do
|
1741
|
+
{ dsc_ciminstance: { value: { 'something' => 1, 'another' => 'two' }, mof_type: 'NestedCimInstance[]', mof_is_embedded: true } }
|
1742
|
+
end
|
1743
|
+
|
1744
|
+
it 'casts the Cim Instance value as [CimInstance[]]in the property hash' do
|
1745
|
+
expect(result).to match(/ciminstance = \[CimInstance\[\]\]@\{'something' = 1; 'another' = 'two'\}/)
|
1746
|
+
end
|
1747
|
+
end
|
1748
|
+
end
|
1749
|
+
end
|
1750
|
+
|
1751
|
+
context '.ps_script_content' do
|
1752
|
+
let(:gem_root) { File.expand_path('../../../../../../', __FILE__) }
|
1753
|
+
let(:template_path) { "#{gem_root}/lib/puppet/provider/dsc_base_provider" }
|
1754
|
+
let(:functions_file_handle) { instance_double('File', 'functions_file') }
|
1755
|
+
let(:preamble_file_handle) { instance_double('File', 'preamble_file') }
|
1756
|
+
let(:postscript_file_handle) { instance_double('File', 'postscript_file') }
|
1757
|
+
let(:expected_script_content) do
|
1758
|
+
"Functions Block\nPreamble Block\n\n\n\nParameters Block\nPostscript Block"
|
1759
|
+
end
|
1760
|
+
|
1761
|
+
before(:each) do
|
1762
|
+
allow(File).to receive(:new).with("#{template_path}/invoke_dsc_resource_functions.ps1").and_return(functions_file_handle)
|
1763
|
+
allow(functions_file_handle).to receive(:read).and_return('Functions Block')
|
1764
|
+
allow(File).to receive(:new).with("#{template_path}/invoke_dsc_resource_preamble.ps1").and_return(preamble_file_handle)
|
1765
|
+
allow(preamble_file_handle).to receive(:read).and_return('Preamble Block')
|
1766
|
+
allow(File).to receive(:new).with("#{template_path}/invoke_dsc_resource_postscript.ps1").and_return(postscript_file_handle)
|
1767
|
+
allow(postscript_file_handle).to receive(:read).and_return('Postscript Block')
|
1768
|
+
allow(provider).to receive(:munge_psmodulepath).and_return('')
|
1769
|
+
allow(provider).to receive(:munge_psmodulepath).with('ClassBasedResource').and_return('PSModulePath Block')
|
1770
|
+
allow(provider).to receive(:prepare_credentials).and_return('')
|
1771
|
+
allow(provider).to receive(:prepare_credentials).with('ResourceWithCredentials').and_return('Credential Block')
|
1772
|
+
allow(provider).to receive(:prepare_cim_instances).and_return('')
|
1773
|
+
allow(provider).to receive(:prepare_cim_instances).with('ResourceWithCimInstances').and_return('Cim Instance Block')
|
1774
|
+
allow(provider).to receive(:invoke_params).and_return('Parameters Block')
|
1775
|
+
end
|
1776
|
+
|
1777
|
+
it 'returns a powershell script with the helper functions' do
|
1778
|
+
expect(provider.ps_script_content('Basic')).to match("Functions Block\n")
|
1779
|
+
end
|
1780
|
+
it 'includes the preamble' do
|
1781
|
+
expect(provider.ps_script_content('Basic')).to match("Preamble Block\n")
|
1782
|
+
end
|
1783
|
+
it 'includes the module path block, if needed' do
|
1784
|
+
expect(provider.ps_script_content('Basic')).not_to match("PSModulePath Block\n")
|
1785
|
+
expect(provider.ps_script_content('ClassBasedResource')).to match("PSModulePath Block\n")
|
1786
|
+
end
|
1787
|
+
it 'includes the credential block, if needed' do
|
1788
|
+
expect(provider.ps_script_content('Basic')).not_to match("Credential Block\n")
|
1789
|
+
expect(provider.ps_script_content('ResourceWithCredentials')).to match("Credential Block\n")
|
1790
|
+
end
|
1791
|
+
it 'includes the cim instances block, if needed' do
|
1792
|
+
expect(provider.ps_script_content('Basic')).not_to match("Cim Instance Block\n")
|
1793
|
+
expect(provider.ps_script_content('ResourceWithCimInstances')).to match("Cim Instance Block\n")
|
1794
|
+
end
|
1795
|
+
it 'includes the parameters block' do
|
1796
|
+
expect(provider.ps_script_content('Basic')).to match("Parameters Block\n")
|
1797
|
+
end
|
1798
|
+
it 'includes the postscript block' do
|
1799
|
+
expect(provider.ps_script_content('Basic')).to match('Postscript Block')
|
1800
|
+
end
|
1801
|
+
it 'returns a single string with all the blocks joined' do
|
1802
|
+
expect(provider.ps_script_content('Basic')).to eq(expected_script_content)
|
1803
|
+
end
|
1804
|
+
end
|
1805
|
+
|
1806
|
+
context '.format' do
|
1807
|
+
let(:sensitive_string) { Puppet::Pops::Types::PSensitiveType::Sensitive.new('foo') }
|
1808
|
+
|
1809
|
+
it 'uses Pwsh::Util to format the values' do
|
1810
|
+
expect(Pwsh::Util).to receive(:format_powershell_value).with('foo').and_return('bar')
|
1811
|
+
expect(provider.format('foo')).to eq('bar')
|
1812
|
+
end
|
1813
|
+
it 'handles sensitive values especially' do
|
1814
|
+
expect(Pwsh::Util).to receive(:format_powershell_value).with(sensitive_string).and_raise(RuntimeError, 'Could not format Sensitive [value redacted]')
|
1815
|
+
expect(provider).to receive(:unwrap).with(sensitive_string).and_return('foo#PuppetSensitive')
|
1816
|
+
expect(Pwsh::Util).to receive(:format_powershell_value).with('foo#PuppetSensitive').and_return("'foo#PuppetSensitive'")
|
1817
|
+
expect(provider.format(sensitive_string)).to eq("'foo#PuppetSensitive'")
|
1818
|
+
end
|
1819
|
+
it 'raises an error if Pwsh::Util raises any error not related to unwrapping a sensitive string' do
|
1820
|
+
expect(Pwsh::Util).to receive(:format_powershell_value).with('foo').and_raise(RuntimeError, 'Ope!')
|
1821
|
+
expect { provider.format('foo') }.to raise_error(RuntimeError, 'Ope!')
|
1822
|
+
end
|
1823
|
+
end
|
1824
|
+
|
1825
|
+
context '.unwrap' do
|
1826
|
+
let(:sensitive_string) { Puppet::Pops::Types::PSensitiveType::Sensitive.new('foo') }
|
1827
|
+
let(:unwrapped_string) { 'foo#PuppetSensitive' }
|
1828
|
+
|
1829
|
+
it 'unwraps a sensitive string, appending "#PuppetSensitive" to the end' do
|
1830
|
+
expect(provider.unwrap(sensitive_string)).to eq(unwrapped_string)
|
1831
|
+
end
|
1832
|
+
it 'handles sensitive values in a hash' do
|
1833
|
+
expect(provider.unwrap({ key: sensitive_string })).to eq({ key: unwrapped_string })
|
1834
|
+
end
|
1835
|
+
it 'handles sensitive values in an array' do
|
1836
|
+
expect(provider.unwrap([1, sensitive_string])).to eq([1, unwrapped_string])
|
1837
|
+
end
|
1838
|
+
it 'handles sensitive values in a deeply nested structure' do
|
1839
|
+
sensitive_structure = {
|
1840
|
+
array: [sensitive_string, 'ope'],
|
1841
|
+
hash: {
|
1842
|
+
nested_value: sensitive_string,
|
1843
|
+
nested_array: [sensitive_string, 'bar'],
|
1844
|
+
nested_hash: {
|
1845
|
+
deeply_nested_value: sensitive_string
|
1846
|
+
}
|
1847
|
+
}
|
1848
|
+
}
|
1849
|
+
|
1850
|
+
result = provider.unwrap(sensitive_structure)
|
1851
|
+
|
1852
|
+
expect(result[:array]).to eq([unwrapped_string, 'ope'])
|
1853
|
+
expect(result[:hash][:nested_value]).to eq(unwrapped_string)
|
1854
|
+
expect(result[:hash][:nested_array]).to eq([unwrapped_string, 'bar'])
|
1855
|
+
expect(result[:hash][:nested_hash][:deeply_nested_value]).to eq(unwrapped_string)
|
1856
|
+
end
|
1857
|
+
it 'returns the input if it does not include any sensitive strings' do
|
1858
|
+
expect(provider.unwrap('foo bar baz')).to eq('foo bar baz')
|
1859
|
+
end
|
1860
|
+
end
|
1861
|
+
|
1862
|
+
context '.escape_quotes' do
|
1863
|
+
let(:no_quotes) { 'foo bar baz' }
|
1864
|
+
let(:single_quotes) { "foo 'bar' baz" }
|
1865
|
+
let(:double_quotes) { 'foo "bar" baz' }
|
1866
|
+
let(:mixed_quotes) { "'foo' \"bar\" '\"baz\"'" }
|
1867
|
+
|
1868
|
+
it 'returns the original string if no single quotes are passed' do
|
1869
|
+
expect(provider.escape_quotes(no_quotes)).to eq(no_quotes)
|
1870
|
+
expect(provider.escape_quotes(double_quotes)).to eq(double_quotes)
|
1871
|
+
end
|
1872
|
+
it "replaces single ' with '' in a given string" do
|
1873
|
+
expect(provider.escape_quotes(single_quotes)).to eq("foo ''bar'' baz")
|
1874
|
+
expect(provider.escape_quotes(mixed_quotes)).to eq("''foo'' \"bar\" ''\"baz\"''")
|
1875
|
+
end
|
1876
|
+
end
|
1877
|
+
|
1878
|
+
context '.redact_secrets' do
|
1879
|
+
let(:unsensitive_string) { 'some very unsecret text' }
|
1880
|
+
let(:sensitive_string) { "$foo = New-PSCredential -User foo -Password 'foo#PuppetSensitive'" }
|
1881
|
+
let(:redacted_string) { "$foo = New-PSCredential -User foo -Password '#<Sensitive [value redacted]>'" }
|
1882
|
+
let(:sensitive_array) { "@('a', 'b#PuppetSensitive', 'c')" }
|
1883
|
+
let(:redacted_array) { "@('a', '#<Sensitive [value redacted]>', 'c')" }
|
1884
|
+
let(:sensitive_hash) { "@{a = 'foo'; b = 'bar#PuppetSensitive'; c = 1}" }
|
1885
|
+
let(:redacted_hash) { "@{a = 'foo'; b = '#<Sensitive [value redacted]>'; c = 1}" }
|
1886
|
+
let(:sensitive_complex) { "@{a = 'foo'; b = 'bar#PuppetSensitive'; c = @('a', 'b#PuppetSensitive', 'c')}" }
|
1887
|
+
let(:redacted_complex) { "@{a = 'foo'; b = '#<Sensitive [value redacted]>'; c = @('a', '#<Sensitive [value redacted]>', 'c')}" }
|
1888
|
+
let(:multiline_sensitive_string) do
|
1889
|
+
<<~SENSITIVE.strip
|
1890
|
+
$foo = New-PSCredential -User foo -Password 'foo#PuppetSensitive'
|
1891
|
+
$bar = New-PSCredential -User bar -Password 'bar#PuppetSensitive'
|
1892
|
+
SENSITIVE
|
1893
|
+
end
|
1894
|
+
let(:multiline_redacted_string) do
|
1895
|
+
<<~REDACTED.strip
|
1896
|
+
$foo = New-PSCredential -User foo -Password '#<Sensitive [value redacted]>'
|
1897
|
+
$bar = New-PSCredential -User bar -Password '#<Sensitive [value redacted]>'
|
1898
|
+
REDACTED
|
1899
|
+
end
|
1900
|
+
let(:multiline_sensitive_complex) do
|
1901
|
+
<<~SENSITIVE.strip
|
1902
|
+
@{
|
1903
|
+
a = 'foo'
|
1904
|
+
b = 'bar#PuppetSensitive'
|
1905
|
+
c = @('a', 'b#PuppetSensitive', 'c', 'd#PuppetSensitive')
|
1906
|
+
d = @{
|
1907
|
+
a = 'foo#PuppetSensitive'
|
1908
|
+
b = @('a', 'b#PuppetSensitive')
|
1909
|
+
c = @('a', @{ x = 'y#PuppetSensitive' })
|
1910
|
+
}
|
1911
|
+
}
|
1912
|
+
SENSITIVE
|
1913
|
+
end
|
1914
|
+
let(:multiline_redacted_complex) do
|
1915
|
+
<<~REDACTED.strip
|
1916
|
+
@{
|
1917
|
+
a = 'foo'
|
1918
|
+
b = '#<Sensitive [value redacted]>'
|
1919
|
+
c = @('a', '#<Sensitive [value redacted]>', 'c', '#<Sensitive [value redacted]>')
|
1920
|
+
d = @{
|
1921
|
+
a = '#<Sensitive [value redacted]>'
|
1922
|
+
b = @('a', '#<Sensitive [value redacted]>')
|
1923
|
+
c = @('a', @{ x = '#<Sensitive [value redacted]>' })
|
1924
|
+
}
|
1925
|
+
}
|
1926
|
+
REDACTED
|
1927
|
+
end
|
1928
|
+
|
1929
|
+
it 'does not modify a string without any secrets' do
|
1930
|
+
expect(provider.redact_secrets(unsensitive_string)).to eq(unsensitive_string)
|
1931
|
+
end
|
1932
|
+
it 'replaces any unwrapped secret with "#<Sensitive [Value redacted]>"' do
|
1933
|
+
expect(provider.redact_secrets(sensitive_string)).to eq(redacted_string)
|
1934
|
+
expect(provider.redact_secrets(sensitive_array)).to eq(redacted_array)
|
1935
|
+
expect(provider.redact_secrets(sensitive_hash)).to eq(redacted_hash)
|
1936
|
+
expect(provider.redact_secrets(sensitive_complex)).to eq(redacted_complex)
|
1937
|
+
end
|
1938
|
+
it 'replaces unwrapped secrets in a multiline string' do
|
1939
|
+
expect(provider.redact_secrets(multiline_sensitive_string)).to eq(multiline_redacted_string)
|
1940
|
+
expect(provider.redact_secrets(multiline_sensitive_complex)).to eq(multiline_redacted_complex)
|
1941
|
+
end
|
1942
|
+
end
|
1943
|
+
|
1944
|
+
context '.remove_secret_identifiers' do
|
1945
|
+
let(:unsensitive_string) { 'some very unsecret text' }
|
1946
|
+
let(:sensitive_string) { "$foo = New-PSCredential -User foo -Password 'foo#PuppetSensitive'" }
|
1947
|
+
let(:redacted_string) { "$foo = New-PSCredential -User foo -Password 'foo'" }
|
1948
|
+
let(:sensitive_array) { "@('a', 'b#PuppetSensitive', 'c')" }
|
1949
|
+
let(:redacted_array) { "@('a', 'b', 'c')" }
|
1950
|
+
let(:sensitive_hash) { "@{a = 'foo'; b = 'bar#PuppetSensitive'; c = 1}" }
|
1951
|
+
let(:redacted_hash) { "@{a = 'foo'; b = 'bar'; c = 1}" }
|
1952
|
+
let(:sensitive_complex) { "@{a = 'foo'; b = 'bar#PuppetSensitive'; c = @('a', 'b#PuppetSensitive', 'c')}" }
|
1953
|
+
let(:redacted_complex) { "@{a = 'foo'; b = 'bar'; c = @('a', 'b', 'c')}" }
|
1954
|
+
let(:multiline_sensitive_string) do
|
1955
|
+
<<~SENSITIVE.strip
|
1956
|
+
$foo = New-PSCredential -User foo -Password 'foo#PuppetSensitive'
|
1957
|
+
$bar = New-PSCredential -User bar -Password 'bar#PuppetSensitive'
|
1958
|
+
SENSITIVE
|
1959
|
+
end
|
1960
|
+
let(:multiline_redacted_string) do
|
1961
|
+
<<~REDACTED.strip
|
1962
|
+
$foo = New-PSCredential -User foo -Password 'foo'
|
1963
|
+
$bar = New-PSCredential -User bar -Password 'bar'
|
1964
|
+
REDACTED
|
1965
|
+
end
|
1966
|
+
let(:multiline_sensitive_complex) do
|
1967
|
+
<<~SENSITIVE.strip
|
1968
|
+
@{
|
1969
|
+
a = 'foo'
|
1970
|
+
b = 'bar#PuppetSensitive'
|
1971
|
+
c = @('a', 'b#PuppetSensitive', 'c', 'd#PuppetSensitive')
|
1972
|
+
d = @{
|
1973
|
+
a = 'foo#PuppetSensitive'
|
1974
|
+
b = @('a', 'b#PuppetSensitive')
|
1975
|
+
c = @('a', @{ x = 'y#PuppetSensitive' })
|
1976
|
+
}
|
1977
|
+
}
|
1978
|
+
SENSITIVE
|
1979
|
+
end
|
1980
|
+
let(:multiline_redacted_complex) do
|
1981
|
+
<<~REDACTED.strip
|
1982
|
+
@{
|
1983
|
+
a = 'foo'
|
1984
|
+
b = 'bar'
|
1985
|
+
c = @('a', 'b', 'c', 'd')
|
1986
|
+
d = @{
|
1987
|
+
a = 'foo'
|
1988
|
+
b = @('a', 'b')
|
1989
|
+
c = @('a', @{ x = 'y' })
|
1990
|
+
}
|
1991
|
+
}
|
1992
|
+
REDACTED
|
1993
|
+
end
|
1994
|
+
|
1995
|
+
it 'does not modify a string without any secrets' do
|
1996
|
+
expect(provider.remove_secret_identifiers(unsensitive_string)).to eq(unsensitive_string)
|
1997
|
+
end
|
1998
|
+
it 'removes the secret identifier from any unwrapped secret' do
|
1999
|
+
expect(provider.remove_secret_identifiers(sensitive_string)).to eq(redacted_string)
|
2000
|
+
expect(provider.remove_secret_identifiers(sensitive_array)).to eq(redacted_array)
|
2001
|
+
expect(provider.remove_secret_identifiers(sensitive_hash)).to eq(redacted_hash)
|
2002
|
+
expect(provider.remove_secret_identifiers(sensitive_complex)).to eq(redacted_complex)
|
2003
|
+
end
|
2004
|
+
it 'removes the secret identifier from any unwrapped secrets in a multiline string' do
|
2005
|
+
expect(provider.remove_secret_identifiers(multiline_sensitive_string)).to eq(multiline_redacted_string)
|
2006
|
+
expect(provider.remove_secret_identifiers(multiline_sensitive_complex)).to eq(multiline_redacted_complex)
|
2007
|
+
end
|
2008
|
+
end
|
2009
|
+
|
2010
|
+
context '.ps_manager' do
|
2011
|
+
before(:each) do
|
2012
|
+
allow(Pwsh::Manager).to receive(:powershell_path).and_return('pwsh')
|
2013
|
+
allow(Pwsh::Manager).to receive(:powershell_args).and_return('args')
|
2014
|
+
end
|
2015
|
+
it 'Initializes an instance of the Pwsh::Manager' do
|
2016
|
+
expect(Puppet::Util::Log).to receive(:level).and_return(:normal)
|
2017
|
+
expect(Pwsh::Manager).to receive(:instance).with('pwsh', 'args', debug: false)
|
2018
|
+
expect { provider.ps_manager }.not_to raise_error
|
2019
|
+
end
|
2020
|
+
it 'passes debug as true if Puppet::Util::Log.level is debug' do
|
2021
|
+
expect(Puppet::Util::Log).to receive(:level).and_return(:debug)
|
2022
|
+
expect(Pwsh::Manager).to receive(:instance).with('pwsh', 'args', debug: true)
|
2023
|
+
expect { provider.ps_manager }.not_to raise_error
|
2024
|
+
end
|
2025
|
+
end
|
2026
|
+
end
|