ruby-pwsh 0.10.2 → 0.11.0.rc.1

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