diff options
Diffstat (limited to 'spec/unit')
54 files changed, 3408 insertions, 716 deletions
diff --git a/spec/unit/api_client/registration_spec.rb b/spec/unit/api_client/registration_spec.rb index 845c217f72..15a9c30482 100644 --- a/spec/unit/api_client/registration_spec.rb +++ b/spec/unit/api_client/registration_spec.rb @@ -22,16 +22,45 @@ require 'tempfile' require 'chef/api_client/registration' describe Chef::ApiClient::Registration do + let(:key_location) do make_tmpname("client-registration-key") end - let(:registration) { Chef::ApiClient::Registration.new("silent-bob", key_location) } + let(:client_name) { "silent-bob" } + + subject(:registration) { Chef::ApiClient::Registration.new(client_name, key_location) } - let :private_key_data do + let(:private_key_data) do File.open(Chef::Config[:validation_key], "r") {|f| f.read.chomp } end + let(:http_mock) { double("Chef::REST mock") } + + let(:expected_post_data) do + { :name => client_name, :admin => false } + end + + let(:expected_put_data) do + { :name => client_name, :admin => false, :private_key => true } + end + + let(:server_v10_response) do + {"uri" => "https://chef.local/clients/#{client_name}", + "private_key" => "--begin rsa key etc--"} + end + + # Server v11 includes `json_class` on all replies + let(:server_v11_response) do + response = Chef::ApiClient.new + response.name(client_name) + response.private_key("--begin rsa key etc--") + response + end + + let(:response_409) { Net::HTTPConflict.new("1.1", "409", "Conflict") } + let(:exception_409) { Net::HTTPServerException.new("409 conflict", response_409) } + before do Chef::Config[:validation_client_name] = "test-validator" Chef::Config[:validation_key] = File.expand_path('ssl/private_key.pem', CHEF_SPEC_DATA) @@ -39,8 +68,6 @@ describe Chef::ApiClient::Registration do after do File.unlink(key_location) if File.exist?(key_location) - Chef::Config[:validation_client_name] = nil - Chef::Config[:validation_key] = nil end it "has an HTTP client configured with validator credentials" do @@ -50,57 +77,107 @@ describe Chef::ApiClient::Registration do end describe "when creating/updating the client on the server" do - let(:http_mock) { double("Chef::REST mock") } - before do registration.stub(:http_api).and_return(http_mock) end it "creates a new ApiClient on the server using the validator identity" do - response = {"uri" => "https://chef.local/clients/silent-bob", - "private_key" => "--begin rsa key etc--"} http_mock.should_receive(:post). - with("clients", :name => 'silent-bob', :admin => false). - and_return(response) - registration.create_or_update.should == response + with("clients", expected_post_data). + and_return(server_v10_response) + registration.create_or_update.should == server_v10_response registration.private_key.should == "--begin rsa key etc--" end context "and the client already exists on a Chef 10 server" do it "requests a new key from the server and saves it" do - response = {"name" => "silent-bob", "private_key" => "--begin rsa key etc--" } - - response_409 = Net::HTTPConflict.new("1.1", "409", "Conflict") - exception_409 = Net::HTTPServerException.new("409 conflict", response_409) - - http_mock.should_receive(:post).and_raise(exception_409) + http_mock.should_receive(:post).with("clients", expected_post_data). + and_raise(exception_409) http_mock.should_receive(:put). - with("clients/silent-bob", :name => 'silent-bob', :admin => false, :private_key => true). - and_return(response) - registration.create_or_update.should == response + with("clients/#{client_name}", expected_put_data). + and_return(server_v10_response) + registration.create_or_update.should == server_v10_response registration.private_key.should == "--begin rsa key etc--" end end context "and the client already exists on a Chef 11 server" do it "requests a new key from the server and saves it" do - response = Chef::ApiClient.new - response.name("silent-bob") - response.private_key("--begin rsa key etc--") - - response_409 = Net::HTTPConflict.new("1.1", "409", "Conflict") - exception_409 = Net::HTTPServerException.new("409 conflict", response_409) - http_mock.should_receive(:post).and_raise(exception_409) http_mock.should_receive(:put). - with("clients/silent-bob", :name => 'silent-bob', :admin => false, :private_key => true). - and_return(response) - registration.create_or_update.should == response + with("clients/#{client_name}", expected_put_data). + and_return(server_v11_response) + registration.create_or_update.should == server_v11_response registration.private_key.should == "--begin rsa key etc--" end end end + context "when local key generation is enabled", :nofocus do + let(:generated_private_key_pem) { IO.read(File.expand_path('ssl/private_key.pem', CHEF_SPEC_DATA)) } + let(:generated_private_key) { OpenSSL::PKey::RSA.new(generated_private_key_pem) } + let(:generated_public_key) { generated_private_key.public_key } + + let(:expected_post_data) do + { :name => client_name, :admin => false, :public_key => generated_public_key.to_pem } + end + + let(:expected_put_data) do + { :name => client_name, :admin => false, :public_key => generated_public_key.to_pem } + end + + let(:create_with_pkey_response) do + { + "uri" => "", + "public_key" => generated_public_key.to_pem + } + end + + let(:update_with_pkey_response) do + {"name"=>client_name, + "admin"=>false, + "public_key"=> generated_public_key, + "validator"=>false, + "private_key"=>false, + "clientname"=>client_name} + end + + + before do + registration.stub(:http_api).and_return(http_mock) + Chef::Config.local_key_generation = true + OpenSSL::PKey::RSA.should_receive(:generate).with(2048).and_return(generated_private_key) + end + + it "posts a locally generated public key to the server to create a client" do + http_mock.should_receive(:post). + with("clients", expected_post_data). + and_return(create_with_pkey_response) + registration.create_or_update.should == create_with_pkey_response + registration.private_key.should == generated_private_key_pem + end + + it "puts a locally generated public key to the server to update a client" do + http_mock.should_receive(:post). + with("clients", expected_post_data). + and_raise(exception_409) + http_mock.should_receive(:put). + with("clients/#{client_name}", expected_put_data). + and_return(update_with_pkey_response) + registration.create_or_update.should == update_with_pkey_response + registration.private_key.should == generated_private_key_pem + end + + it "writes the generated private key to disk" do + http_mock.should_receive(:post). + with("clients", expected_post_data). + and_return(create_with_pkey_response) + registration.run + IO.read(key_location).should == generated_private_key_pem + end + + end + describe "when writing the private key to disk" do before do registration.stub(:private_key).and_return('--begin rsa key etc--') @@ -125,16 +202,12 @@ describe Chef::ApiClient::Registration do describe "when registering a client" do - let(:http_mock) { double("Chef::REST mock") } - before do registration.stub(:http_api).and_return(http_mock) end it "creates the client on the server and writes the key" do - response = {"uri" => "http://chef.local/clients/silent-bob", - "private_key" => "--begin rsa key etc--" } - http_mock.should_receive(:post).ordered.and_return(response) + http_mock.should_receive(:post).ordered.and_return(server_v10_response) registration.run IO.read(key_location).should == "--begin rsa key etc--" end @@ -149,9 +222,7 @@ describe Chef::ApiClient::Registration do http_mock.should_receive(:post).ordered.and_raise(exception_500) # 4 http_mock.should_receive(:post).ordered.and_raise(exception_500) # 5 - response = {"uri" => "http://chef.local/clients/silent-bob", - "private_key" => "--begin rsa key etc--" } - http_mock.should_receive(:post).ordered.and_return(response) + http_mock.should_receive(:post).ordered.and_return(server_v10_response) registration.run IO.read(key_location).should == "--begin rsa key etc--" end diff --git a/spec/unit/api_client_spec.rb b/spec/unit/api_client_spec.rb index 4ccd64bafe..8657fa59a8 100644 --- a/spec/unit/api_client_spec.rb +++ b/spec/unit/api_client_spec.rb @@ -164,6 +164,52 @@ describe Chef::ApiClient do end + describe "when loading from JSON" do + before do + end + + before(:each) do + client = { + "name" => "black", + "clientname" => "black", + "public_key" => "crowes", + "private_key" => "monkeypants", + "admin" => true, + "validator" => true, + "json_class" => "Chef::ApiClient" + } + @http_client = double("Chef::REST mock") + Chef::REST.stub(:new).and_return(@http_client) + @http_client.should_receive(:get).with("clients/black").and_return(client) + @client = Chef::ApiClient.load(client['name']) + end + + it "should deserialize to a Chef::ApiClient object" do + @client.should be_a_kind_of(Chef::ApiClient) + end + + it "preserves the name" do + @client.name.should == "black" + end + + it "preserves the public key" do + @client.public_key.should == "crowes" + end + + it "preserves the admin status" do + @client.admin.should be_a_kind_of(Chef::TrueClass) + end + + it "preserves the 'validator' status" do + @client.validator.should be_a_kind_of(Chef::TrueClass) + end + + it "includes the private key if present" do + @client.private_key.should == "monkeypants" + end + + end + describe "with correctly configured API credentials" do before do Chef::Config[:node_name] = "silent-bob" diff --git a/spec/unit/application/client_spec.rb b/spec/unit/application/client_spec.rb index f84932073f..8b4ea6a077 100644 --- a/spec/unit/application/client_spec.rb +++ b/spec/unit/application/client_spec.rb @@ -127,7 +127,7 @@ describe Chef::Application::Client, "configure_chef" do end describe Chef::Application::Client, "run_application", :unix_only do - before do + before(:each) do @pipe = IO.pipe @app = Chef::Application::Client.new @app.stub(:run_chef_client) do @@ -147,4 +147,51 @@ describe Chef::Application::Client, "run_application", :unix_only do IO.select([@pipe[0]], nil, nil, 0).should_not be_nil @pipe[0].gets.should == "finished\n" end + + describe "when splay is set" do + before do + Chef::Config[:splay] = 10 + Chef::Config[:interval] = 10 + + run_count = 0 + + # uncomment to debug failures... + # Chef::Log.init($stderr) + # Chef::Log.level = :debug + + @app.stub(:run_chef_client) do + + run_count += 1 + if run_count > 3 + exit 0 + end + + # If everything is fine, sending USR1 to self should prevent + # app to go into splay sleep forever. + Process.kill("USR1", Process.pid) + end + + number_of_sleep_calls = 0 + + # This is a very complicated way of writing + # @app.should_receive(:sleep).once. + # We have to do it this way because the main loop of + # Chef::Application::Client swallows most exceptions, and we need to be + # able to expose our expectation failures to the parent process in the test. + @app.stub(:sleep) do |arg| + number_of_sleep_calls += 1 + if number_of_sleep_calls > 1 + exit 127 + end + end + end + + it "shouldn't sleep when sent USR1" do + pid = fork do + @app.run_application + end + _pid, result = Process.waitpid2(pid) + result.exitstatus.should == 0 + end + end end diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb index 58a643d403..9688cce2f4 100644 --- a/spec/unit/client_spec.rb +++ b/spec/unit/client_spec.rb @@ -24,29 +24,54 @@ require 'chef/run_context' require 'chef/rest' require 'rbconfig' -shared_examples_for Chef::Client do +describe Chef::Client do + + let(:hostname) { "hostname" } + let(:machinename) { "machinename.example.org" } + let(:fqdn) { "hostname.example.org" } + + let(:ohai_data) do + { :fqdn => fqdn, + :hostname => hostname, + :machinename => machinename, + :platform => 'example-platform', + :platform_version => 'example-platform-1.0', + :data => {} + } + end + + let(:ohai_system) do + ohai_system = double( "Ohai::System", + :all_plugins => true, + :data => ohai_data) + ohai_system.stub(:[]) do |key| + ohai_data[key] + end + ohai_system + end + + let(:node) do + Chef::Node.new.tap do |n| + n.name(fqdn) + n.chef_environment("_default") + end + end + + let(:json_attribs) { nil } + let(:client_opts) { {} } + + let(:client) do + Chef::Client.new(json_attribs, client_opts).tap do |c| + c.node = node + end + end + before do Chef::Log.logger = Logger.new(StringIO.new) # Node/Ohai data - @hostname = "hostname" - @fqdn = "hostname.example.org" - Chef::Config[:node_name] = @fqdn - ohai_data = { :fqdn => @fqdn, - :hostname => @hostname, - :platform => 'example-platform', - :platform_version => 'example-platform-1.0', - :data => {} } - ohai_data.stub(:all_plugins).and_return(true) - ohai_data.stub(:data).and_return(ohai_data) - Ohai::System.stub(:new).and_return(ohai_data) - - @node = Chef::Node.new - @node.name(@fqdn) - @node.chef_environment("_default") - - @client = Chef::Client.new - @client.node = @node + #Chef::Config[:node_name] = fqdn + Ohai::System.stub(:new).and_return(ohai_system) end describe "authentication protocol selection" do @@ -58,7 +83,7 @@ shared_examples_for Chef::Client do it "does not force the authentication protocol to 1.1" do Chef::Config[:node_name] = ("f" * 90) # ugly that this happens as a side effect of a getter :( - @client.node_name + client.node_name Chef::Config[:authentication_protocol_version].should == "1.0" end end @@ -67,7 +92,7 @@ shared_examples_for Chef::Client do it "sets the authentication protocol to version 1.1" do Chef::Config[:node_name] = ("f" * 91) # ugly that this happens as a side effect of a getter :( - @client.node_name + client.node_name Chef::Config[:authentication_protocol_version].should == "1.1" end end @@ -75,9 +100,6 @@ shared_examples_for Chef::Client do describe "configuring output formatters" do context "when no formatter has been configured" do - before do - @client = Chef::Client.new - end context "and STDOUT is a TTY" do before do @@ -85,7 +107,7 @@ shared_examples_for Chef::Client do end it "configures the :doc formatter" do - @client.formatters_for_run.should == [[:doc]] + client.formatters_for_run.should == [[:doc]] end context "and force_logger is set" do @@ -95,7 +117,7 @@ shared_examples_for Chef::Client do it "configures the :null formatter" do Chef::Config[:force_logger].should be_true - @client.formatters_for_run.should == [[:null]] + client.formatters_for_run.should == [[:null]] end end @@ -108,7 +130,7 @@ shared_examples_for Chef::Client do end it "configures the :null formatter" do - @client.formatters_for_run.should == [[:null]] + client.formatters_for_run.should == [[:null]] end context "and force_formatter is set" do @@ -116,7 +138,7 @@ shared_examples_for Chef::Client do Chef::Config[:force_formatter] = true end it "it configures the :doc formatter" do - @client.formatters_for_run.should == [[:doc]] + client.formatters_for_run.should == [[:doc]] end end end @@ -126,16 +148,15 @@ shared_examples_for Chef::Client do context "when a formatter is configured" do context "with no output path" do before do - @client = Chef::Client.new Chef::Config.add_formatter(:min) end it "does not configure a default formatter" do - @client.formatters_for_run.should == [[:min, nil]] + client.formatters_for_run.should == [[:min, nil]] end it "configures the formatter for STDOUT/STDERR" do - configured_formatters = @client.configure_formatters + configured_formatters = client.configure_formatters min_formatter = configured_formatters[0] min_formatter.output.out.should == STDOUT min_formatter.output.err.should == STDERR @@ -144,7 +165,6 @@ shared_examples_for Chef::Client do context "with an output path" do before do - @client = Chef::Client.new @tmpout = Tempfile.open("rspec-for-client-formatter-selection-#{Process.pid}") Chef::Config.add_formatter(:min, @tmpout.path) end @@ -155,7 +175,7 @@ shared_examples_for Chef::Client do end it "configures the formatter for the file path" do - configured_formatters = @client.configure_formatters + configured_formatters = client.configure_formatters min_formatter = configured_formatters[0] min_formatter.output.out.path.should == @tmpout.path min_formatter.output.err.path.should == @tmpout.path @@ -165,93 +185,216 @@ shared_examples_for Chef::Client do end end - describe "run" do - - it "should identify the node and run ohai, then register the client" do - mock_chef_rest_for_node = double("Chef::REST (node)") - mock_chef_rest_for_cookbook_sync = double("Chef::REST (cookbook sync)") - mock_chef_rest_for_node_save = double("Chef::REST (node save)") - mock_chef_runner = double("Chef::Runner") - - # --Client.register - # Make sure Client#register thinks the client key doesn't - # exist, so it tries to register and create one. - File.should_receive(:exists?).with(Chef::Config[:client_key]).exactly(1).times.and_return(false) - - # Client.register will register with the validation client name. - Chef::ApiClient::Registration.any_instance.should_receive(:run) - # Client.register will then turn around create another - # Chef::REST object, this time with the client key it got from the - # previous step. - Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url], @fqdn, Chef::Config[:client_key]).exactly(1).and_return(mock_chef_rest_for_node) - - # --Client#build_node - # looks up the node, which we will return, then later saves it. - Chef::Node.should_receive(:find_or_create).with(@fqdn).and_return(@node) - - # --ResourceReporter#node_load_completed - # gets a run id from the server for storing resource history - # (has its own tests, so stubbing it here.) - Chef::ResourceReporter.any_instance.should_receive(:node_load_completed) - - # --ResourceReporter#run_completed - # updates the server with the resource history - # (has its own tests, so stubbing it here.) - Chef::ResourceReporter.any_instance.should_receive(:run_completed) - # --Client#setup_run_context - # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync - # - Chef::CookbookSynchronizer.any_instance.should_receive(:sync_cookbooks) - Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(mock_chef_rest_for_cookbook_sync) - mock_chef_rest_for_cookbook_sync.should_receive(:post).with("environments/_default/cookbook_versions", {:run_list => []}).and_return({}) - - # --Client#converge - Chef::Runner.should_receive(:new).and_return(mock_chef_runner) - mock_chef_runner.should_receive(:converge).and_return(true) - - # --Client#save_updated_node - Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(mock_chef_rest_for_node_save) - mock_chef_rest_for_node_save.should_receive(:put_rest).with("nodes/#{@fqdn}", @node).and_return(true) - - Chef::RunLock.any_instance.should_receive(:acquire) - Chef::RunLock.any_instance.should_receive(:save_pid) - Chef::RunLock.any_instance.should_receive(:release) - - # Post conditions: check that node has been filled in correctly - @client.should_receive(:run_started) - @client.should_receive(:run_completed_successfully) + describe "a full client run" do + shared_examples_for "a successful client run" do + let(:http_node_load) { double("Chef::REST (node)") } + let(:http_cookbook_sync) { double("Chef::REST (cookbook sync)") } + let(:http_node_save) { double("Chef::REST (node save)") } + let(:runner) { double("Chef::Runner") } - if(Chef::Config[:client_fork] && !windows?) - require 'stringio' - if(Chef::Config[:pipe_node]) - pipe_sim = StringIO.new - pipe_sim.should_receive(:close).exactly(4).and_return(nil) - res = '' - pipe_sim.should_receive(:puts) do |string| - res.replace(string) - end - pipe_sim.should_receive(:gets).and_return(res) - IO.should_receive(:pipe).and_return([pipe_sim, pipe_sim]) - IO.should_receive(:select).and_return(true) + let(:api_client_exists?) { false } + + let(:stdout) { StringIO.new } + let(:stderr) { StringIO.new } + + let(:enable_fork) { false } + + def stub_for_register + # --Client.register + # Make sure Client#register thinks the client key doesn't + # exist, so it tries to register and create one. + File.should_receive(:exists?).with(Chef::Config[:client_key]).exactly(1).times.and_return(api_client_exists?) + + unless api_client_exists? + # Client.register will register with the validation client name. + Chef::ApiClient::Registration.any_instance.should_receive(:run) end - proc_ret = Class.new.new - proc_ret.should_receive(:success?).and_return(true) - Process.should_receive(:waitpid2).and_return([1, proc_ret]) - @client.should_receive(:exit).and_return(nil) - @client.should_receive(:fork) do |&block| - block.call + end + + def stub_for_node_load + # Client.register will then turn around create another + # Chef::REST object, this time with the client key it got from the + # previous step. + Chef::REST.should_receive(:new). + with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key]). + exactly(1). + and_return(http_node_load) + + # --Client#build_node + # looks up the node, which we will return, then later saves it. + Chef::Node.should_receive(:find_or_create).with(fqdn).and_return(node) + + # --ResourceReporter#node_load_completed + # gets a run id from the server for storing resource history + # (has its own tests, so stubbing it here.) + Chef::ResourceReporter.any_instance.should_receive(:node_load_completed) + end + + def stub_for_sync_cookbooks + # --Client#setup_run_context + # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync + # + Chef::CookbookSynchronizer.any_instance.should_receive(:sync_cookbooks) + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) + http_cookbook_sync.should_receive(:post). + with("environments/_default/cookbook_versions", {:run_list => []}). + and_return({}) + end + + def stub_for_converge + # --Client#converge + Chef::Runner.should_receive(:new).and_return(runner) + runner.should_receive(:converge).and_return(true) + + # --ResourceReporter#run_completed + # updates the server with the resource history + # (has its own tests, so stubbing it here.) + Chef::ResourceReporter.any_instance.should_receive(:run_completed) + end + + def stub_for_node_save + # --Client#save_updated_node + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_node_save) + http_node_save.should_receive(:put_rest).with("nodes/#{fqdn}", node).and_return(true) + end + + def stub_for_run + Chef::RunLock.any_instance.should_receive(:acquire) + Chef::RunLock.any_instance.should_receive(:save_pid) + Chef::RunLock.any_instance.should_receive(:release) + + # Post conditions: check that node has been filled in correctly + client.should_receive(:run_started) + client.should_receive(:run_completed_successfully) + end + + before do + Chef::Config[:client_fork] = enable_fork + + stub_const("Chef::Client::STDOUT_FD", stdout) + stub_const("Chef::Client::STDERR_FD", stderr) + + stub_for_register + stub_for_node_load + stub_for_sync_cookbooks + stub_for_converge + stub_for_node_save + stub_for_run + end + + it "runs ohai, sets up authentication, loads node state, synchronizes policy, and converges" do + # This is what we're testing. + client.run + + # fork is stubbed, so we can see the outcome of the run + node.automatic_attrs[:platform].should == "example-platform" + node.automatic_attrs[:platform_version].should == "example-platform-1.0" + end + end + + + describe "when running chef-client without fork" do + + include_examples "a successful client run" + end + + describe "when running chef-client with forking enabled", :unix_only do + include_examples "a successful client run" do + let(:process_status) do + double("Process::Status") + end + + let(:enable_fork) { true } + + before do + Process.should_receive(:waitpid2).and_return([1, process_status]) + + process_status.should_receive(:success?).and_return(true) + client.should_receive(:exit).and_return(nil) + client.should_receive(:fork).and_yield end end - # This is what we're testing. - @client.run + end + + describe "when the client key already exists" do + + let(:api_client_exists?) { true } + + include_examples "a successful client run" + end + + describe "when an override run list is given" do + let(:client_opts) { {:override_runlist => "recipe[override_recipe]"} } - if(!Chef::Config[:client_fork] || Chef::Config[:pipe_node]) - @node.automatic_attrs[:platform].should == "example-platform" - @node.automatic_attrs[:platform_version].should == "example-platform-1.0" + it "should permit spaces in overriding run list" do + Chef::Client.new(nil, :override_runlist => 'role[a], role[b]') + end + + describe "when running the client" do + include_examples "a successful client run" do + + before do + # Client will try to compile and run override_recipe + Chef::RunContext::CookbookCompiler.any_instance.should_receive(:compile) + end + + def stub_for_sync_cookbooks + # --Client#setup_run_context + # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync + # + Chef::CookbookSynchronizer.any_instance.should_receive(:sync_cookbooks) + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) + http_cookbook_sync.should_receive(:post). + with("environments/_default/cookbook_versions", {:run_list => ["override_recipe"]}). + and_return({}) + end + + def stub_for_node_save + # Expect NO node save + node.should_not_receive(:save) + end + end end end + describe "when a permanent run list is passed as an option" do + + include_examples "a successful client run" do + + let(:new_runlist) { "recipe[new_run_list_recipe]" } + let(:client_opts) { {:runlist => new_runlist} } + + def stub_for_sync_cookbooks + # --Client#setup_run_context + # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync + # + Chef::CookbookSynchronizer.any_instance.should_receive(:sync_cookbooks) + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) + http_cookbook_sync.should_receive(:post). + with("environments/_default/cookbook_versions", {:run_list => ["new_run_list_recipe"]}). + and_return({}) + end + + before do + # Client will try to compile and run the new_run_list_recipe, but we + # do not create a fixture for this. + Chef::RunContext::CookbookCompiler.any_instance.should_receive(:compile) + end + + it "sets the new run list on the node" do + client.run + node.run_list.should == Chef::RunList.new(new_runlist) + end + + end + end + + end + + + describe "when handling run failures" do + it "should remove the run_lock on failure of #load_node" do @run_lock = double("Chef::RunLock", :acquire => true) Chef::RunLock.stub(:new).and_return(@run_lock) @@ -260,64 +403,64 @@ shared_examples_for Chef::Client do Chef::EventDispatch::Dispatcher.stub(:new).and_return(@events) # @events is created on Chef::Client.new, so we need to recreate it after mocking - @client = Chef::Client.new - @client.stub(:load_node).and_raise(Exception) + client = Chef::Client.new + client.stub(:load_node).and_raise(Exception) @run_lock.should_receive(:release) if(Chef::Config[:client_fork] && !windows?) - @client.should_receive(:fork) do |&block| + client.should_receive(:fork) do |&block| block.call end end - lambda { @client.run }.should raise_error(Exception) + lambda { client.run }.should raise_error(Exception) end + end - describe "when notifying other objects of the status of the chef run" do - before do - Chef::Client.clear_notifications - Chef::Node.stub(:find_or_create).and_return(@node) - @node.stub(:save) - @client.load_node - @client.build_node - end - - it "notifies observers that the run has started" do - notified = false - Chef::Client.when_run_starts do |run_status| - run_status.node.should == @node - notified = true - end + describe "when notifying other objects of the status of the chef run" do + before do + Chef::Client.clear_notifications + Chef::Node.stub(:find_or_create).and_return(node) + node.stub(:save) + client.load_node + client.build_node + end - @client.run_started - notified.should be_true + it "notifies observers that the run has started" do + notified = false + Chef::Client.when_run_starts do |run_status| + run_status.node.should == node + notified = true end - it "notifies observers that the run has completed successfully" do - notified = false - Chef::Client.when_run_completes_successfully do |run_status| - run_status.node.should == @node - notified = true - end + client.run_started + notified.should be_true + end - @client.run_completed_successfully - notified.should be_true + it "notifies observers that the run has completed successfully" do + notified = false + Chef::Client.when_run_completes_successfully do |run_status| + run_status.node.should == node + notified = true end - it "notifies observers that the run failed" do - notified = false - Chef::Client.when_run_fails do |run_status| - run_status.node.should == @node - notified = true - end + client.run_completed_successfully + notified.should be_true + end - @client.run_failed - notified.should be_true + it "notifies observers that the run failed" do + notified = false + Chef::Client.when_run_fails do |run_status| + run_status.node.should == node + notified = true end + + client.run_failed + notified.should be_true end end describe "build_node" do it "should expand the roles and recipes for the node" do - @node.run_list << "role[role_containing_cookbook1]" + node.run_list << "role[role_containing_cookbook1]" role_containing_cookbook1 = Chef::Role.new role_containing_cookbook1.name("role_containing_cookbook1") role_containing_cookbook1.run_list << "cookbook1" @@ -329,37 +472,33 @@ shared_examples_for Chef::Client do Chef::REST.should_receive(:new).and_return(mock_chef_rest) # check pre-conditions. - @node[:roles].should be_nil - @node[:recipes].should be_nil + node[:roles].should be_nil + node[:recipes].should be_nil - @client.policy_builder.stub(:node).and_return(@node) + client.policy_builder.stub(:node).and_return(node) # chefspec and possibly others use the return value of this method - @client.build_node.should == @node + client.build_node.should == node # check post-conditions. - @node[:roles].should_not be_nil - @node[:roles].length.should == 1 - @node[:roles].should include("role_containing_cookbook1") - @node[:recipes].should_not be_nil - @node[:recipes].length.should == 1 - @node[:recipes].should include("cookbook1") + node[:roles].should_not be_nil + node[:roles].length.should == 1 + node[:roles].should include("role_containing_cookbook1") + node[:recipes].should_not be_nil + node[:recipes].length.should == 1 + node[:recipes].should include("cookbook1") end end describe "windows_admin_check" do - before do - @client = Chef::Client.new - end - context "platform is not windows" do before do Chef::Platform.stub(:windows?).and_return(false) end it "shouldn't be called" do - @client.should_not_receive(:has_admin_privileges?) - @client.do_windows_admin_check + client.should_not_receive(:has_admin_privileges?) + client.do_windows_admin_check end end @@ -369,91 +508,46 @@ shared_examples_for Chef::Client do end it "should be called" do - @client.should_receive(:has_admin_privileges?) - @client.do_windows_admin_check + client.should_receive(:has_admin_privileges?) + client.do_windows_admin_check end context "admin privileges exist" do before do - @client.should_receive(:has_admin_privileges?).and_return(true) + client.should_receive(:has_admin_privileges?).and_return(true) end it "should not log a warning message" do Chef::Log.should_not_receive(:warn) - @client.do_windows_admin_check + client.do_windows_admin_check end context "fatal admin check is configured" do it "should not raise an exception" do - @client.do_windows_admin_check.should_not raise_error + client.do_windows_admin_check #should not raise end end end context "admin privileges doesn't exist" do before do - @client.should_receive(:has_admin_privileges?).and_return(false) + client.should_receive(:has_admin_privileges?).and_return(false) end it "should log a warning message" do Chef::Log.should_receive(:warn) - @client.do_windows_admin_check + client.do_windows_admin_check end context "fatal admin check is configured" do it "should raise an exception" do - @client.do_windows_admin_check.should_not raise_error + client.do_windows_admin_check # should not raise end end end end end - describe "when a run list override is provided" do - before do - @node = Chef::Node.new - @node.name(@fqdn) - @node.chef_environment("_default") - @node.automatic_attrs[:platform] = "example-platform" - @node.automatic_attrs[:platform_version] = "example-platform-1.0" - end - - it "should permit spaces in overriding run list" do - @client = Chef::Client.new(nil, :override_runlist => 'role[a], role[b]') - end - - it "should override the run list and skip the final node save" do - @client = Chef::Client.new(nil, :override_runlist => 'role[test_role]') - @client.node = @node - - @node.run_list << "role[role_containing_cookbook1]" - - override_role = Chef::Role.new - override_role.name 'test_role' - override_role.run_list << 'cookbook1' - - original_runlist = @node.run_list.dup - - mock_chef_rest = double("Chef::REST") - mock_chef_rest.should_receive(:get_rest).with("roles/test_role").and_return(override_role) - Chef::REST.should_receive(:new).and_return(mock_chef_rest) - - @node.should_not_receive(:save) - - @client.policy_builder.stub(:node).and_return(@node) - @client.policy_builder.build_node - - @node[:roles].should_not be_nil - @node[:roles].should eql(['test_role']) - @node[:recipes].should eql(['cookbook1']) - - @client.save_updated_node - - @node.run_list.should == original_runlist - - end - end - describe "assert_cookbook_path_not_empty" do before do Chef::Config[:solo] = true @@ -462,24 +556,46 @@ shared_examples_for Chef::Client do context "when any directory of cookbook_path contains no cookbook" do it "raises CookbookNotFound error" do expect do - @client.send(:assert_cookbook_path_not_empty, nil) + client.send(:assert_cookbook_path_not_empty, nil) end.to raise_error(Chef::Exceptions::CookbookNotFound, 'None of the cookbook paths set in Chef::Config[:cookbook_path], ["/path/to/invalid/cookbook_path"], contain any cookbooks') end end end -end + describe "setting node name" do + context "when machinename, hostname and fqdn are all set" do + it "favors the fqdn" do + expect(client.node_name).to eql(fqdn) + end + end -describe Chef::Client do - Chef::Config[:client_fork] = false - it_behaves_like Chef::Client -end + context "when fqdn is missing" do + # ohai 7 should always have machinename == return of hostname + let(:fqdn) { nil } + it "favors the machinename" do + expect(client.node_name).to eql(machinename) + end + end -describe "Chef::Client Forked" do - before do - Chef::Config[:client_fork] = true - end + context "when fqdn and machinename are missing" do + # ohai 6 will not have machinename, return the short hostname + let(:fqdn) { nil } + let(:machinename) { nil } + it "falls back to hostname" do + expect(client.node_name).to eql(hostname) + end + end + + context "when they're all missing" do + let(:machinename) { nil } + let(:hostname) { nil } + let(:fqdn) { nil } - it_behaves_like Chef::Client + it "throws an exception" do + expect { client.node_name }.to raise_error(Chef::Exceptions::CannotDetermineNodeName) + end + end + end end + diff --git a/spec/unit/cookbook/chefignore_spec.rb b/spec/unit/cookbook/chefignore_spec.rb index aacb60c012..e529a6d05a 100644 --- a/spec/unit/cookbook/chefignore_spec.rb +++ b/spec/unit/cookbook/chefignore_spec.rb @@ -36,4 +36,14 @@ describe Chef::Cookbook::Chefignore do @chefignore.ignored?('recipes/ignoreme.rb').should be_true @chefignore.ignored?('recipes/dontignoreme.rb').should be_false end + + context "when using the single cookbook pattern" do + before do + @chefignore = Chef::Cookbook::Chefignore.new(File.join(CHEF_SPEC_DATA, 'standalone_cookbook')) + end + + it "loads the globs in the chefignore file" do + @chefignore.ignores.should =~ %w[recipes/ignoreme.rb ignored vendor/bundle/*] + end + end end diff --git a/spec/unit/cookbook/metadata_spec.rb b/spec/unit/cookbook/metadata_spec.rb index cba2aff5da..88c4a1a5f5 100644 --- a/spec/unit/cookbook/metadata_spec.rb +++ b/spec/unit/cookbook/metadata_spec.rb @@ -402,7 +402,7 @@ describe Chef::Cookbook::Metadata do @meta.attributes["db/mysql/databases"][:recipes].should == [] end - it "should allow the default value to be a string, array, or hash" do + it "should allow the default value to be a string, array, hash, boolean or numeric" do lambda { @meta.attribute("db/mysql/databases", :default => []) }.should_not raise_error @@ -413,10 +413,54 @@ describe Chef::Cookbook::Metadata do @meta.attribute("db/mysql/databases", :default => "alice in chains") }.should_not raise_error lambda { + @meta.attribute("db/mysql/databases", :default => 1337) + }.should_not raise_error + lambda { + @meta.attribute("db/mysql/databases", :default => true) + }.should_not raise_error + lambda { @meta.attribute("db/mysql/databases", :required => :not_gonna_do_it) }.should raise_error(ArgumentError) end + it "should limit the types allowed in the choice array" do + options = { + :type => "string", + :choice => [ "test1", "test2" ], + :default => "test1" + } + lambda { + @meta.attribute("test_cookbook/test", options) + }.should_not raise_error + + options = { + :type => "boolean", + :choice => [ true, false ], + :default => true + } + lambda { + @meta.attribute("test_cookbook/test", options) + }.should_not raise_error + + options = { + :type => "numeric", + :choice => [ 1337, 420 ], + :default => 1337 + } + lambda { + @meta.attribute("test_cookbook/test", options) + }.should_not raise_error + + options = { + :type => "numeric", + :choice => [ true, "false" ], + :default => false + } + lambda { + @meta.attribute("test_cookbook/test", options) + }.should raise_error + end + it "should error if default used with calculated" do lambda { attrs = { diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb index 85d6950a45..a674f6ab40 100644 --- a/spec/unit/cookbook/syntax_check_spec.rb +++ b/spec/unit/cookbook/syntax_check_spec.rb @@ -24,11 +24,21 @@ describe Chef::Cookbook::SyntaxCheck do let(:cookbook_path) { File.join(CHEF_SPEC_DATA, 'cookbooks', 'openldap') } let(:syntax_check) { Chef::Cookbook::SyntaxCheck.new(cookbook_path) } + let(:open_ldap_cookbook_files) { + %w{ attributes/default.rb + attributes/smokey.rb + definitions/client.rb + definitions/server.rb + metadata.rb + recipes/default.rb + recipes/gigantor.rb + recipes/one.rb }.map{ |f| File.join(cookbook_path, f) } +} + before do Chef::Log.logger = Logger.new(StringIO.new) Chef::Log.level = :warn # suppress "Syntax OK" messages - @attr_files = %w{default.rb smokey.rb}.map { |f| File.join(cookbook_path, 'attributes', f) } @defn_files = %w{client.rb server.rb}.map { |f| File.join(cookbook_path, 'definitions', f)} @recipes = %w{default.rb gigantor.rb one.rb}.map { |f| File.join(cookbook_path, 'recipes', f) } @@ -48,6 +58,23 @@ describe Chef::Cookbook::SyntaxCheck do Chef::Config[:cookbook_path] = File.dirname(cookbook_path) syntax_check = Chef::Cookbook::SyntaxCheck.for_cookbook(:openldap) syntax_check.cookbook_path.should == cookbook_path + syntax_check.ruby_files.sort.should == open_ldap_cookbook_files.sort + end + + it "creates a syntax checker given the cookbook name and cookbook_path" do + syntax_check = Chef::Cookbook::SyntaxCheck.for_cookbook(:openldap, File.join(CHEF_SPEC_DATA, 'cookbooks')) + syntax_check.cookbook_path.should == cookbook_path + syntax_check.ruby_files.sort.should == open_ldap_cookbook_files.sort + end + + context "when using a standalone cookbook" do + let(:cookbook_path) { File.join(CHEF_SPEC_DATA, 'standalone_cookbook') } + + it "creates a syntax checker given the cookbook name and cookbook_path for a standalone cookbook" do + syntax_check = Chef::Cookbook::SyntaxCheck.for_cookbook(:standalone_cookbook, CHEF_SPEC_DATA) + syntax_check.cookbook_path.should == cookbook_path + syntax_check.ruby_files.should == [File.join(cookbook_path, 'recipes/default.rb')] + end end describe "when first created" do diff --git a/spec/unit/cookbook_spec.rb b/spec/unit/cookbook_spec.rb index ca4f4adc08..9bcea97d98 100644 --- a/spec/unit/cookbook_spec.rb +++ b/spec/unit/cookbook_spec.rb @@ -68,16 +68,6 @@ describe Chef::CookbookVersion do @cookbook.preferred_filename(@node, :files, 'a-filename', 'the-checksum').should be_nil end - it "should allow you to include a fully-qualified recipe using the DSL" do - # DSL method include_recipe allows multiple arguments, so extract the first - @node.should_receive(:loaded_recipe).with(:openldap, "gigantor") - recipe = @run_context.include_recipe("openldap::gigantor").first - - recipe.recipe_name.should == "gigantor" - recipe.cookbook_name.should == :openldap - @run_context.resource_collection[0].name.should == "blanket" - end - it "should raise an ArgumentException if you try to load a bad recipe name" do lambda { @cookbook.load_recipe("doesnt_exist", @node) }.should raise_error(ArgumentError) end diff --git a/spec/unit/dsl/reboot_pending_spec.rb b/spec/unit/dsl/reboot_pending_spec.rb new file mode 100644 index 0000000000..8576ae168a --- /dev/null +++ b/spec/unit/dsl/reboot_pending_spec.rb @@ -0,0 +1,100 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, 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. +# + +require "chef/dsl/reboot_pending" +require "spec_helper" + +describe Chef::DSL::RebootPending do + describe "reboot_pending?" do + describe "in isoloation" do + let(:recipe) { Object.new.extend(Chef::DSL::RebootPending) } + + before do + recipe.stub(:platform?).and_return(false) + end + + context "platform is windows" do + before do + recipe.stub(:platform?).with('windows').and_return(true) + recipe.stub(:registry_key_exists?).and_return(false) + recipe.stub(:registry_value_exists?).and_return(false) + end + + it 'should return true if "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations" exists' do + recipe.stub(:registry_value_exists?).with('HKLM\SYSTEM\CurrentControlSet\Control\Session Manager', { :name => 'PendingFileRenameOperations' }).and_return(true) + expect(recipe.reboot_pending?).to be_true + end + + it 'should return true if "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" exists' do + recipe.stub(:registry_key_exists?).with('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired').and_return(true) + expect(recipe.reboot_pending?).to be_true + end + + it 'should return true if key "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired" exists' do + recipe.stub(:registry_key_exists?).with('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired').and_return(true) + expect(recipe.reboot_pending?).to be_true + end + + it 'should return true if value "HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile" contains specific data' do + recipe.stub(:registry_key_exists?).with('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile').and_return(true) + recipe.stub(:registry_get_values).with('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile').and_return( + [{:name => "Flags", :type => :dword, :data => 3}]) + expect(recipe.reboot_pending?).to be_true + end + end + + context "platform is ubuntu" do + before do + recipe.stub(:platform?).with('ubuntu').and_return(true) + end + + it 'should return true if /var/run/reboot-required exists' do + File.stub(:exists?).with('/var/run/reboot-required').and_return(true) + expect(recipe.reboot_pending?).to be_true + end + + it 'should return false if /var/run/reboot-required does not exist' do + File.stub(:exists?).with('/var/run/reboot-required').and_return(false) + expect(recipe.reboot_pending?).to be_false + end + end + + context "platform is not supported" do + it 'should raise an exception' do + recipe.stub_chain(:node, :[]).with(:platform).and_return('msdos') + expect { recipe.reboot_pending? }.to raise_error(Chef::Exceptions::UnsupportedPlatform) + end + end + end # describe in isolation + + describe "in a recipe" do + it "responds to reboot_pending?" do + # Chef::Recipe.new(cookbook_name, recipe_name, run_context(node, cookbook_collection, events)) + recipe = Chef::Recipe.new(nil,nil,Chef::RunContext.new(Chef::Node.new, {}, nil)) + expect(recipe).to respond_to(:reboot_pending?) + end + end # describe in a recipe + + describe "in a resource" do + it "responds to reboot_pending?" do + resource = Chef::Resource::new("Crackerjack::Timing", nil) + expect(resource).to respond_to(:reboot_pending?) + end + end # describe in a resource + end +end diff --git a/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb b/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb new file mode 100644 index 0000000000..a016cbfeb8 --- /dev/null +++ b/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb @@ -0,0 +1,56 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, 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. +# + +require 'spec_helper' + +describe Chef::GuardInterpreter::ResourceGuardInterpreter do + before(:each) do + node = Chef::Node.new + + node.default["kernel"] = Hash.new + node.default["kernel"][:machine] = :x86_64.to_s + + run_context = Chef::RunContext.new(node, nil, nil) + + @resource = Chef::Resource.new("powershell_unit_test", run_context) + @resource.stub(:run_action) + @resource.stub(:updated).and_return(true) + end + + describe "when evaluating a guard resource" do + let(:resource) { @resource } + + it "should allow guard interpreter to be set to Chef::Resource::Script" do + resource.guard_interpreter(:script) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(false) + resource.only_if("echo hi") + end + + it "should allow guard interpreter to be set to Chef::Resource::PowershellScript derived indirectly from Chef::Resource::Script" do + resource.guard_interpreter(:powershell_script) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(false) + resource.only_if("echo hi") + end + + it "should raise an exception if guard_interpreter is set to a resource not derived from Chef::Resource::Script" do + resource.guard_interpreter(:file) + expect { resource.only_if("echo hi") }.to raise_error ArgumentError + end + end +end + diff --git a/spec/unit/http/simple_spec.rb b/spec/unit/http/simple_spec.rb new file mode 100644 index 0000000000..b33ef1d553 --- /dev/null +++ b/spec/unit/http/simple_spec.rb @@ -0,0 +1,32 @@ +# +# Author:: Serdar Sutay (<serdar@opscode.com>) +# Copyright:: Copyright (c) 2014 Chef Software, 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. +# + +require 'spec_helper' + +describe Chef::HTTP::Simple do + it "should have content length validation middleware after compressor middleware" do + client = Chef::HTTP::Simple.new("dummy.com") + middlewares = client.instance_variable_get(:@middlewares) + content_length = middlewares.find_index { |e| e.is_a? Chef::HTTP::ValidateContentLength } + decompressor = middlewares.find_index { |e| e.is_a? Chef::HTTP::Decompressor } + + content_length.should_not be_nil + decompressor.should_not be_nil + (decompressor < content_length).should be_true + end +end diff --git a/spec/unit/http/validate_content_length_spec.rb b/spec/unit/http/validate_content_length_spec.rb new file mode 100644 index 0000000000..091f2b0757 --- /dev/null +++ b/spec/unit/http/validate_content_length_spec.rb @@ -0,0 +1,187 @@ +# +# Author:: Serdar Sutay (<serdar@opscode.com>) +# Copyright:: Copyright (c) 2014 Chef Software, 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. +# + +require 'spec_helper' +require 'stringio' + +describe Chef::HTTP::ValidateContentLength do + class TestClient < Chef::HTTP + use Chef::HTTP::ValidateContentLength + end + + let(:method) { "GET" } + let(:url) { "http://dummy.com" } + let(:headers) { {} } + let(:data) { false } + + let(:request) { } + let(:return_value) { "200" } + + # Test Variables + let(:request_type) { :streaming } + let(:content_length_value) { 23 } + let(:streaming_length) { 23 } + let(:response_body) { "Thanks for checking in." } + let(:response_headers) { + { + "content-length" => content_length_value + } + } + + let(:response) { + m = double('HttpResponse', :body => response_body) + m.stub(:[]) do |key| + response_headers[key] + end + + m + } + + let(:middleware) { + client = TestClient.new(url) + client.middlewares[0] + } + + def run_content_length_validation + stream_handler = middleware.stream_response_handler(response) + middleware.handle_request(method, url, headers, data) + + case request_type + when :streaming + # First stream the data + data_length = streaming_length + while data_length > 0 + chunk_size = data_length > 10 ? 10 : data_length + stream_handler.handle_chunk(double("Chunk", :bytesize => chunk_size)) + data_length -= chunk_size + end + + # Finally call stream complete + middleware.handle_stream_complete(response, request, return_value) + when :direct + middleware.handle_response(response, request, return_value) + else + raise "Unknown request_type: #{request_type}" + end + end + + let(:debug_stream) { StringIO.new } + let(:debug_output) { debug_stream.string } + + before(:each) { + Chef::Log.level = :debug + Chef::Log.stub(:debug) do |message| + debug_stream.puts message + end + } + + describe "without response body" do + let(:request_type) { :direct } + let(:response_body) { "Thanks for checking in." } + + it "shouldn't raise error" do + lambda { run_content_length_validation }.should_not raise_error + end + end + + describe "without Content-Length header" do + let(:response_headers) { { } } + + [ "direct", "streaming" ].each do |req_type| + describe "when running #{req_type} request" do + let(:request_type) { req_type.to_sym } + + it "should skip validation and log for debug" do + run_content_length_validation + debug_output.should include("HTTP server did not include a Content-Length header in response") + end + end + end + end + + describe "with correct Content-Length header" do + [ "direct", "streaming" ].each do |req_type| + describe "when running #{req_type} request" do + let(:request_type) { req_type.to_sym } + + it "should validate correctly" do + run_content_length_validation + debug_output.should include("Content-Length validated correctly.") + end + end + end + end + + describe "with wrong Content-Length header" do + let(:content_length_value) { 25 } + [ "direct", "streaming" ].each do |req_type| + describe "when running #{req_type} request" do + let(:request_type) { req_type.to_sym } + + it "should raise ContentLengthMismatch error" do + lambda { run_content_length_validation }.should raise_error(Chef::Exceptions::ContentLengthMismatch) + end + end + end + end + + describe "when download is interrupted" do + let(:streaming_length) { 12 } + + it "should raise ContentLengthMismatch error" do + lambda { run_content_length_validation }.should raise_error(Chef::Exceptions::ContentLengthMismatch) + end + end + + describe "when Transfer-Encoding & Content-Length is set" do + let(:response_headers) { + { + "content-length" => content_length_value, + "transfer-encoding" => "chunked" + } + } + + [ "direct", "streaming" ].each do |req_type| + describe "when running #{req_type} request" do + let(:request_type) { req_type.to_sym } + + it "should skip validation and log for debug" do + run_content_length_validation + debug_output.should include("Transfer-Encoding header is set, skipping Content-Length check.") + end + end + end + end + + describe "when client is being reused" do + before do + run_content_length_validation + debug_output.should include("Content-Length validated correctly.") + end + + it "should reset internal counter" do + middleware.instance_variable_get(:@content_length_counter).should be_nil + end + + it "should validate correctly second time" do + run_content_length_validation + debug_output.should include("Content-Length validated correctly.") + end + end + +end diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb index b055cadcee..501e678a2b 100644 --- a/spec/unit/knife/bootstrap_spec.rb +++ b/spec/unit/knife/bootstrap_spec.rb @@ -124,12 +124,21 @@ describe Chef::Knife::Bootstrap do end describe "specifying no_proxy with various entries" do - subject(:knife) { described_class.new } - let(:options){ ["--bootstrap-no-proxy", setting] } + subject(:knife) do + k = described_class.new + k.instance_variable_set("@template_file", template_file) + k.parse_options(options) + k.merge_configs + k + end + + # Include a data bag secret in the options to prevent Bootstrap from + # attempting to access /etc/chef/encrypted_data_bag_secret, which + # can fail when the file exists but can't be accessed by the user + # running the tests. + let(:options){ ["--bootstrap-no-proxy", setting, "-s", "foo"] } let(:template_file) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "no_proxy.erb")) } let(:rendered_template) do - knife.instance_variable_set("@template_file", template_file) - knife.parse_options(options) template_string = knife.read_template knife.render_template(template_string) end diff --git a/spec/unit/knife/client_bulk_delete_spec.rb b/spec/unit/knife/client_bulk_delete_spec.rb index bedd4911c5..7df7d02e9b 100644 --- a/spec/unit/knife/client_bulk_delete_spec.rb +++ b/spec/unit/knife/client_bulk_delete_spec.rb @@ -19,60 +19,145 @@ require 'spec_helper' describe Chef::Knife::ClientBulkDelete do - before(:each) do - Chef::Log.logger = Logger.new(StringIO.new) - - Chef::Config[:node_name] = "webmonkey.example.com" - @knife = Chef::Knife::ClientBulkDelete.new - @knife.name_args = ["."] - @stdout = StringIO.new - @knife.ui.stub(:stdout).and_return(@stdout) - @knife.ui.stub(:confirm).and_return(true) - @clients = Hash.new - %w{tim dan stephen}.each do |client_name| + let(:stdout_io) { StringIO.new } + let(:stdout) {stdout_io.string} + + let(:knife) { + k = Chef::Knife::ClientBulkDelete.new + k.name_args = name_args + k.config = option_args + k.ui.stub(:stdout).and_return(stdout_io) + k.ui.stub(:confirm).and_return(knife_confirm) + k.ui.stub(:confirm_without_exit).and_return(knife_confirm) + k + } + + let(:name_args) { [ "." ] } + let(:option_args) { {} } + + let(:knife_confirm) { true } + + let(:nonvalidator_client_names) { %w{tim dan stephen} } + let(:nonvalidator_clients) { + clients = Hash.new + + nonvalidator_client_names.each do |client_name| client = Chef::ApiClient.new() client.name(client_name) client.stub(:destroy).and_return(true) - @clients[client_name] = client + clients[client_name] = client + end + + clients + } + + let(:validator_client_names) { %w{myorg-validator} } + let(:validator_clients) { + clients = Hash.new + + validator_client_names.each do |validator_client_name| + validator_client = Chef::ApiClient.new() + validator_client.name(validator_client_name) + validator_client.stub(:validator).and_return(true) + validator_client.stub(:destroy).and_return(true) + clients[validator_client_name] = validator_client end - Chef::ApiClient.stub(:list).and_return(@clients) + + clients + } + + let(:client_names) { nonvalidator_client_names + validator_client_names} + let(:clients) { + nonvalidator_clients.merge(validator_clients) + } + + before(:each) do + Chef::ApiClient.stub(:list).and_return(clients) end describe "run" do + describe "without a regex" do + let(:name_args) { [ ] } - it "should get the list of the clients" do - Chef::ApiClient.should_receive(:list).and_return(@clients) - @knife.run + it "should exit if the regex is not provided" do + lambda { knife.run }.should raise_error(SystemExit) + end end - it "should print the clients you are about to delete" do - @knife.run - @stdout.string.should match(/#{@knife.ui.list(@clients.keys.sort, :columns_down)}/) - end + describe "with any clients" do + it "should get the list of the clients" do + Chef::ApiClient.should_receive(:list) + knife.run + end - it "should confirm you really want to delete them" do - @knife.ui.should_receive(:confirm) - @knife.run - end + it "should print the name of the clients" do + knife.run + client_names.each do |client_name| + stdout.should include(client_name) + end + end - it "should delete each client" do - @clients.each_value do |c| - c.should_receive(:destroy) + it "should confirm you really want to delete them" do + knife.ui.should_receive(:confirm) + knife.run end - @knife.run - end - it "should only delete clients that match the regex" do - @knife.name_args = ["tim"] - @clients["tim"].should_receive(:destroy) - @clients["stephen"].should_not_receive(:destroy) - @clients["dan"].should_not_receive(:destroy) - @knife.run + describe "without --delete-validators" do + it "should mention that validator clients wont be deleted" do + knife.run + stdout.should include("Following clients are validators and will not be deleted.") + info = stdout.index "Following clients are validators and will not be deleted." + val = stdout.index "myorg-validator" + (val > info).should be_true + end + + it "should only delete nonvalidator clients" do + nonvalidator_clients.each_value do |c| + c.should_receive(:destroy) + end + + validator_clients.each_value do |c| + c.should_not_receive(:destroy) + end + + knife.run + end + end + + describe "with --delete-validators" do + let(:option_args) { {:delete_validators => true} } + + it "should mention that validator clients will be deleted" do + knife.run + stdout.should include("The following validators will be deleted") + end + + it "should confirm twice" do + knife.ui.should_receive(:confirm).once + knife.ui.should_receive(:confirm_without_exit).once + knife.run + end + + it "should delete all clients" do + clients.each_value do |c| + c.should_receive(:destroy) + end + + knife.run + end + end end - it "should exit if the regex is not provided" do - @knife.name_args = [] - lambda { @knife.run }.should raise_error(SystemExit) + describe "with some clients" do + let(:name_args) { [ "^ti" ] } + + it "should only delete clients that match the regex" do + clients["tim"].should_receive(:destroy) + clients["stephen"].should_not_receive(:destroy) + clients["dan"].should_not_receive(:destroy) + clients["myorg-validator"].should_not_receive(:destroy) + knife.run + end end end end diff --git a/spec/unit/knife/client_create_spec.rb b/spec/unit/knife/client_create_spec.rb index 69c55ba015..897cee8974 100644 --- a/spec/unit/knife/client_create_spec.rb +++ b/spec/unit/knife/client_create_spec.rb @@ -25,7 +25,9 @@ describe Chef::Knife::ClientCreate do Chef::Config[:node_name] = "webmonkey.example.com" @knife = Chef::Knife::ClientCreate.new @knife.config = { - :file => nil + :file => nil, + :admin => false, + :validator => false } @knife.name_args = [ "adam" ] @client = Chef::ApiClient.new @@ -49,6 +51,16 @@ describe Chef::Knife::ClientCreate do @knife.run end + it "by default it is not an admin" do + @client.should_receive(:admin).with(false) + @knife.run + end + + it "by default it is not a validator" do + @client.should_receive(:validator).with(false) + @knife.run + end + it "should allow you to edit the data" do @knife.should_receive(:edit_data).with(@client) @knife.run @@ -70,5 +82,21 @@ describe Chef::Knife::ClientCreate do end end + describe "with -a or --admin" do + it "should create an admin client" do + @knife.config[:admin] = true + @client.should_receive(:admin).with(true) + @knife.run + end + end + + describe "with --validator" do + it "should create an validator client" do + @knife.config[:validator] = true + @client.should_receive(:validator).with(true) + @knife.run + end + end + end end diff --git a/spec/unit/knife/client_delete_spec.rb b/spec/unit/knife/client_delete_spec.rb index 9ebccbae15..01b49b3d7c 100644 --- a/spec/unit/knife/client_delete_spec.rb +++ b/spec/unit/knife/client_delete_spec.rb @@ -21,12 +21,16 @@ require 'spec_helper' describe Chef::Knife::ClientDelete do before(:each) do @knife = Chef::Knife::ClientDelete.new + # defaults + @knife.config = { + :delete_validators => false + } @knife.name_args = [ 'adam' ] end describe 'run' do it 'should delete the client' do - @knife.should_receive(:delete_object).with(Chef::ApiClient, 'adam') + @knife.should_receive(:delete_object).with(Chef::ApiClient, 'adam', 'client') @knife.run end @@ -37,4 +41,43 @@ describe Chef::Knife::ClientDelete do lambda { @knife.run }.should raise_error(SystemExit) end end + + describe 'with a validator' do + before(:each) do + Chef::Knife::UI.stub(:confirm).and_return(true) + @knife.stub(:confirm).and_return(true) + @client = Chef::ApiClient.new + Chef::ApiClient.should_receive(:load).and_return(@client) + end + + it 'should delete non-validator client if --force is not set' do + @knife.config[:delete_validators] = false + @client.should_receive(:destroy).and_return(@client) + @knife.should_receive(:msg) + + @knife.run + end + + it 'should delete non-validator client if --force is set' do + @knife.config[:delete_validators] = true + @client.should_receive(:destroy).and_return(@client) + @knife.should_receive(:msg) + + @knife.run + end + + it 'should not delete validator client if --force is not set' do + @client.validator(true) + @knife.ui.should_receive(:fatal) + lambda { @knife.run}.should raise_error(SystemExit) + end + + it 'should delete validator client if --force is set' do + @knife.config[:delete_validators] = true + @client.should_receive(:destroy).and_return(@client) + @knife.should_receive(:msg) + + @knife.run + end + end end diff --git a/spec/unit/knife/cookbook_upload_spec.rb b/spec/unit/knife/cookbook_upload_spec.rb index 65331b3952..5c7a4c1125 100644 --- a/spec/unit/knife/cookbook_upload_spec.rb +++ b/spec/unit/knife/cookbook_upload_spec.rb @@ -23,178 +23,271 @@ require 'chef/cookbook_uploader' require 'timeout' describe Chef::Knife::CookbookUpload do - before(:each) do - @knife = Chef::Knife::CookbookUpload.new - @knife.name_args = ['test_cookbook'] + let(:cookbook) { Chef::CookbookVersion.new('test_cookbook') } + + let(:cookbooks_by_name) do + {cookbook.name => cookbook} + end - @cookbook = Chef::CookbookVersion.new('test_cookbook') + let(:cookbook_loader) do + cookbook_loader = cookbooks_by_name.dup + cookbook_loader.stub(:merged_cookbooks).and_return([]) + cookbook_loader.stub(:load_cookbooks).and_return(cookbook_loader) + cookbook_loader + end + + let(:cookbook_uploader) { double(:upload_cookbooks => nil) } - @cookbook_loader = {} - @cookbook_loader.stub(:[]).and_return(@cookbook) - @cookbook_loader.stub(:merged_cookbooks).and_return([]) - @cookbook_loader.stub(:load_cookbooks).and_return(@cookbook_loader) - Chef::CookbookLoader.stub(:new).and_return(@cookbook_loader) + let(:output) { StringIO.new } + + let(:name_args) { ['test_cookbook'] } - @output = StringIO.new - @knife.ui.stub(:stdout).and_return(@output) - @knife.ui.stub(:stderr).and_return(@output) + let(:knife) do + k = Chef::Knife::CookbookUpload.new + k.name_args = name_args + k.ui.stub(:stdout).and_return(output) + k.ui.stub(:stderr).and_return(output) + k + end + + before(:each) do + Chef::CookbookLoader.stub(:new).and_return(cookbook_loader) end describe 'with --concurrency' do it 'should upload cookbooks with predefined concurrency' do - @cookbook_uploader = double(:upload_cookbooks => nil) Chef::CookbookVersion.stub(:list_all_versions).and_return({}) - @knife.config[:concurrency] = 3 - @test_cookbook = Chef::CookbookVersion.new('test_cookbook') - @cookbook_loader.stub(:each).and_yield("test_cookbook", @test_cookbook) - @cookbook_loader.stub(:cookbook_names).and_return(["test_cookbook"]) + knife.config[:concurrency] = 3 + test_cookbook = Chef::CookbookVersion.new('test_cookbook') + cookbook_loader.stub(:each).and_yield("test_cookbook", test_cookbook) + cookbook_loader.stub(:cookbook_names).and_return(["test_cookbook"]) Chef::CookbookUploader.should_receive(:new).with( kind_of(Array), kind_of(Array), {:force=>nil, :concurrency => 3}).and_return(double("Chef::CookbookUploader", :upload_cookbooks=> true)) - @knife.run + knife.run end end describe 'run' do before(:each) do - @cookbook_uploader = double(:upload_cookbooks => nil) - Chef::CookbookUploader.stub(:new => @cookbook_uploader) + Chef::CookbookUploader.stub(:new => cookbook_uploader) Chef::CookbookVersion.stub(:list_all_versions).and_return({}) end it 'should print usage and exit when a cookbook name is not provided' do - @knife.name_args = [] - @knife.should_receive(:show_usage) - @knife.ui.should_receive(:fatal) - lambda { @knife.run }.should raise_error(SystemExit) + knife.name_args = [] + knife.should_receive(:show_usage) + knife.ui.should_receive(:fatal) + lambda { knife.run }.should raise_error(SystemExit) end describe 'when specifying a cookbook name' do it 'should upload the cookbook' do - @knife.should_receive(:upload).once - @knife.run + knife.should_receive(:upload).once + knife.run end it 'should report on success' do - @knife.should_receive(:upload).once - @knife.ui.should_receive(:info).with(/Uploaded 1 cookbook/) - @knife.run + knife.should_receive(:upload).once + knife.ui.should_receive(:info).with(/Uploaded 1 cookbook/) + knife.run end end describe 'when specifying the same cookbook name twice' do it 'should upload the cookbook only once' do - @knife.name_args = ['test_cookbook', 'test_cookbook'] - @knife.should_receive(:upload).once - @knife.run + knife.name_args = ['test_cookbook', 'test_cookbook'] + knife.should_receive(:upload).once + knife.run + end + end + + context "when uploading a cookbook that uses deprecated overlays" do + + before do + cookbook_loader.stub(:merged_cookbooks).and_return(['test_cookbook']) + cookbook_loader.stub(:merged_cookbook_paths). + and_return({'test_cookbook' => %w{/path/one/test_cookbook /path/two/test_cookbook}}) + end + + it "emits a warning" do + knife.run + expected_message=<<-E +WARNING: The cookbooks: test_cookbook exist in multiple places in your cookbook_path. +A composite version of these cookbooks has been compiled for uploading. + +IMPORTANT: In a future version of Chef, this behavior will be removed and you will no longer +be able to have the same version of a cookbook in multiple places in your cookbook_path. +WARNING: The affected cookbooks are located: +test_cookbook: + /path/one/test_cookbook + /path/two/test_cookbook +E + output.string.should include(expected_message) end end describe 'when specifying a cookbook name among many' do - before(:each) do - @knife.name_args = ['test_cookbook1'] - @cookbooks = { + let(:name_args) { ['test_cookbook1'] } + + let(:cookbooks_by_name) do + { 'test_cookbook1' => Chef::CookbookVersion.new('test_cookbook1'), 'test_cookbook2' => Chef::CookbookVersion.new('test_cookbook2'), 'test_cookbook3' => Chef::CookbookVersion.new('test_cookbook3') } - @cookbook_loader = {} - @cookbook_loader.stub(:merged_cookbooks).and_return([]) - @cookbook_loader.stub(:[]) { |ckbk| @cookbooks[ckbk] } - Chef::CookbookLoader.stub(:new).and_return(@cookbook_loader) end it "should read only one cookbook" do - @cookbook_loader.should_receive(:[]).once.with('test_cookbook1') - @knife.run + cookbook_loader.should_receive(:[]).once.with('test_cookbook1').and_call_original + knife.run end it "should not read all cookbooks" do - @cookbook_loader.should_not_receive(:load_cookbooks) - @knife.run + cookbook_loader.should_not_receive(:load_cookbooks) + knife.run end it "should upload only one cookbook" do - @knife.should_receive(:upload).exactly(1).times - @knife.run + knife.should_receive(:upload).exactly(1).times + knife.run end end # This is testing too much. We should break it up. describe 'when specifying a cookbook name with dependencies' do + let(:name_args) { ["test_cookbook2"] } + + let(:cookbooks_by_name) do + { "test_cookbook1" => test_cookbook1, + "test_cookbook2" => test_cookbook2, + "test_cookbook3" => test_cookbook3 } + end + + let(:test_cookbook1) { Chef::CookbookVersion.new('test_cookbook1') } + + let(:test_cookbook2) do + c = Chef::CookbookVersion.new('test_cookbook2') + c.metadata.depends("test_cookbook3") + c + end + + let(:test_cookbook3) do + c = Chef::CookbookVersion.new('test_cookbook3') + c.metadata.depends("test_cookbook1") + c.metadata.depends("test_cookbook2") + c + end + it "should upload all dependencies once" do - @knife.name_args = ["test_cookbook2"] - @knife.config[:depends] = true - @test_cookbook1 = Chef::CookbookVersion.new('test_cookbook1') - @test_cookbook2 = Chef::CookbookVersion.new('test_cookbook2') - @test_cookbook3 = Chef::CookbookVersion.new('test_cookbook3') - @test_cookbook2.metadata.depends("test_cookbook3") - @test_cookbook3.metadata.depends("test_cookbook1") - @test_cookbook3.metadata.depends("test_cookbook2") - @cookbook_loader.stub(:[]) do |ckbk| - { "test_cookbook1" => @test_cookbook1, - "test_cookbook2" => @test_cookbook2, - "test_cookbook3" => @test_cookbook3 }[ckbk] - end - @knife.stub(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2", "test_cookbook3"]) - @knife.should_receive(:upload).exactly(3).times - Timeout::timeout(5) do - @knife.run + knife.config[:depends] = true + knife.stub(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2", "test_cookbook3"]) + knife.should_receive(:upload).exactly(3).times + lambda do + Timeout::timeout(5) do + knife.run + end end.should_not raise_error end end + describe 'when specifying a cookbook name with missing dependencies' do + let(:cookbook_dependency) { Chef::CookbookVersion.new('dependency') } + + before(:each) do + cookbook.metadata.depends("dependency") + cookbook_loader.stub(:[]) do |ckbk| + { "test_cookbook" => cookbook, + "dependency" => cookbook_dependency}[ckbk] + end + knife.stub(:cookbook_names).and_return(["cookbook_dependency", "test_cookbook"]) + @stdout, @stderr, @stdin = StringIO.new, StringIO.new, StringIO.new + knife.ui = Chef::Knife::UI.new(@stdout, @stderr, @stdin, {}) + end + + it 'should exit and not upload the cookbook' do + cookbook_loader.should_receive(:[]).once.with('test_cookbook') + cookbook_loader.should_not_receive(:load_cookbooks) + cookbook_uploader.should_not_receive(:upload_cookbooks) + expect {knife.run}.to raise_error(SystemExit) + end + + it 'should output a message for a single missing dependency' do + expect {knife.run}.to raise_error(SystemExit) + @stderr.string.should include('Cookbook test_cookbook depends on cookbooks which are not currently') + @stderr.string.should include('being uploaded and cannot be found on the server.') + @stderr.string.should include("The missing cookbook(s) are: 'dependency' version '>= 0.0.0'") + end + + it 'should output a message for a multiple missing dependencies which are concatenated' do + cookbook_dependency2 = Chef::CookbookVersion.new('dependency2') + cookbook.metadata.depends("dependency2") + cookbook_loader.stub(:[]) do |ckbk| + { "test_cookbook" => cookbook, + "dependency" => cookbook_dependency, + "dependency2" => cookbook_dependency2}[ckbk] + end + knife.stub(:cookbook_names).and_return(["dependency", "dependency2", "test_cookbook"]) + expect {knife.run}.to raise_error(SystemExit) + @stderr.string.should include('Cookbook test_cookbook depends on cookbooks which are not currently') + @stderr.string.should include('being uploaded and cannot be found on the server.') + @stderr.string.should include("The missing cookbook(s) are:") + @stderr.string.should include("'dependency' version '>= 0.0.0'") + @stderr.string.should include("'dependency2' version '>= 0.0.0'") + end + end + it "should freeze the version of the cookbooks if --freeze is specified" do - @knife.config[:freeze] = true - @cookbook.should_receive(:freeze_version).once - @knife.run + knife.config[:freeze] = true + cookbook.should_receive(:freeze_version).once + knife.run end describe 'with -a or --all' do before(:each) do - @knife.config[:all] = true + knife.config[:all] = true @test_cookbook1 = Chef::CookbookVersion.new('test_cookbook1') @test_cookbook2 = Chef::CookbookVersion.new('test_cookbook2') - @cookbook_loader.stub(:each).and_yield("test_cookbook1", @test_cookbook1).and_yield("test_cookbook2", @test_cookbook2) - @cookbook_loader.stub(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2"]) + cookbook_loader.stub(:each).and_yield("test_cookbook1", @test_cookbook1).and_yield("test_cookbook2", @test_cookbook2) + cookbook_loader.stub(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2"]) end it 'should upload all cookbooks' do - @knife.should_receive(:upload).once - @knife.run + knife.should_receive(:upload).once + knife.run end it 'should report on success' do - @knife.should_receive(:upload).once - @knife.ui.should_receive(:info).with(/Uploaded all cookbooks/) - @knife.run + knife.should_receive(:upload).once + knife.ui.should_receive(:info).with(/Uploaded all cookbooks/) + knife.run end it 'should update the version constraints for an environment' do - @knife.stub(:assert_environment_valid!).and_return(true) - @knife.config[:environment] = "production" - @knife.should_receive(:update_version_constraints).once - @knife.run + knife.stub(:assert_environment_valid!).and_return(true) + knife.config[:environment] = "production" + knife.should_receive(:update_version_constraints).once + knife.run end end describe 'when a frozen cookbook exists on the server' do it 'should fail to replace it' do exception = Chef::Exceptions::CookbookFrozen.new - @cookbook_uploader.should_receive(:upload_cookbooks). + cookbook_uploader.should_receive(:upload_cookbooks). and_raise(exception) - @knife.ui.stub(:error) - @knife.ui.should_receive(:error).with(exception) - lambda { @knife.run }.should raise_error(SystemExit) + knife.ui.stub(:error) + knife.ui.should_receive(:error).with(exception) + lambda { knife.run }.should raise_error(SystemExit) end it 'should not update the version constraints for an environment' do - @knife.stub(:assert_environment_valid!).and_return(true) - @knife.config[:environment] = "production" - @knife.stub(:upload).and_raise(Chef::Exceptions::CookbookFrozen) - @knife.ui.should_receive(:error).with(/Failed to upload 1 cookbook/) - @knife.ui.should_receive(:warn).with(/Not updating version constraints/) - @knife.should_not_receive(:update_version_constraints) - lambda { @knife.run }.should raise_error(SystemExit) + knife.stub(:assert_environment_valid!).and_return(true) + knife.config[:environment] = "production" + knife.stub(:upload).and_raise(Chef::Exceptions::CookbookFrozen) + knife.ui.should_receive(:error).with(/Failed to upload 1 cookbook/) + knife.ui.should_receive(:warn).with(/Not updating version constraints/) + knife.should_not_receive(:update_version_constraints) + lambda { knife.run }.should raise_error(SystemExit) end end end # run diff --git a/spec/unit/knife/core/bootstrap_context_spec.rb b/spec/unit/knife/core/bootstrap_context_spec.rb index 47261e2068..17e265edad 100644 --- a/spec/unit/knife/core/bootstrap_context_spec.rb +++ b/spec/unit/knife/core/bootstrap_context_spec.rb @@ -41,13 +41,19 @@ describe Chef::Knife::Core::BootstrapContext do bootstrap_context.start_chef.should eq "chef-client -j /etc/chef/first-boot.json -E _default" end + describe "when in verbosity mode" do + let(:config) { {:verbosity => 2} } + it "adds '-l debug' when verbosity is >= 2" do + bootstrap_context.start_chef.should eq "chef-client -j /etc/chef/first-boot.json -l debug -E _default" + end + end + it "reads the validation key" do bootstrap_context.validation_key.should eq IO.read(File.join(CHEF_SPEC_DATA, 'ssl', 'private_key.pem')) end it "generates the config file data" do expected=<<-EXPECTED -log_level :auto log_location STDOUT chef_server_url "http://chef.example.com:4444" validation_client_name "chef-validator-testing" @@ -56,6 +62,10 @@ EXPECTED bootstrap_context.config_content.should eq expected end + it "does not set a default log_level" do + expect(bootstrap_context.config_content).not_to match(/log_level/) + end + describe "alternate chef-client path" do let(:chef_config){ {:chef_client_path => '/usr/local/bin/chef-client'} } it "runs chef-client from another path when specified" do diff --git a/spec/unit/knife/core/ui_spec.rb b/spec/unit/knife/core/ui_spec.rb index c626747918..9044bc2f2f 100644 --- a/spec/unit/knife/core/ui_spec.rb +++ b/spec/unit/knife/core/ui_spec.rb @@ -406,61 +406,132 @@ EOM end describe "confirm" do - before(:each) do - @question = "monkeys rule" - @stdout = StringIO.new - @ui.stub(:stdout).and_return(@stdout) - @ui.stdin.stub(:readline).and_return("y") + let(:stdout) {StringIO.new} + let(:output) {stdout.string} + + let(:question) { "monkeys rule" } + let(:answer) { 'y' } + + let(:default_choice) { nil } + let(:append_instructions) { true } + + def run_confirm + @ui.stub(:stdout).and_return(stdout) + @ui.stdin.stub(:readline).and_return(answer) + @ui.confirm(question, append_instructions, default_choice) end - it "should return true if you answer Y" do - @ui.stdin.stub(:readline).and_return("Y") - @ui.confirm(@question).should == true + def run_confirm_without_exit + @ui.stub(:stdout).and_return(stdout) + @ui.stdin.stub(:readline).and_return(answer) + @ui.confirm_without_exit(question, append_instructions, default_choice) end - it "should return true if you answer y" do - @ui.stdin.stub(:readline).and_return("y") - @ui.confirm(@question).should == true + shared_examples_for "confirm with positive answer" do + it "confirm should return true" do + run_confirm.should be_true + end + + it "confirm_without_exit should return true" do + run_confirm_without_exit.should be_true + end end - it "should exit 3 if you answer N" do - @ui.stdin.stub(:readline).and_return("N") - lambda { - @ui.confirm(@question) - }.should raise_error(SystemExit) { |e| e.status.should == 3 } + shared_examples_for "confirm with negative answer" do + it "confirm should exit 3" do + lambda { + run_confirm + }.should raise_error(SystemExit) { |e| e.status.should == 3 } + end + + it "confirm_without_exit should return false" do + run_confirm_without_exit.should be_false + end end - it "should exit 3 if you answer n" do - @ui.stdin.stub(:readline).and_return("n") - lambda { - @ui.confirm(@question) - }.should raise_error(SystemExit) { |e| e.status.should == 3 } + describe "with default choice set to true" do + let(:default_choice) { true } + + it "should show 'Y/n' in the instructions" do + run_confirm + output.should include("Y/n") + end + + describe "with empty answer" do + let(:answer) { "" } + + it_behaves_like "confirm with positive answer" + end + + describe "with answer N " do + let(:answer) { "N" } + + it_behaves_like "confirm with negative answer" + end end - describe "with --y or --yes passed" do - it "should return true" do - @ui.config[:yes] = true - @ui.confirm(@question).should == true + describe "with default choice set to false" do + let(:default_choice) { false } + + it "should show 'y/N' in the instructions" do + run_confirm + output.should include("y/N") + end + + describe "with empty answer" do + let(:answer) { "" } + + it_behaves_like "confirm with negative answer" + end + + describe "with answer N " do + let(:answer) { "Y" } + + it_behaves_like "confirm with positive answer" end end - describe "when asking for free-form user input" do - it "asks a question and returns the answer provided by the user" do - out = StringIO.new - @ui.stub(:stdout).and_return(out) - @ui.stub(:stdin).and_return(StringIO.new("http://mychefserver.example.com\n")) - @ui.ask_question("your chef server URL?").should == "http://mychefserver.example.com" - out.string.should == "your chef server URL?" + ["Y", "y"].each do |answer| + describe "with answer #{answer}" do + let(:answer) { answer } + + it_behaves_like "confirm with positive answer" end + end - it "suggests a default setting and returns the default when the user's response only contains whitespace" do - out = StringIO.new - @ui.stub(:stdout).and_return(out) - @ui.stub(:stdin).and_return(StringIO.new(" \n")) - @ui.ask_question("your chef server URL? ", :default => 'http://localhost:4000').should == "http://localhost:4000" - out.string.should == "your chef server URL? [http://localhost:4000] " + ["N", "n"].each do |answer| + describe "with answer #{answer}" do + let(:answer) { answer } + + it_behaves_like "confirm with negative answer" end end + describe "with --y or --yes passed" do + it "should return true" do + @ui.config[:yes] = true + run_confirm.should be_true + output.should eq("") + end + end + end + + describe "when asking for free-form user input" do + it "asks a question and returns the answer provided by the user" do + out = StringIO.new + @ui.stub(:stdout).and_return(out) + @ui.stub(:stdin).and_return(StringIO.new("http://mychefserver.example.com\n")) + @ui.ask_question("your chef server URL?").should == "http://mychefserver.example.com" + out.string.should == "your chef server URL?" + end + + it "suggests a default setting and returns the default when the user's response only contains whitespace" do + out = StringIO.new + @ui.stub(:stdout).and_return(out) + @ui.stub(:stdin).and_return(StringIO.new(" \n")) + @ui.ask_question("your chef server URL? ", :default => 'http://localhost:4000').should == "http://localhost:4000" + out.string.should == "your chef server URL? [http://localhost:4000] " + end end + end diff --git a/spec/unit/knife/node_run_list_add_spec.rb b/spec/unit/knife/node_run_list_add_spec.rb index 61f2e6af4c..bd33a359a2 100644 --- a/spec/unit/knife/node_run_list_add_spec.rb +++ b/spec/unit/knife/node_run_list_add_spec.rb @@ -65,6 +65,29 @@ describe Chef::Knife::NodeRunListAdd do end end + describe "with -b or --before specified" do + it "should add to the run list before the specified entry" do + @node.run_list << "role[acorns]" + @node.run_list << "role[barn]" + @knife.config[:before] = "role[acorns]" + @knife.run + @node.run_list[0].should == "role[monkey]" + @node.run_list[1].should == "role[acorns]" + @node.run_list[2].should == "role[barn]" + end + end + + describe "with both --after and --before specified" do + it "exits with an error" do + @node.run_list << "role[acorns]" + @node.run_list << "role[barn]" + @knife.config[:before] = "role[acorns]" + @knife.config[:after] = "role[acorns]" + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + end + describe "with more than one role or recipe" do it "should add to the run list all the entries" do @knife.name_args = [ "adam", "role[monkey],role[duck]" ] @@ -98,7 +121,7 @@ describe Chef::Knife::NodeRunListAdd do end end - describe "with more than one role or recipe as different arguments and list separated by comas" do + describe "with more than one role or recipe as different arguments and list separated by commas" do it "should add to the run list all the entries" do @knife.name_args = [ "adam", "role[monkey]", "role[duck],recipe[bird::fly]" ] @node.run_list << "role[acorns]" diff --git a/spec/unit/knife/ssh_spec.rb b/spec/unit/knife/ssh_spec.rb index eff7c9ba5b..9247db3c90 100644 --- a/spec/unit/knife/ssh_spec.rb +++ b/spec/unit/knife/ssh_spec.rb @@ -54,7 +54,7 @@ describe Chef::Knife::Ssh do @knife.config[:attribute] = "ipaddress" @knife.config[:override_attribute] = "ipaddress" configure_query([@node_foo, @node_bar]) - @knife.should_receive(:session_from_list).with(['10.0.0.1', '10.0.0.2']) + @knife.should_receive(:session_from_list).with([['10.0.0.1', nil], ['10.0.0.2', nil]]) @knife.configure_session end @@ -62,14 +62,17 @@ describe Chef::Knife::Ssh do @knife.config[:attribute] = "config_file" # this value will be the config file @knife.config[:override_attribute] = "ipaddress" # this is the value of the command line via #configure_attribute configure_query([@node_foo, @node_bar]) - @knife.should_receive(:session_from_list).with(['10.0.0.1', '10.0.0.2']) + @knife.should_receive(:session_from_list).with([['10.0.0.1', nil], ['10.0.0.2', nil]]) @knife.configure_session end end it "searchs for and returns an array of fqdns" do configure_query([@node_foo, @node_bar]) - @knife.should_receive(:session_from_list).with(['foo.example.org', 'bar.example.org']) + @knife.should_receive(:session_from_list).with([ + ['foo.example.org', nil], + ['bar.example.org', nil] + ]) @knife.configure_session end @@ -83,7 +86,10 @@ describe Chef::Knife::Ssh do it "returns an array of cloud public hostnames" do configure_query([@node_foo, @node_bar]) - @knife.should_receive(:session_from_list).with(['ec2-10-0-0-1.compute-1.amazonaws.com', 'ec2-10-0-0-2.compute-1.amazonaws.com']) + @knife.should_receive(:session_from_list).with([ + ['ec2-10-0-0-1.compute-1.amazonaws.com', nil], + ['ec2-10-0-0-2.compute-1.amazonaws.com', nil] + ]) @knife.configure_session end @@ -179,12 +185,17 @@ describe Chef::Knife::Ssh do end it "uses the port from an ssh config file" do - @knife.session_from_list(['the.b.org']) + @knife.session_from_list([['the.b.org', nil]]) @knife.session.servers[0].port.should == 23 end + it "uses the port from a cloud attr" do + @knife.session_from_list([['the.b.org', 123]]) + @knife.session.servers[0].port.should == 123 + end + it "uses the user from an ssh config file" do - @knife.session_from_list(['the.b.org']) + @knife.session_from_list([['the.b.org', 123]]) @knife.session.servers[0].user.should == "locutus" end end diff --git a/spec/unit/knife/ssl_check_spec.rb b/spec/unit/knife/ssl_check_spec.rb new file mode 100644 index 0000000000..32405a5977 --- /dev/null +++ b/spec/unit/knife/ssl_check_spec.rb @@ -0,0 +1,187 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, 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. +# + +require "spec_helper" +require 'stringio' + +describe Chef::Knife::SslCheck do + + let(:name_args) { [] } + let(:stdout_io) { StringIO.new } + let(:stderr_io) { StringIO.new } + + def stderr + stderr_io.string + end + + def stdout + stdout_io.string + end + + subject(:ssl_check) do + s = Chef::Knife::SslCheck.new + s.ui.stub(:stdout).and_return(stdout_io) + s.ui.stub(:stderr).and_return(stderr_io) + s.name_args = name_args + s + end + + before do + Chef::Config.chef_server_url = "https://example.com:8443/chef-server" + end + + context "when no arguments are given" do + it "uses the chef_server_url as the host to check" do + expect(ssl_check.host).to eq("example.com") + expect(ssl_check.port).to eq(8443) + end + end + + context "when a specific URI is given" do + let(:name_args) { %w{https://example.test:10443/foo} } + + it "checks the SSL configuration against the given host" do + expect(ssl_check.host).to eq("example.test") + expect(ssl_check.port).to eq(10443) + end + end + + context "when an invalid URI is given" do + + let(:name_args) { %w{foo.test} } + + it "prints an error and exits" do + expect { ssl_check.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl check [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `foo.test' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + + context "and its malformed enough to make URI.parse barf" do + + let(:name_args) { %w{ftp://lkj\\blah:example.com/blah} } + + it "prints an error and exits" do + expect { ssl_check.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl check [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `#{name_args[0]}' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + end + end + + describe "verifying the remote certificate" do + let(:name_args) { %w{https://foo.example.com:8443} } + + let(:tcp_socket) { double(TCPSocket) } + let(:ssl_socket) { double(OpenSSL::SSL::SSLSocket) } + + before do + TCPSocket.should_receive(:new).with("foo.example.com", 8443).and_return(tcp_socket) + OpenSSL::SSL::SSLSocket.should_receive(:new).with(tcp_socket, ssl_check.verify_peer_ssl_context).and_return(ssl_socket) + end + + def run + ssl_check.run + rescue Exception + #puts "OUT: #{stdout_io.string}" + #puts "ERR: #{stderr_io.string}" + raise + end + + context "when the remote host's certificate is valid" do + + before do + ssl_socket.should_receive(:connect) # no error + ssl_socket.should_receive(:post_connection_check).with("foo.example.com") # no error + end + + it "prints a success message" do + ssl_check.run + expect(stdout_io.string).to include("Successfully verified certificates from `foo.example.com'") + end + end + + describe "and the certificate is not valid" do + + let(:tcp_socket_for_debug) { double(TCPSocket) } + let(:ssl_socket_for_debug) { double(OpenSSL::SSL::SSLSocket) } + + let(:self_signed_crt_path) { File.join(CHEF_SPEC_DATA, "trusted_certs", "example.crt") } + let(:self_signed_crt) { OpenSSL::X509::Certificate.new(File.read(self_signed_crt_path)) } + + before do + trap(:INT, "DEFAULT") + + TCPSocket.should_receive(:new). + with("foo.example.com", 8443). + and_return(tcp_socket_for_debug) + OpenSSL::SSL::SSLSocket.should_receive(:new). + with(tcp_socket_for_debug, ssl_check.noverify_peer_ssl_context). + and_return(ssl_socket_for_debug) + end + + context "when the certificate's CN does not match the hostname" do + before do + ssl_socket.should_receive(:connect) # no error + ssl_socket.should_receive(:post_connection_check). + with("foo.example.com"). + and_raise(OpenSSL::SSL::SSLError) + ssl_socket_for_debug.should_receive(:connect) + ssl_socket_for_debug.should_receive(:peer_cert).and_return(self_signed_crt) + end + + it "shows the CN used by the certificate and prints an error" do + expect { run }.to raise_error(SystemExit) + expect(stderr).to include("The SSL cert is signed by a trusted authority but is not valid for the given hostname") + expect(stderr).to include("You are attempting to connect to: 'foo.example.com'") + expect(stderr).to include("The server's certificate belongs to 'example.local'") + end + + end + + context "when the cert is not signed by any trusted authority" do + before do + ssl_socket.should_receive(:connect). + and_raise(OpenSSL::SSL::SSLError) + ssl_socket_for_debug.should_receive(:connect) + ssl_socket_for_debug.should_receive(:peer_cert).and_return(self_signed_crt) + end + + it "shows the CN used by the certificate and prints an error" do + expect { run }.to raise_error(SystemExit) + expect(stderr).to include("The SSL certificate of foo.example.com could not be verified") + end + + end + end + + end + +end + diff --git a/spec/unit/knife/ssl_fetch_spec.rb b/spec/unit/knife/ssl_fetch_spec.rb new file mode 100644 index 0000000000..0d3c8913f7 --- /dev/null +++ b/spec/unit/knife/ssl_fetch_spec.rb @@ -0,0 +1,151 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, 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. +# + +require 'spec_helper' +require 'chef/knife/ssl_fetch' + +describe Chef::Knife::SslFetch do + + let(:name_args) { [] } + let(:stdout_io) { StringIO.new } + let(:stderr_io) { StringIO.new } + + def stderr + stderr_io.string + end + + def stdout + stdout_io.string + end + + subject(:ssl_fetch) do + s = Chef::Knife::SslFetch.new + s.name_args = name_args + s.ui.stub(:stdout).and_return(stdout_io) + s.ui.stub(:stderr).and_return(stderr_io) + s + end + + context "when no arguments are given" do + + before do + Chef::Config.chef_server_url = "https://example.com:8443/chef-server" + end + + it "uses the chef_server_url as the host to fetch" do + expect(ssl_fetch.host).to eq("example.com") + expect(ssl_fetch.port).to eq(8443) + end + end + + context "when a specific URI is given" do + let(:name_args) { %w{https://example.test:10443/foo} } + + it "fetchs the SSL configuration against the given host" do + expect(ssl_fetch.host).to eq("example.test") + expect(ssl_fetch.port).to eq(10443) + end + end + + context "when an invalid URI is given" do + + let(:name_args) { %w{foo.test} } + + it "prints an error and exits" do + expect { ssl_fetch.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl fetch [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `foo.test' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + + context "and its malformed enough to make URI.parse barf" do + + let(:name_args) { %w{ftp://lkj\\blah:example.com/blah} } + + it "prints an error and exits" do + expect { ssl_fetch.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl fetch [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `#{name_args[0]}' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + end + end + + describe "normalizing CNs for use as paths" do + + it "normalizes '*' to 'wildcard'" do + expect(ssl_fetch.normalize_cn("*.example.com")).to eq("wildcard_example_com") + end + + it "normalizes non-alnum and hyphen characters to underscores" do + expect(ssl_fetch.normalize_cn("Billy-Bob's Super Awesome CA!")).to eq("Billy-Bob_s_Super_Awesome_CA_") + end + + end + + describe "fetching the remote cert chain" do + + let(:name_args) { %w{https://foo.example.com:8443} } + + let(:tcp_socket) { double(TCPSocket) } + let(:ssl_socket) { double(OpenSSL::SSL::SSLSocket) } + + let(:self_signed_crt_path) { File.join(CHEF_SPEC_DATA, "trusted_certs", "example.crt") } + let(:self_signed_crt) { OpenSSL::X509::Certificate.new(File.read(self_signed_crt_path)) } + + let(:trusted_certs_dir) { Dir.mktmpdir } + + def run + ssl_fetch.run + rescue Exception + puts "OUT: #{stdout_io.string}" + puts "ERR: #{stderr_io.string}" + raise + end + + before do + Chef::Config.trusted_certs_dir = trusted_certs_dir + + TCPSocket.should_receive(:new).with("foo.example.com", 8443).and_return(tcp_socket) + OpenSSL::SSL::SSLSocket.should_receive(:new).with(tcp_socket, ssl_fetch.noverify_peer_ssl_context).and_return(ssl_socket) + ssl_socket.should_receive(:connect) + ssl_socket.should_receive(:peer_cert_chain).and_return([self_signed_crt]) + end + + after do + FileUtils.rm_rf(trusted_certs_dir) + end + + it "fetches the cert chain and writes the certs to the trusted_certs_dir" do + run + stored_cert_path = File.join(trusted_certs_dir, "example_local.crt") + expect(File).to exist(stored_cert_path) + expect(File.read(stored_cert_path)).to eq(File.read(self_signed_crt_path)) + end + end +end diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb index daace18106..8bf85bf604 100644 --- a/spec/unit/knife_spec.rb +++ b/spec/unit/knife_spec.rb @@ -22,6 +22,7 @@ module KnifeSpecs end require 'spec_helper' +require 'uri' describe Chef::Knife do before(:each) do @@ -141,6 +142,60 @@ describe Chef::Knife do end + describe "the headers include X-Remote-Request-Id" do + + let(:headers) {{"Accept"=>"application/json", + "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + 'X-Chef-Version' => Chef::VERSION, + "Host"=>"api.opscode.piab:443", + "X-REMOTE-REQUEST-ID"=>request_id}} + + let(:request_id) {"1234"} + + let(:request_mock) { {} } + + let(:rest) do + Net::HTTP.stub(:new).and_return(http_client) + Chef::RequestID.instance.stub(:request_id).and_return(request_id) + Chef::Config.stub(:chef_server_url).and_return("https://api.opscode.piab") + command = Chef::Knife.run(%w{test yourself}) + rest = command.noauth_rest + rest + end + + let!(:http_client) do + http_client = Net::HTTP.new(url.host, url.port) + http_client.stub(:request).and_yield(http_response).and_return(http_response) + http_client + end + + let(:url) { URI.parse("https://api.opscode.piab") } + + let(:http_response) do + http_response = Net::HTTPSuccess.new("1.1", "200", "successful rest req") + http_response.stub(:read_body) + http_response.stub(:body).and_return(body) + http_response["Content-Length"] = body.bytesize.to_s + http_response + end + + let(:body) { "ninja" } + + before(:each) do + Chef::Config[:chef_server_url] = "https://api.opscode.piab" + 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 "confirms that the headers include X-Remote-Request-Id" do + Net::HTTP::Get.should_receive(:new).with("/monkey", headers).and_return(request_mock) + rest.get_rest("monkey") + end + end + describe "when running a command" do before(:each) do if KnifeSpecs.const_defined?(:TestYourself) diff --git a/spec/unit/mixin/deep_merge_spec.rb b/spec/unit/mixin/deep_merge_spec.rb index 0a7bbffa41..76f5c68a29 100644 --- a/spec/unit/mixin/deep_merge_spec.rb +++ b/spec/unit/mixin/deep_merge_spec.rb @@ -284,6 +284,10 @@ describe Chef::Mixin::DeepMerge do ret.should == {"property" => ["1","2","3","4","5","6"]} end + it "should not error merging un-dupable objects" do + @dm.deep_merge(nil, 4) + end + end describe "role_merge" do @@ -347,5 +351,18 @@ describe Chef::Mixin::DeepMerge do merged_result["top_level_a"]["1_deep_b"].should == %w[B B B] end + it "does not mutate deeply-nested original hashes by default" do + merge_ee_hash = {"top_level_a" => {"1_deep_a" => { "2_deep_a" => { "3_deep_a" => "foo" }}}} + merge_with_hash = {"top_level_a" => {"1_deep_a" => { "2_deep_a" => { "3_deep_b" => "bar" }}}} + @dm.hash_only_merge(merge_ee_hash, merge_with_hash) + merge_ee_hash.should == {"top_level_a" => {"1_deep_a" => { "2_deep_a" => { "3_deep_a" => "foo" }}}} + merge_with_hash.should == {"top_level_a" => {"1_deep_a" => { "2_deep_a" => { "3_deep_b" => "bar" }}}} + end + + it "does not error merging un-dupable items" do + merge_ee_hash = {"top_level_a" => 1, "top_level_b" => false} + merge_with_hash = {"top_level_a" => 2, "top_level_b" => true } + @dm.hash_only_merge(merge_ee_hash, merge_with_hash) + end end end diff --git a/spec/unit/node/attribute_spec.rb b/spec/unit/node/attribute_spec.rb index ef3fc60cc6..bab2e33aa9 100644 --- a/spec/unit/node/attribute_spec.rb +++ b/spec/unit/node/attribute_spec.rb @@ -488,6 +488,13 @@ describe Chef::Node::Attribute do end end + describe "dup" do + it "array can be duped even if some elements can't" do + @attributes.default[:foo] = %w[foo bar baz] + Array(1..3) + [nil, true, false, [ "el", 0, nil ] ] + @attributes.default[:foo].dup + end + end + describe "has_key?" do it "should return true if an attribute exists" do @attributes.has_key?("music").should == true diff --git a/spec/unit/node/immutable_collections_spec.rb b/spec/unit/node/immutable_collections_spec.rb index 0c2b878cd2..d7abfa26e6 100644 --- a/spec/unit/node/immutable_collections_spec.rb +++ b/spec/unit/node/immutable_collections_spec.rb @@ -54,6 +54,32 @@ describe Chef::Node::ImmutableMash do @immutable_mash[:top_level_4][:level2].should be_a(Chef::Node::ImmutableMash) end + describe "to_hash" do + before do + @copy = @immutable_mash.to_hash + end + + it "converts an immutable mash to a new mutable hash" do + @copy.should be_instance_of(Hash) + end + + it "converts an immutable nested mash to a new mutable hash" do + @copy['top_level_4']['level2'].should be_instance_of(Hash) + end + + it "converts an immutable nested array to a new mutable array" do + @copy['top_level_2'].should be_instance_of(Array) + end + + it "should create a mash with the same content" do + @copy.should == @immutable_mash + end + + it 'should allow mutation' do + lambda { @copy['m'] = 'm' }.should_not raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + + end [ :[]=, @@ -86,7 +112,9 @@ end describe Chef::Node::ImmutableArray do before do - @immutable_array = Chef::Node::ImmutableArray.new(%w[foo bar baz]) + @immutable_array = Chef::Node::ImmutableArray.new(%w[foo bar baz] + Array(1..3) + [nil, true, false, [ "el", 0, nil ] ]) + immutable_mash = Chef::Node::ImmutableMash.new({:m => 'm'}) + @immutable_nested_array = Chef::Node::ImmutableArray.new(["level1",@immutable_array, immutable_mash]) end ## @@ -130,10 +158,41 @@ describe Chef::Node::ImmutableArray do end end + it "can be duped even if some elements can't" do + @immutable_array.dup + end + it "returns a mutable version of itself when duped" do mutable = @immutable_array.dup mutable[0] = :value mutable[0].should == :value end + + describe "to_a" do + before do + @copy = @immutable_nested_array.to_a + end + + it "converts an immutable array to a new mutable array" do + @copy.should be_instance_of(Array) + end + + it "converts an immutable nested array to a new mutable array" do + @copy[1].should be_instance_of(Array) + end + + it "converts an immutable nested mash to a new mutable hash" do + @copy[2].should be_instance_of(Hash) + end + + it "should create an array with the same content" do + @copy.should == @immutable_nested_array + end + + it 'should allow mutation' do + lambda { @copy << 'm' }.should_not raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + end + end diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb index f2a78f87cd..832e10f645 100644 --- a/spec/unit/node_spec.rb +++ b/spec/unit/node_spec.rb @@ -724,6 +724,15 @@ describe Chef::Node do json.should =~ /\"run_list\":\[\"role\[Cthulu\]\",\"role\[Hastur\]\"\]/ end + it "should serialize the correct run list", :json => true do + node.run_list << "role[marxist]" + node.run_list << "role[leninist]" + node.override_runlist << "role[stalinist]" + node.run_list.should be_include("role[stalinist]") + json = Chef::JSONCompat.to_json(node) + json.should =~ /\"run_list\":\[\"role\[marxist\]\",\"role\[leninist\]\"\]/ + end + it "merges the override components into a combined override object" do node.attributes.role_override["role override"] = "role override" node.attributes.env_override["env override"] = "env override" diff --git a/spec/unit/platform/query_helpers_spec.rb b/spec/unit/platform/query_helpers_spec.rb new file mode 100644 index 0000000000..2414bdf552 --- /dev/null +++ b/spec/unit/platform/query_helpers_spec.rb @@ -0,0 +1,32 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, 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. +# + +require 'spec_helper' + +describe "Chef::Platform#windows_server_2003?" do + it "returns false early when not on windows" do + Chef::Platform.stub(:windows?).and_return(false) + expect(Chef::Platform).not_to receive(:require) + expect(Chef::Platform.windows_server_2003?).to be_false + end + + # CHEF-4888: Need to call WIN32OLE.ole_initialize in new threads + it "does not raise an exception" do + expect { Thread.fork { Chef::Platform.windows_server_2003? }.join }.not_to raise_error + end +end diff --git a/spec/unit/platform_spec.rb b/spec/unit/platform_spec.rb index e0386a1a61..3d7aef98a0 100644 --- a/spec/unit/platform_spec.rb +++ b/spec/unit/platform_spec.rb @@ -37,7 +37,8 @@ describe "Chef::Platform supports" do :mswin, :mingw32, :windows, - :gcel + :gcel, + :ibm_powerkvm ].each do |platform| it "#{platform}" do Chef::Platform.platforms.should have_key(platform) @@ -47,209 +48,226 @@ end describe Chef::Platform do - before :all do - @original_platform_map = Chef::Platform.platforms - end + context "while testing with fake data" do - after :all do || - Chef::Platform.platforms = @original_platform_map - end + before :all do + @original_platform_map = Chef::Platform.platforms + end - before(:each) do - Chef::Platform.platforms = { - :darwin => { - ">= 10.11" => { - :file => "new_darwinian" + after :all do || + Chef::Platform.platforms = @original_platform_map + end + + before(:each) do + Chef::Platform.platforms = { + :darwin => { + ">= 10.11" => { + :file => "new_darwinian" + }, + "9.2.2" => { + :file => "darwinian", + :else => "thing" + }, + :default => { + :file => "old school", + :snicker => "snack" + } }, - "9.2.2" => { - :file => "darwinian", - :else => "thing" + :mars_volta => { }, :default => { - :file => "old school", - :snicker => "snack" + :file => Chef::Provider::File, + :pax => "brittania", + :cat => "nice" } - }, - :mars_volta => { - }, - :default => { - :file => Chef::Provider::File, - :pax => "brittania", - :cat => "nice" } - } - @events = Chef::EventDispatch::Dispatcher.new - end + @events = Chef::EventDispatch::Dispatcher.new + end - it "should allow you to look up a platform by name and version, returning the provider map for it" do - pmap = Chef::Platform.find("Darwin", "9.2.2") - pmap.should be_a_kind_of(Hash) - pmap[:file].should eql("darwinian") - end + it "should allow you to look up a platform by name and version, returning the provider map for it" do + pmap = Chef::Platform.find("Darwin", "9.2.2") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql("darwinian") + end - it "should allow you to look up a platform by name and version using \"greater than\" style operators" do - pmap = Chef::Platform.find("Darwin", "11.1.0") - pmap.should be_a_kind_of(Hash) - pmap[:file].should eql("new_darwinian") - end + it "should allow you to look up a platform by name and version using \"greater than\" style operators" do + pmap = Chef::Platform.find("Darwin", "11.1.0") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql("new_darwinian") + end - it "should use the default providers for an os if the specific version does not exist" do - pmap = Chef::Platform.find("Darwin", "1") - pmap.should be_a_kind_of(Hash) - pmap[:file].should eql("old school") - end + it "should use the default providers for an os if the specific version does not exist" do + pmap = Chef::Platform.find("Darwin", "1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql("old school") + end - it "should use the default providers if the os doesn't give me a default, but does exist" do - pmap = Chef::Platform.find("mars_volta", "1") - pmap.should be_a_kind_of(Hash) - pmap[:file].should eql(Chef::Provider::File) - end + it "should use the default providers if the os doesn't give me a default, but does exist" do + pmap = Chef::Platform.find("mars_volta", "1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql(Chef::Provider::File) + end - it "should use the default provider if the os does not exist" do - pmap = Chef::Platform.find("AIX", "1") - pmap.should be_a_kind_of(Hash) - pmap[:file].should eql(Chef::Provider::File) - end + it "should use the default provider if the os does not exist" do + pmap = Chef::Platform.find("AIX", "1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql(Chef::Provider::File) + end - it "should merge the defaults for an os with the specific version" do - pmap = Chef::Platform.find("Darwin", "9.2.2") - pmap[:file].should eql("darwinian") - pmap[:snicker].should eql("snack") - end + it "should merge the defaults for an os with the specific version" do + pmap = Chef::Platform.find("Darwin", "9.2.2") + pmap[:file].should eql("darwinian") + pmap[:snicker].should eql("snack") + end - it "should merge the defaults for an os with the universal defaults" do - pmap = Chef::Platform.find("Darwin", "9.2.2") - pmap[:file].should eql("darwinian") - pmap[:pax].should eql("brittania") - end + it "should merge the defaults for an os with the universal defaults" do + pmap = Chef::Platform.find("Darwin", "9.2.2") + pmap[:file].should eql("darwinian") + pmap[:pax].should eql("brittania") + end - it "should allow you to look up a provider for a platform directly by symbol" do - Chef::Platform.find_provider("Darwin", "9.2.2", :file).should eql("darwinian") - end + it "should allow you to look up a provider for a platform directly by symbol" do + Chef::Platform.find_provider("Darwin", "9.2.2", :file).should eql("darwinian") + end - it "should raise an exception if a provider cannot be found for a resource type" do - lambda { Chef::Platform.find_provider("Darwin", "9.2.2", :coffee) }.should raise_error(ArgumentError) - end + it "should raise an exception if a provider cannot be found for a resource type" do + lambda { Chef::Platform.find_provider("Darwin", "9.2.2", :coffee) }.should raise_error(ArgumentError) + end - it "should look up a provider for a resource with a Chef::Resource object" do - kitty = Chef::Resource::Cat.new("loulou") - Chef::Platform.find_provider("Darwin", "9.2.2", kitty).should eql("nice") - end + it "should look up a provider for a resource with a Chef::Resource object" do + kitty = Chef::Resource::Cat.new("loulou") + Chef::Platform.find_provider("Darwin", "9.2.2", kitty).should eql("nice") + end - it "should look up a provider with a node and a Chef::Resource object" do - kitty = Chef::Resource::Cat.new("loulou") - node = Chef::Node.new - node.name("Intel") - node.automatic_attrs[:platform] = "mac_os_x" - node.automatic_attrs[:platform_version] = "9.2.2" - Chef::Platform.find_provider_for_node(node, kitty).should eql("nice") - end + it "should look up a provider with a node and a Chef::Resource object" do + kitty = Chef::Resource::Cat.new("loulou") + node = Chef::Node.new + node.name("Intel") + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "9.2.2" + Chef::Platform.find_provider_for_node(node, kitty).should eql("nice") + end - it "should not throw an exception when the platform version has an unknown format" do - Chef::Platform.find_provider(:darwin, "bad-version", :file).should eql("old school") - end + it "should not throw an exception when the platform version has an unknown format" do + Chef::Platform.find_provider(:darwin, "bad-version", :file).should eql("old school") + end - it "should prefer an explicit provider" do - kitty = Chef::Resource::Cat.new("loulou") - kitty.stub(:provider).and_return(Chef::Provider::File) - node = Chef::Node.new - node.name("Intel") - node.automatic_attrs[:platform] = "mac_os_x" - node.automatic_attrs[:platform_version] = "9.2.2" - Chef::Platform.find_provider_for_node(node, kitty).should eql(Chef::Provider::File) - end + it "should prefer an explicit provider" do + kitty = Chef::Resource::Cat.new("loulou") + kitty.stub(:provider).and_return(Chef::Provider::File) + node = Chef::Node.new + node.name("Intel") + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "9.2.2" + Chef::Platform.find_provider_for_node(node, kitty).should eql(Chef::Provider::File) + end - it "should look up a provider based on the resource name if nothing else matches" do - kitty = Chef::Resource::Cat.new("loulou") - class Chef::Provider::Cat < Chef::Provider; end - Chef::Platform.platforms[:default].delete(:cat) - node = Chef::Node.new - node.name("Intel") - node.automatic_attrs[:platform] = "mac_os_x" - node.automatic_attrs[:platform_version] = "8.5" - Chef::Platform.find_provider_for_node(node, kitty).should eql(Chef::Provider::Cat) - end + it "should look up a provider based on the resource name if nothing else matches" do + kitty = Chef::Resource::Cat.new("loulou") + class Chef::Provider::Cat < Chef::Provider; end + Chef::Platform.platforms[:default].delete(:cat) + node = Chef::Node.new + node.name("Intel") + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "8.5" + Chef::Platform.find_provider_for_node(node, kitty).should eql(Chef::Provider::Cat) + end - def setup_file_resource - node = Chef::Node.new - node.automatic_attrs[:platform] = "mac_os_x" - node.automatic_attrs[:platform_version] = "9.2.2" - run_context = Chef::RunContext.new(node, {}, @events) - [ Chef::Resource::File.new("whateva", run_context), run_context ] - end + def setup_file_resource + node = Chef::Node.new + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "9.2.2" + run_context = Chef::RunContext.new(node, {}, @events) + [ Chef::Resource::File.new("whateva", run_context), run_context ] + end - it "returns a provider object given a Chef::Resource object which has a valid run context and an action" do - file, run_context = setup_file_resource - provider = Chef::Platform.provider_for_resource(file, :foo) - provider.should be_an_instance_of(Chef::Provider::File) - provider.new_resource.should equal(file) - provider.run_context.should equal(run_context) - end + it "returns a provider object given a Chef::Resource object which has a valid run context and an action" do + file, run_context = setup_file_resource + provider = Chef::Platform.provider_for_resource(file, :foo) + provider.should be_an_instance_of(Chef::Provider::File) + provider.new_resource.should equal(file) + provider.run_context.should equal(run_context) + end - it "returns a provider object given a Chef::Resource object which has a valid run context without an action" do - file, run_context = setup_file_resource - provider = Chef::Platform.provider_for_resource(file) - provider.should be_an_instance_of(Chef::Provider::File) - provider.new_resource.should equal(file) - provider.run_context.should equal(run_context) - end + it "returns a provider object given a Chef::Resource object which has a valid run context without an action" do + file, run_context = setup_file_resource + provider = Chef::Platform.provider_for_resource(file) + provider.should be_an_instance_of(Chef::Provider::File) + provider.new_resource.should equal(file) + provider.run_context.should equal(run_context) + end - it "raises an error when trying to find the provider for a resource with no run context" do - file = Chef::Resource::File.new("whateva") - lambda {Chef::Platform.provider_for_resource(file)}.should raise_error(ArgumentError) - end + it "raises an error when trying to find the provider for a resource with no run context" do + file = Chef::Resource::File.new("whateva") + lambda {Chef::Platform.provider_for_resource(file)}.should raise_error(ArgumentError) + end - it "does not support finding a provider by resource and node -- a run context is required" do - lambda {Chef::Platform.provider_for_node('node', 'resource')}.should raise_error(NotImplementedError) - end + it "does not support finding a provider by resource and node -- a run context is required" do + lambda {Chef::Platform.provider_for_node('node', 'resource')}.should raise_error(NotImplementedError) + end + + it "should update the provider map with map" do + Chef::Platform.set( + :platform => :darwin, + :version => "9.2.2", + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:darwin]["9.2.2"][:file].should eql("masterful") + Chef::Platform.set( + :platform => :darwin, + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:darwin][:default][:file].should eql("masterful") + Chef::Platform.set( + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:default][:file].should eql("masterful") + + Chef::Platform.set( + :platform => :hero, + :version => "9.2.2", + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:hero]["9.2.2"][:file].should eql("masterful") + + Chef::Platform.set( + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:default][:file].should eql("masterful") + + Chef::Platform.platforms = {} + + Chef::Platform.set( + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:default][:file].should eql("masterful") + + Chef::Platform.platforms = { :neurosis => {} } + Chef::Platform.set(:platform => :neurosis, :resource => :package, :provider => "masterful") + Chef::Platform.platforms[:neurosis][:default][:package].should eql("masterful") - it "should update the provider map with map" do - Chef::Platform.set( - :platform => :darwin, - :version => "9.2.2", - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:darwin]["9.2.2"][:file].should eql("masterful") - Chef::Platform.set( - :platform => :darwin, - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:darwin][:default][:file].should eql("masterful") - Chef::Platform.set( - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:default][:file].should eql("masterful") - - Chef::Platform.set( - :platform => :hero, - :version => "9.2.2", - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:hero]["9.2.2"][:file].should eql("masterful") - - Chef::Platform.set( - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:default][:file].should eql("masterful") - - Chef::Platform.platforms = {} - - Chef::Platform.set( - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:default][:file].should eql("masterful") - - Chef::Platform.platforms = { :neurosis => {} } - Chef::Platform.set(:platform => :neurosis, :resource => :package, :provider => "masterful") - Chef::Platform.platforms[:neurosis][:default][:package].should eql("masterful") + end end + context "while testing the configured platform data" do + + it "should use the solaris package provider on Solaris <11" do + pmap = Chef::Platform.find("Solaris2", "5.9") + pmap[:package].should eql(Chef::Provider::Package::Solaris) + end + + it "should use the IPS package provider on Solaris 11" do + pmap = Chef::Platform.find("Solaris2", "5.11") + pmap[:package].should eql(Chef::Provider::Package::Ips) + end + + end end diff --git a/spec/unit/policy_builder/expand_node_object_spec.rb b/spec/unit/policy_builder/expand_node_object_spec.rb index 5c6f39d28c..a1e0b881d5 100644 --- a/spec/unit/policy_builder/expand_node_object_spec.rb +++ b/spec/unit/policy_builder/expand_node_object_spec.rb @@ -244,7 +244,7 @@ describe Chef::PolicyBuilder::ExpandNodeObject do it "sets the override run_list on the node" do expect(node.run_list).to eq([override_runlist]) - expect(policy_builder.original_runlist).to eq(primary_runlist) + expect(node.primary_runlist).to eq(primary_runlist) end it "reports that a temporary policy is being used" do diff --git a/spec/unit/provider/cron_spec.rb b/spec/unit/provider/cron_spec.rb index 3a7a96c549..b78266fb25 100644 --- a/spec/unit/provider/cron_spec.rb +++ b/spec/unit/provider/cron_spec.rb @@ -19,15 +19,137 @@ require 'spec_helper' describe Chef::Provider::Cron do + describe "when with special time string" do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Cron.new("cronhole some stuff", @run_context) + @new_resource.user "root" + @new_resource.minute "30" + @new_resource.command "/bin/true" + @new_resource.time :reboot + @provider = Chef::Provider::Cron.new(@new_resource, @run_context) + end + + context "with a matching entry in the user's crontab" do + before :each do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +@reboot /bin/true param1 param2 +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment +CRONTAB + end + + it "should set cron_exists" do + @provider.load_current_resource + @provider.cron_exists.should == true + @provider.cron_empty.should == false + end + + it "should pull the details out of the cron line" do + cron = @provider.load_current_resource + cron.time.should == :reboot + cron.command.should == '/bin/true param1 param2' + end + + it "should pull env vars out" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +MAILTO=foo@example.com +SHELL=/bin/foosh +PATH=/bin:/foo +HOME=/home/foo +@reboot /bin/true param1 param2 +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment +CRONTAB + cron = @provider.load_current_resource + cron.mailto.should == 'foo@example.com' + cron.shell.should == '/bin/foosh' + cron.path.should == '/bin:/foo' + cron.home.should == '/home/foo' + cron.time.should == :reboot + cron.command.should == '/bin/true param1 param2' + end + + it "should parse and load generic and standard environment variables from cron entry" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +# Chef Name: cronhole some stuff +MAILTO=warn@example.com +TEST=lol +FLAG=1 +@reboot /bin/true +CRONTAB + cron = @provider.load_current_resource + + cron.mailto.should == "warn@example.com" + cron.environment.should == {"TEST" => "lol", "FLAG" => "1"} + end + + it "should not break with variables that match the cron resource internals" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +# Chef Name: cronhole some stuff +MINUTE=40 +REBOOT=midnight +TEST=lol +ENVIRONMENT=production +@reboot /bin/true +CRONTAB + cron = @provider.load_current_resource + + cron.time.should == :reboot + cron.environment.should == {"MINUTE" => "40", "REBOOT" => "midnight", "TEST" => "lol", "ENVIRONMENT" => "production"} + end + + it "should report the match" do + Chef::Log.should_receive(:debug).with("Found cron '#{@new_resource.name}'") + @provider.load_current_resource + end + + describe "action_create" do + before :each do + @provider.stub!(:write_crontab) + @provider.stub!(:read_crontab).and_return(nil) + end + + context "when there is no existing crontab" do + before :each do + @provider.cron_exists = false + @provider.cron_empty = true + end + + it "should create a crontab with the entry" do + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +# Chef Name: cronhole some stuff +@reboot /bin/true + ENDCRON + @provider.run_action(:create) + end + end + end + end + end + before do @node = Chef::Node.new @events = Chef::EventDispatch::Dispatcher.new @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Cron.new("cronhole some stuff", @run_context) @new_resource.user "root" @new_resource.minute "30" @new_resource.command "/bin/true" - @provider = Chef::Provider::Cron.new(@new_resource, @run_context) end @@ -110,6 +232,7 @@ CRONTAB cron.day.should == '*' cron.month.should == '1' cron.weekday.should == '*' + cron.time.should == nil cron.command.should == '/bin/true param1 param2' end @@ -138,6 +261,7 @@ CRONTAB cron.day.should == '*' cron.month.should == '1' cron.weekday.should == '*' + cron.time.should == nil cron.command.should == '/bin/true param1 param2' end @@ -227,6 +351,7 @@ CRONTAB cron.day.should == '*' cron.month.should == '*' cron.weekday.should == '*' + cron.time.should == nil cron.command.should == nil end @@ -244,6 +369,7 @@ CRONTAB cron.day.should == '*' cron.month.should == '*' cron.weekday.should == '*' + cron.time.should == nil cron.command.should == nil end @@ -265,6 +391,7 @@ CRONTAB cron.day.should == '*' cron.month.should == '*' cron.weekday.should == '*' + cron.time.should == nil cron.command.should == nil end end @@ -286,6 +413,11 @@ CRONTAB end end + it "should return true if special time string doesn't match" do + @new_resource.send(:time, :reboot) + @provider.cron_different?.should eql(true) + end + it "should return true if environment doesn't match" do @new_resource.environment "FOO" => "something_else" @provider.cron_different?.should eql(true) @@ -833,4 +965,46 @@ MAILTO=foo@example.com end end + + describe "weekday_in_crontab" do + context "when weekday is symbol" do + it "should return weekday in crontab format" do + @new_resource.weekday :wednesday + @provider.send(:weekday_in_crontab).should eq("3") + end + + it "should raise an error with an unknown weekday" do + expect { @new_resource.weekday :caturday }.to raise_error(RangeError) + end + end + + context "when weekday is a number in a string" do + it "should return the string" do + @new_resource.weekday "3" + @provider.send(:weekday_in_crontab).should eq("3") + end + + it "should raise an error with an out of range number" do + expect { @new_resource.weekday "-1" }.to raise_error(RangeError) + end + end + + context "when weekday is string with the name of the week" do + it "should return the string" do + @new_resource.weekday "mon" + @provider.send(:weekday_in_crontab).should eq("mon") + end + end + + context "when weekday is an integer" do + it "should return the integer" do + @new_resource.weekday 1 + @provider.send(:weekday_in_crontab).should eq("1") + end + + it "should raise an error with an out of range integer" do + expect { @new_resource.weekday 45 }.to raise_error(RangeError) + end + end + end end diff --git a/spec/unit/provider/group_spec.rb b/spec/unit/provider/group_spec.rb index 9ff9f85c7c..b138f6b210 100644 --- a/spec/unit/provider/group_spec.rb +++ b/spec/unit/provider/group_spec.rb @@ -96,6 +96,11 @@ describe Chef::Provider::User do @provider.compare_group.should be_false end + it "should coerce an integer to a string for comparison" do + @current_resource.stub!(:gid).and_return("500") + @provider.compare_group.should be_false + end + it "should return false if append is true and the group member(s) already exists" do @current_resource.members << "extra_user" @new_resource.stub(:append).and_return(true) diff --git a/spec/unit/provider/ifconfig/debian_spec.rb b/spec/unit/provider/ifconfig/debian_spec.rb index c2e2d1bfd1..c6a37fdd5b 100644 --- a/spec/unit/provider/ifconfig/debian_spec.rb +++ b/spec/unit/provider/ifconfig/debian_spec.rb @@ -53,38 +53,264 @@ describe Chef::Provider::Ifconfig::Debian do let(:config_filename_ifcfg) { "/etc/network/interfaces.d/ifcfg-#{new_resource.device}" } - describe "generate_config for action_add" do + describe "generate_config" do - let(:config_file_ifaces) { StringIO.new } + context "when writing a file" do + let(:config_file_ifcfg) { StringIO.new } - let(:config_file_ifcfg) { StringIO.new } + let(:tempfile) { Tempfile.new("rspec-chef-ifconfig-debian") } - before do - expect(FileUtils).to receive(:cp) - expect(File).to receive(:open).with(config_filename_ifaces).and_return(StringIO.new) - expect(File).to receive(:open).with(config_filename_ifaces, "w").and_yield(config_file_ifaces) - expect(File).to receive(:new).with(config_filename_ifcfg, "w").and_return(config_file_ifcfg) - expect(File).to receive(:exist?).with(config_filename_ifaces).and_return(true) - end + let(:tempdir_path) { Dir.mktmpdir("rspec-chef-ifconfig-debian-dir") } + + let(:config_filename_ifcfg) { "#{tempdir_path}/ifcfg-#{new_resource.device}" } + + before do + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_FILE", tempfile.path) + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_DOT_D_DIR", tempdir_path) + expect(File).to receive(:new).with(config_filename_ifcfg, "w").and_return(config_file_ifcfg) + end + + it "should write a network-script" do + provider.run_action(:add) + expect(config_file_ifcfg.string).to match(/^iface eth0 inet static\s*$/) + expect(config_file_ifcfg.string).to match(/^\s+address 10\.0\.0\.1\s*$/) + expect(config_file_ifcfg.string).to match(/^\s+netmask 255\.255\.254\.0\s*$/) + end + + context "when the interface_dot_d directory does not exist" do + before do + FileUtils.rmdir tempdir_path + expect(File.exists?(tempdir_path)).to be_false + end + + it "should create the /etc/network/interfaces.d directory" do + provider.run_action(:add) + expect(File.exists?(tempdir_path)).to be_true + expect(File.directory?(tempdir_path)).to be_true + end - it "should create network-scripts directory" do - expect(File).to receive(:directory?).with(File.dirname(config_filename_ifcfg)).and_return(false) - expect(Dir).to receive(:mkdir).with(File.dirname(config_filename_ifcfg)) - provider.run_action(:add) + it "should mark the resource as updated" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end + + context "when the interface_dot_d directory exists" do + before do + expect(File.exists?(tempdir_path)).to be_true + end + + it "should still mark the resource as updated (we still write a file to it)" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end end - it "should write configure network-scripts directory" do - expect(File).to receive(:directory?).with(File.dirname(config_filename_ifcfg)).and_return(true) - provider.run_action(:add) - expect(config_file_ifaces.string).to match(/^\s*source\s+\/etc\/network\/interfaces[.]d\/[*]\s*$/) + context "when the file is up-to-date" do + let(:tempfile) { Tempfile.new("rspec-chef-ifconfig-debian") } + + let(:tempdir_path) { Dir.mktmpdir("rspec-chef-ifconfig-debian-dir") } + + let(:config_filename_ifcfg) { "#{tempdir_path}/ifcfg-#{new_resource.device}" } + + before do + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_FILE", tempfile.path) + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_DOT_D_DIR", tempdir_path) + config_file_ifcfg = StringIO.new(<<-EOF +iface eth0 inet static + address 10.0.0.1 + netmask 255.255.254.0 +EOF + ) + expect(File).to receive(:new).with(config_filename_ifcfg, "w").and_return(config_file_ifcfg) + expect(File.exists?(tempdir_path)).to be_true # since the file exists, the enclosing dir must also exist + end + + context "when the /etc/network/interfaces file has the source line" do + let(:expected_string) do + <<-EOF +a line +source #{tempdir_path}/* +another line +EOF + end + + before do + tempfile.write(expected_string) + tempfile.close + end + + it "should preserve all the contents" do + provider.run_action(:add) + expect(IO.read(tempfile.path)).to eq(expected_string) + end + + it "should not mark the resource as updated" do + provider.run_action(:add) + pending "superclass ifconfig provider is not idempotent" + expect(new_resource.updated_by_last_action?).to be_false + end + end + + context "when the /etc/network/interfaces file does not have the source line" do + let(:expected_string) do + <<-EOF +a line +another line +source #{tempdir_path}/* +EOF + end + + before do + tempfile.write("a line\nanother line\n") + tempfile.close + end + + it "should preserve the original contents and add the source line" do + provider.run_action(:add) + expect(IO.read(tempfile.path)).to eq(expected_string) + end + + it "should mark the resource as updated" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end end - it "should write a network-script" do - expect(File).to receive(:directory?).with(File.dirname(config_filename_ifcfg)).and_return(true) - provider.run_action(:add) - expect(config_file_ifcfg.string).to match(/^iface eth0 inet static\s*$/) - expect(config_file_ifcfg.string).to match(/^\s+address 10\.0\.0\.1\s*$/) - expect(config_file_ifcfg.string).to match(/^\s+netmask 255\.255\.254\.0\s*$/) + describe "when running under why run" do + + before do + Chef::Config[:why_run] = true + end + + after do + Chef::Config[:why_run] = false + end + + context "when writing a file" do + let(:config_file_ifcfg) { StringIO.new } + + let(:tempfile) { Tempfile.new("rspec-chef-ifconfig-debian") } + + let(:tempdir_path) { Dir.mktmpdir("rspec-chef-ifconfig-debian-dir") } + + let(:config_filename_ifcfg) { "#{tempdir_path}/ifcfg-#{new_resource.device}" } + + before do + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_FILE", tempfile.path) + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_DOT_D_DIR", tempdir_path) + expect(File).not_to receive(:new).with(config_filename_ifcfg, "w") + end + + it "should write a network-script" do + provider.run_action(:add) + expect(config_file_ifcfg.string).not_to match(/^iface eth0 inet static\s*$/) + expect(config_file_ifcfg.string).not_to match(/^\s+address 10\.0\.0\.1\s*$/) + expect(config_file_ifcfg.string).not_to match(/^\s+netmask 255\.255\.254\.0\s*$/) + end + + context "when the interface_dot_d directory does not exist" do + before do + FileUtils.rmdir tempdir_path + expect(File.exists?(tempdir_path)).to be_false + end + + it "should not create the /etc/network/interfaces.d directory" do + provider.run_action(:add) + expect(File.exists?(tempdir_path)).not_to be_true + end + + it "should mark the resource as updated" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end + + context "when the interface_dot_d directory exists" do + before do + expect(File.exists?(tempdir_path)).to be_true + end + + it "should still mark the resource as updated (we still write a file to it)" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end + end + + context "when the file is up-to-date" do + let(:tempfile) { Tempfile.new("rspec-chef-ifconfig-debian") } + + let(:tempdir_path) { Dir.mktmpdir("rspec-chef-ifconfig-debian-dir") } + + let(:config_filename_ifcfg) { "#{tempdir_path}/ifcfg-#{new_resource.device}" } + + before do + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_FILE", tempfile.path) + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_DOT_D_DIR", tempdir_path) + config_file_ifcfg = StringIO.new(<<-EOF +iface eth0 inet static + address 10.0.0.1 + netmask 255.255.254.0 + EOF + ) + expect(File).not_to receive(:new).with(config_filename_ifcfg, "w") + expect(File.exists?(tempdir_path)).to be_true # since the file exists, the enclosing dir must also exist + end + + context "when the /etc/network/interfaces file has the source line" do + let(:expected_string) do + <<-EOF +a line +source #{tempdir_path}/* +another line + EOF + end + + before do + tempfile.write(expected_string) + tempfile.close + end + + it "should preserve all the contents" do + provider.run_action(:add) + expect(IO.read(tempfile.path)).to eq(expected_string) + end + + it "should not mark the resource as updated" do + provider.run_action(:add) + pending "superclass ifconfig provider is not idempotent" + expect(new_resource.updated_by_last_action?).to be_false + end + end + + context "when the /etc/network/interfaces file does not have the source line" do + let(:expected_string) do + <<-EOF +a line +another line +source #{tempdir_path}/* + EOF + end + + before do + tempfile.write("a line\nanother line\n") + tempfile.close + end + + it "should preserve the original contents and not add the source line" do + provider.run_action(:add) + expect(IO.read(tempfile.path)).to eq("a line\nanother line\n") + end + + it "should mark the resource as updated" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end + end end end @@ -98,4 +324,5 @@ describe Chef::Provider::Ifconfig::Debian do provider.run_action(:delete) end end + end diff --git a/spec/unit/provider/mount/mount_spec.rb b/spec/unit/provider/mount/mount_spec.rb index 99e78590f1..e27cf71e01 100644 --- a/spec/unit/provider/mount/mount_spec.rb +++ b/spec/unit/provider/mount/mount_spec.rb @@ -141,6 +141,17 @@ describe Chef::Provider::Mount::Mount do @provider.current_resource.mounted.should be_true end + it "should set mounted true if the symlink target of the device is relative and is found in the mounts list - CHEF-4957" do + target = "xsdz1" + + ::File.stub(:symlink?).with("#{@new_resource.device}").and_return(true) + ::File.stub(:readlink).with("#{@new_resource.device}").and_return(target) + + @provider.stub(:shell_out!).and_return(OpenStruct.new(:stdout => "/dev/xsdz1 on /tmp/foo type ext3 (rw)\n")) + @provider.load_current_resource() + @provider.current_resource.mounted.should be_true + end + it "should set mounted true if the mount point is found last in the mounts list" do mount = "/dev/sdy1 on #{@new_resource.mount_point} type ext3 (rw)\n" mount << "#{@new_resource.device} on #{@new_resource.mount_point} type ext3 (rw)\n" @@ -199,6 +210,20 @@ describe Chef::Provider::Mount::Mount do @provider.current_resource.enabled.should be_true end + it "should set enabled to true if the symlink target is relative and is in fstab - CHEF-4957" do + target = "xsdz1" + + ::File.stub(:symlink?).with("#{@new_resource.device}").and_return(true) + ::File.stub(:readlink).with("#{@new_resource.device}").and_return(target) + + fstab = "/dev/sdz1 /tmp/foo ext3 defaults 1 2\n" + + ::File.stub(:foreach).with("/etc/fstab").and_yield fstab + + @provider.load_current_resource + @provider.current_resource.enabled.should be_true + end + it "should set enabled to false if the mount point is not in fstab" do fstab = "/dev/sdy1 #{@new_resource.mount_point} ext3 defaults 1 2\n" ::File.stub(:foreach).with("/etc/fstab").and_yield fstab diff --git a/spec/unit/provider/ohai_spec.rb b/spec/unit/provider/ohai_spec.rb index 8b8a6b5939..2085f44309 100644 --- a/spec/unit/provider/ohai_spec.rb +++ b/spec/unit/provider/ohai_spec.rb @@ -41,9 +41,8 @@ describe Chef::Provider::Ohai do :newdata => "somevalue" } } - mock_ohai.stub(:all_plugins).and_return(true) - mock_ohai.stub(:require_plugin).and_return(true) - mock_ohai.stub(:data).and_return(mock_ohai[:data], + mock_ohai.stub!(:all_plugins).and_return(true) + mock_ohai.stub!(:data).and_return(mock_ohai[:data], mock_ohai[:data2]) Ohai::System.stub(:new).and_return(mock_ohai) Chef::Platform.stub(:find_platform_and_version).and_return({ "platform" => @platform, diff --git a/spec/unit/provider/package/dpkg_spec.rb b/spec/unit/provider/package/dpkg_spec.rb index 6ba7695a1e..22edeb7b9b 100644 --- a/spec/unit/provider/package/dpkg_spec.rb +++ b/spec/unit/provider/package/dpkg_spec.rb @@ -72,6 +72,10 @@ describe Chef::Provider::Package::Dpkg do it 'if distro-specific version provided' do check_version('1.11.4-1ubuntu1~lucid') end + + it 'returns the version if an epoch is used' do + check_version('1:1.8.3-2') + end end it "gets the source package name from dpkg-deb correctly when the package name has `-', `+' or `.' characters" do diff --git a/spec/unit/provider/package/windows/msi_spec.rb b/spec/unit/provider/package/windows/msi_spec.rb new file mode 100644 index 0000000000..c8a63ad066 --- /dev/null +++ b/spec/unit/provider/package/windows/msi_spec.rb @@ -0,0 +1,60 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, 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. +# + +require 'spec_helper' + +describe Chef::Provider::Package::Windows::MSI, :windows_only do + let(:node) { double('Chef::Node') } + let(:events) { double('Chef::Events').as_null_object } # mock all the methods + let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } + let(:new_resource) { Chef::Resource::WindowsPackage.new("calculator.msi") } + let(:provider) { Chef::Provider::Package::Windows::MSI.new(new_resource) } + + describe "expand_options" do + it "returns an empty string if passed no options" do + expect(provider.expand_options(nil)).to eql "" + end + + it "returns a string with a leading space if passed options" do + expect(provider.expand_options("--train nope --town no_way")).to eql(" --train nope --town no_way") + end + end + + describe "installed_version" do + it "returns the installed version" do + provider.stub(:get_product_property).and_return("{23170F69-40C1-2702-0920-000001000000}") + provider.stub(:get_installed_version).with("{23170F69-40C1-2702-0920-000001000000}").and_return("3.14159.1337.42") + expect(provider.installed_version).to eql("3.14159.1337.42") + end + end + + describe "package_version" do + it "returns the version of a package" do + provider.stub(:get_product_property).with(/calculator.msi$/, "ProductVersion").and_return(42) + expect(provider.package_version).to eql(42) + end + end + + describe "install_package" do + # calls shell_out! + end + + describe "remove_package" do + # calls shell_out! + end +end diff --git a/spec/unit/provider/package/windows_spec.rb b/spec/unit/provider/package/windows_spec.rb new file mode 100644 index 0000000000..e94404eea5 --- /dev/null +++ b/spec/unit/provider/package/windows_spec.rb @@ -0,0 +1,80 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, 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. +# + +require 'spec_helper' + +describe Chef::Provider::Package::Windows, :windows_only do + let(:node) { double('Chef::Node') } + let(:events) { double('Chef::Events').as_null_object } # mock all the methods + let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } + let(:new_resource) { Chef::Resource::WindowsPackage.new("calculator.msi") } + let(:provider) { Chef::Provider::Package::Windows.new(new_resource, run_context) } + + describe "load_current_resource" do + before(:each) do + provider.stub(:package_provider).and_return(double('package_provider', + :installed_version => "1.0", :package_version => "2.0")) + end + + it "creates a current resource with the name of the new resource" do + provider.load_current_resource + expect(provider.current_resource).to be_a(Chef::Resource::WindowsPackage) + expect(provider.current_resource.name).to eql("calculator.msi") + end + + it "sets the current version if the package is installed" do + provider.load_current_resource + expect(provider.current_resource.version).to eql("1.0") + end + + it "sets the version to be installed" do + provider.load_current_resource + expect(provider.new_resource.version).to eql("2.0") + end + end + + describe "package_provider" do + it "sets the package provider to MSI if the the installer type is :msi" do + provider.stub(:installer_type).and_return(:msi) + expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::MSI) + end + + it "raises an error if the installer_type is unknown" do + provider.stub(:installer_type).and_return(:apt_for_windows) + expect { provider.package_provider }.to raise_error + end + end + + describe "installer_type" do + it "it returns @installer_type if it is set" do + provider.new_resource.installer_type("downeaster") + expect(provider.installer_type).to eql("downeaster") + end + + it "sets installer_type to msi if the source ends in .msi" do + provider.new_resource.source("microsoft_installer.msi") + expect(provider.installer_type).to eql(:msi) + end + + it "raises an error if it cannot determine the installer type" do + provider.new_resource.installer_type(nil) + provider.new_resource.source("tomfoolery.now") + expect { provider.installer_type }.to raise_error(ArgumentError) + end + end +end diff --git a/spec/unit/provider/service/macosx_spec.rb b/spec/unit/provider/service/macosx_spec.rb index 65639f2084..1e9656aeac 100644 --- a/spec/unit/provider/service/macosx_spec.rb +++ b/spec/unit/provider/service/macosx_spec.rb @@ -46,14 +46,32 @@ describe Chef::Provider::Service::Macosx do let(:events) {Chef::EventDispatch::Dispatcher.new} let(:run_context) { Chef::RunContext.new(node, {}, events) } let(:provider) { described_class.new(new_resource, run_context) } - let(:stdout) { StringIO.new } + let(:launchctl_stdout) { StringIO.new } + let(:plutil_stdout) { String.new <<-XML } +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>io.redis.redis-server</string> +</dict> +</plist> +XML ["redis-server", "io.redis.redis-server"].each do |service_name| before do Dir.stub(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) provider.stub(:shell_out!). with("launchctl list", {:group => 1001, :user => 101}). - and_return(double("ouput", :stdout => stdout)) + and_return(double("Status", :stdout => launchctl_stdout)) + provider.stub(:shell_out). + with(/launchctl list /, + {:group => nil, :user => nil}). + and_return(double("Status", + :stdout => launchctl_stdout, :exitstatus => 0)) + provider.stub(:shell_out!). + with(/plutil -convert xml1 -o/). + and_return(double("Status", :stdout => plutil_stdout)) File.stub(:stat).and_return(double("stat", :gid => 1001, :uid => 101)) end @@ -64,7 +82,7 @@ describe Chef::Provider::Service::Macosx do describe "#load_current_resource" do context "when launchctl returns pid in service list" do - let(:stdout) { StringIO.new <<-SVC_LIST } + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } 12761 - 0x100114220.old.machinit.thing 7777 - io.redis.redis-server - - com.lol.stopped-thing @@ -84,21 +102,21 @@ describe Chef::Provider::Service::Macosx do end describe "running unsupported actions" do + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } +12761 - 0x100114220.old.machinit.thing +7777 - io.redis.redis-server +- - com.lol.stopped-thing +SVC_LIST + before do Dir.stub(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) end - it "should throw an exception when enable action is attempted" do - lambda {provider.run_action(:enable)}.should raise_error(Chef::Exceptions::UnsupportedAction) - end it "should throw an exception when reload action is attempted" do lambda {provider.run_action(:reload)}.should raise_error(Chef::Exceptions::UnsupportedAction) end - it "should throw an exception when disable action is attempted" do - lambda {provider.run_action(:disable)}.should raise_error(Chef::Exceptions::UnsupportedAction) - end end context "when launchctl returns empty service pid" do - let(:stdout) { StringIO.new <<-SVC_LIST } + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } 12761 - 0x100114220.old.machinit.thing - - io.redis.redis-server - - com.lol.stopped-thing @@ -118,7 +136,7 @@ describe Chef::Provider::Service::Macosx do end context "when launchctl doesn't return service entry at all" do - let(:stdout) { StringIO.new <<-SVC_LIST } + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } 12761 - 0x100114220.old.machinit.thing - - com.lol.stopped-thing SVC_LIST diff --git a/spec/unit/provider/service/solaris_smf_service_spec.rb b/spec/unit/provider/service/solaris_smf_service_spec.rb index 887c1f6b5f..af1351a4ff 100644 --- a/spec/unit/provider/service/solaris_smf_service_spec.rb +++ b/spec/unit/provider/service/solaris_smf_service_spec.rb @@ -54,34 +54,47 @@ describe Chef::Provider::Service::Solaris do describe "when discovering the current service state" do it "should create a current resource with the name of the new resource" do - @provider.stub(:popen4).with("/bin/svcs -l chef").and_return(@status) + @provider.stub!(:shell_out!).with("/bin/svcs -l chef").and_return(@status) Chef::Resource::Service.should_receive(:new).and_return(@current_resource) @provider.load_current_resource end it "should return the current resource" do - @provider.stub(:popen4).with("/bin/svcs -l chef").and_return(@status) + @provider.stub!(:shell_out!).with("/bin/svcs -l chef").and_return(@status) @provider.load_current_resource.should eql(@current_resource) end - it "should popen4 '/bin/svcs -l service_name'" do - @provider.should_receive(:popen4).with("/bin/svcs -l chef").and_return(@status) + it "should call '/bin/svcs -l service_name'" do + @provider.should_receive(:shell_out!).with("/bin/svcs -l chef").and_return(@status) @provider.load_current_resource end it "should mark service as not running" do - @provider.stub(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.stub!(:shell_out!).and_return(@status) @current_resource.should_receive(:running).with(false) @provider.load_current_resource end it "should mark service as running" do - @stdout.stub(:each).and_yield("state online") - @provider.stub(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @status = mock("Status", :exitstatus => 0, :stdout => 'state online') + @provider.stub!(:shell_out!).and_return(@status) @current_resource.should_receive(:running).with(true) @provider.load_current_resource end + + it "should not mark service as maintenance" do + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @provider.maintenance.should be_false + end + + it "should mark service as maintenance" do + @status = mock("Status", :exitstatus => 0, :stdout => 'state maintenance') + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @provider.maintenance.should be_true + end end describe "when enabling the service" do @@ -91,19 +104,31 @@ describe Chef::Provider::Service::Solaris do end it "should call svcadm enable -s chef" do - @new_resource.stub(:enable_command).and_return("#{@new_resource.enable_command}") + @new_resource.stub!(:enable_command).and_return("#{@new_resource.enable_command}") + @provider.should_not_receive(:shell_out!).with("/usr/sbin/svcadm clear #{@current_resource.service_name}") @provider.should_receive(:shell_out!).with("/usr/sbin/svcadm enable -s #{@current_resource.service_name}").and_return(@status) - @provider.enable_service.should be_true + @provider.enable_service.should be_true @current_resource.enabled.should be_true end it "should call svcadm enable -s chef for start_service" do - @new_resource.stub(:start_command).and_return("#{@new_resource.start_command}") + @new_resource.stub!(:start_command).and_return("#{@new_resource.start_command}") + @provider.should_not_receive(:shell_out!).with("/usr/sbin/svcadm clear #{@current_resource.service_name}") @provider.should_receive(:shell_out!).with("/usr/sbin/svcadm enable -s #{@current_resource.service_name}").and_return(@status) @provider.start_service.should be_true @current_resource.enabled.should be_true end + it "should call svcadm clear chef for start_service when state maintenance" do + @status = mock("Status", :exitstatus => 0, :stdout => 'state maintenance') + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @new_resource.stub!(:enable_command).and_return("#{@new_resource.enable_command}") + @provider.should_receive(:shell_out!).with("/usr/sbin/svcadm clear #{@current_resource.service_name}").and_return(@status) + @provider.should_receive(:shell_out!).with("/usr/sbin/svcadm enable -s #{@current_resource.service_name}").and_return(@status) + @provider.enable_service.should be_true + @current_resource.enabled.should be_true + end end diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb index b0cd04b245..2bdf470143 100644 --- a/spec/unit/recipe_spec.rb +++ b/spec/unit/recipe_spec.rb @@ -339,6 +339,7 @@ describe Chef::Recipe do describe "include_recipe" do it "should evaluate another recipe with include_recipe" do node.should_receive(:loaded_recipe).with(:openldap, "gigantor") + run_context.stub(:unreachable_cookbook?).with(:openldap).and_return(false) run_context.include_recipe "openldap::gigantor" res = run_context.resource_collection.resources(:cat => "blanket") res.name.should eql("blanket") @@ -347,6 +348,7 @@ describe Chef::Recipe do it "should load the default recipe for a cookbook if include_recipe is called without a ::" do node.should_receive(:loaded_recipe).with(:openldap, "default") + run_context.stub(:unreachable_cookbook?).with(:openldap).and_return(false) run_context.include_recipe "openldap" res = run_context.resource_collection.resources(:cat => "blanket") res.name.should eql("blanket") @@ -355,12 +357,14 @@ describe Chef::Recipe do it "should store that it has seen a recipe in the run_context" do node.should_receive(:loaded_recipe).with(:openldap, "default") + run_context.stub(:unreachable_cookbook?).with(:openldap).and_return(false) run_context.include_recipe "openldap" run_context.loaded_recipe?("openldap").should be_true end it "should not include the same recipe twice" do node.should_receive(:loaded_recipe).with(:openldap, "default").exactly(:once) + run_context.stub(:unreachable_cookbook?).with(:openldap).and_return(false) cookbook_collection[:openldap].should_receive(:load_recipe).with("default", run_context) recipe.include_recipe "openldap" cookbook_collection[:openldap].should_not_receive(:load_recipe).with("default", run_context) diff --git a/spec/unit/resource/conditional_spec.rb b/spec/unit/resource/conditional_spec.rb index 1be7bcea71..4df185bcd6 100644 --- a/spec/unit/resource/conditional_spec.rb +++ b/spec/unit/resource/conditional_spec.rb @@ -24,12 +24,13 @@ describe Chef::Resource::Conditional do Mixlib::ShellOut.any_instance.stub(:run_command).and_return(nil) @status = OpenStruct.new(:success? => true) Mixlib::ShellOut.any_instance.stub(:status).and_return(@status) + @parent_resource = Chef::Resource.new(nil, Chef::Node.new) end describe "when created as an `only_if`" do describe "after running a successful command" do before do - @conditional = Chef::Resource::Conditional.only_if("true") + @conditional = Chef::Resource::Conditional.only_if(@parent_resource, "true") end it "indicates that resource convergence should continue" do @@ -40,7 +41,7 @@ describe Chef::Resource::Conditional do describe "after running a negative/false command" do before do @status.send("success?=", false) - @conditional = Chef::Resource::Conditional.only_if("false") + @conditional = Chef::Resource::Conditional.only_if(@parent_resource, "false") end it "indicates that resource convergence should not continue" do @@ -50,8 +51,8 @@ describe Chef::Resource::Conditional do describe 'after running a command which timed out' do before do - @conditional = Chef::Resource::Conditional.only_if("false") - @conditional.stub(:shell_out).and_raise(Chef::Exceptions::CommandTimeout) + @conditional = Chef::Resource::Conditional.only_if(@parent_resource, "false") + Chef::GuardInterpreter::DefaultGuardInterpreter.any_instance.stub(:shell_out).and_raise(Chef::Exceptions::CommandTimeout) end it 'indicates that resource convergence should not continue' do @@ -66,7 +67,7 @@ describe Chef::Resource::Conditional do describe "after running a block that returns a truthy value" do before do - @conditional = Chef::Resource::Conditional.only_if { Object.new } + @conditional = Chef::Resource::Conditional.only_if(@parent_resource) { Object.new } end it "indicates that resource convergence should continue" do @@ -76,7 +77,7 @@ describe Chef::Resource::Conditional do describe "after running a block that returns a falsey value" do before do - @conditional = Chef::Resource::Conditional.only_if { nil } + @conditional = Chef::Resource::Conditional.only_if(@parent_resource) { nil } end it "indicates that resource convergence should not continue" do @@ -88,7 +89,7 @@ describe Chef::Resource::Conditional do describe "when created as a `not_if`" do describe "after running a successful/true command" do before do - @conditional = Chef::Resource::Conditional.not_if("true") + @conditional = Chef::Resource::Conditional.not_if(@parent_resource, "true") end it "indicates that resource convergence should not continue" do @@ -99,7 +100,7 @@ describe Chef::Resource::Conditional do describe "after running a failed/false command" do before do @status.send("success?=", false) - @conditional = Chef::Resource::Conditional.not_if("false") + @conditional = Chef::Resource::Conditional.not_if(@parent_resource, "false") end it "indicates that resource convergence should continue" do @@ -109,8 +110,8 @@ describe Chef::Resource::Conditional do describe 'after running a command which timed out' do before do - @conditional = Chef::Resource::Conditional.not_if("false") - @conditional.stub(:shell_out).and_raise(Chef::Exceptions::CommandTimeout) + @conditional = Chef::Resource::Conditional.not_if(@parent_resource, "false") + Chef::GuardInterpreter::DefaultGuardInterpreter.any_instance.stub(:shell_out).and_raise(Chef::Exceptions::CommandTimeout) end it 'indicates that resource convergence should continue' do @@ -125,7 +126,7 @@ describe Chef::Resource::Conditional do describe "after running a block that returns a truthy value" do before do - @conditional = Chef::Resource::Conditional.not_if { Object.new } + @conditional = Chef::Resource::Conditional.not_if(@parent_resource) { Object.new } end it "indicates that resource convergence should not continue" do @@ -135,7 +136,7 @@ describe Chef::Resource::Conditional do describe "after running a block that returns a falsey value" do before do - @conditional = Chef::Resource::Conditional.not_if { nil } + @conditional = Chef::Resource::Conditional.not_if(@parent_resource) { nil } end it "indicates that resource convergence should continue" do diff --git a/spec/unit/resource/cron_spec.rb b/spec/unit/resource/cron_spec.rb index 355a7f09ba..cf821e3d32 100644 --- a/spec/unit/resource/cron_spec.rb +++ b/spec/unit/resource/cron_spec.rb @@ -143,8 +143,13 @@ describe Chef::Resource::Cron do lambda { @resource.month "13" }.should raise_error(RangeError) end - it "should reject any weekday over 7" do - lambda { @resource.weekday "8" }.should raise_error(RangeError) + describe "weekday" do + it "should reject any weekday over 7" do + lambda { @resource.weekday "8" }.should raise_error(RangeError) + end + it "should reject any symbols which don't represent day of week" do + lambda { @resource.weekday :foo }.should raise_error(RangeError) + end end it "should convert integer schedule values to a string" do diff --git a/spec/unit/resource/powershell_spec.rb b/spec/unit/resource/powershell_spec.rb index a35e37c696..da20c4f0bf 100644 --- a/spec/unit/resource/powershell_spec.rb +++ b/spec/unit/resource/powershell_spec.rb @@ -36,7 +36,91 @@ describe Chef::Resource::PowershellScript do @resource.should be_a_kind_of(Chef::Resource::PowershellScript) end - context "windowsscript" do + it "should set convert_boolean_return to false by default" do + @resource.convert_boolean_return.should == false + end + + it "should return the value for convert_boolean_return that was set" do + @resource.convert_boolean_return true + @resource.convert_boolean_return.should == true + @resource.convert_boolean_return false + @resource.convert_boolean_return.should == false + end + + context "when using guards" do + let(:resource) { @resource } + before(:each) do + resource.stub(:run_action) + resource.stub(:updated).and_return(true) + end + + it "inherits exactly the :cwd, :environment, :group, :path, :user, :umask, and :architecture attributes from a parent resource class" do + inherited_difference = Chef::Resource::PowershellScript.guard_inherited_attributes - + [:cwd, :environment, :group, :path, :user, :umask, :architecture ] + + inherited_difference.should == [] + end + + it "should allow guard interpreter to be set to Chef::Resource::Script" do + resource.guard_interpreter(:script) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(false) + resource.only_if("echo hi") + end + + it "should allow guard interpreter to be set to Chef::Resource::Bash derived from Chef::Resource::Script" do + resource.guard_interpreter(:bash) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(false) + resource.only_if("echo hi") + end + + it "should allow guard interpreter to be set to Chef::Resource::PowershellScript derived indirectly from Chef::Resource::Script" do + resource.guard_interpreter(:powershell_script) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(false) + resource.only_if("echo hi") + end + + it "should enable convert_boolean_return by default for guards in the context of powershell_script when no guard params are specified" do + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(true) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:block_from_attributes).with( + {:convert_boolean_return => true, :code => "$true"}).and_return(Proc.new {}) + resource.only_if("$true") + end + + it "should enable convert_boolean_return by default for guards in non-Chef::Resource::Script derived resources when no guard params are specified" do + node = Chef::Node.new + run_context = Chef::RunContext.new(node, nil, nil) + file_resource = Chef::Resource::File.new('idontexist', run_context) + file_resource.guard_interpreter :powershell_script + + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:block_from_attributes).with( + {:convert_boolean_return => true, :code => "$true"}).and_return(Proc.new {}) + resource.only_if("$true") + end + + it "should enable convert_boolean_return by default for guards in the context of powershell_script when guard params are specified" do + guard_parameters = {:cwd => '/etc/chef', :architecture => :x86_64} + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:block_from_attributes).with( + {:convert_boolean_return => true, :code => "$true"}.merge(guard_parameters)).and_return(Proc.new {}) + resource.only_if("$true", guard_parameters) + end + + it "should pass convert_boolean_return as true if it was specified as true in a guard parameter" do + guard_parameters = {:cwd => '/etc/chef', :convert_boolean_return => true, :architecture => :x86_64} + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:block_from_attributes).with( + {:convert_boolean_return => true, :code => "$true"}.merge(guard_parameters)).and_return(Proc.new {}) + resource.only_if("$true", guard_parameters) + end + + it "should pass convert_boolean_return as false if it was specified as true in a guard parameter" do + other_guard_parameters = {:cwd => '/etc/chef', :architecture => :x86_64} + parameters_with_boolean_disabled = other_guard_parameters.merge({:convert_boolean_return => false, :code => "$true"}) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:block_from_attributes).with( + parameters_with_boolean_disabled).and_return(Proc.new {}) + resource.only_if("$true", parameters_with_boolean_disabled) + end + end + + context "as a script running in Windows-based scripting language" do let(:resource_instance) { @resource } let(:resource_instance_name ) { @resource.command } let(:resource_name) { :powershell_script } @@ -44,5 +128,4 @@ describe Chef::Resource::PowershellScript do it_should_behave_like "a Windows script resource" end - end diff --git a/spec/unit/resource/subversion_spec.rb b/spec/unit/resource/subversion_spec.rb index 67593c5a7c..ae06ce665a 100644 --- a/spec/unit/resource/subversion_spec.rb +++ b/spec/unit/resource/subversion_spec.rb @@ -55,4 +55,9 @@ describe Chef::Resource::Subversion do @svn.svn_arguments.should be_nil end + it "hides password from custom exception message" do + @svn.svn_password "l33th4x0rpa$$w0rd" + e = @svn.customize_exception(Chef::Exceptions::Exec.new "Exception with password #{@svn.svn_password}") + e.message.include?(@svn.svn_password).should be_false + end end diff --git a/spec/unit/resource/windows_package_spec.rb b/spec/unit/resource/windows_package_spec.rb new file mode 100644 index 0000000000..c9ef8d910c --- /dev/null +++ b/spec/unit/resource/windows_package_spec.rb @@ -0,0 +1,74 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, 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. +# + +require 'spec_helper' + +describe Chef::Resource::WindowsPackage, "initialize", :windows_only do + + let(:resource) { Chef::Resource::WindowsPackage.new("solitaire.msi") } + + it "returns a Chef::Resource::WindowsPackage" do + expect(resource).to be_a_kind_of(Chef::Resource::WindowsPackage) + end + + it "sets the resource_name to :windows_package" do + expect(resource.resource_name).to eql(:windows_package) + end + + it "sets the provider to Chef::Provider::Package::Windows" do + expect(resource.provider).to eql(Chef::Provider::Package::Windows) + end + + it "supports setting installer_type" do + resource.installer_type("msi") + expect(resource.installer_type).to eql("msi") + end + + # String, Integer + [ "600", 600 ].each do |val| + it "supports setting a timeout as a #{val.class}" do + resource.timeout(val) + expect(resource.timeout).to eql(val) + end + end + + # String, Integer, Array + [ "42", 42, [47, 48, 49] ].each do |val| + it "supports setting an alternate return value as a #{val.class}" do + resource.returns(val) + expect(resource.returns).to eql(val) + end + end + + it "coverts a source to an absolute path" do + ::File.stub(:absolute_path).and_return("c:\\Files\\frost.msi") + resource.source("frost.msi") + expect(resource.source).to eql "c:\\Files\\frost.msi" + end + + it "converts slashes to backslashes in the source path" do + ::File.stub(:absolute_path).and_return("c:\\frost.msi") + resource.source("c:/frost.msi") + expect(resource.source).to eql "c:\\frost.msi" + end + + it "defaults source to the resource name" do + # it's a little late to stub out File.absolute_path + expect(resource.source).to include("solitaire.msi") + end +end diff --git a/spec/unit/resource_reporter_spec.rb b/spec/unit/resource_reporter_spec.rb index 52fd44e692..d412234596 100644 --- a/spec/unit/resource_reporter_spec.rb +++ b/spec/unit/resource_reporter_spec.rb @@ -38,7 +38,6 @@ describe Chef::ResourceReporter do @rest_client = double("Chef::REST (mock)") @rest_client.stub(:post_rest).and_return(true) @resource_reporter = Chef::ResourceReporter.new(@rest_client) - @run_id = @resource_reporter.run_id @new_resource = Chef::Resource::File.new("/tmp/a-file.txt") @new_resource.cookbook_name = "monkey" @cookbook_version = double("Cookbook::Version", :version => "1.2.3") @@ -49,6 +48,7 @@ describe Chef::ResourceReporter do @events = Chef::EventDispatch::Dispatcher.new @run_context = Chef::RunContext.new(@node, {}, @events) @run_status = Chef::RunStatus.new(@node, @events) + @run_id = @run_status.run_id Time.stub(:now).and_return(@start_time, @end_time) end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index e9a60c9861..60f3bdb8ea 100644 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -344,7 +344,7 @@ describe Chef::Resource do expected_keys = [ :allowed_actions, :params, :provider, :updated, :updated_by_last_action, :before, :supports, :noop, :ignore_failure, :name, :source_line, - :action, :retries, :retry_delay, :elapsed_time] + :action, :retries, :retry_delay, :elapsed_time, :guard_interpreter] (hash.keys - expected_keys).should == [] (expected_keys - hash.keys).should == [] hash[:name].should eql("funk") @@ -526,6 +526,28 @@ describe Chef::Resource do snitch_var2.should be_false end + describe "guard_interpreter attribute" do + let(:resource) { @resource } + + it "should be set to :default by default" do + resource.guard_interpreter.should == :default + end + + it "if set to :default should return :default when read" do + resource.guard_interpreter(:default) + resource.guard_interpreter.should == :default + end + + it "should raise Chef::Exceptions::ValidationFailed on an attempt to set the guard_interpreter attribute to something other than a Symbol" do + expect { resource.guard_interpreter('command_dot_com') }.to raise_error(Chef::Exceptions::ValidationFailed) + end + + it "should not raise an exception when setting the guard interpreter attribute to a Symbol" do + Chef::GuardInterpreter::ResourceGuardInterpreter.stub(:new).and_return(nil) + expect { resource.guard_interpreter(:command_dot_com) }.not_to raise_error + end + end + end describe "should_skip?" do diff --git a/spec/unit/rest_spec.rb b/spec/unit/rest_spec.rb index a53b4c9507..a8eb1ac7db 100644 --- a/spec/unit/rest_spec.rb +++ b/spec/unit/rest_spec.rb @@ -59,17 +59,38 @@ describe Chef::REST do let(:log_stringio) { StringIO.new } + let(:request_id) {"1234"} + let(:rest) do Chef::REST::CookieJar.stub(:instance).and_return({}) + Chef::RequestID.instance.stub(:request_id).and_return(request_id) rest = Chef::REST.new(base_url, nil, nil) Chef::REST::CookieJar.instance.clear rest end + let(:standard_read_headers) {{"Accept"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id}} + let(:standard_write_headers) {{"Accept"=>"application/json", "Content-Type"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id}} + before(:each) do Chef::Log.init(log_stringio) end + it "should have content length validation middleware after compressor middleware" do + middlewares = rest.instance_variable_get(:@middlewares) + content_length = middlewares.find_index { |e| e.is_a? Chef::HTTP::ValidateContentLength } + decompressor = middlewares.find_index { |e| e.is_a? Chef::HTTP::Decompressor } + + content_length.should_not be_nil + decompressor.should_not be_nil + (decompressor < content_length).should be_true + end + + it "should allow the options hash to be frozen" do + options = {}.freeze + # should not raise any exception + Chef::REST.new(base_url, nil, nil, options) + end describe "calling an HTTP verb on a path or absolute URL" do it "adds a relative URL to the base url it was initialized with" do @@ -82,7 +103,7 @@ describe Chef::REST do it "makes a :GET request with the composed url object" do rest.should_receive(:send_http_request). - with(:GET, monkey_uri, STANDARD_READ_HEADERS, false). + with(:GET, monkey_uri, standard_read_headers, false). and_return([1,2,3]) rest.should_receive(:apply_response_middleware).with(1,2,3).and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) @@ -94,12 +115,9 @@ describe Chef::REST do rest.get_rest("monkey", true) end - STANDARD_READ_HEADERS = {"Accept"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"} - STANDARD_WRITE_HEADERS = {"Accept"=>"application/json", "Content-Type"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"} - it "makes a :DELETE request with the composed url object" do rest.should_receive(:send_http_request). - with(:DELETE, monkey_uri, STANDARD_READ_HEADERS, false). + with(:DELETE, monkey_uri, standard_read_headers, false). and_return([1,2,3]) rest.should_receive(:apply_response_middleware).with(1,2,3).and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) @@ -108,7 +126,7 @@ describe Chef::REST do it "makes a :POST request with the composed url object and data" do rest.should_receive(:send_http_request). - with(:POST, monkey_uri, STANDARD_WRITE_HEADERS, "\"data\""). + with(:POST, monkey_uri, standard_write_headers, "\"data\""). and_return([1,2,3]) rest.should_receive(:apply_response_middleware).with(1,2,3).and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) @@ -117,7 +135,7 @@ describe Chef::REST do it "makes a :PUT request with the composed url object and data" do rest.should_receive(:send_http_request). - with(:PUT, monkey_uri, STANDARD_WRITE_HEADERS, "\"data\""). + with(:PUT, monkey_uri, standard_write_headers, "\"data\""). and_return([1,2,3]) rest.should_receive(:apply_response_middleware).with(1,2,3).and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) @@ -142,27 +160,27 @@ describe Chef::REST do it 'calls the authn middleware' do data = "\"secure data\"" - auth_headers = STANDARD_WRITE_HEADERS.merge({"auth_done"=>"yep"}) + auth_headers = standard_write_headers.merge({"auth_done"=>"yep"}) rest.authenticator.should_receive(:handle_request). - with(:POST, monkey_uri, STANDARD_WRITE_HEADERS, data). + with(:POST, monkey_uri, standard_write_headers, data). and_return([:POST, monkey_uri, auth_headers, data]) rest.should_receive(:send_http_request). with(:POST, monkey_uri, auth_headers, data). and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) - rest.raw_http_request(:POST, monkey_uri, STANDARD_WRITE_HEADERS, data) + rest.raw_http_request(:POST, monkey_uri, standard_write_headers, data) end it 'sets correct authn headers' do data = "\"secure data\"" - method, uri, auth_headers, d = rest.authenticator.handle_request(:POST, monkey_uri, STANDARD_WRITE_HEADERS, data) + method, uri, auth_headers, d = rest.authenticator.handle_request(:POST, monkey_uri, standard_write_headers, data) rest.should_receive(:send_http_request). with(:POST, monkey_uri, auth_headers, data). and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) - rest.raw_http_request(:POST, monkey_uri, STANDARD_WRITE_HEADERS, data) + rest.raw_http_request(:POST, monkey_uri, standard_write_headers, data) end end @@ -244,6 +262,7 @@ describe Chef::REST do let(:rest) do Net::HTTP.stub(:new).and_return(http_client) Chef::REST::CookieJar.stub(:instance).and_return({}) + Chef::RequestID.instance.stub(:request_id).and_return(request_id) rest = Chef::REST.new(base_url, nil, nil) Chef::REST::CookieJar.instance.clear rest @@ -254,6 +273,7 @@ describe Chef::REST do 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, + 'X-REMOTE-REQUEST-ID' => request_id } end @@ -275,6 +295,7 @@ describe Chef::REST do 'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, 'Host' => host_header, + 'X-REMOTE-REQUEST-ID' => request_id } end @@ -287,6 +308,11 @@ describe Chef::REST do rest.request(:GET, url, {}) end + it "should always include the X-Remote-Request-Id header" do + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", base_headers).and_return(request_mock) + rest.request(:GET, url, {}) + end + it "sets the user agent to chef-client" do # XXX: must reset to default b/c knife changes the UA Chef::REST::RESTRequest.user_agent = Chef::REST::RESTRequest::DEFAULT_UA @@ -342,6 +368,7 @@ describe Chef::REST do let(:rest) do Net::HTTP.stub(:new).and_return(http_client) Chef::REST::CookieJar.instance["#{url.host}:#{url.port}"] = "cookie monster" + Chef::RequestID.instance.stub(:request_id).and_return(request_id) rest = Chef::REST.new(base_url, nil, nil) rest end @@ -542,7 +569,20 @@ describe Chef::REST do expected_headers = {'Accept' => "*/*", 'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, - 'Host' => host_header} + 'Host' => host_header, + 'X-REMOTE-REQUEST-ID'=> request_id + } + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) + rest.streaming_request(url, {}) + end + + it "build a new HTTP GET request with the X-Remote-Request-Id header" do + expected_headers = {'Accept' => "*/*", + 'X-Chef-Version' => Chef::VERSION, + 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, + 'Host' => host_header, + 'X-REMOTE-REQUEST-ID'=> request_id + } Net::HTTP::Get.should_receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) rest.streaming_request(url, {}) end diff --git a/spec/unit/run_context/cookbook_compiler_spec.rb b/spec/unit/run_context/cookbook_compiler_spec.rb index 52f4772206..5c50c3dd4b 100644 --- a/spec/unit/run_context/cookbook_compiler_spec.rb +++ b/spec/unit/run_context/cookbook_compiler_spec.rb @@ -170,5 +170,17 @@ describe Chef::RunContext::CookbookCompiler do :"circular-dep1", :"test-with-circular-deps"] end + + it "determines if a cookbook is in the list of cookbooks reachable by dependency" do + node.run_list("test-with-deps::default", "test-with-deps::server") + compiler.cookbook_order.should == [:dependency1, :dependency2, :"test-with-deps"] + compiler.unreachable_cookbook?(:dependency1).should be_false + compiler.unreachable_cookbook?(:dependency2).should be_false + compiler.unreachable_cookbook?(:'test-with-deps').should be_false + compiler.unreachable_cookbook?(:'circular-dep1').should be_true + compiler.unreachable_cookbook?(:'circular-dep2').should be_true + end + + end end diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb index 39b8a8a50d..813102527b 100644 --- a/spec/unit/run_context_spec.rb +++ b/spec/unit/run_context_spec.rb @@ -79,6 +79,13 @@ describe Chef::RunContext do @node.include_attribute("test::george") end + it "raises an error when attempting to include_recipe from a cookbook not reachable by run list or dependencies" do + @node.should_receive(:loaded_recipe).with(:ancient, "aliens") + lambda do + @run_context.include_recipe("ancient::aliens") + # In CHEF-5120, this becomes a Chef::Exceptions::MissingCookbookDependency error: + end.should raise_error(Chef::Exceptions::CookbookNotFound) + end end diff --git a/spec/unit/util/editor_spec.rb b/spec/unit/util/editor_spec.rb new file mode 100644 index 0000000000..06370f7de0 --- /dev/null +++ b/spec/unit/util/editor_spec.rb @@ -0,0 +1,152 @@ +require 'spec_helper' +require 'chef/util/editor' + +describe Chef::Util::Editor do + describe '#initialize' do + it 'takes an Enumerable of lines' do + editor = described_class.new(File.open(__FILE__)) + expect(editor.lines).to be == IO.readlines(__FILE__) + end + + it 'makes a copy of an Array' do + array = Array.new + editor = described_class.new(array) + expect(editor.lines).to_not be(array) + end + end + + subject(:editor) { described_class.new(input_lines) } + let(:input_lines) { ['one', 'two', 'two', 'three'] } + + describe '#append_line_after' do + context 'when there is no match' do + subject(:execute) { editor.append_line_after('missing', 'new') } + + it('returns the number of added lines') { should be == 0 } + it 'does not add any lines' do + expect { execute }.to_not change { editor.lines } + end + end + + context 'when there is a match' do + subject(:execute) { editor.append_line_after('two', 'new') } + + it('returns the number of added lines') { should be == 2 } + it 'adds a line after each match' do + execute + expect(editor.lines).to be == ['one', 'two', 'new', 'two', 'new', 'three'] + end + end + + it 'matches a Regexp' do + expect(editor.append_line_after(/^ee/, 'new')).to be == 0 + expect(editor.append_line_after(/ee$/, 'new')).to be == 1 + end + end + + describe '#append_line_if_missing' do + context 'when there is no match' do + subject(:execute) { editor.append_line_if_missing('missing', 'new') } + + it('returns the number of added lines') { should be == 1 } + it 'adds a line to the end' do + execute + expect(editor.lines).to be == ['one', 'two', 'two', 'three', 'new'] + end + end + + context 'when there is a match' do + subject(:execute) { editor.append_line_if_missing('one', 'new') } + + it('returns the number of added lines') { should be == 0 } + it 'does not add any lines' do + expect { execute }.to_not change { editor.lines } + end + end + + it 'matches a Regexp' do + expect(editor.append_line_if_missing(/ee$/, 'new')).to be == 0 + expect(editor.append_line_if_missing(/^ee/, 'new')).to be == 1 + end + end + + describe '#remove_lines' do + context 'when there is no match' do + subject(:execute) { editor.remove_lines('missing') } + + it('returns the number of removed lines') { should be == 0 } + it 'does not remove any lines' do + expect { execute }.to_not change { editor.lines } + end + end + + context 'when there is a match' do + subject(:execute) { editor.remove_lines('two') } + + it('returns the number of removed lines') { should be == 2 } + it 'removes the matching lines' do + execute + expect(editor.lines).to be == ['one', 'three'] + end + end + + it 'matches a Regexp' do + expect(editor.remove_lines(/^ee/)).to be == 0 + expect(editor.remove_lines(/ee$/)).to be == 1 + end + end + + describe '#replace' do + context 'when there is no match' do + subject(:execute) { editor.replace('missing', 'new') } + + it('returns the number of changed lines') { should be == 0 } + it 'does not change any lines' do + expect { execute }.to_not change { editor.lines } + end + end + + context 'when there is a match' do + subject(:execute) { editor.replace('two', 'new') } + + it('returns the number of changed lines') { should be == 2 } + it 'replaces the matching portions' do + execute + expect(editor.lines).to be == ['one', 'new', 'new', 'three'] + end + end + + it 'matches a Regexp' do + expect(editor.replace(/^ee/, 'new')).to be == 0 + expect(editor.replace(/ee$/, 'new')).to be == 1 + expect(editor.lines).to be == ['one', 'two', 'two', 'thrnew'] + end + end + + describe '#replace_lines' do + context 'when there is no match' do + subject(:execute) { editor.replace_lines('missing', 'new') } + + it('returns the number of changed lines') { should be == 0 } + it 'does not change any lines' do + expect { execute }.to_not change { editor.lines } + end + end + + context 'when there is a match' do + subject(:execute) { editor.replace_lines('two', 'new') } + + it('returns the number of replaced lines') { should be == 2 } + it 'replaces the matching line' do + execute + expect(editor.lines).to be == ['one', 'new', 'new', 'three'] + end + end + + it 'matches a Regexp' do + expect(editor.replace_lines(/^ee/, 'new')).to be == 0 + expect(editor.replace_lines(/ee$/, 'new')).to be == 1 + expect(editor.lines).to be == ['one', 'two', 'two', 'new'] + end + end +end diff --git a/spec/unit/util/file_edit_spec.rb b/spec/unit/util/file_edit_spec.rb index d1d87a6bda..139b29d9ce 100644 --- a/spec/unit/util/file_edit_spec.rb +++ b/spec/unit/util/file_edit_spec.rb @@ -17,6 +17,7 @@ # require 'spec_helper' +require 'tempfile' describe Chef::Util::FileEdit do @@ -80,6 +81,17 @@ new line inserted EOF end + let(:append_twice) do + <<-EOF +127.0.0.1 localhost +255.255.255.255 broadcasthost +::1 localhost +fe80::1%lo0 localhost +once +twice + EOF + end + let(:target_file) do f = Tempfile.open('file_edit_spec') f.write(starting_content) @@ -104,7 +116,7 @@ new line inserted # CHEF-5018: people have monkey patched this and it has accidentally been broken it "should read the contents into memory as an array" do - expect(fedit.send(:contents)).to be_instance_of(Array) + expect(fedit.send(:editor).lines).to be_instance_of(Array) end end @@ -123,18 +135,21 @@ new line inserted describe "search_file_replace" do it "should accept regex passed in as a string (not Regexp object) and replace the match if there is one" do fedit.search_file_replace("localhost", "replacement") + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(localhost_replaced) end it "should accept regex passed in as a Regexp object and replace the match if there is one" do fedit.search_file_replace(/localhost/, "replacement") + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(localhost_replaced) end it "should do nothing if there isn't a match" do fedit.search_file_replace(/pattern/, "replacement") + fedit.unwritten_changes?.should be_false fedit.write_file expect(edited_file_contents).to eq(starting_content) end @@ -143,6 +158,7 @@ new line inserted describe "search_file_replace_line" do it "should search for match and replace the whole line" do fedit.search_file_replace_line(/localhost/, "replacement line") + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(localhost_line_replaced) end @@ -151,6 +167,7 @@ new line inserted describe "search_file_delete" do it "should search for match and delete the match" do fedit.search_file_delete(/localhost/) + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(localhost_deleted) end @@ -159,6 +176,7 @@ new line inserted describe "search_file_delete_line" do it "should search for match and delete the matching line" do fedit.search_file_delete_line(/localhost/) + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(localhost_line_deleted) end @@ -167,6 +185,7 @@ new line inserted describe "insert_line_after_match" do it "should search for match and insert the given line after the matching line" do fedit.insert_line_after_match(/localhost/, "new line inserted") + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(append_after_all_localhost) end @@ -175,14 +194,31 @@ new line inserted describe "insert_line_if_no_match" do it "should search for match and insert the given line if no line match" do fedit.insert_line_if_no_match(/pattern/, "new line inserted") + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(append_after_content) end it "should do nothing if there is a match" do fedit.insert_line_if_no_match(/localhost/, "replacement") + fedit.unwritten_changes?.should be_false fedit.write_file expect(edited_file_contents).to eq(starting_content) end + + it "should work more than once" do + fedit.insert_line_if_no_match(/missing/, "once") + fedit.insert_line_if_no_match(/missing/, "twice") + fedit.write_file + expect(edited_file_contents).to eq(append_twice) + end + end + + describe "file_edited" do + it "should return true if a file got edited" do + fedit.insert_line_if_no_match(/pattern/, "new line inserted") + fedit.write_file + expect(fedit.file_edited?).to be_true + end end end |