ruby-pwsh 0.10.2 → 0.10.3
Sign up to get free protection for your applications and to get access to all the features.
- 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/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 +14 -19
- 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 -204
- 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
|