summaryrefslogtreecommitdiff
path: root/spec/unit/knife_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/unit/knife_spec.rb')
-rw-r--r--spec/unit/knife_spec.rb295
1 files changed, 295 insertions, 0 deletions
diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb
new file mode 100644
index 0000000000..0517014db0
--- /dev/null
+++ b/spec/unit/knife_spec.rb
@@ -0,0 +1,295 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Fixtures for subcommand loading live in this namespace
+module KnifeSpecs
+end
+
+require 'spec_helper'
+
+describe Chef::Knife do
+ before(:each) do
+ Chef::Log.logger = Logger.new(StringIO.new)
+
+ Chef::Config[:node_name] = "webmonkey.example.com"
+ @knife = Chef::Knife.new
+ @knife.ui.stub!(:puts)
+ @knife.ui.stub!(:print)
+ Chef::Log.stub!(:init)
+ Chef::Log.stub!(:level)
+ [:debug, :info, :warn, :error, :crit].each do |level_sym|
+ Chef::Log.stub!(level_sym)
+ end
+ Chef::Knife.stub!(:puts)
+ @stdout = StringIO.new
+ end
+
+ describe "after loading a subcommand" do
+ before do
+ Chef::Knife.reset_subcommands!
+
+ if KnifeSpecs.const_defined?(:TestNameMapping)
+ KnifeSpecs.send(:remove_const, :TestNameMapping)
+ end
+
+ if KnifeSpecs.const_defined?(:TestExplicitCategory)
+ KnifeSpecs.send(:remove_const, :TestExplicitCategory)
+ end
+
+ Kernel.load(File.join(CHEF_SPEC_DATA, 'knife_subcommand', 'test_name_mapping.rb'))
+ Kernel.load(File.join(CHEF_SPEC_DATA, 'knife_subcommand', 'test_explicit_category.rb'))
+ end
+
+ it "has a category based on its name" do
+ KnifeSpecs::TestNameMapping.subcommand_category.should == 'test'
+ end
+
+ it "has an explictly defined category if set" do
+ KnifeSpecs::TestExplicitCategory.subcommand_category.should == 'cookbook site'
+ end
+
+ it "can reference the subcommand by its snake cased name" do
+ Chef::Knife.subcommands['test_name_mapping'].should equal(KnifeSpecs::TestNameMapping)
+ end
+
+ it "lists subcommands by category" do
+ Chef::Knife.subcommands_by_category['test'].should include('test_name_mapping')
+ end
+
+ it "lists subcommands by category when the subcommands have explicit categories" do
+ Chef::Knife.subcommands_by_category['cookbook site'].should include('test_explicit_category')
+ end
+
+ end
+
+ describe "after loading all subcommands" do
+ before do
+ Chef::Knife.reset_subcommands!
+ Chef::Knife.load_commands
+ end
+
+ it "references a subcommand class by its snake cased name" do
+ class SuperAwesomeCommand < Chef::Knife
+ end
+
+ Chef::Knife.load_commands
+
+ Chef::Knife.subcommands.should have_key("super_awesome_command")
+ Chef::Knife.subcommands["super_awesome_command"].should == SuperAwesomeCommand
+ end
+
+ it "guesses a category from a given ARGV" do
+ Chef::Knife.subcommands_by_category["cookbook"] << :cookbook
+ Chef::Knife.subcommands_by_category["cookbook site"] << :cookbook_site
+ Chef::Knife.guess_category(%w{cookbook foo bar baz}).should == 'cookbook'
+ Chef::Knife.guess_category(%w{cookbook site foo bar baz}).should == 'cookbook site'
+ Chef::Knife.guess_category(%w{cookbook site --help}).should == 'cookbook site'
+ end
+
+ it "finds a subcommand class based on ARGV" do
+ Chef::Knife.subcommands["cookbook_site_vendor"] = :CookbookSiteVendor
+ Chef::Knife.subcommands["cookbook"] = :Cookbook
+ Chef::Knife.subcommand_class_from(%w{cookbook site vendor --help foo bar baz}).should == :CookbookSiteVendor
+ end
+
+ end
+
+ describe "when running a command" do
+ before(:each) do
+ if KnifeSpecs.const_defined?(:TestYourself)
+ KnifeSpecs.send :remove_const, :TestYourself
+ end
+ Kernel.load(File.join(CHEF_SPEC_DATA, 'knife_subcommand', 'test_yourself.rb'))
+ Chef::Knife.subcommands.each { |name, klass| Chef::Knife.subcommands.delete(name) unless klass.kind_of?(Class) }
+ end
+
+ it "merges the global knife CLI options" do
+ extra_opts = {}
+ extra_opts[:editor] = {:long=>"--editor EDITOR",
+ :description=>"Set the editor to use for interactive commands",
+ :short=>"-e EDITOR",
+ :default=>"/usr/bin/vim"}
+
+ # there is special hackery to return the subcommand instance going on here.
+ command = Chef::Knife.run(%w{test yourself}, extra_opts)
+ editor_opts = command.options[:editor]
+ editor_opts[:long].should == "--editor EDITOR"
+ editor_opts[:description].should == "Set the editor to use for interactive commands"
+ editor_opts[:short].should == "-e EDITOR"
+ editor_opts[:default].should == "/usr/bin/vim"
+ end
+
+ it "creates an instance of the subcommand and runs it" do
+ command = Chef::Knife.run(%w{test yourself})
+ command.should be_an_instance_of(KnifeSpecs::TestYourself)
+ command.ran.should be_true
+ end
+
+ it "passes the command specific args to the subcommand" do
+ command = Chef::Knife.run(%w{test yourself with some args})
+ command.name_args.should == %w{with some args}
+ end
+
+ it "excludes the command name from the name args when parts are joined with underscores" do
+ command = Chef::Knife.run(%w{test_yourself with some args})
+ command.name_args.should == %w{with some args}
+ end
+
+ it "exits if no subcommand matches the CLI args" do
+ Chef::Knife.ui.stub!(:stdout).and_return(@stdout)
+ Chef::Knife.ui.should_receive(:fatal)
+ lambda {Chef::Knife.run(%w{fuuu uuuu fuuuu})}.should raise_error(SystemExit) { |e| e.status.should_not == 0 }
+ end
+
+ end
+
+ describe "when first created" do
+ before do
+ unless KnifeSpecs.const_defined?(:TestYourself)
+ Kernel.load(File.join(CHEF_SPEC_DATA, 'knife_subcommand', 'test_yourself.rb'))
+ end
+ @knife = KnifeSpecs::TestYourself.new(%w{with some args -s scrogramming})
+ end
+
+ it "it parses the options passed to it" do
+ @knife.config[:scro].should == 'scrogramming'
+ end
+
+ it "extracts its command specific args from the full arg list" do
+ @knife.name_args.should == %w{with some args}
+ end
+
+ end
+
+ describe "when formatting exceptions" do
+ before do
+ @stdout, @stderr, @stdin = StringIO.new, StringIO.new, StringIO.new
+ @knife.ui = Chef::Knife::UI.new(@stdout, @stderr, @stdin, {})
+ @knife.should_receive(:exit).with(100)
+ end
+
+ it "formats 401s nicely" do
+ response = Net::HTTPUnauthorized.new("1.1", "401", "Unauthorized")
+ response.instance_variable_set(:@read, true) # I hate you, net/http.
+ response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "y u no syncronize your clock?"))
+ @knife.stub!(:run).and_raise(Net::HTTPServerException.new("401 Unauthorized", response))
+ @knife.run_with_pretty_exceptions
+ @stderr.string.should match(/ERROR: Failed to authenticate to/)
+ @stdout.string.should match(/Response: y u no syncronize your clock\?/)
+ end
+
+ it "formats 403s nicely" do
+ response = Net::HTTPForbidden.new("1.1", "403", "Forbidden")
+ response.instance_variable_set(:@read, true) # I hate you, net/http.
+ response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "y u no administrator"))
+ @knife.stub!(:run).and_raise(Net::HTTPServerException.new("403 Forbidden", response))
+ @knife.stub!(:username).and_return("sadpanda")
+ @knife.run_with_pretty_exceptions
+ @stderr.string.should match(%r[ERROR: You authenticated successfully to http.+ as sadpanda but you are not authorized for this action])
+ @stdout.string.should match(%r[Response: y u no administrator])
+ end
+
+ it "formats 400s nicely" do
+ response = Net::HTTPBadRequest.new("1.1", "400", "Bad Request")
+ response.instance_variable_set(:@read, true) # I hate you, net/http.
+ response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "y u search wrong"))
+ @knife.stub!(:run).and_raise(Net::HTTPServerException.new("400 Bad Request", response))
+ @knife.run_with_pretty_exceptions
+ @stderr.string.should match(%r[ERROR: The data in your request was invalid])
+ @stdout.string.should match(%r[Response: y u search wrong])
+ end
+
+ it "formats 404s nicely" do
+ response = Net::HTTPNotFound.new("1.1", "404", "Not Found")
+ response.instance_variable_set(:@read, true) # I hate you, net/http.
+ response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "nothing to see here"))
+ @knife.stub!(:run).and_raise(Net::HTTPServerException.new("404 Not Found", response))
+ @knife.run_with_pretty_exceptions
+ @stderr.string.should match(%r[ERROR: The object you are looking for could not be found])
+ @stdout.string.should match(%r[Response: nothing to see here])
+ end
+
+ it "formats 500s nicely" do
+ response = Net::HTTPInternalServerError.new("1.1", "500", "Internal Server Error")
+ response.instance_variable_set(:@read, true) # I hate you, net/http.
+ response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "sad trombone"))
+ @knife.stub!(:run).and_raise(Net::HTTPFatalError.new("500 Internal Server Error", response))
+ @knife.run_with_pretty_exceptions
+ @stderr.string.should match(%r[ERROR: internal server error])
+ @stdout.string.should match(%r[Response: sad trombone])
+ end
+
+ it "formats 502s nicely" do
+ response = Net::HTTPBadGateway.new("1.1", "502", "Bad Gateway")
+ response.instance_variable_set(:@read, true) # I hate you, net/http.
+ response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "sadder trombone"))
+ @knife.stub!(:run).and_raise(Net::HTTPFatalError.new("502 Bad Gateway", response))
+ @knife.run_with_pretty_exceptions
+ @stderr.string.should match(%r[ERROR: bad gateway])
+ @stdout.string.should match(%r[Response: sadder trombone])
+ end
+
+ it "formats 503s nicely" do
+ response = Net::HTTPServiceUnavailable.new("1.1", "503", "Service Unavailable")
+ response.instance_variable_set(:@read, true) # I hate you, net/http.
+ response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "saddest trombone"))
+ @knife.stub!(:run).and_raise(Net::HTTPFatalError.new("503 Service Unavailable", response))
+ @knife.run_with_pretty_exceptions
+ @stderr.string.should match(%r[ERROR: Service temporarily unavailable])
+ @stdout.string.should match(%r[Response: saddest trombone])
+ end
+
+ it "formats other HTTP errors nicely" do
+ response = Net::HTTPPaymentRequired.new("1.1", "402", "Payment Required")
+ response.instance_variable_set(:@read, true) # I hate you, net/http.
+ response.stub!(:body).and_return(Chef::JSONCompat.to_json(:error => "nobugfixtillyoubuy"))
+ @knife.stub!(:run).and_raise(Net::HTTPServerException.new("402 Payment Required", response))
+ @knife.run_with_pretty_exceptions
+ @stderr.string.should match(%r[ERROR: Payment Required])
+ @stdout.string.should match(%r[Response: nobugfixtillyoubuy])
+ end
+
+ it "formats NameError and NoMethodError nicely" do
+ @knife.stub!(:run).and_raise(NameError.new("Undefined constant FUUU"))
+ @knife.run_with_pretty_exceptions
+ @stderr.string.should match(%r[ERROR: knife encountered an unexpected error])
+ @stdout.string.should match(%r[This may be a bug in the 'knife' knife command or plugin])
+ @stdout.string.should match(%r[Exception: NameError: Undefined constant FUUU])
+ end
+
+ it "formats missing private key errors nicely" do
+ @knife.stub!(:run).and_raise(Chef::Exceptions::PrivateKeyMissing.new('key not there'))
+ @knife.stub!(:api_key).and_return("/home/root/.chef/no-key-here.pem")
+ @knife.run_with_pretty_exceptions
+ @stderr.string.should match(%r[ERROR: Your private key could not be loaded from /home/root/.chef/no-key-here.pem])
+ @stdout.string.should match(%r[Check your configuration file and ensure that your private key is readable])
+ end
+
+ it "formats connection refused errors nicely" do
+ @knife.stub!(:run).and_raise(Errno::ECONNREFUSED.new('y u no shut up'))
+ @knife.run_with_pretty_exceptions
+ # Errno::ECONNREFUSED message differs by platform
+ # *nix = Errno::ECONNREFUSED: Connection refused
+ # win32: Errno::ECONNREFUSED: No connection could be made because the target machine actively refused it.
+ @stderr.string.should match(%r[ERROR: Network Error: .* - y u no shut up])
+ @stdout.string.should match(%r[Check your knife configuration and network settings])
+ end
+ end
+
+end