ruby-pwsh 0.10.1 → 0.10.3

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