rkerberos 0.2.2 → 0.3.0

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.
@@ -4,12 +4,13 @@
4
4
  require 'rkerberos'
5
5
  require 'tmpdir'
6
6
  require 'fileutils'
7
- require 'pty'
8
- require 'expect'
9
-
10
7
 
8
+ unless File::ALT_SEPARATOR
9
+ require 'pty'
10
+ require 'expect'
11
+ end
11
12
 
12
- RSpec.describe Kerberos::Krb5::Keytab do
13
+ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
13
14
  before(:all) do
14
15
  @realm = Kerberos::Kadm5::Config.new.realm
15
16
  @keytab_file = File.join(Dir.tmpdir, 'test.keytab')
@@ -36,21 +37,49 @@ RSpec.describe Kerberos::Krb5::Keytab do
36
37
  subject(:keytab) { described_class.new }
37
38
 
38
39
  describe 'constructor' do
39
- it 'accepts an optional name' do
40
- expect { described_class.new("FILE:/usr/local/var/keytab") }.not_to raise_error
41
- expect { described_class.new("FILE:/bogus/keytab") }.not_to raise_error
40
+ it 'accepts an optional name keyword' do
41
+ expect { described_class.new(name: "FILE:/usr/local/var/keytab") }.not_to raise_error
42
+ expect { described_class.new(name: "FILE:/bogus/keytab") }.not_to raise_error
42
43
  end
43
44
 
44
45
  it 'raises error for invalid residual type' do
45
46
  expect {
46
- described_class.new("BOGUS:/tmp/keytab")
47
+ described_class.new(name: "BOGUS:/tmp/keytab")
47
48
  }.to raise_error(Kerberos::Krb5::Keytab::Exception)
48
49
  end
50
+
51
+ it 'accepts a context keyword argument' do
52
+ ctx = Kerberos::Krb5::Context.new
53
+ expect { described_class.new(name: @keytab_name, context: ctx) }.not_to raise_error
54
+ end
55
+
56
+ it 'works with context and no name' do
57
+ ctx = Kerberos::Krb5::Context.new
58
+ expect { described_class.new(context: ctx) }.not_to raise_error
59
+ end
60
+
61
+ it 'rejects positional arguments' do
62
+ expect { described_class.new(@keytab_name) }.to raise_error(ArgumentError)
63
+ end
64
+
65
+ it 'rejects unknown keywords' do
66
+ expect { described_class.new(unknown: true) }.to raise_error(ArgumentError, /unknown keyword/)
67
+ end
68
+
69
+ it 'raises TypeError for non-Context context argument' do
70
+ expect { described_class.new(context: "bad") }.to raise_error(TypeError)
71
+ end
72
+
73
+ it 'raises error for a closed context' do
74
+ ctx = Kerberos::Krb5::Context.new
75
+ ctx.close
76
+ expect { described_class.new(context: ctx) }.to raise_error(Kerberos::Krb5::Exception)
77
+ end
49
78
  end
50
79
 
51
80
  describe '#keytab_name and #keytab_type' do
52
81
  it 'returns the underlying name and type strings' do
53
- kt = described_class.new(@keytab_name)
82
+ kt = described_class.new(name: @keytab_name)
54
83
  expect(kt).to respond_to(:keytab_name)
55
84
  expect(kt).to respond_to(:keytab_type)
56
85
 
@@ -66,30 +95,30 @@ RSpec.describe Kerberos::Krb5::Keytab do
66
95
 
67
96
  describe '#close' do
68
97
  it 'returns true' do
69
- kt = described_class.new(@keytab_name)
98
+ kt = described_class.new(name: @keytab_name)
70
99
  expect(kt.close).to eq(true)
71
100
  end
72
101
 
73
102
  it 'can be called multiple times without error' do
74
- kt = described_class.new(@keytab_name)
103
+ kt = described_class.new(name: @keytab_name)
75
104
  kt.close
76
105
  expect { kt.close }.not_to raise_error
77
106
  end
78
107
 
79
108
  it 'raises an error when calling keytab_name after close' do
80
- kt = described_class.new(@keytab_name)
109
+ kt = described_class.new(name: @keytab_name)
81
110
  kt.close
82
111
  expect { kt.keytab_name }.to raise_error(Kerberos::Krb5::Exception)
83
112
  end
84
113
 
85
114
  it 'raises an error when calling keytab_type after close' do
86
- kt = described_class.new(@keytab_name)
115
+ kt = described_class.new(name: @keytab_name)
87
116
  kt.close
88
117
  expect { kt.keytab_type }.to raise_error(Kerberos::Krb5::Exception)
89
118
  end
90
119
 
91
120
  it 'does not segfault when garbage collected after close' do
92
- kt = described_class.new(@keytab_name)
121
+ kt = described_class.new(name: @keytab_name)
93
122
  kt.close
94
123
  kt = nil
95
124
  GC.start
@@ -151,7 +180,7 @@ RSpec.describe Kerberos::Krb5::Keytab do
151
180
 
152
181
  describe '#each' do
153
182
  it 'yields keytab entries for a valid keytab' do
154
- kt = described_class.new(@keytab_name)
183
+ kt = described_class.new(name: @keytab_name)
155
184
  entries = []
156
185
  kt.each { |entry| entries << entry }
157
186
  expect(entries.length).to eq(2)
@@ -165,14 +194,14 @@ RSpec.describe Kerberos::Krb5::Keytab do
165
194
  end
166
195
 
167
196
  it 'does not leak resources when the block raises' do
168
- kt = described_class.new(@keytab_name)
197
+ kt = described_class.new(name: @keytab_name)
169
198
  expect {
170
199
  kt.each { |_| raise "boom" }
171
200
  }.to raise_error(RuntimeError, "boom")
172
201
  end
173
202
 
174
203
  it 'does not leak resources when the block breaks' do
175
- kt = described_class.new(@keytab_name)
204
+ kt = described_class.new(name: @keytab_name)
176
205
  result = catch(:done) do
177
206
  kt.each { |_| throw :done, :escaped }
178
207
  :completed
@@ -183,7 +212,7 @@ RSpec.describe Kerberos::Krb5::Keytab do
183
212
 
184
213
  describe '#get_entry' do
185
214
  it 'finds an entry by principal name' do
186
- kt = described_class.new(@keytab_name)
215
+ kt = described_class.new(name: @keytab_name)
187
216
  entry = kt.get_entry("testuser1@#{@realm}")
188
217
  expect(entry).to be_a(Kerberos::Krb5::Keytab::Entry)
189
218
  expect(entry.principal).to eq("testuser1@#{@realm}")
@@ -193,14 +222,14 @@ RSpec.describe Kerberos::Krb5::Keytab do
193
222
  end
194
223
 
195
224
  it 'finds an entry filtering by vno' do
196
- kt = described_class.new(@keytab_name)
225
+ kt = described_class.new(name: @keytab_name)
197
226
  entry = kt.get_entry("testuser1@#{@realm}", 1)
198
227
  expect(entry.principal).to eq("testuser1@#{@realm}")
199
228
  expect(entry.vno).to eq(1)
200
229
  end
201
230
 
202
231
  it 'finds an entry filtering by vno and enctype' do
203
- kt = described_class.new(@keytab_name)
232
+ kt = described_class.new(name: @keytab_name)
204
233
  # aes128-cts-hmac-sha1-96 is enctype 17
205
234
  entry = kt.get_entry("testuser1@#{@realm}", 1, 17)
206
235
  expect(entry.principal).to eq("testuser1@#{@realm}")
@@ -209,21 +238,21 @@ RSpec.describe Kerberos::Krb5::Keytab do
209
238
  end
210
239
 
211
240
  it 'raises an error for a non-existent principal' do
212
- kt = described_class.new(@keytab_name)
241
+ kt = described_class.new(name: @keytab_name)
213
242
  expect {
214
243
  kt.get_entry("bogus@#{@realm}")
215
244
  }.to raise_error(Kerberos::Krb5::Exception)
216
245
  end
217
246
 
218
247
  it 'is aliased as find' do
219
- kt = described_class.new(@keytab_name)
248
+ kt = described_class.new(name: @keytab_name)
220
249
  expect(kt.method(:find)).to eq(kt.method(:get_entry))
221
250
  end
222
251
  end
223
252
 
224
253
  describe '#dup' do
225
254
  it 'creates an independent handle referring to same keytab' do
226
- kt1 = described_class.new(@keytab_name)
255
+ kt1 = described_class.new(name: @keytab_name)
227
256
  kt2 = kt1.dup
228
257
  expect(kt2).to be_a(described_class)
229
258
  expect(kt2.keytab_name).to eq(kt1.keytab_name)
@@ -234,8 +263,184 @@ RSpec.describe Kerberos::Krb5::Keytab do
234
263
  end
235
264
 
236
265
  it 'clone is an alias for dup' do
237
- kt = described_class.new(@keytab_name)
266
+ kt = described_class.new(name: @keytab_name)
238
267
  expect(kt.method(:clone)).to eq(kt.method(:dup))
239
268
  end
240
269
  end
270
+
271
+ describe '#have_content?' do
272
+ it 'returns true for a keytab with entries' do
273
+ kt = described_class.new(name: @keytab_name)
274
+ expect(kt.have_content?).to be true
275
+ kt.close
276
+ end
277
+
278
+ it 'returns false for an empty keytab' do
279
+ empty_file = File.join(Dir.tmpdir, 'empty_test.keytab')
280
+ FileUtils.touch(empty_file)
281
+ kt = described_class.new(name: "FILE:#{empty_file}")
282
+ expect(kt.have_content?).to be false
283
+ kt.close
284
+ FileUtils.rm_f(empty_file)
285
+ end
286
+
287
+ it 'raises when the context is closed' do
288
+ kt = described_class.new(name: @keytab_name)
289
+ kt.close
290
+ expect { kt.have_content? }.to raise_error(Kerberos::Krb5::Exception)
291
+ end
292
+ end
293
+
294
+ describe '#add_entry' do
295
+ let(:add_kt_file) { File.join(Dir.tmpdir, 'add_test.keytab') }
296
+ let(:add_kt_name) { "FILE:#{add_kt_file}" }
297
+
298
+ after(:each) { FileUtils.rm_f(add_kt_file) }
299
+
300
+ it 'adds an entry that is retrievable via get_entry' do
301
+ kt = described_class.new(name: add_kt_name)
302
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1)
303
+
304
+ entry = kt.get_entry("testuser1@#{@realm}")
305
+ expect(entry).to be_a(Kerberos::Krb5::Keytab::Entry)
306
+ expect(entry.principal).to eq("testuser1@#{@realm}")
307
+ expect(entry.vno).to eq(1)
308
+ # default enctype is aes256-cts-hmac-sha1-96 = 18
309
+ expect(entry.key).to eq(18)
310
+ kt.close
311
+ end
312
+
313
+ it 'accepts an explicit enctype' do
314
+ kt = described_class.new(name: add_kt_name)
315
+ # aes128-cts-hmac-sha1-96 = 17
316
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 2, enctype: 17)
317
+
318
+ entry = kt.get_entry("testuser1@#{@realm}", 2, 17)
319
+ expect(entry.vno).to eq(2)
320
+ expect(entry.key).to eq(17)
321
+ kt.close
322
+ end
323
+
324
+ it 'can add multiple entries for the same principal' do
325
+ kt = described_class.new(name: add_kt_name)
326
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1, enctype: 18)
327
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1, enctype: 17)
328
+
329
+ entries = []
330
+ kt.each { |e| entries << e }
331
+ expect(entries.length).to eq(2)
332
+ kt.close
333
+ end
334
+
335
+ it 'returns self for chaining' do
336
+ kt = described_class.new(name: add_kt_name)
337
+ result = kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret')
338
+ expect(result).to equal(kt)
339
+ kt.close
340
+ end
341
+
342
+ it 'raises ArgumentError when principal is missing' do
343
+ kt = described_class.new(name: add_kt_name)
344
+ expect { kt.add_entry(password: 'secret') }.to raise_error(ArgumentError)
345
+ kt.close
346
+ end
347
+
348
+ it 'raises ArgumentError when password is missing' do
349
+ kt = described_class.new(name: add_kt_name)
350
+ expect { kt.add_entry(principal: "testuser1@#{@realm}") }.to raise_error(ArgumentError)
351
+ kt.close
352
+ end
353
+
354
+ it 'raises ArgumentError with no arguments' do
355
+ kt = described_class.new(name: add_kt_name)
356
+ expect { kt.add_entry }.to raise_error(ArgumentError)
357
+ kt.close
358
+ end
359
+
360
+ it 'rejects unknown keywords' do
361
+ kt = described_class.new(name: add_kt_name)
362
+ expect {
363
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', unknown: true)
364
+ }.to raise_error(ArgumentError, /unknown keyword/)
365
+ kt.close
366
+ end
367
+
368
+ it 'raises when the keytab is closed' do
369
+ kt = described_class.new(name: add_kt_name)
370
+ kt.close
371
+ expect {
372
+ kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret')
373
+ }.to raise_error(Kerberos::Krb5::Exception)
374
+ end
375
+ end
376
+
377
+ describe '#remove_entry' do
378
+ let(:rm_kt_file) { File.join(Dir.tmpdir, 'remove_test.keytab') }
379
+ let(:rm_kt_name) { "FILE:#{rm_kt_file}" }
380
+
381
+ before(:each) do
382
+ @rm_kt = described_class.new(name: rm_kt_name)
383
+ @rm_kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 1, enctype: 18)
384
+ @rm_kt.add_entry(principal: "testuser1@#{@realm}", password: 'secret', vno: 2, enctype: 17)
385
+ @rm_kt.add_entry(principal: "testuser2@#{@realm}", password: 'secret', vno: 1, enctype: 18)
386
+ end
387
+
388
+ after(:each) do
389
+ @rm_kt.close rescue nil
390
+ FileUtils.rm_f(rm_kt_file)
391
+ end
392
+
393
+ it 'removes all entries for a principal when vno and enctype are defaulted' do
394
+ @rm_kt.remove_entry(principal: "testuser1@#{@realm}")
395
+ entries = []
396
+ @rm_kt.each { |e| entries << e }
397
+ principals = entries.map(&:principal)
398
+ expect(principals).not_to include("testuser1@#{@realm}")
399
+ expect(principals).to include("testuser2@#{@realm}")
400
+ end
401
+
402
+ it 'removes only the entry matching a specific vno' do
403
+ @rm_kt.remove_entry(principal: "testuser1@#{@realm}", vno: 1)
404
+ entry = @rm_kt.get_entry("testuser1@#{@realm}")
405
+ expect(entry.vno).to eq(2)
406
+ end
407
+
408
+ it 'removes only the entry matching a specific enctype' do
409
+ @rm_kt.remove_entry(principal: "testuser1@#{@realm}", enctype: 17)
410
+ entry = @rm_kt.get_entry("testuser1@#{@realm}")
411
+ expect(entry.key).to eq(18)
412
+ end
413
+
414
+ it 'returns self for chaining' do
415
+ result = @rm_kt.remove_entry(principal: "testuser2@#{@realm}")
416
+ expect(result).to equal(@rm_kt)
417
+ end
418
+
419
+ it 'raises ArgumentError when principal is missing' do
420
+ expect { @rm_kt.remove_entry(vno: 1) }.to raise_error(ArgumentError)
421
+ end
422
+
423
+ it 'raises ArgumentError with no arguments' do
424
+ expect { @rm_kt.remove_entry }.to raise_error(ArgumentError)
425
+ end
426
+
427
+ it 'rejects unknown keywords' do
428
+ expect {
429
+ @rm_kt.remove_entry(principal: "testuser1@#{@realm}", unknown: true)
430
+ }.to raise_error(ArgumentError, /unknown keyword/)
431
+ end
432
+
433
+ it 'raises when the keytab is closed' do
434
+ @rm_kt.close
435
+ expect {
436
+ @rm_kt.remove_entry(principal: "testuser1@#{@realm}")
437
+ }.to raise_error(Kerberos::Krb5::Exception)
438
+ end
439
+
440
+ it 'raises for a non-existent entry' do
441
+ expect {
442
+ @rm_kt.remove_entry(principal: "nobody@#{@realm}")
443
+ }.to raise_error(Kerberos::Krb5::Keytab::Exception)
444
+ end
445
+ end
241
446
  end
data/spec/krb5_spec.rb CHANGED
@@ -1,18 +1,20 @@
1
1
  # spec/krb5_spec.rb
2
2
  # RSpec tests for Kerberos::Krb5
3
3
 
4
- require 'rkerberos'
4
+ require 'spec_helper'
5
5
  require 'open3'
6
- require 'pty'
7
- require 'expect'
6
+
7
+ unless File::ALT_SEPARATOR
8
+ require 'pty'
9
+ require 'expect'
10
+ end
8
11
 
9
12
  RSpec.describe Kerberos::Krb5 do
10
13
  before(:all) do
14
+ krb5_conf = RSpec.configuration.krb5_conf
11
15
  @cache_found = true
12
16
  Open3.popen3('klist') { |_, _, stderr| @cache_found = false unless stderr.gets.nil? }
13
- @krb5_conf = ENV['KRB5_CONFIG'] || '/etc/krb5.conf'
14
- @realm = IO.read(@krb5_conf).split("\n").grep(/default_realm/).first.split('=').last.lstrip.chomp
15
-
17
+ @realm = IO.read(krb5_conf).split("\n").grep(/default_realm/).first.split('=').last.lstrip.chomp
16
18
  end
17
19
 
18
20
  subject(:krb5) { described_class.new }
@@ -21,7 +23,7 @@ RSpec.describe Kerberos::Krb5 do
21
23
  let(:service) { 'kadmin/admin' }
22
24
 
23
25
  it 'has the correct version constant' do
24
- expect(Kerberos::Krb5::VERSION).to eq('0.2.2')
26
+ expect(Kerberos::Krb5::VERSION).to eq('0.3.0')
25
27
  end
26
28
 
27
29
  it 'accepts a block and yields itself' do
@@ -29,6 +31,31 @@ RSpec.describe Kerberos::Krb5 do
29
31
  described_class.new { |k| expect(k).to be_a(described_class) }
30
32
  end
31
33
 
34
+ describe 'constructor' do
35
+ it 'accepts a context keyword argument' do
36
+ context = Kerberos::Krb5::Context.new
37
+ k = described_class.new(context: context)
38
+ expect(k).to be_a(described_class)
39
+ expect(k.get_default_realm).to eq(@realm)
40
+ k.close
41
+ end
42
+
43
+ it 'raises TypeError if context is not a Kerberos::Krb5::Context' do
44
+ expect { described_class.new(context: 'bad') }.to raise_error(TypeError, /context must be/)
45
+ end
46
+
47
+ it 'raises an error if the context is closed' do
48
+ context = Kerberos::Krb5::Context.new
49
+ context.close
50
+ expect { described_class.new(context: context) }.to raise_error(Kerberos::Krb5::Exception, /context is closed/)
51
+ end
52
+
53
+ it 'rejects positional option hashes and unknown keywords' do
54
+ expect { described_class.new({context: nil}) }.to raise_error(ArgumentError)
55
+ expect { described_class.new(unknown: true) }.to raise_error(ArgumentError, /unknown keyword/)
56
+ end
57
+ end
58
+
32
59
  describe '#get_default_realm' do
33
60
  it 'responds to get_default_realm' do
34
61
  expect(krb5).to respond_to(:get_default_realm)
@@ -48,7 +75,7 @@ RSpec.describe Kerberos::Krb5 do
48
75
  end
49
76
  end
50
77
 
51
- describe '#verify_init_creds' do
78
+ describe '#verify_init_creds', :kadm5 do
52
79
  # Some KDC setups may not correctly set the initial password during
53
80
  # entrypoint startup; enforce it here via the admin API so the test is
54
81
  # deterministic.
@@ -66,57 +93,78 @@ RSpec.describe Kerberos::Krb5 do
66
93
  expect(krb5).to respond_to(:verify_init_creds)
67
94
  end
68
95
 
96
+ it 'requires get_init_creds_password arguments as keywords' do
97
+ expect { krb5.get_init_creds_password(user, 'changeme') }.to raise_error(ArgumentError)
98
+ expect {
99
+ krb5.get_init_creds_password(principal: user, password: 'changeme', unknown: true)
100
+ }.to raise_error(ArgumentError, /unknown keyword/)
101
+ end
102
+
69
103
  it 'raises when no credentials have been acquired' do
70
104
  expect { krb5.verify_init_creds }.to raise_error(Kerberos::Krb5::Exception)
71
105
  end
72
106
 
73
107
  it 'validates argument types' do
74
- expect { krb5.verify_init_creds(true) }.to raise_error(TypeError)
75
- expect { krb5.verify_init_creds(nil, true) }.to raise_error(TypeError)
76
- expect { krb5.verify_init_creds(nil, nil, true) }.to raise_error(TypeError)
108
+ expect { krb5.verify_init_creds(server: true) }.to raise_error(TypeError)
109
+ expect { krb5.verify_init_creds(keytab: true) }.to raise_error(TypeError)
110
+ expect { krb5.verify_init_creds(ccache: true) }.to raise_error(TypeError)
111
+ end
112
+
113
+ it 'accepts only documented keyword arguments' do
114
+ expect { krb5.verify_init_creds(nil) }.to raise_error(ArgumentError)
115
+ expect { krb5.verify_init_creds(unknown: true) }.to raise_error(ArgumentError, /unknown keyword/)
77
116
  end
78
117
 
79
118
  it 'verifies credentials obtained via password' do
80
- krb5.get_init_creds_password(user, 'changeme')
119
+ krb5.get_init_creds_password(principal: user, password: 'changeme')
81
120
  expect(krb5.verify_init_creds).to be true
82
121
  end
83
122
 
84
123
  it 'accepts a server principal string' do
85
- krb5.get_init_creds_password(user, 'changeme')
86
- expect(krb5.verify_init_creds("kadmin/admin@#{@realm}")).to be true
124
+ krb5.get_init_creds_password(principal: user, password: 'changeme')
125
+ expect(krb5.verify_init_creds(server: "kadmin/admin@#{@realm}")).to be true
87
126
  end
88
127
 
89
128
  it 'accepts a Keytab object' do
90
- krb5.get_init_creds_password(user, 'changeme')
129
+ krb5.get_init_creds_password(principal: user, password: 'changeme')
91
130
  kt = Kerberos::Krb5::Keytab.new
92
- expect(krb5.verify_init_creds(nil, kt)).to be true
131
+ expect(krb5.verify_init_creds(keytab: kt)).to be true
93
132
  end
94
133
 
95
134
  it 'stores additional credentials in provided CredentialsCache' do
96
135
  ccache = Kerberos::Krb5::CredentialsCache.new
97
- krb5.get_init_creds_password(user, 'changeme')
98
- expect(krb5.verify_init_creds(nil, nil, ccache)).to be true
136
+ krb5.get_init_creds_password(principal: user, password: 'changeme')
137
+ expect(krb5.verify_init_creds(ccache: ccache)).to be true
99
138
  expect(ccache.primary_principal).to be_a(String)
100
139
  expect(ccache.primary_principal).to include('@')
101
140
  end
102
141
 
103
142
  it 'provides authenticate! which acquires and verifies (Zanarotti mitigation)' do
104
143
  expect(krb5).to respond_to(:authenticate!)
105
- expect(krb5.authenticate!(user, 'changeme')).to be true
144
+ expect(krb5.authenticate!(principal: user, password: 'changeme')).to be true
106
145
  expect(krb5.verify_init_creds).to be true
107
146
  end
108
147
 
109
148
  it 'accepts an optional service argument' do
110
- expect { krb5.authenticate!(user, 'changeme', 'kadmin/changepw') }.not_to raise_error
149
+ expect {
150
+ krb5.authenticate!(principal: user, password: 'changeme', service: 'kadmin/changepw')
151
+ }.not_to raise_error
111
152
  expect(krb5.verify_init_creds).to be true
112
153
  end
113
154
 
114
155
  it 'validates argument types for authenticate!' do
115
- expect { krb5.authenticate!(true, true) }.to raise_error(TypeError)
156
+ expect { krb5.authenticate!(principal: true, password: true) }.to raise_error(TypeError)
157
+ end
158
+
159
+ it 'requires authenticate! arguments as keywords' do
160
+ expect { krb5.authenticate!(user, 'changeme') }.to raise_error(ArgumentError)
161
+ expect {
162
+ krb5.authenticate!(principal: user, password: 'changeme', unknown: true)
163
+ }.to raise_error(ArgumentError, /unknown keyword/)
116
164
  end
117
165
  end
118
166
 
119
- describe '#change_password' do
167
+ describe '#change_password', :kadm5 do
120
168
  before do
121
169
  # Ensure testuser1 has a known password before each test.
122
170
  Kerberos::Kadm5.new(
@@ -152,16 +200,16 @@ RSpec.describe Kerberos::Krb5 do
152
200
  end
153
201
 
154
202
  it 'changes the password successfully' do
155
- krb5.get_init_creds_password(user, 'changeme')
203
+ krb5.get_init_creds_password(principal: user, password: 'changeme')
156
204
  expect(krb5.change_password('changeme', 'Newpass99!')).to be true
157
205
  # Verify we can authenticate with the new password
158
206
  krb5_check = described_class.new
159
- expect(krb5_check.get_init_creds_password(user, 'Newpass99!')).to be true
207
+ expect(krb5_check.get_init_creds_password(principal: user, password: 'Newpass99!')).to be true
160
208
  krb5_check.close
161
209
  end
162
210
 
163
211
  it 'raises with a meaningful message when the old password is wrong' do
164
- krb5.get_init_creds_password(user, 'changeme')
212
+ krb5.get_init_creds_password(principal: user, password: 'changeme')
165
213
  expect {
166
214
  krb5.change_password('wrongpass', 'Newpass99!')
167
215
  }.to raise_error(Kerberos::Krb5::Exception, /krb5_(get_init_creds_password|change_password)/)
@@ -178,7 +226,7 @@ RSpec.describe Kerberos::Krb5 do
178
226
  let(:compliant_pw) { 'Changeme1!' }
179
227
 
180
228
  it 'raises an exception with the KDC rejection reason' do
181
- krb5.get_init_creds_password(policy_user, compliant_pw)
229
+ krb5.get_init_creds_password(principal: policy_user, password: compliant_pw)
182
230
  expect {
183
231
  krb5.change_password(compliant_pw, 'a')
184
232
  }.to raise_error(Kerberos::Krb5::Exception, /krb5_change_password/)
@@ -186,7 +234,7 @@ RSpec.describe Kerberos::Krb5 do
186
234
  end
187
235
  end
188
236
 
189
- describe '#get_init_creds_keytab' do
237
+ describe '#get_init_creds_keytab', :unix do
190
238
  before(:each) do
191
239
  @kt_file = File.join(Dir.tmpdir, "test_get_init_creds_#{Process.pid}_#{rand(10000)}.keytab")
192
240
 
@@ -206,18 +254,83 @@ RSpec.describe Kerberos::Krb5 do
206
254
  expect(krb5).to respond_to(:get_init_creds_keytab)
207
255
  end
208
256
 
257
+ it 'accepts only documented keyword arguments' do
258
+ expect { krb5.get_init_creds_keytab(user) }.to raise_error(ArgumentError)
259
+ expect { krb5.get_init_creds_keytab(unknown: true) }.to raise_error(ArgumentError, /unknown keyword/)
260
+ end
261
+
209
262
  it 'acquires credentials for a principal from a supplied keytab file' do
210
263
  kt_name = "FILE:#{@kt_file}"
211
- expect { krb5.get_init_creds_keytab(user, kt_name) }.not_to raise_error
264
+ expect { krb5.get_init_creds_keytab(principal: user, keytab: kt_name) }.not_to raise_error
212
265
  expect(krb5.verify_init_creds).to be true
213
266
  end
214
267
 
215
268
  it 'accepts a CredentialsCache to receive credentials' do
216
269
  ccache = Kerberos::Krb5::CredentialsCache.new
217
270
  kt_name = "FILE:#{@kt_file}"
218
- expect { krb5.get_init_creds_keytab(user, kt_name, nil, ccache) }.not_to raise_error
271
+ expect { krb5.get_init_creds_keytab(principal: user, keytab: kt_name, ccache: ccache) }.not_to raise_error
219
272
  expect(ccache.primary_principal).to be_a(String)
220
273
  expect(ccache.primary_principal).to include('@')
221
274
  end
222
275
  end
276
+
277
+ describe '.thread_safe?' do
278
+ it 'returns a boolean' do
279
+ result = described_class.thread_safe?
280
+ expect([true, false]).to include(result)
281
+ end
282
+
283
+ it 'is callable without an instance' do
284
+ expect(described_class).to respond_to(:thread_safe?)
285
+ end
286
+ end
287
+
288
+ describe '#get_host_realm' do
289
+ it 'responds to get_host_realm' do
290
+ expect(krb5).to respond_to(:get_host_realm)
291
+ end
292
+
293
+ it 'returns an array of realm strings' do
294
+ result = krb5.get_host_realm('localhost')
295
+ expect(result).to be_a(Array)
296
+ result.each { |r| expect(r).to be_a(String) }
297
+ end
298
+
299
+ it 'returns a non-empty array for a known hostname' do
300
+ result = krb5.get_host_realm('localhost')
301
+ expect(result).not_to be_empty
302
+ end
303
+
304
+ it 'raises TypeError for non-string argument' do
305
+ expect { krb5.get_host_realm(123) }.to raise_error(TypeError)
306
+ end
307
+
308
+ it 'raises ArgumentError when called without arguments' do
309
+ expect { krb5.get_host_realm }.to raise_error(ArgumentError)
310
+ end
311
+ end
312
+
313
+ describe '#expand_hostname' do
314
+ it 'responds to expand_hostname' do
315
+ expect(krb5).to respond_to(:expand_hostname)
316
+ end
317
+
318
+ it 'returns a string' do
319
+ result = krb5.expand_hostname('localhost')
320
+ expect(result).to be_a(String)
321
+ end
322
+
323
+ it 'returns a non-empty string' do
324
+ result = krb5.expand_hostname('localhost')
325
+ expect(result.size).to be > 0
326
+ end
327
+
328
+ it 'raises TypeError for non-string argument' do
329
+ expect { krb5.expand_hostname(123) }.to raise_error(TypeError)
330
+ end
331
+
332
+ it 'raises ArgumentError when called without arguments' do
333
+ expect { krb5.expand_hostname }.to raise_error(ArgumentError)
334
+ end
335
+ end
223
336
  end