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.
@@ -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