ruby-pwsh 0.10.2 → 0.11.0.rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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