backup 3.0.26 → 3.0.27

Sign up to get free protection for your applications and to get access to all the features.
@@ -374,19 +374,18 @@ describe Backup::Database::MySQL do
374
374
  end
375
375
 
376
376
  describe 'deprecations' do
377
- after do
378
- Backup::Database::MySQL.clear_defaults!
379
- end
380
-
381
377
  describe '#utility_path' do
382
378
  before do
383
379
  Backup::Database::MySQL.any_instance.stubs(:utility)
384
- Backup::Logger.expects(:warn).with(
385
- instance_of(Backup::Errors::ConfigurationError)
386
- )
387
- Backup::Logger.expects(:warn).with(
388
- "Backup::Database::MySQL.mysqldump_utility is being set to 'foo'"
389
- )
380
+ Backup::Logger.expects(:warn).with {|err|
381
+ err.should be_an_instance_of Backup::Errors::ConfigurationError
382
+ err.message.should match(
383
+ /Use MySQL#mysqldump_utility instead/
384
+ )
385
+ }
386
+ end
387
+ after do
388
+ Backup::Database::MySQL.clear_defaults!
390
389
  end
391
390
 
392
391
  context 'when set directly' do
@@ -316,19 +316,18 @@ describe Backup::Database::PostgreSQL do
316
316
  end
317
317
 
318
318
  describe 'deprecations' do
319
- after do
320
- Backup::Database::PostgreSQL.clear_defaults!
321
- end
322
-
323
319
  describe '#utility_path' do
324
320
  before do
325
321
  Backup::Database::PostgreSQL.any_instance.stubs(:utility)
326
- Backup::Logger.expects(:warn).with(
327
- instance_of(Backup::Errors::ConfigurationError)
328
- )
329
- Backup::Logger.expects(:warn).with(
330
- "Backup::Database::PostgreSQL.pg_dump_utility is being set to 'foo'"
331
- )
322
+ Backup::Logger.expects(:warn).with {|err|
323
+ err.should be_an_instance_of Backup::Errors::ConfigurationError
324
+ err.message.should match(
325
+ /Use PostgreSQL#pg_dump_utility instead/
326
+ )
327
+ }
328
+ end
329
+ after do
330
+ Backup::Database::PostgreSQL.clear_defaults!
332
331
  end
333
332
 
334
333
  context 'when set directly' do
@@ -297,19 +297,18 @@ describe Backup::Database::Redis do
297
297
  end
298
298
 
299
299
  describe 'deprecations' do
300
- after do
301
- Backup::Database::Redis.clear_defaults!
302
- end
303
-
304
300
  describe '#utility_path' do
305
301
  before do
306
302
  Backup::Database::Redis.any_instance.stubs(:utility)
307
- Backup::Logger.expects(:warn).with(
308
- instance_of(Backup::Errors::ConfigurationError)
309
- )
310
- Backup::Logger.expects(:warn).with(
311
- "Backup::Database::Redis.redis_cli_utility is being set to 'foo'"
312
- )
303
+ Backup::Logger.expects(:warn).with {|err|
304
+ err.should be_an_instance_of Backup::Errors::ConfigurationError
305
+ err.message.should match(
306
+ /Use Redis#redis_cli_utility instead/
307
+ )
308
+ }
309
+ end
310
+ after do
311
+ Backup::Database::Redis.clear_defaults!
313
312
  end
314
313
 
315
314
  context 'when set directly' do
@@ -139,19 +139,18 @@ describe Backup::Database::Riak do
139
139
  end
140
140
 
141
141
  describe 'deprecations' do
142
- after do
143
- Backup::Database::Riak.clear_defaults!
144
- end
145
-
146
142
  describe '#utility_path' do
147
143
  before do
148
144
  Backup::Database::Riak.any_instance.stubs(:utility)
149
- Backup::Logger.expects(:warn).with(
150
- instance_of(Backup::Errors::ConfigurationError)
151
- )
152
- Backup::Logger.expects(:warn).with(
153
- "Backup::Database::Riak.riak_admin_utility is being set to 'foo'"
154
- )
145
+ Backup::Logger.expects(:warn).with {|err|
146
+ err.should be_an_instance_of Backup::Errors::ConfigurationError
147
+ err.message.should match(
148
+ /Use Riak#riak_admin_utility instead/
149
+ )
150
+ }
151
+ end
152
+ after do
153
+ Backup::Database::Riak.clear_defaults!
155
154
  end
156
155
 
157
156
  context 'when set directly' do
@@ -5,7 +5,8 @@ require File.expand_path('../../spec_helper.rb', __FILE__)
5
5
  describe Backup::Encryptor::GPG do
6
6
  let(:encryptor) do
7
7
  Backup::Encryptor::GPG.new do |e|
8
- e.key = 'gpg_key'
8
+ e.mode = :symmetric
9
+ e.passphrase = 'test secret'
9
10
  end
10
11
  end
11
12
 
@@ -14,6 +15,30 @@ describe Backup::Encryptor::GPG do
14
15
  superclass.should == Backup::Encryptor::Base
15
16
  end
16
17
 
18
+ it 'supports three modes of operation' do
19
+ Backup::Encryptor::GPG::MODES.should == [:asymmetric, :symmetric, :both]
20
+ end
21
+
22
+ describe '#mode=' do
23
+ it 'should accept valid modes' do
24
+ mode = Backup::Encryptor::GPG::MODES.shuffle.first
25
+ encryptor.mode = mode
26
+ encryptor.mode.should == mode
27
+ end
28
+
29
+ it 'should convert string input to a symbol' do
30
+ mode = Backup::Encryptor::GPG::MODES.shuffle.first
31
+ encryptor.mode = mode.to_s
32
+ encryptor.mode.should == mode
33
+ end
34
+
35
+ it 'should raise an error for invalid modes' do
36
+ expect do
37
+ encryptor.mode = 'foo'
38
+ end.to raise_error(Backup::Errors::Encryptor::GPG::InvalidModeError)
39
+ end
40
+ end # describe '#mode='
41
+
17
42
  describe '#initialize' do
18
43
  after { Backup::Encryptor::GPG.clear_defaults! }
19
44
 
@@ -24,127 +49,861 @@ describe Backup::Encryptor::GPG do
24
49
 
25
50
  context 'when no pre-configured defaults have been set' do
26
51
  it 'should use the values given' do
27
- encryptor.key.should == 'gpg_key'
52
+ encryptor.mode.should == :symmetric
53
+ encryptor.passphrase.should == 'test secret'
28
54
  end
29
55
 
30
56
  it 'should use default values if none are given' do
31
57
  encryptor = Backup::Encryptor::GPG.new
32
- encryptor.key.should be_nil
58
+ encryptor.mode.should == :asymmetric
59
+ encryptor.keys.should be_nil
60
+ encryptor.recipients.should be_nil
61
+ encryptor.passphrase.should be_nil
62
+ encryptor.passphrase_file.should be_nil
63
+ encryptor.gpg_config.should be_nil
64
+ encryptor.gpg_homedir.should be_nil
33
65
  end
34
66
  end # context 'when no pre-configured defaults have been set'
35
67
 
36
68
  context 'when pre-configured defaults have been set' do
37
69
  before do
38
70
  Backup::Encryptor::GPG.defaults do |e|
39
- e.key = 'default_key'
71
+ e.mode = :both
72
+ e.keys = { 'test_key' => 'test public key' }
73
+ e.recipients = 'test_key'
74
+ e.passphrase_file = 'my/pass/file'
40
75
  end
41
76
  end
42
77
 
43
78
  it 'should use pre-configured defaults' do
44
79
  encryptor = Backup::Encryptor::GPG.new
45
- encryptor.key.should == 'default_key'
80
+ encryptor.mode.should == :both
81
+ encryptor.keys.should == { 'test_key' => 'test public key' }
82
+ encryptor.recipients.should == 'test_key'
83
+ encryptor.passphrase_file.should == 'my/pass/file'
46
84
  end
47
85
 
48
86
  it 'should override pre-configured defaults' do
49
- encryptor.key.should == 'gpg_key'
87
+ encryptor.mode.should == :symmetric
88
+ encryptor.keys.should == { 'test_key' => 'test public key' }
89
+ encryptor.recipients.should == 'test_key'
90
+ encryptor.passphrase.should == 'test secret'
91
+ encryptor.passphrase_file.should == 'my/pass/file'
50
92
  end
51
93
  end # context 'when pre-configured defaults have been set'
52
94
  end # describe '#initialize'
53
95
 
54
96
  describe '#encrypt_with' do
55
- it 'should yield the encryption command and extension' do
97
+ before do
56
98
  encryptor.expects(:log!)
57
- encryptor.expects(:extract_encryption_key_email!)
58
- encryptor.expects(:utility).with(:gpg).returns('gpg')
59
- encryptor.expects(:options).returns('command options')
99
+ encryptor.expects(:prepare)
100
+ encryptor.expects(:cleanup) # ensure call
101
+ end
60
102
 
61
- encryptor.encrypt_with do |command, ext|
62
- command.should == 'gpg command options'
63
- ext.should == '.gpg'
103
+ context 'when encryption can be performed' do
104
+ it 'should yield the encryption command and extension' do
105
+ encryptor.expects(:mode_options).twice.returns('mode_options')
106
+ encryptor.expects(:base_options).returns('base_options')
107
+ encryptor.expects(:utility).with(:gpg).returns('gpg')
108
+
109
+ encryptor.encrypt_with do |command, ext|
110
+ command.should == 'gpg base_options mode_options'
111
+ ext.should == '.gpg'
112
+ end
64
113
  end
65
114
  end
66
- end
67
115
 
68
- describe '#extract_encryption_key_email!' do
69
- it 'should extract the encryption_key_email' do
70
- encryptor.expects(:utility).with(:gpg).returns('gpg')
71
- encryptor.expects(:with_tmp_key_file).yields('/path/to/tmpfile')
72
- encryptor.expects(:run).with("gpg --import '/path/to/tmpfile' 2>&1").
73
- returns('gpg: key A1B2C3D4: "User Name (Comment) <user@host>" not changed')
116
+ context 'when encryption can not be performed' do
117
+ it 'should raise an error when no mode_options are returned' do
118
+ encryptor.expects(:mode_options).returns([])
119
+
120
+ expect do
121
+ encryptor.encrypt_with
122
+ end.to raise_error(Backup::Errors::Encryptor::GPG::EncryptionError)
123
+ end
124
+ end
125
+ end # describe '#encrypt_with'
74
126
 
75
- encryptor.send(:extract_encryption_key_email!)
76
- encryptor.instance_variable_get(:@encryption_key_email).should == 'user@host'
127
+ describe '#prepare and #cleanup' do
128
+ it 'should setup required variables' do
129
+ encryptor.instance_variable_set(:@tempdirs, nil)
130
+ FileUtils.expects(:rm_rf).never
131
+ encryptor.send(:prepare)
132
+ encryptor.instance_variable_get(:@tempdirs).should == []
77
133
  end
78
134
 
79
- it 'should use the cached key email if already extracted' do
80
- encryptor.instance_variable_set(:@encryption_key_email, 'foo@host')
81
- encryptor.expects(:utility).never
82
- encryptor.expects(:with_tmp_key_file).never
83
- encryptor.expects(:run).never
135
+ it 'should remove any tempdirs and clear all variables' do
136
+ encryptor.instance_variable_set(:@tempdirs, ['a', 'b'])
137
+ FileUtils.expects(:rm_rf).with(['a', 'b'], {:secure => true})
84
138
 
85
- encryptor.send(:extract_encryption_key_email!)
139
+ encryptor.instance_variable_set(:@base_options, true)
140
+ encryptor.instance_variable_set(:@mode_options, true)
141
+ encryptor.instance_variable_set(:@user_recipients, true)
142
+ encryptor.instance_variable_set(:@user_keys, true)
143
+ encryptor.instance_variable_set(:@system_identifiers, true)
144
+
145
+ encryptor.send(:cleanup)
146
+
147
+ encryptor.instance_variable_get(:@tempdirs).should == []
148
+ encryptor.instance_variable_get(:@base_options).should be_nil
149
+ encryptor.instance_variable_get(:@mode_options).should be_nil
150
+ encryptor.instance_variable_get(:@user_recipients).should be_nil
151
+ encryptor.instance_variable_get(:@user_keys).should be_nil
152
+ encryptor.instance_variable_get(:@system_identifiers).should be_nil
86
153
  end
87
- end
154
+ end # describe '#prepare and #cleanup'
155
+
156
+ describe '#base_options' do
157
+ context 'while caching the return value in @base_options' do
158
+ before { encryptor.instance_variable_set(:@base_options, nil) }
159
+
160
+ context 'when #gpg_homedir is given' do
161
+ it 'should return the proper options' do
162
+ encryptor.expects(:setup_gpg_homedir).once.returns('/a/dir')
163
+ encryptor.expects(:setup_gpg_config).once.returns(false)
164
+
165
+ ret = "--no-tty --homedir '/a/dir'"
166
+ encryptor.send(:base_options).should == ret
167
+ encryptor.send(:base_options).should == ret
168
+ encryptor.instance_variable_get(:@base_options).should == ret
169
+ end
170
+ end
171
+
172
+ context 'when #gpg_config is given' do
173
+ it 'should return the proper options' do
174
+ encryptor.expects(:setup_gpg_homedir).once.returns(false)
175
+ encryptor.expects(:setup_gpg_config).once.returns('/a/file')
88
176
 
89
- describe '#options' do
90
- it 'should return the option string for the gpg command' do
91
- encryptor.instance_variable_set(:@encryption_key_email, 'user@host')
92
- encryptor.send(:options).should == "-e --trust-model always -r 'user@host'"
177
+ ret = "--no-tty --options '/a/file'"
178
+ encryptor.send(:base_options).should == ret
179
+ encryptor.send(:base_options).should == ret
180
+ encryptor.instance_variable_get(:@base_options).should == ret
181
+ end
182
+ end
183
+
184
+ context 'when #gpg_homedir and #gpg_config is given' do
185
+ it 'should return the proper options' do
186
+ encryptor.expects(:setup_gpg_homedir).once.returns('/a/dir')
187
+ encryptor.expects(:setup_gpg_config).once.returns('/a/file')
188
+
189
+ ret = "--no-tty --homedir '/a/dir' --options '/a/file'"
190
+ encryptor.send(:base_options).should == ret
191
+ encryptor.send(:base_options).should == ret
192
+ encryptor.instance_variable_get(:@base_options).should == ret
193
+ end
194
+ end
195
+
196
+ context 'when neither #gpg_homedir and #gpg_config is given' do
197
+ it 'should return the proper options' do
198
+ encryptor.expects(:setup_gpg_homedir).once.returns(false)
199
+ encryptor.expects(:setup_gpg_config).once.returns(false)
200
+
201
+ ret = '--no-tty'
202
+ encryptor.send(:base_options).should == ret
203
+ encryptor.send(:base_options).should == ret
204
+ encryptor.instance_variable_get(:@base_options).should == ret
205
+ end
206
+ end
207
+ end
208
+ end # describe '#base_options'
209
+
210
+ describe '#setup_gpg_homedir' do
211
+ context 'when #gpg_homedir is not set' do
212
+ it 'should return false' do
213
+ encryptor.gpg_homedir = nil
214
+ encryptor.send(:setup_gpg_homedir).should be_false
215
+ end
216
+ end
217
+
218
+ context 'when #gpg_homedir is set' do
219
+ let(:path) { 'some/path' }
220
+ let(:expanded_path) { File.expand_path(path) }
221
+
222
+ before do
223
+ encryptor.gpg_homedir = path
224
+ Backup::Config.stubs(:user).returns('a_user')
225
+ end
226
+
227
+ context 'and no errors occur' do
228
+ before do
229
+ FileUtils.expects(:mkdir_p).with(expanded_path)
230
+ FileUtils.expects(:chown).with('a_user', nil, expanded_path)
231
+ FileUtils.expects(:chmod).with(0700, expanded_path)
232
+ end
233
+
234
+ context 'and the gpg_homedir files exist' do
235
+ before do
236
+ %w{ pubring.gpg secring.gpg trustdb.gpg }.each do |file|
237
+ File.expects(:exist?).with(
238
+ File.join(expanded_path, file)
239
+ ).returns(true)
240
+ end
241
+ end
242
+
243
+ it 'should ensure permissions and return the path' do
244
+ encryptor.expects(:utility).never
245
+ encryptor.send(:setup_gpg_homedir).should == expanded_path
246
+ end
247
+ end
248
+
249
+ context 'and the gpg_homedir files do not exist' do
250
+ before do
251
+ File.stubs(:exist?).returns(false)
252
+ end
253
+
254
+ it 'should call gpg to initialize the files' do
255
+ encryptor.expects(:utility).with(:gpg).returns('gpg')
256
+ encryptor.expects(:run).with(
257
+ "gpg --homedir '#{ expanded_path }' -K 2>&1 >/dev/null"
258
+ )
259
+ encryptor.send(:setup_gpg_homedir).should == expanded_path
260
+ end
261
+ end
262
+ end
263
+
264
+ context 'and errors occur' do
265
+ it 'should wrap and raise the error' do
266
+ File.expects(:expand_path).raises('error message')
267
+
268
+ expect do
269
+ encryptor.send(:setup_gpg_homedir)
270
+ end.to raise_error {|err|
271
+ err.should
272
+ be_an_instance_of Backup::Errors::Encryptor::GPG::HomedirError
273
+ err.message.should
274
+ match(/Reason: RuntimeError\n error message/)
275
+ }
276
+ end
277
+ end
278
+ end
279
+ end # describe '#setup_gpg_homedir'
280
+
281
+ describe '#setup_gpg_config' do
282
+ context 'when #gpg_config is not set' do
283
+ it 'should return false' do
284
+ encryptor.gpg_config = nil
285
+ encryptor.send(:setup_gpg_config).should be_false
286
+ end
93
287
  end
94
- end
95
288
 
96
- describe '#with_tmp_key_file' do
97
- let(:tmp_file) { mock }
98
- let(:s) { sequence '' }
289
+ context 'when #gpg_config is set' do
290
+ before do
291
+ encryptor.gpg_config = <<-EOF
292
+ # a comment
293
+ text which will be
294
+
295
+ \tthe content of a gpg.conf file
296
+ EOF
297
+ Backup::Config.stubs(:tmp_path).returns('/Backup/tmp')
298
+ encryptor.instance_variable_set(:@tempdirs, [])
299
+ end
300
+
301
+ context 'when no errors occur' do
302
+ let(:tempdir) { mock }
303
+ let(:tempfile) { mock }
304
+ let(:tempfile_path) { mock }
305
+ let(:path) { mock }
306
+
307
+ before do
308
+ encryptor.expects(:cleanup).never
309
+ tempfile.stubs(:path).returns(tempfile_path)
310
+ end
311
+
312
+ it 'should create and return the file path' do
313
+ # create temporary directory and convert to a Pathname object
314
+ Dir.expects(:mktmpdir).with(
315
+ 'backup-gpg_config', '/Backup/tmp'
316
+ ).returns(tempdir)
317
+
318
+ # create temporary file within the temporary directory
319
+ Tempfile.expects(:open).with(
320
+ 'backup-gpg_config', tempdir
321
+ ).returns(tempfile)
322
+
323
+ # write the gpg_config, stripping leading tabs/spaces
324
+ tempfile.expects(:write).with(
325
+ "# a comment\n" +
326
+ "text which will be\n" +
327
+ "\n" +
328
+ "the content of a gpg.conf file\n"
329
+ )
330
+ # close the file
331
+ tempfile.expects(:close)
332
+
333
+ # check the config file
334
+ encryptor.expects(:check_gpg_config).with(tempfile_path)
335
+
336
+ # method returns the tempfile's path
337
+ encryptor.send(:setup_gpg_config).should == tempfile_path
338
+
339
+ # tempdir added to @tempdirs
340
+ encryptor.instance_variable_get(:@tempdirs)[0].should == tempdir
341
+ end
342
+ end
343
+
344
+ context 'when errors occur' do
345
+ before do
346
+ encryptor.expects(:cleanup) # run before the error is raised
347
+ end
348
+
349
+ it 'should wrap and raise the error' do
350
+ Dir.expects(:mktmpdir).raises('an error')
351
+
352
+ expect do
353
+ encryptor.send(:setup_gpg_config)
354
+ end.to raise_error {|err|
355
+ err.should be_an_instance_of(
356
+ Backup::Errors::Encryptor::GPG::GPGConfigError
357
+ )
358
+ err.message.should match(
359
+ /Error creating temporary file for #gpg_config/
360
+ )
361
+ err.message.should match(
362
+ /Reason: RuntimeError\n an error/
363
+ )
364
+ }
365
+ end
366
+ end
367
+ end
368
+ end # describe '#setup_gpg_config'
369
+
370
+ describe '#check_gpg_config' do
371
+ let(:cmd_ret) { mock }
372
+ let(:file_path) { '/path/to/tempfile' }
99
373
 
100
374
  before do
101
- tmp_file.stubs(:path).returns('/path/to/tmp_file')
102
- encryptor.stubs(:encryption_key).returns('provided key')
375
+ encryptor.expects(:utility).with(:gpg).returns('gpg')
376
+ encryptor.expects(:run).with(
377
+ "gpg --options '#{ file_path }' --gpgconf-test 2>&1"
378
+ ).returns(cmd_ret)
103
379
  end
104
380
 
105
- it 'should provide a tempfile with the provided key' do
106
- Tempfile.expects(:new).in_sequence(s).
107
- with('backup.pub').
108
- returns(tmp_file)
109
- FileUtils.expects(:chown).in_sequence(s).
110
- with(Backup::Config.user, nil, '/path/to/tmp_file')
111
- FileUtils.expects(:chmod).in_sequence(s).
112
- with(0600, '/path/to/tmp_file')
113
- tmp_file.expects(:write).in_sequence(s).
114
- with('provided key')
115
- tmp_file.expects(:close).in_sequence(s)
116
- tmp_file.expects(:delete).in_sequence(s)
381
+ context 'when no errors are reported' do
382
+ before { cmd_ret.expects(:chomp).returns('') }
117
383
 
118
- encryptor.send(:with_tmp_key_file) do |tmp_file|
119
- tmp_file.should == '/path/to/tmp_file'
384
+ it 'should do nothing' do
385
+ encryptor.send(:check_gpg_config, file_path).should be_nil
120
386
  end
121
387
  end
122
- end
123
388
 
124
- describe '#encryption_key' do
125
- it 'should strip leading whitespace from the given key' do
126
- encryptor.key = <<-KEY
389
+ context 'when errors are reported' do
390
+ let(:error_message) { "gpg: /path/to/tempfile:1: invalid option" }
391
+ before { cmd_ret.expects(:chomp).returns(error_message) }
392
+
393
+ it 'should raise the error message reported' do
394
+ expect do
395
+ encryptor.send(:check_gpg_config, file_path)
396
+ end.to raise_error(RuntimeError, error_message)
397
+ end
398
+ end
399
+ end # describe '#check_gpg_config'
400
+
401
+ describe '#mode_options' do
402
+ let(:s_opts) { "-c --passphrase_file '/some/file'" }
403
+ let(:a_opts) { "-e --trust-model always -r 'identifier'" }
404
+
405
+ context 'while caching the return value in @mode_options' do
406
+ before { encryptor.instance_variable_set(:@mode_options, nil) }
407
+
408
+ context 'when #mode is :symmetric' do
409
+ it 'should return symmetric encryption options' do
410
+ encryptor.expects(:symmetric_options).once.returns(s_opts)
411
+ encryptor.expects(:asymmetric_options).never
412
+
413
+ encryptor.mode = :symmetric
414
+ encryptor.send(:mode_options).should == s_opts
415
+ encryptor.send(:mode_options).should == s_opts
416
+ encryptor.instance_variable_get(:@mode_options).should == s_opts
417
+ end
418
+ end
419
+
420
+ context 'when #mode is :asymmetric' do
421
+ it 'should return asymmetric encryption options' do
422
+ encryptor.expects(:symmetric_options).never
423
+ encryptor.expects(:asymmetric_options).once.returns(a_opts)
424
+
425
+ encryptor.mode = :asymmetric
426
+ encryptor.send(:mode_options).should == a_opts
427
+ encryptor.send(:mode_options).should == a_opts
428
+ encryptor.instance_variable_get(:@mode_options).should == a_opts
429
+ end
430
+ end
431
+
432
+ context 'when #mode is :both' do
433
+ it 'should return both symmetric and asymmetric encryption options' do
434
+ encryptor.expects(:symmetric_options).once.returns(s_opts)
435
+ encryptor.expects(:asymmetric_options).once.returns(a_opts)
436
+
437
+ encryptor.mode = :both
438
+ opts = "#{ s_opts } #{ a_opts }"
439
+
440
+ encryptor.send(:mode_options).should == opts
441
+ encryptor.send(:mode_options).should == opts
442
+ encryptor.instance_variable_get(:@mode_options).should == opts
443
+ end
444
+ end
445
+ end
446
+ end # describe '#mode_options'
447
+
448
+ describe '#symmetric_options' do
449
+ let(:path) { '/path/to/passphrase/file' }
450
+ let(:s_opts) { "-c --passphrase-file '#{ path }'" }
451
+
452
+ context 'when setup_passphrase_file returns a path' do
453
+ it 'should return the options' do
454
+ encryptor.expects(:setup_passphrase_file).returns(path)
455
+ File.expects(:exist?).with(path).returns(true)
456
+
457
+ encryptor.send(:symmetric_options).should == s_opts
458
+ end
459
+ end
460
+
461
+ context 'when setup_passphrase_file returns false' do
462
+ before do
463
+ encryptor.expects(:setup_passphrase_file).returns(false)
464
+ end
465
+
466
+ context 'and no :passphrase_file is set' do
467
+ it 'should return nil and log a warning' do
468
+ encryptor.expects(:passphrase_file).returns(nil)
469
+ Backup::Logger.expects(:warn)
470
+
471
+ encryptor.send(:symmetric_options).should be_nil
472
+ end
473
+ end
474
+
475
+ context 'and a :passphrase_file is set' do
476
+ before do
477
+ encryptor.expects(:passphrase_file).twice.returns(path)
478
+ File.expects(:expand_path).with(path).returns(path)
479
+ end
480
+
481
+ context 'when :passphrase_file exists' do
482
+ it 'should return the options' do
483
+ File.expects(:exist?).with(path).returns(true)
484
+ encryptor.send(:symmetric_options).should == s_opts
485
+ end
486
+ end
487
+
488
+ context 'when :passphrase_file is no valid' do
489
+ it 'should return nil and log a warning' do
490
+ File.expects(:exist?).with(path).returns(false)
491
+ Backup::Logger.expects(:warn)
492
+ encryptor.send(:symmetric_options).should be_nil
493
+ end
494
+ end
495
+ end
496
+ end
497
+ end # describe '#symmetric_options'
498
+
499
+ describe '#setup_passphrase_file' do
500
+ context 'when :passphrase is not set' do
501
+ it 'should return false' do
502
+ encryptor.expects(:passphrase).returns(nil)
503
+ encryptor.send(:setup_passphrase_file).should be_false
504
+ end
505
+ end
506
+
507
+ context 'when :passphrase is set' do
508
+ let(:tempdir) { mock }
509
+ let(:tempfile) { mock }
510
+ let(:tempfile_path) { mock }
511
+
512
+ before do
513
+ encryptor.instance_variable_set(:@tempdirs, [])
514
+ Backup::Config.stubs(:tmp_path).returns('/Backup/tmp')
515
+ encryptor.stubs(:passphrase).returns('a secret')
516
+ tempfile.stubs(:path).returns(tempfile_path)
517
+ end
518
+
519
+ context 'and no errors occur' do
520
+ it 'should return the path for the temp file' do
521
+ # creates temporary directory in Config.tmp_path
522
+ Dir.expects(:mktmpdir).
523
+ with('backup-gpg_passphrase', '/Backup/tmp').
524
+ returns(tempdir)
525
+
526
+ # create the temporary file in that temporary directory
527
+ Tempfile.expects(:open).
528
+ with('backup-gpg_passphrase', tempdir).
529
+ returns(tempfile)
530
+ tempfile.expects(:write).with('a secret')
531
+ tempfile.expects(:close)
532
+
533
+ encryptor.send(:setup_passphrase_file).should == tempfile_path
534
+
535
+ # adds the temporary directory to @tempdirs
536
+ encryptor.instance_variable_get(:@tempdirs)[0].should == tempdir
537
+ end
538
+ end
539
+
540
+ context 'and an error occurs' do
541
+ it 'should return false and log a warning' do
542
+ Dir.expects(:mktmpdir).raises('an error')
543
+ Backup::Logger.expects(:warn).with do |err|
544
+ err.should be_an_instance_of(
545
+ Backup::Errors::Encryptor::GPG::PassphraseError
546
+ )
547
+ err.message.should match(
548
+ /Reason: RuntimeError\n an error/
549
+ )
550
+ end
551
+ encryptor.send(:setup_passphrase_file).should be_false
552
+ end
553
+ end
554
+
555
+ end
556
+ end # describe '#setup_passphrase_file'
557
+
558
+ describe '#asymmetric_options' do
559
+ context 'when recipients are found' do
560
+ it 'should return the options' do
561
+ encryptor.stubs(:user_recipients).returns(['keyid1', 'keyid2'])
562
+ encryptor.send(:asymmetric_options).should ==
563
+ "-e --trust-model always -r 'keyid1' -r 'keyid2'"
564
+ end
565
+ end
566
+
567
+ context 'when no recipients are found' do
568
+ it 'should return nil log a warning' do
569
+ encryptor.expects(:user_recipients).returns([])
570
+ Backup::Logger.expects(:warn)
571
+ encryptor.send(:asymmetric_options).should be_nil
572
+ end
573
+ end
574
+ end # describe '#asymmetric_options'
575
+
576
+ describe '#user_recipients' do
577
+ context 'when an Array of :recipients are given' do
578
+ it 'should return the recipient list and cache the result' do
579
+ encryptor.expects(:recipients).returns(
580
+ ['key_id1', 'key_id2', 'key_id3', 'key_id4']
581
+ )
582
+ encryptor.expects(:clean_identifier).with('key_id1').returns('key_id1')
583
+ encryptor.expects(:clean_identifier).with('key_id2').returns('key_id2')
584
+ encryptor.expects(:clean_identifier).with('key_id3').returns('key_id3')
585
+ encryptor.expects(:clean_identifier).with('key_id4').returns('key_id4')
586
+
587
+ # key_id1 and key_id3 will be found in the system
588
+ encryptor.stubs(:system_identifiers).returns(['key_id1', 'key_id3'])
589
+
590
+ # key_id2 will be imported (key_id returned)
591
+ encryptor.stubs(:user_keys).returns({ 'key_id2' => 'a public key' })
592
+ encryptor.expects(:import_key).
593
+ with('key_id2', 'a public key').
594
+ returns('key_id2')
595
+
596
+ # key_id4 will not be found in user_keys, so a warning will be logged.
597
+ # This will return nil into the array, which will be compacted out.
598
+ Backup::Logger.expects(:warn).with do |msg|
599
+ msg.should match(/'key_id4'/)
600
+ end
601
+
602
+ encryptor.instance_variable_set(:@user_recipients, nil)
603
+ recipient_list = ['key_id1', 'key_id2', 'key_id3']
604
+ encryptor.send(:user_recipients).should == recipient_list
605
+ # results are cached (expectations would fail if called twice)
606
+ encryptor.send(:user_recipients).should == recipient_list
607
+ encryptor.instance_variable_get(:@user_recipients).should == recipient_list
608
+ end
609
+ end
610
+
611
+ context 'when :recipients is a single recipient, given as a String' do
612
+ it 'should return the cleaned identifier in an Array' do
613
+ encryptor.expects(:recipients).returns('key_id')
614
+ # the key will be found in system_identifiers
615
+ encryptor.stubs(:system_identifiers).returns(['key_id'])
616
+ encryptor.expects(:clean_identifier).with('key_id').returns('key_id')
617
+
618
+ encryptor.send(:user_recipients).should == ['key_id']
619
+ end
620
+ end
621
+
622
+ context 'when :recipients is not set' do
623
+ it 'should return an empty Array' do
624
+ encryptor.expects(:recipients).returns(nil)
625
+ encryptor.send(:user_recipients).should == []
626
+ end
627
+ end
628
+ end # describe '#user_recipients'
629
+
630
+ describe '#user_keys' do
631
+ context 'when :keys has been set' do
632
+ before do
633
+ encryptor.expects(:keys).returns(
634
+ { 'key1' => :foo, 'key2' => :foo, 'key3' => :foo }
635
+ )
636
+ encryptor.instance_variable_set(:@user_keys, nil)
637
+ end
638
+
639
+ it 'should return a new Hash of #keys with cleaned identifiers' do
640
+ encryptor.expects(:clean_identifier).with('key1').returns('clean_key1')
641
+ encryptor.expects(:clean_identifier).with('key2').returns('clean_key2')
642
+ encryptor.expects(:clean_identifier).with('key3').returns('clean_key3')
643
+
644
+ Backup::Logger.expects(:warn).never
645
+
646
+ cleaned_hash = {
647
+ 'clean_key1' => :foo, 'clean_key2' => :foo, 'clean_key3' => :foo
648
+ }
649
+ encryptor.send(:user_keys).should == cleaned_hash
650
+ # results are cached (expectations would fail if called twice)
651
+ encryptor.send(:user_keys).should == cleaned_hash
652
+ encryptor.instance_variable_get(:@user_keys).should == cleaned_hash
653
+ end
654
+
655
+ it 'should log a warning if cleaning results in a duplicate identifier' do
656
+ encryptor.expects(:clean_identifier).with('key1').returns('clean_key1')
657
+ encryptor.expects(:clean_identifier).with('key2').returns('clean_key2')
658
+ # return a duplicate key
659
+ encryptor.expects(:clean_identifier).with('key3').returns('clean_key2')
660
+
661
+ Backup::Logger.expects(:warn)
662
+
663
+ cleaned_hash = {
664
+ 'clean_key1' => :foo, 'clean_key2' => :foo
665
+ }
666
+ encryptor.send(:user_keys).should == cleaned_hash
667
+ # results are cached (expectations would fail if called twice)
668
+ encryptor.send(:user_keys).should == cleaned_hash
669
+ encryptor.instance_variable_get(:@user_keys).should == cleaned_hash
670
+ end
671
+ end
672
+
673
+ context 'when :keys has not be set' do
674
+ before do
675
+ encryptor.expects(:keys).returns(nil)
676
+ encryptor.instance_variable_set(:@user_keys, nil)
677
+ end
678
+
679
+ it 'should return an empty hash' do
680
+ encryptor.send(:user_keys).should == {}
681
+ end
682
+ end
683
+ end # describe '#user_keys'
684
+
685
+ describe '#clean_identifier' do
686
+ it 'should remove all spaces and upcase non-email identifiers' do
687
+ encryptor.send(:clean_identifier, ' 9d66 6290 c5f7 ee0f ').
688
+ should == '9D666290C5F7EE0F'
689
+ end
690
+
691
+ # Even though spaces in an email are technically possible,
692
+ # GPG won't allow anything but /[A-Za-z0-9_\-.]/
693
+ it 'should remove all spaces and wrap email addresses in <>' do
694
+ emails = [
695
+ "\t Foo.Bar@example.com ",
696
+ " < Foo-Bar@example.com\t > ",
697
+ "< <Foo_Bar @\texample.com> >"
698
+ ]
699
+ cleaned = [
700
+ '<Foo.Bar@example.com>',
701
+ '<Foo-Bar@example.com>',
702
+ '<Foo_Bar@example.com>'
703
+ ]
704
+
705
+ emails.map {|email|
706
+ encryptor.send(:clean_identifier, email)
707
+ }.should == cleaned
708
+ end
709
+ end # describe '#clean_identifier'
710
+
711
+ describe '#import_key' do
712
+ let(:gpg_return_ok) {
713
+ <<-EOS.gsub(/^ +/, '')
714
+ gpg: keyring `/tmp/.gnupg/secring.gpg' created
715
+ gpg: keyring `/tmp/.gnupg/pubring.gpg' created
716
+ gpg: /tmp/.gnupg/trustdb.gpg: trustdb created
717
+ gpg: key 0x9D666290C5F7EE0F: public key "Backup Test <backup01@foo.com>" imported
718
+ gpg: Total number processed: 1
719
+ gpg: imported: 1 (RSA: 1)
720
+ EOS
721
+ }
722
+ let(:gpg_return_failed) {
723
+ <<-EOS.gsub(/^ +/, '')
724
+ gpg: no valid OpenPGP data found.
725
+ gpg: Total number processed: 0
726
+ EOS
727
+ }
728
+ let(:gpg_key) {
729
+ <<-EOS
127
730
  -----BEGIN PGP PUBLIC KEY BLOCK-----
128
- \tVersion: GnuPG v1.4.11 (Darwin)
731
+ Version: GnuPG v1.4.12 (GNU/Linux)
129
732
 
130
- mQENBE12G/8BCAC4mnlSMYMBwBYTHe5zURcnYYNCORPWOr0iXGiLWuKxYtrDQyLm
131
- X2Nws44Iz7Wp7AuJRAjkitf1cRBgXyDu8wuogXO7JqPmtsUdBCABz9w5NH6IQjgR
132
- WNa3g2n0nokA7Zr5FA4GXoEaYivfbvGiyNpd6P4okH+//G2p+3FIryu5xz+89D1b
133
- =Yvhg
733
+ mI0EUAmiNwEEAKpNP4GVKcjJrTtAh0XKk0NQsId6h/1pzEok2bExkNvD6eSjYRFL
734
+ gXY+pNqaEE6cHrg+uQatVQITX8EoVJhQ9Z1mYJB+g62zqOQPe10Spb381O9y4dN/
735
+ /ge/yL+/+R2CUrKeNF9nSA24+V4mTSqgo7sTnevDzGj4Srzs76MmkpU=
736
+ =TU/B
134
737
  -----END PGP PUBLIC KEY BLOCK-----
135
- KEY
738
+ EOS
739
+ }
740
+ let(:tempfile) { mock }
136
741
 
137
- encryptor.send(:encryption_key).should == <<-KEY
742
+ before do
743
+ Backup::Config.stubs(:tmp_path).returns('/tmp/path')
744
+ encryptor.stubs(:base_options).returns("--some 'base options'")
745
+ encryptor.stubs(:utility).returns('gpg')
746
+ tempfile.stubs(:path).returns('/tmp/file/path')
747
+ end
748
+
749
+ context 'when the import is successful' do
750
+ it 'should return the long key ID' do
751
+ Tempfile.expects(:open).with(
752
+ 'backup-gpg_import', '/tmp/path'
753
+ ).returns(tempfile)
754
+ tempfile.expects(:write).with(<<-EOS)
138
755
  -----BEGIN PGP PUBLIC KEY BLOCK-----
139
- Version: GnuPG v1.4.11 (Darwin)
756
+ Version: GnuPG v1.4.12 (GNU/Linux)
140
757
 
141
- mQENBE12G/8BCAC4mnlSMYMBwBYTHe5zURcnYYNCORPWOr0iXGiLWuKxYtrDQyLm
142
- X2Nws44Iz7Wp7AuJRAjkitf1cRBgXyDu8wuogXO7JqPmtsUdBCABz9w5NH6IQjgR
143
- WNa3g2n0nokA7Zr5FA4GXoEaYivfbvGiyNpd6P4okH+//G2p+3FIryu5xz+89D1b
144
- =Yvhg
758
+ mI0EUAmiNwEEAKpNP4GVKcjJrTtAh0XKk0NQsId6h/1pzEok2bExkNvD6eSjYRFL
759
+ gXY+pNqaEE6cHrg+uQatVQITX8EoVJhQ9Z1mYJB+g62zqOQPe10Spb381O9y4dN/
760
+ /ge/yL+/+R2CUrKeNF9nSA24+V4mTSqgo7sTnevDzGj4Srzs76MmkpU=
761
+ =TU/B
145
762
  -----END PGP PUBLIC KEY BLOCK-----
146
- KEY
763
+ EOS
764
+
765
+ tempfile.expects(:close)
766
+
767
+ encryptor.expects(:run).with(
768
+ "gpg --some 'base options' --keyid-format 0xlong " +
769
+ "--import '/tmp/file/path' 2>&1"
770
+ ).returns(gpg_return_ok)
771
+
772
+ tempfile.expects(:delete)
773
+
774
+ Backup::Logger.expects(:warn).never
775
+
776
+ encryptor.send(:import_key, 'some_identifier', gpg_key).
777
+ should == '9D666290C5F7EE0F'
778
+ end
147
779
  end
148
- end
149
780
 
781
+ context 'when the import is unsuccessful' do
782
+ it 'should return nil and log a warning' do
783
+ Tempfile.expects(:open).raises('an error')
784
+ Backup::Logger.expects(:warn).with {|err|
785
+ err.should be_an_instance_of(
786
+ Backup::Errors::Encryptor::GPG::KeyImportError
787
+ )
788
+ err.message.should match(
789
+ /Public key import failed for 'some_identifier'/
790
+ )
791
+ err.message.should match(
792
+ /Reason: RuntimeError\n an error/
793
+ )
794
+ }
795
+
796
+ encryptor.send(:import_key, 'some_identifier', 'foo').should be_nil
797
+ end
798
+ end
799
+ end # describe '#import_key'
800
+
801
+ describe '#system_identifiers' do
802
+ let(:gpg_output) {
803
+ <<-EOS.gsub(/^ +/, '')
804
+ tru::1:1343402941:0:3:1:5
805
+ pub:-:1024:1:5EFD157FFF9CFEA6:1342808803:::-:::scESC:
806
+ fpr:::::::::72E56E48E362BB402B3344045EFD157FFF9CFEA6:
807
+ uid:-::::1342808803::3BED8A0A5100FE9028BEB53610247518594B60A8::Backup Test (No Email):
808
+ sub:-:1024:1:E6CF1DC860A82E07:1342808803::::::e:
809
+ pub:-:1024:1:570CE9221E3DA3E8:1342808841:::-:::scESC:
810
+ fpr:::::::::616BBC8409C1AED791F8E6F8570CE9221E3DA3E8:
811
+ uid:-::::1342808875::ECFF419EFE4BD3C7CBCCD58FACAD283A9E98FECD::Backup Test <backup04@foo.com>:
812
+ uid:-::::1342808841::DDFD072C193BB45587EBA9D19A7DA1BB0E5E8A22::Backup Test <backup03@foo.com>:
813
+ sub:-:1024:1:B65C0ADEB804268D:1342808841::::::e:
814
+ pub:-:1024:1:54F81C93A7641A16:1342809011:::-:::scESC:
815
+ fpr:::::::::71335B9B960CF3A3071535F454F81C93A7641A16:
816
+ uid:-::::1342809011::2E5801E9C064C2A165B61EE35D50A5F9B64BF345::Backup Test (other email is <backup06@foo.com>) <backup05@foo.com>:
817
+ sub:-:1024:1:5B57BC34628252C7:1342809011::::::e:
818
+ pub:-:1024:1:0A5B6CC9581A88CF:1342809049:::-:::scESC:
819
+ fpr:::::::::E8C459082544924B8AEA06280A5B6CC9581A88CF:
820
+ uid:-::::1342809470::4A404F9ED6780E7E0E02A7F7607828E648789058::Backup Test <backup08@foo.com>:
821
+ uid:-::::::9785ADEBBBCE94CE0FF25774F610F2B11C839E9B::Backup Test <backup07@foo.com>:
822
+ uid:r::::::4AD074B1857819EFA105DFB6C464600AA451BF18::Backup Test <backup09@foo.com>:
823
+ sub:e:1024:1:60A420E39B979B06:1342809049:1342895611:::::e:
824
+ sub:-:1024:1:A05786E7AD5B8352:1342809166::::::e:
825
+ pub:i:1024:1:4A83569F4E5E8D8A:1342810132:::-:::esca:
826
+ fpr:::::::::FFEAD1DB201FB214873E73994A83569F4E5E8D8A:
827
+ uid:-::::::3D41A10AF2437C8C5BF6050FA80FE20CE30769BF::Backup Test <backup10@foo.com>:
828
+ sub:i:1024:1:662F18DB92C8DFD8:1342810132::::::e:
829
+ pub:r:1024:1:15ECEF9ECA136FFF:1342810387:::-:::sc:
830
+ fpr:::::::::3D1CBF3FEFCE5ABB728922F615ECEF9ECA136FFF:
831
+ uid:r::::1342810387::296434E1662AE0B2FF8E93EC3BF3AFE24514D0E0::Backup Test <backup11@foo.com>:
832
+ sub:r:1024:1:097A79EB1F7D4619:1342810387::::::e:
833
+ sub:r:1024:1:39093E8E9057625E:1342810404::::::e:
834
+ pub:e:1024:1:31920687A8A7941B:1342810629:1342897029::-:::sc:
835
+ fpr:::::::::03B399CBC2F4B61019D14BCD31920687A8A7941B:
836
+ uid:e::::1342810629::ED8151565B25281CB92DD1E534701E660126CB0C::Backup Test <backup12@foo.com>:
837
+ sub:e:1024:1:AEF89BEE95042A0F:1342810629:1342897029:::::e:
838
+ pub:-:1024:1:E3DBAEC3FEEA03E2:1342810728:::-:::scSC:
839
+ fpr:::::::::444B0870D985CF70BBB7F4DCE3DBAEC3FEEA03E2:
840
+ uid:-::::1342810796::4D1B8CC29335BF79232CA71210F75CF80318B06A::Backup Test <backup13@foo.com>:
841
+ uid:-::::1342810728::F1422363E8DC1EC3076906505CE66855BB44CAB7::Backup Test <backup14@foo.com>:
842
+ sub:e:1024:1:C95DED316504D17C:1342810728:1342897218:::::e:
843
+ pub:u:1024:1:027B83DB8A82B9CB:1343402840:::u:::scESC:
844
+ fpr:::::::::A20D90150CE4E5F851AD3A9D027B83DB8A82B9CB:
845
+ uid:u::::1343402840::307F1E025E8BEB7DABCADC353291184AD493A28E::Backup Test <backup01@foo.com>:
846
+ sub:u:1024:1:EF31D36414FD8B2B:1343402840::::::e:
847
+ pub:u:1024:1:4CEA6442A4A57A76:1343402867:::u:::scESC:
848
+ fpr:::::::::5742EAFB4CF38014B474671E4CEA6442A4A57A76:
849
+ uid:u::::1343402932::C220D9FF5C9652AA31D3CE0487D88EFF291FA1ED::Backup Test:
850
+ uid:u::::1343402922::E89778553F703C26517AD8321C17C81F3213A782::Backup Test <backup02@foo.com>:
851
+ sub:u:1024:1:140DDC2E97DA3567:1343402867::::::e:
852
+ EOS
853
+ }
854
+
855
+ let(:valid_identifiers) {
856
+ %w{ FF9CFEA6 5EFD157FFF9CFEA6 72E56E48E362BB402B3344045EFD157FFF9CFEA6
857
+ 1E3DA3E8 570CE9221E3DA3E8 616BBC8409C1AED791F8E6F8570CE9221E3DA3E8
858
+ <backup04@foo.com> <backup03@foo.com>
859
+ A7641A16 54F81C93A7641A16 71335B9B960CF3A3071535F454F81C93A7641A16
860
+ <backup05@foo.com>
861
+ 581A88CF 0A5B6CC9581A88CF E8C459082544924B8AEA06280A5B6CC9581A88CF
862
+ <backup08@foo.com> <backup07@foo.com>
863
+ FEEA03E2 E3DBAEC3FEEA03E2 444B0870D985CF70BBB7F4DCE3DBAEC3FEEA03E2
864
+ <backup13@foo.com> <backup14@foo.com>
865
+ 8A82B9CB 027B83DB8A82B9CB A20D90150CE4E5F851AD3A9D027B83DB8A82B9CB
866
+ <backup01@foo.com>
867
+ A4A57A76 4CEA6442A4A57A76 5742EAFB4CF38014B474671E4CEA6442A4A57A76
868
+ <backup02@foo.com> }
869
+ }
870
+ it 'should return an array of all valid identifiers' do
871
+ encryptor.instance_variable_set(:@system_identifiers, nil)
872
+
873
+ encryptor.expects(:utility).with(:gpg).returns('gpg')
874
+ encryptor.expects(:base_options).returns("--base 'options'")
875
+ encryptor.expects(:run).with(
876
+ "gpg --base 'options' --with-colons --fixed-list-mode --fingerprint"
877
+ ).returns(gpg_output)
878
+
879
+ encryptor.send(:system_identifiers).should == valid_identifiers
880
+ # results cached
881
+ encryptor.send(:system_identifiers).should == valid_identifiers
882
+ encryptor.instance_variable_get(:@system_identifiers).
883
+ should == valid_identifiers
884
+ end
885
+ end # describe '#system_identifiers'
886
+
887
+ describe 'deprecations' do
888
+ describe '#key' do
889
+ it 'should import #key, use identifier for #recipients and log a warning' do
890
+ encryptor.expects(:import_key).
891
+ with('deprecated :key', 'a public key').
892
+ returns('an_identifier')
893
+
894
+ Backup::Logger.expects(:warn).with {|err|
895
+ err.should be_an_instance_of(Backup::Errors::ConfigurationError)
896
+ err.message.should match(
897
+ "GPG#key has been deprecated as of backup v.3.0.26"
898
+ )
899
+ err.message.should match(
900
+ "replaced with #keys and #recipients"
901
+ )
902
+ }
903
+
904
+ encryptor.key = 'a public key'
905
+ encryptor.recipients.should == 'an_identifier'
906
+ end
907
+ end # describe '#key'
908
+ end # describe 'deprecations'
150
909
  end