File: nameservice_spec.rb

package info (click to toggle)
puppet 5.5.22-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 21,316 kB
  • sloc: ruby: 254,925; sh: 1,608; xml: 219; makefile: 153; sql: 103
file content (454 lines) | stat: -rw-r--r-- 19,206 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
require 'spec_helper'
require 'puppet/provider/nameservice'
require 'puppet/etc'
require 'puppet_spec/character_encoding'

describe Puppet::Provider::NameService do

  before :each do
    described_class.initvars
    described_class.resource_type = faketype
  end

  # These are values getpwent might give you
  let :users do
    [
      Struct::Passwd.new('root', 'x', 0, 0),
      Struct::Passwd.new('foo', 'x', 1000, 2000),
      nil
    ]
  end

  # These are values getgrent might give you
  let :groups do
    [
      Struct::Group.new('root', 'x', 0, %w{root}),
      Struct::Group.new('bin', 'x', 1, %w{root bin daemon}),
      nil
    ]
  end

  # A fake struct besides Struct::Group and Struct::Passwd
  let :fakestruct do
    Struct.new(:foo, :bar)
  end

  # A fake value get<foo>ent might return
  let :fakeetcobject do
    fakestruct.new('fooval', 'barval')
  end

  # The provider sometimes relies on @resource for valid properties so let's
  # create a fake type with properties that match our fake struct.
  let :faketype do
    Puppet::Type.newtype(:nameservice_dummytype) do
      newparam(:name)
      ensurable
      newproperty(:foo)
      newproperty(:bar)
    end
  end

  let :provider do
    described_class.new(:name => 'bob', :foo => 'fooval', :bar => 'barval')
  end

  let :resource do
    resource = faketype.new(:name => 'bob', :ensure => :present)
    resource.provider = provider
    resource
  end

  # These values simulate what Ruby Etc would return from a host with the "same"
  # user represented in different encodings on disk.
  let(:utf_8_jose) { "Jos\u00E9"}
  let(:utf_8_labeled_as_latin_1_jose) { utf_8_jose.dup.force_encoding(Encoding::ISO_8859_1) }
  let(:valid_latin1_jose) { utf_8_jose.encode(Encoding::ISO_8859_1)}
  let(:invalid_utf_8_jose) { valid_latin1_jose.dup.force_encoding(Encoding::UTF_8) }
  let(:escaped_utf_8_jose) { "Jos\uFFFD".force_encoding(Encoding::UTF_8) }

  let(:utf_8_mixed_users) {
    [
      Struct::Passwd.new('root', 'x', 0, 0),
      Struct::Passwd.new('foo', 'x', 1000, 2000),
      Struct::Passwd.new(utf_8_jose, utf_8_jose, 1001, 2000), # UTF-8 character
      # In a UTF-8 environment, ruby will return strings labeled as UTF-8 even if they're not valid in UTF-8
      Struct::Passwd.new(invalid_utf_8_jose, invalid_utf_8_jose, 1002, 2000),
      nil
    ]
  }

  let(:latin_1_mixed_users) {
    [
      # In a LATIN-1 environment, ruby will return *all* strings labeled as LATIN-1
      Struct::Passwd.new('root'.force_encoding(Encoding::ISO_8859_1), 'x', 0, 0),
      Struct::Passwd.new('foo'.force_encoding(Encoding::ISO_8859_1), 'x', 1000, 2000),
      Struct::Passwd.new(utf_8_labeled_as_latin_1_jose, utf_8_labeled_as_latin_1_jose, 1002, 2000),
      Struct::Passwd.new(valid_latin1_jose, valid_latin1_jose, 1001, 2000), # UTF-8 character
      nil
    ]
  }

  describe "#options" do
    it "should add options for a valid property" do
      described_class.options :foo, :key1 => 'val1', :key2 => 'val2'
      described_class.options :bar, :key3 => 'val3'
      expect(described_class.option(:foo, :key1)).to eq('val1')
      expect(described_class.option(:foo, :key2)).to eq('val2')
      expect(described_class.option(:bar, :key3)).to eq('val3')
    end

    it "should raise an error for an invalid property" do
      expect { described_class.options :baz, :key1 => 'val1' }.to raise_error(
        Puppet::Error, 'baz is not a valid attribute for nameservice_dummytype')
    end
  end

  describe "#option" do
    it "should return the correct value" do
      described_class.options :foo, :key1 => 'val1', :key2 => 'val2'
      expect(described_class.option(:foo, :key2)).to eq('val2')
    end

    it "should symbolize the name first" do
      described_class.options :foo, :key1 => 'val1', :key2 => 'val2'
      expect(described_class.option('foo', :key2)).to eq('val2')
    end

    it "should return nil if no option has been specified earlier" do
      expect(described_class.option(:foo, :key2)).to be_nil
    end

    it "should return nil if no option for that property has been specified earlier" do
      described_class.options :bar, :key2 => 'val2'
      expect(described_class.option(:foo, :key2)).to be_nil
    end

    it "should return nil if no matching key can be found for that property" do
      described_class.options :foo, :key3 => 'val2'
      expect(described_class.option(:foo, :key2)).to be_nil
    end
  end

  describe "#section" do
    it "should raise an error if resource_type has not been set" do
      expect(described_class).to receive(:resource_type).and_return(nil)
      expect { described_class.section }.to raise_error Puppet::Error, 'Cannot determine Etc section without a resource type'
    end

    # the return values are hard coded so I am using types that actually make
    # use of the nameservice provider
    it "should return pw for users" do
      described_class.resource_type = Puppet::Type.type(:user)
      expect(described_class.section).to eq('pw')
    end

    it "should return gr for groups" do
      described_class.resource_type = Puppet::Type.type(:group)
      expect(described_class.section).to eq('gr')
    end
  end

  describe "#listbyname" do
    it "should be deprecated" do
      expect(Puppet).to receive(:deprecation_warning).with(/listbyname is deprecated/)
      described_class.listbyname
    end

    it "should return a list of users if resource_type is user" do
      described_class.resource_type = Puppet::Type.type(:user)
      expect(Puppet::Etc).to receive(:setpwent)
      allow(Puppet::Etc).to receive(:getpwent).and_return(*users)
      expect(Puppet::Etc).to receive(:endpwent)
      expect(described_class.listbyname).to eq(%w{root foo})
    end

    context "encoding handling" do
      described_class.resource_type = Puppet::Type.type(:user)

      # These two tests simulate an environment where there are two users with
      # the same name on disk, but each name is stored on disk in a different
      # encoding
      it "should return names with invalid byte sequences replaced with '?'" do
        allow(Etc).to receive(:getpwent).and_return(*utf_8_mixed_users)
        expect(invalid_utf_8_jose).to_not be_valid_encoding
        result = PuppetSpec::CharacterEncoding.with_external_encoding(Encoding::UTF_8) do
          described_class.listbyname
        end
        expect(result).to eq(['root', 'foo', utf_8_jose, escaped_utf_8_jose])
      end

      it "should return names in their original encoding/bytes if they would not be valid UTF-8" do
        allow(Etc).to receive(:getpwent).and_return(*latin_1_mixed_users)
        result = PuppetSpec::CharacterEncoding.with_external_encoding(Encoding::ISO_8859_1) do
          described_class.listbyname
        end
        expect(result).to eq(['root'.force_encoding(Encoding::UTF_8), 'foo'.force_encoding(Encoding::UTF_8), utf_8_jose, valid_latin1_jose])
      end
    end

    it "should return a list of groups if resource_type is group", :unless => Puppet.features.microsoft_windows? do
      described_class.resource_type = Puppet::Type.type(:group)
      expect(Puppet::Etc).to receive(:setgrent)
      allow(Puppet::Etc).to receive(:getgrent).and_return(*groups)
      expect(Puppet::Etc).to receive(:endgrent)
      expect(described_class.listbyname).to eq(%w{root bin})
    end

    it "should yield if a block given" do
      yield_results = []
      described_class.resource_type = Puppet::Type.type(:user)
      expect(Puppet::Etc).to receive(:setpwent)
      allow(Puppet::Etc).to receive(:getpwent).and_return(*users)
      expect(Puppet::Etc).to receive(:endpwent)
      described_class.listbyname {|x| yield_results << x }
      expect(yield_results).to eq(%w{root foo})
    end
  end

  describe "instances" do
    it "should return a list of objects in UTF-8 with any invalid characters replaced with '?'" do
      # These two tests simulate an environment where there are two users with
      # the same name on disk, but each name is stored on disk in a different
      # encoding
      allow(Etc).to receive(:getpwent).and_return(*utf_8_mixed_users)
      result = PuppetSpec::CharacterEncoding.with_external_encoding(Encoding::UTF_8) do
        described_class.instances
      end
      expect(result.map(&:name)).to eq(
        [
          'root'.force_encoding(Encoding::UTF_8), # started as UTF-8 on disk, returned unaltered as UTF-8
          'foo'.force_encoding(Encoding::UTF_8), # started as UTF-8 on disk, returned unaltered as UTF-8
          utf_8_jose, # started as UTF-8 on disk, returned unaltered as UTF-8
          escaped_utf_8_jose # started as LATIN-1 on disk, but Etc returned as UTF-8 and we escaped invalid chars
        ]
      )
    end

    it "should have object names in their original encoding/bytes if they would not be valid UTF-8" do
      allow(Etc).to receive(:getpwent).and_return(*latin_1_mixed_users)
      result = PuppetSpec::CharacterEncoding.with_external_encoding(Encoding::ISO_8859_1) do
        described_class.instances
      end
      expect(result.map(&:name)).to eq(
        [
          'root'.force_encoding(Encoding::UTF_8), # started as LATIN-1 on disk, we overrode to UTF-8
          'foo'.force_encoding(Encoding::UTF_8), # started as LATIN-1 on disk, we overrode to UTF-8
          utf_8_jose, # started as UTF-8 on disk, returned by Etc as LATIN-1, and we overrode to UTF-8
          valid_latin1_jose # started as LATIN-1 on disk, returned by Etc as valid LATIN-1, and we leave as LATIN-1
        ]
      )
    end

    it "should pass the Puppet::Etc :canonical_name Struct member to the constructor" do
      users = [ Struct::Passwd.new(invalid_utf_8_jose, invalid_utf_8_jose, 1002, 2000), nil ]
      allow(Etc).to receive(:getpwent).and_return(*users)
      expect(described_class).to receive(:new).with(:name => escaped_utf_8_jose, :canonical_name => invalid_utf_8_jose, :ensure => :present)
      described_class.instances
    end
  end

  describe "validate" do
    it "should pass if no check is registered at all" do
      expect { described_class.validate(:foo, 300) }.to_not raise_error
      expect { described_class.validate('foo', 300) }.to_not raise_error
    end

    it "should pass if no check for that property is registered" do
      described_class.verify(:bar, 'Must be 100') { |val| val == 100 }
      expect { described_class.validate(:foo, 300) }.to_not raise_error
      expect { described_class.validate('foo', 300) }.to_not raise_error
    end

    it "should pass if the value is valid" do
      described_class.verify(:foo, 'Must be 100') { |val| val == 100 }
      expect { described_class.validate(:foo, 100) }.to_not raise_error
      expect { described_class.validate('foo', 100) }.to_not raise_error
    end

    it "should raise an error if the value is invalid" do
      described_class.verify(:foo, 'Must be 100') { |val| val == 100 }
      expect { described_class.validate(:foo, 200) }.to raise_error(ArgumentError, 'Invalid value 200: Must be 100')
      expect { described_class.validate('foo', 200) }.to raise_error(ArgumentError, 'Invalid value 200: Must be 100')
    end
  end

  describe "getinfo" do
    before :each do
      # with section=foo we'll call Etc.getfoonam instead of getpwnam or getgrnam
      allow(described_class).to receive(:section).and_return('foo')
      resource # initialize the resource so our provider has a @resource instance variable
    end

    it "should return a hash if we can retrieve something" do
      expect(Puppet::Etc).to receive(:send).with(:getfoonam, 'bob').and_return(fakeetcobject)
      expect(provider).to receive(:info2hash).with(fakeetcobject).and_return(:foo => 'fooval', :bar => 'barval')
      expect(provider.getinfo(true)).to eq({:foo => 'fooval', :bar => 'barval'})
    end

    it "should return nil if we cannot retrieve anything" do
      expect(Puppet::Etc).to receive(:send).with(:getfoonam, 'bob').and_raise(ArgumentError, "can't find bob")
      expect(provider).not_to receive(:info2hash)
      expect(provider.getinfo(true)).to be_nil
    end

    # Nameservice instances track the original resource name on disk, before
    # overriding to UTF-8, in @canonical_name for querying that state on disk
    # again if needed
    it "should use the instance's @canonical_name to query the system" do
      provider_instance = described_class.new(:name => 'foo', :canonical_name => 'original_foo', :ensure => :present)
      expect(Puppet::Etc).to receive(:send).with(:getfoonam, 'original_foo')
      provider_instance.getinfo(true)
    end

    it "should use the instance's name instead of canonical_name if not supplied during instantiation" do
      provider_instance = described_class.new(:name => 'foo', :ensure => :present)
      expect(Puppet::Etc).to receive(:send).with(:getfoonam, 'foo')
      provider_instance.getinfo(true)
    end
  end

  describe "info2hash" do
    it "should return a hash with all properties" do
      # we have to have an implementation of posixmethod which has to
      # convert a propertyname (e.g. comment) into a fieldname of our
      # Struct (e.g. gecos). I do not want to test posixmethod here so
      # let's fake an implementation which does not do any translation. We
      # expect two method invocations because info2hash calls the method
      # twice if the Struct responds to the propertyname (our fake Struct
      # provides values for :foo and :bar) TODO: Fix that
      expect(provider).to receive(:posixmethod).with(:foo).and_return(:foo).twice
      expect(provider).to receive(:posixmethod).with(:bar).and_return(:bar).twice
      expect(provider).to receive(:posixmethod).with(:ensure).and_return(:ensure)
      expect(provider.info2hash(fakeetcobject)).to eq({ :foo => 'fooval', :bar => 'barval' })
    end
  end

  describe "munge" do
    it "should return the input value if no munge method has be defined" do
      expect(provider.munge(:foo, 100)).to eq(100)
    end

    it "should return the munged value otherwise" do
      described_class.options(:foo, :munge => proc { |x| x*2 })
      expect(provider.munge(:foo, 100)).to eq(200)
    end
  end

  describe "unmunge" do
    it "should return the input value if no unmunge method has been defined" do
      expect(provider.unmunge(:foo, 200)).to eq(200)
    end

    it "should return the unmunged value otherwise" do
      described_class.options(:foo, :unmunge => proc { |x| x/2 })
      expect(provider.unmunge(:foo, 200)).to eq(100)
    end
  end


  describe "exists?" do
    it "should return true if we can retrieve anything" do
      expect(provider).to receive(:getinfo).with(true).and_return(:foo => 'fooval', :bar => 'barval')
      expect(provider).to be_exists
    end
    it "should return false if we cannot retrieve anything" do
      expect(provider).to receive(:getinfo).with(true).and_return(nil)
      expect(provider).not_to be_exists
    end
  end

  describe "get" do
    before(:each) {described_class.resource_type = faketype }

    it "should return the correct getinfo value" do
      expect(provider).to receive(:getinfo).with(false).and_return(:foo => 'fooval', :bar => 'barval')
      expect(provider.get(:bar)).to eq('barval')
    end

    it "should unmunge the value first" do
      described_class.options(:bar, :munge => proc { |x| x*2}, :unmunge => proc {|x| x/2})
      expect(provider).to receive(:getinfo).with(false).and_return(:foo => 200, :bar => 500)
      expect(provider.get(:bar)).to eq(250)
    end

    it "should return nil if getinfo cannot retrieve the value" do
      expect(provider).to receive(:getinfo).with(false).and_return(:foo => 'fooval', :bar => 'barval')
      expect(provider.get(:no_such_key)).to be_nil
    end

  end

  describe "set" do
    before :each do
      resource # initialize resource so our provider has a @resource object
      described_class.verify(:foo, 'Must be 100') { |val| val == 100 }
    end

    it "should raise an error on invalid values" do
      expect { provider.set(:foo, 200) }.to raise_error(ArgumentError, 'Invalid value 200: Must be 100')
    end

    it "should execute the modify command on valid values" do
      expect(provider).to receive(:modifycmd).with(:foo, 100).and_return(['/bin/modify', '-f', '100' ])
      expect(provider).to receive(:execute).with(['/bin/modify', '-f', '100'], hash_including(custom_environment: {}))
      provider.set(:foo, 100)
    end

    it "should munge the value first" do
      described_class.options(:foo, :munge => proc { |x| x*2}, :unmunge => proc {|x| x/2})
      expect(provider).to receive(:modifycmd).with(:foo, 200).and_return(['/bin/modify', '-f', '200' ])
      expect(provider).to receive(:execute).with(['/bin/modify', '-f', '200'], hash_including(custom_environment: {}))
      provider.set(:foo, 100)
    end

    it "should fail if the modify command fails" do
      expect(provider).to receive(:modifycmd).with(:foo, 100).and_return(['/bin/modify', '-f', '100' ])
      expect(provider).to receive(:execute).with(['/bin/modify', '-f', '100'], kind_of(Hash)).and_raise(Puppet::ExecutionFailure, "Execution of '/bin/modify' returned 1: some_failure")
      expect { provider.set(:foo, 100) }.to raise_error Puppet::Error, /Could not set foo/
    end
  end

  describe "comments_insync?" do
    # comments_insync? overrides Puppet::Property#insync? and will act on an
    # array containing a should value (the expected value of Puppet::Property
    # @should)
    context "given strings with compatible encodings" do
      it "should return false if the is-value and should-value are not equal" do
        is_value = "foo"
        should_value = ["bar"]
        expect(provider.comments_insync?(is_value, should_value)).to be_falsey
      end

      it "should return true if the is-value and should-value are equal" do
        is_value = "foo"
        should_value = ["foo"]
        expect(provider.comments_insync?(is_value, should_value)).to be_truthy
      end
    end

    context "given strings with incompatible encodings" do
      let(:snowman_iso) { "\u2603".force_encoding(Encoding::ISO_8859_1) }
      let(:snowman_utf8) { "\u2603".force_encoding(Encoding::UTF_8) }
      let(:snowman_binary) { "\u2603".force_encoding(Encoding::ASCII_8BIT) }
      let(:arabic_heh_utf8) { "\u06FF".force_encoding(Encoding::UTF_8) }

      it "should be able to compare unequal strings and return false" do
        expect(Encoding.compatible?(snowman_iso, arabic_heh_utf8)).to be_falsey
        expect(provider.comments_insync?(snowman_iso, [arabic_heh_utf8])).to be_falsey
      end

      it "should be able to compare equal strings and return true" do
        expect(Encoding.compatible?(snowman_binary, snowman_utf8)).to be_falsey
        expect(provider.comments_insync?(snowman_binary, [snowman_utf8])).to be_truthy
      end

      it "should not manipulate the actual encoding of either string" do
        expect(Encoding.compatible?(snowman_binary, snowman_utf8)).to be_falsey
        provider.comments_insync?(snowman_binary, [snowman_utf8])
        expect(snowman_binary.encoding).to eq(Encoding::ASCII_8BIT)
        expect(snowman_utf8.encoding).to eq(Encoding::UTF_8)
      end
    end
  end
end