kitchen-scribe 0.1.0 → 0.2.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.
@@ -24,10 +24,10 @@ class Chef
24
24
 
25
25
  include Chef::Mixin::ShellOut
26
26
 
27
- DEFAULT_CHRONICLE_PATH = ".chronicle"
28
- DEFAULT_REMOTE_NAME = "origin"
29
- DEFAULT_BRANCH = "master"
30
- DEFAULT_COMMIT_MESSAGE = 'Commiting chef state as of %TIME%'
27
+ DEFAULT_CHRONICLE_PATH = ".chronicle" unless const_defined?(:DEFAULT_CHRONICLE_PATH)
28
+ DEFAULT_REMOTE_NAME = "origin" unless const_defined?(:DEFAULT_REMOTE_NAME)
29
+ DEFAULT_BRANCH = "master" unless const_defined?(:DEFAULT_BRANCH)
30
+ DEFAULT_COMMIT_MESSAGE = 'Commiting chef state as of %TIME%' unless const_defined?(:DEFAULT_COMMIT_MESSAGE)
31
31
 
32
32
  banner "knife scribe copy"
33
33
 
@@ -25,8 +25,8 @@ class Chef
25
25
 
26
26
  include Chef::Mixin::ShellOut
27
27
 
28
- DEFAULT_CHRONICLE_PATH = ".chronicle"
29
- DEFAULT_REMOTE_NAME = "origin"
28
+ DEFAULT_CHRONICLE_PATH = ".chronicle" unless const_defined?(:DEFAULT_CHRONICLE_PATH)
29
+ DEFAULT_REMOTE_NAME = "origin" unless const_defined?(:DEFAULT_REMOTE_NAME)
30
30
 
31
31
  banner "knife scribe hire"
32
32
 
@@ -1,4 +1,4 @@
1
1
  module KitchenScribe
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  MAJOR, MINOR, TINY = VERSION.split('.')
4
4
  end
@@ -0,0 +1,916 @@
1
+ #
2
+ # Author:: Pawel Kozlowski (<pawel.kozlowski@u2i.com>)
3
+ # Copyright:: Copyright (c) 2013 Pawel Kozlowski
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require File.expand_path('../../../spec_helper', __FILE__)
20
+
21
+ describe Chef::Knife::ScribeAdjust do
22
+ before(:each) do
23
+ @scribe = Chef::Knife::ScribeAdjust.new
24
+ @scribe.stub(:ui).and_return(double("ui", :fatal => nil, :error => nil))
25
+ end
26
+
27
+ it "responds to #action_merge" do
28
+ @scribe.should respond_to(:action_merge)
29
+ end
30
+
31
+ it "responds to #action_hash_only_merge" do
32
+ @scribe.should respond_to(:action_hash_only_merge)
33
+ end
34
+
35
+ it "responds to #action_overwrite" do
36
+ @scribe.should respond_to(:action_overwrite)
37
+ end
38
+
39
+ it "responds to #action_delete" do
40
+ @scribe.should respond_to(:action_delete)
41
+ end
42
+
43
+ describe "#run" do
44
+
45
+ describe "when no files were given as parameters" do
46
+ before(:each) do
47
+ @scribe.name_args = [ ]
48
+ end
49
+
50
+ it "should show usage and exit if not filename is provided" do
51
+ @scribe.name_args = []
52
+ @scribe.ui.should_receive(:fatal).with("At least one adjustment file needs to be specified!")
53
+ @scribe.should_receive(:show_usage)
54
+ lambda { @scribe.run }.should raise_error(SystemExit)
55
+ end
56
+ end
57
+
58
+ describe "when files were given in parameters" do
59
+ before(:each) do
60
+ @scribe.name_args = [ "spec1.json", "spec2.json" ]
61
+ @scribe.stub(:generate_template)
62
+ @scribe.stub(:parse_adjustments)
63
+ end
64
+
65
+ describe "when generate option has been provided" do
66
+ before(:each) do
67
+ @scribe.config[:generate] = true
68
+ end
69
+
70
+ it "generates adjustment templates for each filename specified" do
71
+ @scribe.name_args.each { |filename| @scribe.should_receive(:generate_template).with(filename) }
72
+ @scribe.run
73
+ end
74
+
75
+ it "doesn't call #parse_adjustments" do
76
+ @scribe.should_not_receive(:parse_adjustments)
77
+ @scribe.run
78
+ end
79
+ end
80
+
81
+ describe "when generate option hasn't been provided" do
82
+ before(:each) do
83
+ @scribe.config[:generate] = false
84
+ end
85
+
86
+ it "doesn't generates adjustment templates for each filename specified" do
87
+ @scribe.should_not_receive(:generate_template)
88
+ @scribe.run
89
+ end
90
+
91
+ it "calls #parse_adjustments method" do
92
+ @scribe.should_receive(:parse_adjustments)
93
+ @scribe.run
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ describe "#parse_adjustments" do
100
+ before(:each) do
101
+ @scribe.stub(:write_adjustments)
102
+ @scribe.stub(:parse_adjustment_file)
103
+ @scribe.stub(:hire)
104
+ @scribe.stub(:record_state)
105
+ @scribe.stub(:diff)
106
+ end
107
+
108
+ it "initializes error structure for each file" do
109
+ @scribe.name_args.each { |filename| @scribe.errors.should_receive(:push).with({"name" => filename, "general" => nil, "adjustments" => {}}) }
110
+ @scribe.parse_adjustments
111
+ end
112
+
113
+ it "parses all adjustments specified" do
114
+ @scribe.name_args.each { |filename| @scribe.should_receive(:parse_adjustment_file).with(filename) }
115
+ @scribe.parse_adjustments
116
+ end
117
+
118
+ describe "when dryrun option has been provided" do
119
+ before(:each) do
120
+ @scribe.config[:dryrun] = true
121
+ end
122
+
123
+ it "prints a diff of all adjustments" do
124
+ @scribe.should_receive(:diff)
125
+ @scribe.parse_adjustments
126
+ end
127
+
128
+ it "doesn't atttempt to writes out adjustments" do
129
+ @scribe.should_not_receive(:write_adjustments)
130
+ @scribe.parse_adjustments
131
+ end
132
+
133
+ describe "when errors occured" do
134
+ before(:each) do
135
+ @scribe.stub(:errors?).and_return(true)
136
+ end
137
+
138
+ it "prints errors but does not exit" do
139
+ @scribe.should_receive(:print_errors)
140
+ lambda { @scribe.parse_adjustments }.should_not raise_error(SystemExit)
141
+ end
142
+ end
143
+ end
144
+
145
+ describe "when dryrun option hasn't been provided" do
146
+ before(:each) do
147
+ @scribe.config[:dryrun] = false
148
+ end
149
+
150
+ describe "when no errors occured" do
151
+ before(:each) do
152
+ @scribe.stub(:errors?).and_return(false)
153
+ end
154
+
155
+ it "doesn't print errors" do
156
+ @scribe.should_not_receive(:print_errors)
157
+ @scribe.parse_adjustments
158
+ end
159
+
160
+ it "writes out all adjustments" do
161
+ @scribe.should_receive(:write_adjustments)
162
+ @scribe.parse_adjustments
163
+ end
164
+
165
+ describe "when document option has been enabled" do
166
+ before(:each) do
167
+ @scribe.config[:document] = true
168
+ @scribe.descriptions.push("Foo").push("Bar\t\n")
169
+ end
170
+
171
+ it "hires a scribe" do
172
+ @scribe.should_receive(:hire)
173
+ @scribe.parse_adjustments
174
+ end
175
+
176
+ it "records the initial and final state of the system with a striped description" do
177
+ @scribe.should_receive(:record_state).with(no_args()).ordered
178
+ @scribe.should_receive(:record_state).with("Foo\nBar").ordered
179
+ @scribe.parse_adjustments
180
+ end
181
+ end
182
+
183
+ describe "when document option hasn't been enabled" do
184
+ before(:each) do
185
+ @scribe.config[:document] = false
186
+ end
187
+
188
+ it "doesn't hire a scribe" do
189
+ @scribe.should_not_receive(:hire)
190
+ @scribe.parse_adjustments
191
+ end
192
+
193
+ it "doesn't record the initial and final state of the system" do
194
+ @scribe.should_not_receive(:record_state)
195
+ @scribe.parse_adjustments
196
+ end
197
+ end
198
+ end
199
+
200
+ describe "when errors occured" do
201
+ before(:each) do
202
+ @scribe.stub(:errors?).and_return(true)
203
+ end
204
+
205
+ it "doesn't write out any adjustments" do
206
+ @scribe.should_not_receive(:write_adjustments)
207
+ lambda { @scribe.parse_adjustments }.should raise_error(SystemExit)
208
+ end
209
+
210
+ it "prints errors" do
211
+ @scribe.should_receive(:print_errors)
212
+ lambda { @scribe.parse_adjustments }.should raise_error(SystemExit)
213
+ end
214
+ end
215
+ end
216
+
217
+ end
218
+
219
+ describe "#generate_template" do
220
+ before(:each) do
221
+ @f1 = double()
222
+ @f1.stub(:write)
223
+ @filename = "spec1.json"
224
+ end
225
+
226
+ describe "when type param is 'enviroment'" do
227
+ before(:each) do
228
+ @scribe.config[:type] = "environment"
229
+ end
230
+
231
+ it "saves the environment template JSON into the specified file" do
232
+ File.should_receive(:open).with(@filename, "w").and_yield(@f1)
233
+ Chef::Knife::ScribeAdjust::TEMPLATE_HASH["adjustments"] = [Chef::Knife::ScribeAdjust::ENVIRONMENT_ADJUSTMENT_TEMPLATE]
234
+ @f1.should_receive(:write).with(JSON.pretty_generate(Chef::Knife::ScribeAdjust::TEMPLATE_HASH))
235
+ @scribe.generate_template @filename
236
+ end
237
+ end
238
+
239
+ describe "when type param is 'node'" do
240
+ before(:each) do
241
+ @scribe.config[:type] = "node"
242
+ end
243
+
244
+ it "saves the environment template JSON into the specified file" do
245
+ File.should_receive(:open).with(@filename, "w").and_yield(@f1)
246
+ Chef::Knife::ScribeAdjust::TEMPLATE_HASH["adjustments"] = [Chef::Knife::ScribeAdjust::NODE_ADJUSTMENT_TEMPLATE]
247
+ @f1.should_receive(:write).with(JSON.pretty_generate(Chef::Knife::ScribeAdjust::TEMPLATE_HASH))
248
+ @scribe.generate_template @filename
249
+ end
250
+ end
251
+
252
+ describe "when type param is 'role'" do
253
+ before(:each) do
254
+ @scribe.config[:type] = "role"
255
+ end
256
+
257
+ it "saves the environment template JSON into the specified file" do
258
+ File.should_receive(:open).with(@filename, "w").and_yield(@f1)
259
+ Chef::Knife::ScribeAdjust::TEMPLATE_HASH["adjustments"] = [Chef::Knife::ScribeAdjust::ROLE_ADJUSTMENT_TEMPLATE]
260
+ @f1.should_receive(:write).with(JSON.pretty_generate(Chef::Knife::ScribeAdjust::TEMPLATE_HASH))
261
+ @scribe.generate_template @filename
262
+ end
263
+ end
264
+
265
+ describe "when type param is not recognized" do
266
+ before(:each) do
267
+ @scribe.config[:type] = "xxxx"
268
+ end
269
+
270
+ it "throws an error through the ui and returns" do
271
+ @scribe.ui.should_receive(:fatal).with("Incorrect adjustment type! Only 'node', 'environment' or 'role' allowed.")
272
+ lambda { @scribe.generate_template @filename }.should raise_error(SystemExit)
273
+ end
274
+ end
275
+ end
276
+
277
+ describe "record_state" do
278
+ before(:each) do
279
+ @copyist_config = double("copyist config", :[]= => nil)
280
+ @copyist = double("copyist", :run => nil, :config => @copyist_config)
281
+ @scribe.config[:chronicle_path] = "test_path"
282
+ @scribe.config[:remote_url] = "test_remote_url"
283
+ @scribe.config[:remote_name] = "test_remote_name"
284
+ Chef::Knife::ScribeCopy.stub(:new).and_return(@copyist)
285
+ end
286
+
287
+ describe "when called for the first time" do
288
+ it "creates and runs a new instance of ScribeCopy" do
289
+ Chef::Knife::ScribeCopy.should_receive(:new).and_return(@copyist)
290
+ @copyist.should_receive(:run)
291
+ @scribe.record_state
292
+ end
293
+
294
+ it "passes all relevant config variables to the hired scribe" do
295
+ [:chronicle_path, :remote_name, :branch].each { |key| @copyist_config.should_receive(:[]=).with(key, @scribe.config[key]) }
296
+ @scribe.record_state
297
+ end
298
+ end
299
+
300
+ describe "when not called for the first time" do
301
+ before(:each) do
302
+ @scribe.record_state
303
+ end
304
+
305
+ it "creates and runs a new instance of ScribeCopy" do
306
+ Chef::Knife::ScribeCopy.should_not_receive(:new)
307
+ @copyist.should_receive(:run)
308
+ @scribe.record_state
309
+ end
310
+
311
+ it "doesn't reconfigure the copyist" do
312
+ [:chronicle_path, :remote_name, :branch].each { |key| @copyist_config.should_not_receive(:[]=).with(key, @scribe.config[key]) }
313
+ @scribe.record_state
314
+ end
315
+ end
316
+
317
+ it "passes its argument as the message for the scribe" do
318
+ arg = double("argument")
319
+ @copyist_config.should_receive(:[]=).with(:message, arg)
320
+ @scribe.record_state(arg)
321
+ end
322
+ end
323
+
324
+ describe "hire" do
325
+ before(:each) do
326
+ @hired_scribe_config = double("hired scribe config", :[]= => nil)
327
+ @hired_scribe = double("hired scribe", :run => nil, :config => @hired_scribe_config)
328
+ @scribe.config[:chronicle_path] = "test_path"
329
+ @scribe.config[:remote_url] = "test_remote_url"
330
+ @scribe.config[:remote_name] = "test_remote_name"
331
+ Chef::Knife::ScribeHire.stub(:new).and_return(@hired_scribe)
332
+ end
333
+
334
+ it "creates and runs a new instance of ScribeHire" do
335
+ Chef::Knife::ScribeHire.should_receive(:new).and_return(@hired_scribe)
336
+ @hired_scribe.should_receive(:run)
337
+ @scribe.hire
338
+ end
339
+
340
+ it "passes all relevant config variables to the hired scribe" do
341
+ [:chronicle_path, :remote_url, :remote_name].each { |key| @hired_scribe_config.should_receive(:[]=).with(key, @scribe.config[key]) }
342
+ @scribe.hire
343
+ end
344
+ end
345
+
346
+ describe "#adjustment_file_valid?" do
347
+ before(:each) do
348
+ @scribe.errors.push({"name" => "filename", "general" => nil, "adjustments" => {}})
349
+ end
350
+
351
+ describe "when the contants of the file is not a Hash" do
352
+ it "saves an appropriate general error to the error hash and returns false" do
353
+ [1,[],nil,"test"].each do |not_a_hash|
354
+ @scribe.errors.last.should_receive(:[]=).with("general", "Adjustment file must contain a JSON hash!")
355
+ @scribe.adjustment_file_valid?(not_a_hash).should be_false
356
+ end
357
+ end
358
+ end
359
+
360
+ describe "when the adjustment hash is missing 'adjustments'" do
361
+ it "saves an appropriate general error to the error hash and returns false" do
362
+ parsed_file = { "author_email" => "test@mail.com",
363
+ "author_name" => "test",
364
+ "description" => "test description"
365
+ }
366
+ @scribe.errors.last.should_receive(:[]=).with("general", "Adjustment file must contain an array of adjustments!")
367
+ @scribe.adjustment_file_valid?(parsed_file).should be_false
368
+ end
369
+ end
370
+
371
+ describe "when the adjustment hash is missing 'adjustments' key or it doesn't point to an array" do
372
+ it "saves an appropriate general error to the error hash and returns false" do
373
+ parsed_file = { "author_email" => "test@mail.com",
374
+ "author_name" => "test",
375
+ "description" => "test description",
376
+ "adjustments" => 1
377
+ }
378
+ [1,{},nil,"test"].each do |not_an_array|
379
+ parsed_file["adjustments"] = not_an_array
380
+ @scribe.errors.last.should_receive(:[]=).with("general", "Adjustment file must contain an array of adjustments!")
381
+ @scribe.adjustment_file_valid?(parsed_file).should be_false
382
+ end
383
+ end
384
+ end
385
+ end
386
+
387
+ describe "#adjustment_valid?" do
388
+ before(:each) do
389
+ @scribe.errors.push({"name" => "filename", "general" => nil, "adjustments" => {}})
390
+ end
391
+
392
+ describe "when the adjustment hash is not a Hash" do
393
+ it "writes an appropriate adjustment related message into the errors hash and returns false" do
394
+ [1,[],nil,"test"].each do |not_a_hash|
395
+ @scribe.errors.last["adjustments"].should_receive(:store).with(0, "Adjustment must be a JSON hash!")
396
+ @scribe.adjustment_valid?(not_a_hash, 0).should be_false
397
+ end
398
+ end
399
+ end
400
+
401
+ describe "when the adjustment hash is missing a value" do
402
+ it "writes an appropriate adjustment related message into the errors hash and returns false" do
403
+ complete_params_hash = { "action" => "merge",
404
+ "type" => "node",
405
+ "search" => "name:test",
406
+ "adjustment" => { }
407
+ }
408
+ complete_params_hash.keys.each do |missing_param|
409
+ incomplete_params_hash = complete_params_hash.clone
410
+ incomplete_params_hash.delete(missing_param)
411
+ @scribe.errors.last["adjustments"].should_receive(:store).with(0, "Adjustment hash must contain " + missing_param + "!")
412
+ @scribe.adjustment_valid?(incomplete_params_hash, 0).should be_false
413
+ end
414
+ end
415
+ end
416
+
417
+ describe "when action is incorrect" do
418
+ before(:each) do
419
+ @adjustment_hash = { "action" => "xxxxxx",
420
+ "type" => "environment",
421
+ "search" => "test",
422
+ "adjustment" => { "a" => 1, "b" => 2 }
423
+ }
424
+ end
425
+
426
+ it "returns false with ui fatal message" do
427
+ @scribe.should_receive(:respond_to?).with("action_" + @adjustment_hash["action"]).and_return(false)
428
+ @scribe.errors.last["adjustments"].should_receive(:store).with(0, "Incorrect action!")
429
+ @scribe.adjustment_valid?(@adjustment_hash, 0).should be_false
430
+ end
431
+ end
432
+
433
+ describe "when action is correct" do
434
+ before(:each) do
435
+ @adjustment_hash = { "action" => "merge",
436
+ "type" => "environment",
437
+ "search" => "test",
438
+ "adjustment" => { "a" => 1, "b" => 2 }
439
+ }
440
+ end
441
+
442
+ it "returns true without any ui message" do
443
+ @scribe.should_receive(:respond_to?).with("action_" + @adjustment_hash["action"]).and_return(true)
444
+ @scribe.errors.last["adjustments"].should_not_receive(:store).with(0, "Incorrect action!")
445
+ @scribe.adjustment_valid?(@adjustment_hash, 0).should be_true
446
+ end
447
+ end
448
+ end
449
+
450
+ describe "#write_adjustments" do
451
+ before(:each) do
452
+ @adjusted_env = double("adjusted environment")
453
+ @adjusted_env.stub(:[]).with("chef_type").and_return("environment")
454
+ @adjusted_node = double("adjusted node")
455
+ @adjusted_node.stub(:[]).with("chef_type").and_return("node")
456
+ @scribe.changes["environment:test_env"] = {
457
+ "original" => { "chef_type" => "environment", "name" => "test_env" },
458
+ "adjusted" => @adjusted_env
459
+ }
460
+ @scribe.changes["node:test_node"] = {
461
+ "original" => { "chef_type" => "node", "name" => "test_node" },
462
+ "adjusted" => @adjusted_node
463
+ }
464
+ @env_class = double("env class")
465
+ @node_class = double("node class")
466
+ @env_object = double("env object")
467
+ @node_object = double("node_object")
468
+ end
469
+
470
+ it "saves each adjusted version on the chef server" do
471
+ Chef.should_receive(:const_get).with("Environment").and_return(@env_class)
472
+ @env_class.should_receive(:json_create).with(@adjusted_env).and_return(@env_object)
473
+ @env_object.should_receive(:save)
474
+ Chef.should_receive(:const_get).with("Node").and_return(@node_class)
475
+ @node_class.should_receive(:json_create).with(@adjusted_node).and_return(@node_object)
476
+ @node_object.should_receive(:save)
477
+ @scribe.write_adjustments
478
+ end
479
+ end
480
+
481
+ describe "#parse_adjustment_file" do
482
+ before(:each) do
483
+ @filename = "spec1.json"
484
+ @file = double("adjustment file")
485
+ File.stub(:open).and_yield(@file)
486
+ File.stub(:open).with(@filename, "r").and_yield(@file)
487
+ @adjustment_hash = { "author_name" => "test",
488
+ "author_email" => "test@test.com",
489
+ "description" => "test description",
490
+ "adjustments" => [{ "action" => "BAD_ACTION",
491
+ "type" => "environment",
492
+ "search" => "test",
493
+ "adjustment" => { "a" => 1, "b" => 2 }
494
+ },
495
+ { "action" => "delete",
496
+ "type" => "node",
497
+ "search" => "foo:bar",
498
+ "adjustment" => [ "c" ]
499
+ }
500
+ ]
501
+ }
502
+ File.stub(:exists?).and_return(true)
503
+ @scribe.stub(:apply_adjustment)
504
+ @scribe.errors.push({"name" => @filename, "general" => nil, "adjustments" => {}})
505
+ end
506
+
507
+ it "checks if the file exists" do
508
+ File.should_receive(:exists?).with(@filename).and_return(false)
509
+ @scribe.parse_adjustment_file(@filename)
510
+ end
511
+
512
+ describe "when the file does not exist" do
513
+ before(:each) do
514
+ File.stub(:exists?).and_return(false)
515
+ end
516
+
517
+ it "writes a general error into the errors hash" do
518
+ @scribe.errors.last.should_receive(:[]=).with("general", "File does not exist!")
519
+ @scribe.parse_adjustment_file(@filename)
520
+ end
521
+
522
+ it "doesn't attempt to open the file" do
523
+ File.should_not_receive(:open).with(@filename)
524
+ @scribe.parse_adjustment_file(@filename)
525
+ end
526
+ end
527
+
528
+ it "parses the adjustment file" do
529
+ File.should_receive(:open).with(@filename, "r").and_yield(@file)
530
+ JSON.should_receive(:load).with(@file).and_return(@adjustment_hash)
531
+ @scribe.parse_adjustment_file(@filename)
532
+ end
533
+
534
+ describe "when the JSON file is malformed" do
535
+ it "returns writes a fatal error through the ui" do
536
+ @scribe.errors.last.should_receive(:[]=).with("general", "Malformed JSON!")
537
+ @scribe.parse_adjustment_file(@filename)
538
+ end
539
+
540
+ it "doesn't throw an exception" do
541
+ File.should_receive(:open).with(@filename, "r").and_yield('{"a" : 3, b => ]')
542
+ lambda { @scribe.parse_adjustment_file(@filename) }.should_not raise_error(JSON::ParserError)
543
+ end
544
+ end
545
+
546
+ describe "when the file exists and is well formed" do
547
+ before(:each) do
548
+ JSON.stub(:load).and_return(@adjustment_hash)
549
+ end
550
+
551
+ it "checks if the adjustment file is valid" do
552
+ @scribe.should_receive(:adjustment_file_valid?).with(@adjustment_hash).and_return(false)
553
+ @scribe.parse_adjustment_file(@filename)
554
+ end
555
+
556
+ describe "if the adjustment file is correct" do
557
+ it "applies each adjustment if it's correct'" do
558
+ @scribe.should_receive(:adjustment_valid?).with(@adjustment_hash["adjustments"][0], 0).and_return(false)
559
+ @scribe.should_receive(:adjustment_valid?).with(@adjustment_hash["adjustments"][1], 1).and_return(true)
560
+ @scribe.should_receive(:apply_adjustment).with(@adjustment_hash["adjustments"][1])
561
+ @scribe.parse_adjustment_file(@filename)
562
+ end
563
+ end
564
+
565
+ describe "if all adjustments are correct" do
566
+ before(:each) do
567
+ @scribe.stub(:adjustment_valid?).and_return(true)
568
+ end
569
+
570
+ it "adds the description to the descriptions array" do
571
+ @scribe.descriptions.should_receive(:push).with(@adjustment_hash["description"])
572
+ @scribe.parse_adjustment_file(@filename)
573
+ end
574
+ end
575
+
576
+
577
+ describe "if at least one adjustment was correct" do
578
+ before(:each) do
579
+ @scribe.stub(:adjustment_valid?).and_return(true,false)
580
+ @scribe.errors.last["adjustments"].store(1, "Foo")
581
+ end
582
+
583
+ it "adds the description to the descriptions array" do
584
+ @scribe.descriptions.should_receive(:push).with(@adjustment_hash["description"] + "[with errors]")
585
+ @scribe.parse_adjustment_file(@filename)
586
+ end
587
+ end
588
+
589
+ describe "if no adjustment was correct" do
590
+ before(:each) do
591
+ @scribe.stub(:adjustment_valid?).and_return(false,false)
592
+ @scribe.errors.last["adjustments"].store(0, "Foo")
593
+ @scribe.errors.last["adjustments"].store(1, "Bar")
594
+ end
595
+
596
+ it "doesn't add the description to the descriptions array" do
597
+ @scribe.descriptions.should_not_receive(:push).with(@adjustment_hash["description"])
598
+ @scribe.parse_adjustment_file(@filename)
599
+ end
600
+ end
601
+ end
602
+ end
603
+
604
+ describe "#apply_adjustment" do
605
+ before(:each) do
606
+ @adjustment = { "action" => "merge",
607
+ "type" => "environment",
608
+ "search" => "test",
609
+ "adjustment" => { "a" => 1, "b" => 2 }
610
+ }
611
+
612
+ @scribe.stub(:adjustment_valid?).and_return(true)
613
+ @query = double("Chef query")
614
+ Chef::Search::Query.stub(:new).and_return(@query)
615
+ @chef_obj = double("chef_object")
616
+ @chef_obj.stub(:to_hash).and_return( { "name" => "test_name", "chef_type" => "test_type", "a" => 3, "c" => 3 } )
617
+ chef_obj_class = double("chef_object_class")
618
+ json_create_return_obj = double("final_chef_object")
619
+ json_create_return_obj.stub(:save)
620
+ chef_obj_class.stub(:json_create).and_return(json_create_return_obj)
621
+ @chef_obj.stub(:class).and_return(chef_obj_class)
622
+ @query.stub(:search).and_yield(@chef_obj)
623
+ end
624
+
625
+ describe "when search parameter doesn't contain a ':' character" do
626
+ before(:each) do
627
+ @adjustment["search"] = "test_name"
628
+ end
629
+
630
+ it "performs a search using the 'search' parameter as a name" do
631
+ @query.should_receive(:search).with(@adjustment["type"], "name:" + @adjustment["search"])
632
+ @scribe.apply_adjustment(@adjustment)
633
+ end
634
+ end
635
+
636
+ describe "when search parameter contains a ':' character" do
637
+ before(:each) do
638
+ @adjustment["search"] = "foo:test_name"
639
+ end
640
+
641
+ it "performs a search using the 'search' parameter as a complete query" do
642
+ @query.should_receive(:search).with(@adjustment["type"], @adjustment["search"])
643
+ @scribe.apply_adjustment(@adjustment)
644
+ end
645
+ end
646
+
647
+ it "checks if the a change to a given object is already pending" do
648
+ @scribe.changes.should_receive(:has_key?).with(@chef_obj.to_hash["chef_type"] + ":" + @chef_obj.to_hash["name"])
649
+ @scribe.apply_adjustment(@adjustment)
650
+ end
651
+
652
+ describe "if the key doesn't exist" do
653
+ it "saves the original in the changes hash" do
654
+ @scribe.changes.should_receive(:store).with(@chef_obj.to_hash["chef_type"] + ":" + @chef_obj.to_hash["name"],
655
+ { "original" => @chef_obj.to_hash }
656
+ ).and_call_original
657
+ @scribe.apply_adjustment(@adjustment)
658
+ end
659
+ end
660
+
661
+ it "applies each subsequent adjustment to the already adjusted version" do
662
+ adjusted_hash = @chef_obj.to_hash.dup.merge({"a" => "b"})
663
+ changes_hash = { "original" => @chef_obj.to_hash, "adjusted" => adjusted_hash }
664
+ @scribe.changes[@chef_obj.to_hash["chef_type"] + ":" + @chef_obj.to_hash["name"]] = changes_hash
665
+ @scribe.should_receive(("action_" + @adjustment["action"]).to_sym).with(adjusted_hash, @adjustment["adjustment"])
666
+ @scribe.apply_adjustment(@adjustment)
667
+ end
668
+
669
+ it "saves the changed version in the changes hash" do
670
+ adjusted_hash = @chef_obj.to_hash.dup.merge({ "a" => "b" })
671
+ changes_hash = { "original" => @chef_obj.to_hash, "adjusted" => adjusted_hash }
672
+ @scribe.changes[@chef_obj.to_hash["chef_type"] + ":" + @chef_obj.to_hash["name"]] = changes_hash
673
+ adjusted_hash = @scribe.send(("action_" + @adjustment["action"]).to_sym, adjusted_hash, @adjustment["adjustment"])
674
+ changes_hash.should_receive(:store).with("adjusted", adjusted_hash).and_call_original
675
+ @scribe.apply_adjustment(@adjustment)
676
+ end
677
+ end
678
+
679
+ describe "#action_overwrite" do
680
+ it "performs a standard hash merge when both base and overwrite_with are hashes" do
681
+ base = { "a" => 1, "b" => [1,2,3], "c" => { "x" => 1, "y" => 2 } }
682
+ overwrite_with = { "b" => [4], "c" => { "z" => 1, "y" => 3}, "d" => 3 }
683
+ base.should_receive(:merge).with(overwrite_with)
684
+ @scribe.action_overwrite(base,overwrite_with)
685
+ end
686
+
687
+ it "returns base hash if overwrite_with is nil" do
688
+ base = {"foo" => "bar"}
689
+ overwrite_with = nil
690
+ result = @scribe.action_overwrite(base,overwrite_with)
691
+ result.should eq(base)
692
+ end
693
+
694
+ it "returns the overwrite if base is not a hash" do
695
+ base = "test"
696
+ overwrite_with = {"a" => 1}
697
+ result = @scribe.action_overwrite(base,overwrite_with)
698
+ result.should eq(overwrite_with)
699
+ end
700
+ end
701
+
702
+ describe "#deep_delete" do
703
+ it "calls #seedp_delete! with duplicates of it's arguments" do
704
+ delete_from = double("delete_from")
705
+ delete_spec = double("delete_spec")
706
+ delete_from_dup = double("delete_from_dup")
707
+ delete_spec_dup = double("delete_spec_dup")
708
+ delete_from.should_receive(:dup).and_return(delete_from_dup)
709
+ delete_spec.should_receive(:dup).and_return(delete_spec_dup)
710
+ @scribe.should_receive(:deep_delete!).with(delete_from_dup, delete_spec_dup)
711
+ @scribe.deep_delete(delete_from, delete_spec)
712
+ end
713
+ end
714
+
715
+ describe "#deep_delete!" do
716
+ describe "when both base and overwrite_with are hashes" do
717
+ before(:each) do
718
+ @delete_from = { "a" => 1, "b" => [3,2,1], "c" => { "x" => 1, "y" => 2 } }
719
+ end
720
+
721
+ describe "when the spec intructs it to delete a top level key" do
722
+ before(:each) do
723
+ @delete_spec = "c"
724
+ end
725
+
726
+ it "deletes it" do
727
+ @scribe.deep_delete!(@delete_from,@delete_spec).keys.should_not include("c")
728
+ end
729
+ end
730
+
731
+ describe "when the spec intructs it to delete a nested key" do
732
+ before(:each) do
733
+ @delete_spec = { "c" => "x" }
734
+ end
735
+
736
+ it "doesn't delete the top level key" do
737
+ @scribe.deep_delete!(@delete_from,@delete_spec).keys.should include("c")
738
+ end
739
+
740
+ it "deletes it" do
741
+ @scribe.deep_delete!(@delete_from,@delete_spec)["c"].keys.should_not include("x")
742
+ end
743
+ end
744
+
745
+ describe "when the spec intructs it to delete an array key that exists" do
746
+ before(:each) do
747
+ @delete_spec = { "b" => 1 }
748
+ end
749
+
750
+ it "deletes it" do
751
+ @scribe.deep_delete!(@delete_from,@delete_spec)["b"].should eq([3,1])
752
+ end
753
+ end
754
+
755
+ describe "when the spec intructs it to delete an array key that doesn't exist" do
756
+ before(:each) do
757
+ @delete_spec = { "b" => 10 }
758
+ end
759
+
760
+ it "does nothing" do
761
+ @scribe.deep_delete!(@delete_from,@delete_spec)["b"].should eq(@delete_from["b"])
762
+ end
763
+ end
764
+
765
+ describe "when the spec intructs it to delete a hash key that doesn't exist" do
766
+ before(:each) do
767
+ @delete_spec = { "c" => { "not_here" => [1,2] } }
768
+ end
769
+
770
+ it "does nothing" do
771
+ @scribe.deep_delete!(@delete_from,@delete_spec)["c"].should eq(@delete_from["c"])
772
+ end
773
+ end
774
+
775
+ describe "when the spec intructs it to delete a set of nested keys" do
776
+ before(:each) do
777
+ @delete_spec = { "c" => ["x", "y"] }
778
+ end
779
+
780
+ it "doesn't delete the top level key" do
781
+ @scribe.deep_delete!(@delete_from,@delete_spec).keys.should include("c")
782
+ end
783
+
784
+ it "deletes both of them" do
785
+ @scribe.deep_delete!(@delete_from,@delete_spec)["c"].keys.should_not include("x","y")
786
+ end
787
+ end
788
+
789
+ describe "when the spec intructs it to delete a set of top level keys" do
790
+ before(:each) do
791
+ @delete_spec = [ "b", "c"]
792
+ end
793
+
794
+ it "deletes both of them" do
795
+ @scribe.deep_delete!(@delete_from,@delete_spec).keys.should_not include("b","c")
796
+ end
797
+ end
798
+
799
+ end
800
+
801
+ it "returns delete_from if delete_spec is nil" do
802
+ delete_from = {"foo" => "bar"}
803
+ delete_spec = nil
804
+ result = @scribe.deep_delete!(delete_from,delete_spec)
805
+ result.should eq(delete_from)
806
+ end
807
+
808
+ it "returns delete_from if delete_from is not a hash or an array" do
809
+ base = "test"
810
+ overwrite_with = {"a" => 1}
811
+ result = @scribe.action_overwrite(base,overwrite_with)
812
+ result.should eq(overwrite_with)
813
+ end
814
+ end
815
+
816
+ describe "#errors?" do
817
+ describe "when no errors have occured" do
818
+ before(:each) do
819
+ for i in 1..3
820
+ @scribe.errors.push({"name" => "filename" + i.to_s, "general" => nil, "adjustments" => {}})
821
+ end
822
+ end
823
+
824
+ it "returns false" do
825
+ @scribe.errors?.should be_false
826
+ end
827
+ end
828
+
829
+ describe "when general error has occured" do
830
+ before(:each) do
831
+ for i in 1..3
832
+ @scribe.errors.push({"name" => "filename" + i.to_s, "general" => nil, "adjustments" => {}})
833
+ end
834
+ @scribe.errors.push({"name" => "filename4", "general" => "foo", "adjustments" => {}})
835
+ end
836
+
837
+ it "returns true" do
838
+ @scribe.errors?.should be_true
839
+ end
840
+ end
841
+
842
+ describe "when an adjustment specific error has occured" do
843
+ before(:each) do
844
+ @scribe.errors.push({"name" => "filename0", "general" => nil, "adjustments" => { 2 => "bar"}})
845
+ for i in 1..3
846
+ @scribe.errors.push({"name" => "filename" + i.to_s, "general" => nil, "adjustments" => {}})
847
+ end
848
+ end
849
+
850
+ it "returns true" do
851
+ @scribe.errors?.should be_true
852
+ end
853
+ end
854
+ end
855
+
856
+ describe "#print_errors" do
857
+ it "prints all the errors in the right format" do
858
+ @scribe.errors.push({"name" => "filename1", "general" => nil, "adjustments" => {}})
859
+ @scribe.errors.push({"name" => "filename2", "general" => nil, "adjustments" => { 2 => "bar"}})
860
+ @scribe.errors.push({"name" => "filename3", "general" => "Foo", "adjustments" => {}})
861
+ @scribe.ui.should_receive(:error).with("ERRORS OCCURED:")
862
+ @scribe.ui.should_receive(:error).with("filename2")
863
+ @scribe.ui.should_receive(:error).with("\t[Adjustment 2]: bar")
864
+ @scribe.ui.should_receive(:error).with("filename3")
865
+ @scribe.ui.should_receive(:error).with("\tFoo")
866
+ @scribe.print_errors
867
+ end
868
+ end
869
+
870
+ describe "#diff" do
871
+ before(:each) do
872
+ @scribe.changes["environment:test_env"] = {
873
+ "original" => { "chef_type" => "environment", "name" => "test_env", "default_attributes" => { "foo" => "bar" }},
874
+ "adjusted" => { "chef_type" => "environment", "name" => "test_env", "default_attributes" => {}}
875
+ }
876
+ @scribe.changes["node:test_node"] = {
877
+ "original" => { "chef_type" => "node", "name" => "test_node" , "attributes" => { "foo" => "bar" }},
878
+ "adjusted" => { "chef_type" => "node", "name" => "test_node" , "attributes" => { "foo" => "bar2" }}
879
+ }
880
+ @original_file = double("original file", :write => nil, :close => nil, :unlink => nil, :path => "original_path", :rewind => nil)
881
+ @adjusted_file = double("adjusted file", :write => nil, :close => nil, :unlink => nil, :path => "adjusted_path", :rewind => nil)
882
+ @scribe.ui.stub(:info)
883
+ @scribe.stub(:shell_out).and_return(double("command", :stdout => "aaa"))
884
+ end
885
+
886
+ it "creates and then deletes two temp files" do
887
+ Tempfile.should_receive(:new).with("original").and_return(@original_file)
888
+ Tempfile.should_receive(:new).with("adjusted").and_return(@adjusted_file)
889
+ @original_file.should_receive(:close)
890
+ @original_file.should_receive(:unlink)
891
+ @adjusted_file.should_receive(:close)
892
+ @adjusted_file.should_receive(:unlink)
893
+ @scribe.diff
894
+ end
895
+
896
+ describe "for each changed object" do
897
+ it "saves both original and adjusted hashes to the tempfiles" do
898
+ Tempfile.stub(:new).with("original").and_return(@original_file)
899
+ Tempfile.stub(:new).with("adjusted").and_return(@adjusted_file)
900
+ @scribe.changes.values.each do |change|
901
+ @original_file.should_receive(:write).with(JSON.pretty_generate(change["original"]))
902
+ @adjusted_file.should_receive(:write).with(JSON.pretty_generate(change["adjusted"]))
903
+ end
904
+ @scribe.diff
905
+ end
906
+
907
+ it "runs a diff on both files" do
908
+ @scribe.ui.should_receive(:info).with("[environment:test_env]")
909
+ @scribe.ui.should_receive(:info).with("aaa").twice
910
+ @scribe.ui.should_receive(:info).with("[node:test_node]")
911
+ @scribe.diff
912
+ end
913
+ end
914
+ end
915
+
916
+ end