diff options
Diffstat (limited to 'spec/unit')
22 files changed, 1132 insertions, 329 deletions
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 bd80d39237..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,94 +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) @@ -261,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" @@ -330,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 @@ -370,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 @@ -463,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/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_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/knife/client_bulk_delete_spec.rb b/spec/unit/knife/client_bulk_delete_spec.rb index bedd4911c5..f490851cbc 100644 --- a/spec/unit/knife/client_bulk_delete_spec.rb +++ b/spec/unit/knife/client_bulk_delete_spec.rb @@ -19,60 +19,143 @@ 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 + } + + 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).twice + 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/core/bootstrap_context_spec.rb b/spec/unit/knife/core/bootstrap_context_spec.rb index 47261e2068..3b166443b6 100644 --- a/spec/unit/knife/core/bootstrap_context_spec.rb +++ b/spec/unit/knife/core/bootstrap_context_spec.rb @@ -47,7 +47,6 @@ describe Chef::Knife::Core::BootstrapContext do 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 +55,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..f9c1ac9fd0 100644 --- a/spec/unit/knife/core/ui_spec.rb +++ b/spec/unit/knife/core/ui_spec.rb @@ -437,6 +437,15 @@ EOM }.should raise_error(SystemExit) { |e| e.status.should == 3 } end + it "should not exit 3 if you answer n and return false if asked for it" do + @ui.stdin.stub(:readline).and_return("n") + value = nil + lambda { + value = @ui.confirm(@question,true,false) + }.should_not raise_error(SystemExit) + value.should be(false) + end + describe "with --y or --yes passed" do it "should return true" do @ui.config[:yes] = true 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/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..f30c1970f7 100644 --- a/spec/unit/node/immutable_collections_spec.rb +++ b/spec/unit/node/immutable_collections_spec.rb @@ -86,7 +86,7 @@ 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 ] ]) end ## @@ -130,6 +130,10 @@ 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 diff --git a/spec/unit/platform_spec.rb b/spec/unit/platform_spec.rb index e0386a1a61..72d0a9184a 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) 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/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/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/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/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 |