rkerberos 0.2.3 → 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.
@@ -37,21 +37,49 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
37
37
  subject(:keytab) { described_class.new }
38
38
 
39
39
  describe 'constructor' do
40
- it 'accepts an optional name' do
41
- expect { described_class.new("FILE:/usr/local/var/keytab") }.not_to raise_error
42
- 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
43
43
  end
44
44
 
45
45
  it 'raises error for invalid residual type' do
46
46
  expect {
47
- described_class.new("BOGUS:/tmp/keytab")
47
+ described_class.new(name: "BOGUS:/tmp/keytab")
48
48
  }.to raise_error(Kerberos::Krb5::Keytab::Exception)
49
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
50
78
  end
51
79
 
52
80
  describe '#keytab_name and #keytab_type' do
53
81
  it 'returns the underlying name and type strings' do
54
- kt = described_class.new(@keytab_name)
82
+ kt = described_class.new(name: @keytab_name)
55
83
  expect(kt).to respond_to(:keytab_name)
56
84
  expect(kt).to respond_to(:keytab_type)
57
85
 
@@ -67,30 +95,30 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
67
95
 
68
96
  describe '#close' do
69
97
  it 'returns true' do
70
- kt = described_class.new(@keytab_name)
98
+ kt = described_class.new(name: @keytab_name)
71
99
  expect(kt.close).to eq(true)
72
100
  end
73
101
 
74
102
  it 'can be called multiple times without error' do
75
- kt = described_class.new(@keytab_name)
103
+ kt = described_class.new(name: @keytab_name)
76
104
  kt.close
77
105
  expect { kt.close }.not_to raise_error
78
106
  end
79
107
 
80
108
  it 'raises an error when calling keytab_name after close' do
81
- kt = described_class.new(@keytab_name)
109
+ kt = described_class.new(name: @keytab_name)
82
110
  kt.close
83
111
  expect { kt.keytab_name }.to raise_error(Kerberos::Krb5::Exception)
84
112
  end
85
113
 
86
114
  it 'raises an error when calling keytab_type after close' do
87
- kt = described_class.new(@keytab_name)
115
+ kt = described_class.new(name: @keytab_name)
88
116
  kt.close
89
117
  expect { kt.keytab_type }.to raise_error(Kerberos::Krb5::Exception)
90
118
  end
91
119
 
92
120
  it 'does not segfault when garbage collected after close' do
93
- kt = described_class.new(@keytab_name)
121
+ kt = described_class.new(name: @keytab_name)
94
122
  kt.close
95
123
  kt = nil
96
124
  GC.start
@@ -152,7 +180,7 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
152
180
 
153
181
  describe '#each' do
154
182
  it 'yields keytab entries for a valid keytab' do
155
- kt = described_class.new(@keytab_name)
183
+ kt = described_class.new(name: @keytab_name)
156
184
  entries = []
157
185
  kt.each { |entry| entries << entry }
158
186
  expect(entries.length).to eq(2)
@@ -166,14 +194,14 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
166
194
  end
167
195
 
168
196
  it 'does not leak resources when the block raises' do
169
- kt = described_class.new(@keytab_name)
197
+ kt = described_class.new(name: @keytab_name)
170
198
  expect {
171
199
  kt.each { |_| raise "boom" }
172
200
  }.to raise_error(RuntimeError, "boom")
173
201
  end
174
202
 
175
203
  it 'does not leak resources when the block breaks' do
176
- kt = described_class.new(@keytab_name)
204
+ kt = described_class.new(name: @keytab_name)
177
205
  result = catch(:done) do
178
206
  kt.each { |_| throw :done, :escaped }
179
207
  :completed
@@ -184,7 +212,7 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
184
212
 
185
213
  describe '#get_entry' do
186
214
  it 'finds an entry by principal name' do
187
- kt = described_class.new(@keytab_name)
215
+ kt = described_class.new(name: @keytab_name)
188
216
  entry = kt.get_entry("testuser1@#{@realm}")
189
217
  expect(entry).to be_a(Kerberos::Krb5::Keytab::Entry)
190
218
  expect(entry.principal).to eq("testuser1@#{@realm}")
@@ -194,14 +222,14 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
194
222
  end
195
223
 
196
224
  it 'finds an entry filtering by vno' do
197
- kt = described_class.new(@keytab_name)
225
+ kt = described_class.new(name: @keytab_name)
198
226
  entry = kt.get_entry("testuser1@#{@realm}", 1)
199
227
  expect(entry.principal).to eq("testuser1@#{@realm}")
200
228
  expect(entry.vno).to eq(1)
201
229
  end
202
230
 
203
231
  it 'finds an entry filtering by vno and enctype' do
204
- kt = described_class.new(@keytab_name)
232
+ kt = described_class.new(name: @keytab_name)
205
233
  # aes128-cts-hmac-sha1-96 is enctype 17
206
234
  entry = kt.get_entry("testuser1@#{@realm}", 1, 17)
207
235
  expect(entry.principal).to eq("testuser1@#{@realm}")
@@ -210,21 +238,21 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
210
238
  end
211
239
 
212
240
  it 'raises an error for a non-existent principal' do
213
- kt = described_class.new(@keytab_name)
241
+ kt = described_class.new(name: @keytab_name)
214
242
  expect {
215
243
  kt.get_entry("bogus@#{@realm}")
216
244
  }.to raise_error(Kerberos::Krb5::Exception)
217
245
  end
218
246
 
219
247
  it 'is aliased as find' do
220
- kt = described_class.new(@keytab_name)
248
+ kt = described_class.new(name: @keytab_name)
221
249
  expect(kt.method(:find)).to eq(kt.method(:get_entry))
222
250
  end
223
251
  end
224
252
 
225
253
  describe '#dup' do
226
254
  it 'creates an independent handle referring to same keytab' do
227
- kt1 = described_class.new(@keytab_name)
255
+ kt1 = described_class.new(name: @keytab_name)
228
256
  kt2 = kt1.dup
229
257
  expect(kt2).to be_a(described_class)
230
258
  expect(kt2.keytab_name).to eq(kt1.keytab_name)
@@ -235,8 +263,184 @@ RSpec.describe Kerberos::Krb5::Keytab, :kadm5 do
235
263
  end
236
264
 
237
265
  it 'clone is an alias for dup' do
238
- kt = described_class.new(@keytab_name)
266
+ kt = described_class.new(name: @keytab_name)
239
267
  expect(kt.method(:clone)).to eq(kt.method(:dup))
240
268
  end
241
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
242
446
  end
data/spec/krb5_spec.rb CHANGED
@@ -23,7 +23,7 @@ RSpec.describe Kerberos::Krb5 do
23
23
  let(:service) { 'kadmin/admin' }
24
24
 
25
25
  it 'has the correct version constant' do
26
- expect(Kerberos::Krb5::VERSION).to eq('0.2.3')
26
+ expect(Kerberos::Krb5::VERSION).to eq('0.3.0')
27
27
  end
28
28
 
29
29
  it 'accepts a block and yields itself' do
@@ -31,6 +31,31 @@ RSpec.describe Kerberos::Krb5 do
31
31
  described_class.new { |k| expect(k).to be_a(described_class) }
32
32
  end
33
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
+
34
59
  describe '#get_default_realm' do
35
60
  it 'responds to get_default_realm' do
36
61
  expect(krb5).to respond_to(:get_default_realm)
@@ -68,53 +93,74 @@ RSpec.describe Kerberos::Krb5 do
68
93
  expect(krb5).to respond_to(:verify_init_creds)
69
94
  end
70
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
+
71
103
  it 'raises when no credentials have been acquired' do
72
104
  expect { krb5.verify_init_creds }.to raise_error(Kerberos::Krb5::Exception)
73
105
  end
74
106
 
75
107
  it 'validates argument types' do
76
- expect { krb5.verify_init_creds(true) }.to raise_error(TypeError)
77
- expect { krb5.verify_init_creds(nil, true) }.to raise_error(TypeError)
78
- 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/)
79
116
  end
80
117
 
81
118
  it 'verifies credentials obtained via password' do
82
- krb5.get_init_creds_password(user, 'changeme')
119
+ krb5.get_init_creds_password(principal: user, password: 'changeme')
83
120
  expect(krb5.verify_init_creds).to be true
84
121
  end
85
122
 
86
123
  it 'accepts a server principal string' do
87
- krb5.get_init_creds_password(user, 'changeme')
88
- 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
89
126
  end
90
127
 
91
128
  it 'accepts a Keytab object' do
92
- krb5.get_init_creds_password(user, 'changeme')
129
+ krb5.get_init_creds_password(principal: user, password: 'changeme')
93
130
  kt = Kerberos::Krb5::Keytab.new
94
- expect(krb5.verify_init_creds(nil, kt)).to be true
131
+ expect(krb5.verify_init_creds(keytab: kt)).to be true
95
132
  end
96
133
 
97
134
  it 'stores additional credentials in provided CredentialsCache' do
98
135
  ccache = Kerberos::Krb5::CredentialsCache.new
99
- krb5.get_init_creds_password(user, 'changeme')
100
- 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
101
138
  expect(ccache.primary_principal).to be_a(String)
102
139
  expect(ccache.primary_principal).to include('@')
103
140
  end
104
141
 
105
142
  it 'provides authenticate! which acquires and verifies (Zanarotti mitigation)' do
106
143
  expect(krb5).to respond_to(:authenticate!)
107
- expect(krb5.authenticate!(user, 'changeme')).to be true
144
+ expect(krb5.authenticate!(principal: user, password: 'changeme')).to be true
108
145
  expect(krb5.verify_init_creds).to be true
109
146
  end
110
147
 
111
148
  it 'accepts an optional service argument' do
112
- 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
113
152
  expect(krb5.verify_init_creds).to be true
114
153
  end
115
154
 
116
155
  it 'validates argument types for authenticate!' do
117
- 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/)
118
164
  end
119
165
  end
120
166
 
@@ -154,16 +200,16 @@ RSpec.describe Kerberos::Krb5 do
154
200
  end
155
201
 
156
202
  it 'changes the password successfully' do
157
- krb5.get_init_creds_password(user, 'changeme')
203
+ krb5.get_init_creds_password(principal: user, password: 'changeme')
158
204
  expect(krb5.change_password('changeme', 'Newpass99!')).to be true
159
205
  # Verify we can authenticate with the new password
160
206
  krb5_check = described_class.new
161
- 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
162
208
  krb5_check.close
163
209
  end
164
210
 
165
211
  it 'raises with a meaningful message when the old password is wrong' do
166
- krb5.get_init_creds_password(user, 'changeme')
212
+ krb5.get_init_creds_password(principal: user, password: 'changeme')
167
213
  expect {
168
214
  krb5.change_password('wrongpass', 'Newpass99!')
169
215
  }.to raise_error(Kerberos::Krb5::Exception, /krb5_(get_init_creds_password|change_password)/)
@@ -180,7 +226,7 @@ RSpec.describe Kerberos::Krb5 do
180
226
  let(:compliant_pw) { 'Changeme1!' }
181
227
 
182
228
  it 'raises an exception with the KDC rejection reason' do
183
- krb5.get_init_creds_password(policy_user, compliant_pw)
229
+ krb5.get_init_creds_password(principal: policy_user, password: compliant_pw)
184
230
  expect {
185
231
  krb5.change_password(compliant_pw, 'a')
186
232
  }.to raise_error(Kerberos::Krb5::Exception, /krb5_change_password/)
@@ -208,18 +254,83 @@ RSpec.describe Kerberos::Krb5 do
208
254
  expect(krb5).to respond_to(:get_init_creds_keytab)
209
255
  end
210
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
+
211
262
  it 'acquires credentials for a principal from a supplied keytab file' do
212
263
  kt_name = "FILE:#{@kt_file}"
213
- 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
214
265
  expect(krb5.verify_init_creds).to be true
215
266
  end
216
267
 
217
268
  it 'accepts a CredentialsCache to receive credentials' do
218
269
  ccache = Kerberos::Krb5::CredentialsCache.new
219
270
  kt_name = "FILE:#{@kt_file}"
220
- 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
221
272
  expect(ccache.primary_principal).to be_a(String)
222
273
  expect(ccache.primary_principal).to include('@')
223
274
  end
224
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
225
336
  end
data/spec/policy_spec.rb CHANGED
@@ -21,6 +21,10 @@ RSpec.describe 'Kerberos::Kadm5::Policy', :kadm5 do
21
21
  it 'must be present' do
22
22
  expect { klass.new(max_life: 10000) }.to raise_error(ArgumentError)
23
23
  end
24
+ it 'rejects positional option hashes and unknown keywords' do
25
+ expect { klass.new({name: 'test'}) }.to raise_error(ArgumentError)
26
+ expect { klass.new(name: 'test', unknown: true) }.to raise_error(ArgumentError, /unknown keyword/)
27
+ end
24
28
  end
25
29
 
26
30
  describe 'min_life' do