# # Author:: Adam Leff (<adamleff@chef.io) # Author:: Ryan Cragun (<ryan@chef.io>) # # Copyright:: Copyright 2012-2016, 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/data_collector" require "chef/resource_builder" describe Chef::DataCollector do describe ".register_reporter?" do context "when no data collector URL is configured" do it "returns false" do Chef::Config[:data_collector][:server_url] = nil expect(Chef::DataCollector.register_reporter?).to be_falsey end end context "when a data collector URL is configured" do before do Chef::Config[:data_collector][:server_url] = "http://data_collector" end context "when operating in why_run mode" do it "returns false" do Chef::Config[:why_run] = true expect(Chef::DataCollector.register_reporter?).to be_falsey end end context "when not operating in why_run mode" do before do Chef::Config[:why_run] = false end context "when report is enabled for current mode" do it "returns true" do allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(true) expect(Chef::DataCollector.register_reporter?).to be_truthy end end context "when report is disabled for current mode" do it "returns false" do allow(Chef::DataCollector).to receive(:reporter_enabled_for_current_mode?).and_return(false) expect(Chef::DataCollector.register_reporter?).to be_falsey end end end end end describe ".reporter_enabled_for_current_mode?" do context "when running in solo mode" do before do Chef::Config[:solo] = true Chef::Config[:local_mode] = false end context "when data_collector_mode is :solo" do it "returns true" do Chef::Config[:data_collector][:mode] = :solo expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true) end end context "when data_collector_mode is :client" do it "returns false" do Chef::Config[:data_collector][:mode] = :client expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(false) end end context "when data_collector_mode is :both" do it "returns true" do Chef::Config[:data_collector][:mode] = :both expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true) end end end context "when running in local mode" do before do Chef::Config[:solo] = false Chef::Config[:local_mode] = true end context "when data_collector_mode is :solo" do it "returns true" do Chef::Config[:data_collector][:mode] = :solo expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true) end end context "when data_collector_mode is :client" do it "returns false" do Chef::Config[:data_collector][:mode] = :client expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(false) end end context "when data_collector_mode is :both" do it "returns true" do Chef::Config[:data_collector][:mode] = :both expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true) end end end context "when running in client mode" do before do Chef::Config[:solo] = false Chef::Config[:local_mode] = false end context "when data_collector_mode is :solo" do it "returns false" do Chef::Config[:data_collector][:mode] = :solo expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(false) end end context "when data_collector_mode is :client" do it "returns true" do Chef::Config[:data_collector][:mode] = :client expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true) end end context "when data_collector_mode is :both" do it "returns true" do Chef::Config[:data_collector][:mode] = :both expect(Chef::DataCollector.reporter_enabled_for_current_mode?).to eq(true) end end end end end describe Chef::DataCollector::Reporter do let(:reporter) { described_class.new } let(:run_status) { Chef::RunStatus.new(Chef::Node.new, Chef::EventDispatch::Dispatcher.new) } before do Chef::Config[:data_collector][:server_url] = "http://my-data-collector-server.mycompany.com" end describe "#run_started" do before do allow(reporter).to receive(:update_run_status) allow(reporter).to receive(:send_to_data_collector) allow(Chef::DataCollector::Messages).to receive(:run_start_message) end it "updates the run status" do expect(reporter).to receive(:update_run_status).with(run_status) reporter.run_started(run_status) end it "sends the RunStart message output to the Data Collector server" do expect(Chef::DataCollector::Messages) .to receive(:run_start_message) .with(run_status) .and_return(key: "value") expect(reporter).to receive(:send_to_data_collector).with('{"key":"value"}') reporter.run_started(run_status) end end describe "#run_completed" do it "sends the run completion" do node = Chef::Node.new expect(reporter).to receive(:send_run_completion).with(status: "success") reporter.run_completed(node) end end describe "#run_failed" do it "updates the exception and sends the run completion" do expect(reporter).to receive(:send_run_completion).with(status: "failure") reporter.run_failed("test_exception") end end describe "#converge_start" do it "stashes the run_context for later use" do reporter.converge_start("test_context") expect(reporter.run_context).to eq("test_context") end end describe "#converge_complete" do it "detects and processes any unprocessed resources" do expect(reporter).to receive(:detect_unprocessed_resources) reporter.converge_complete end end describe "#converge_failed" do it "detects and processes any unprocessed resources" do expect(reporter).to receive(:detect_unprocessed_resources) reporter.converge_failed("exception") end end describe "#resource_current_state_loaded" do let(:new_resource) { double("new_resource") } let(:action) { double("action") } let(:current_resource) { double("current_resource") } let(:resource_report) { double("resource_report") } context "when resource is a nested resource" do it "does not update the resource report" do allow(reporter).to receive(:nested_resource?).and_return(true) expect(reporter).not_to receive(:update_current_resource_report) reporter.resource_current_state_loaded(new_resource, action, current_resource) end end context "when resource is not a nested resource" do it "creates the resource report and stores it as the current one" do allow(reporter).to receive(:nested_resource?).and_return(false) expect(reporter).to receive(:create_resource_report) .with(new_resource, action, current_resource) .and_return(resource_report) expect(reporter).to receive(:update_current_resource_report).with(resource_report) reporter.resource_current_state_loaded(new_resource, action, current_resource) end end end describe "#resource_up_to_date" do let(:new_resource) { double("new_resource") } let(:action) { double("action") } let(:resource_report) { double("resource_report") } before do allow(reporter).to receive(:nested_resource?) allow(reporter).to receive(:current_resource_report).and_return(resource_report) allow(resource_report).to receive(:up_to_date) end context "when the resource is a nested resource" do it "does not mark the resource report as up-to-date" do allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(true) expect(resource_report).not_to receive(:up_to_date) reporter.resource_up_to_date(new_resource, action) end end context "when the resource is not a nested resource" do it "marks the resource report as up-to-date" do allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(false) expect(resource_report).to receive(:up_to_date) reporter.resource_up_to_date(new_resource, action) end end end describe "#resource_skipped" do let(:new_resource) { double("new_resource") } let(:action) { double("action") } let(:conditional) { double("conditional") } let(:resource_report) { double("resource_report") } before do allow(reporter).to receive(:nested_resource?) allow(reporter).to receive(:create_resource_report).and_return(resource_report) allow(resource_report).to receive(:skipped) end context "when the resource is a nested resource" do it "does not mark the resource report as skipped" do allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(true) expect(resource_report).not_to receive(:skipped).with(conditional) reporter.resource_skipped(new_resource, action, conditional) end end context "when the resource is not a nested resource" do it "creates the resource report and stores it as the current one" do allow(reporter).to receive(:nested_resource?).and_return(false) expect(reporter).to receive(:create_resource_report) .with(new_resource, action) .and_return(resource_report) expect(reporter).to receive(:update_current_resource_report).with(resource_report) reporter.resource_skipped(new_resource, action, conditional) end it "marks the resource report as skipped" do allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(false) expect(resource_report).to receive(:skipped).with(conditional) reporter.resource_skipped(new_resource, action, conditional) end end end describe "#resource_updated" do let(:resource_report) { double("resource_report") } before do allow(reporter).to receive(:current_resource_report).and_return(resource_report) allow(resource_report).to receive(:updated) end it "marks the resource report as updated" do expect(resource_report).to receive(:updated) reporter.resource_updated("new_resource", "action") end end describe "#resource_failed" do let(:new_resource) { double("new_resource") } let(:action) { double("action") } let(:exception) { double("exception") } let(:error_mapper) { double("error_mapper") } let(:resource_report) { double("resource_report") } before do allow(reporter).to receive(:update_error_description) allow(reporter).to receive(:current_resource_report).and_return(resource_report) allow(resource_report).to receive(:failed) allow(Chef::Formatters::ErrorMapper).to receive(:resource_failed).and_return(error_mapper) allow(error_mapper).to receive(:for_json) end it "updates the error description" do expect(Chef::Formatters::ErrorMapper).to receive(:resource_failed).with( new_resource, action, exception ).and_return(error_mapper) expect(error_mapper).to receive(:for_json).and_return("error_description") expect(reporter).to receive(:update_error_description).with("error_description") reporter.resource_failed(new_resource, action, exception) end context "when the resource is not a nested resource" do it "marks the resource report as failed" do allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(false) expect(resource_report).to receive(:failed).with(exception) reporter.resource_failed(new_resource, action, exception) end end context "when the resource is a nested resource" do it "does not mark the resource report as failed" do allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(true) expect(resource_report).not_to receive(:failed).with(exception) reporter.resource_failed(new_resource, action, exception) end end end describe "#resource_completed" do let(:new_resource) { double("new_resource") } let(:resource_report) { double("resource_report") } before do allow(reporter).to receive(:update_current_resource_report) allow(reporter).to receive(:add_resource_report) allow(reporter).to receive(:current_resource_report) allow(resource_report).to receive(:finish) end context "when there is no current resource report" do it "does not touch the current resource report" do allow(reporter).to receive(:current_resource_report).and_return(nil) expect(reporter).not_to receive(:update_current_resource_report) reporter.resource_completed(new_resource) end end context "when there is a current resource report" do before do allow(reporter).to receive(:current_resource_report).and_return(resource_report) end context "when the resource is a nested resource" do it "does not mark the resource as finished" do allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(true) expect(resource_report).not_to receive(:finish) reporter.resource_completed(new_resource) end end context "when the resource is not a nested resource" do before do allow(reporter).to receive(:nested_resource?).with(new_resource).and_return(false) end it "marks the current resource report as finished" do expect(resource_report).to receive(:finish) reporter.resource_completed(new_resource) end it "nils out the current resource report" do expect(reporter).to receive(:update_current_resource_report).with(nil) reporter.resource_completed(new_resource) end end end end describe "#run_list_expanded" do it "sets the expanded run list" do reporter.run_list_expanded("test_run_list") expect(reporter.expanded_run_list).to eq("test_run_list") end end describe "#run_list_expand_failed" do let(:node) { double("node") } let(:error_mapper) { double("error_mapper") } let(:exception) { double("exception") } it "updates the error description" do expect(Chef::Formatters::ErrorMapper).to receive(:run_list_expand_failed).with( node, exception ).and_return(error_mapper) expect(error_mapper).to receive(:for_json).and_return("error_description") expect(reporter).to receive(:update_error_description).with("error_description") reporter.run_list_expand_failed(node, exception) end end describe "#cookbook_resolution_failed" do let(:error_mapper) { double("error_mapper") } let(:exception) { double("exception") } let(:expanded_run_list) { double("expanded_run_list") } it "updates the error description" do expect(Chef::Formatters::ErrorMapper).to receive(:cookbook_resolution_failed).with( expanded_run_list, exception ).and_return(error_mapper) expect(error_mapper).to receive(:for_json).and_return("error_description") expect(reporter).to receive(:update_error_description).with("error_description") reporter.cookbook_resolution_failed(expanded_run_list, exception) end end describe "#cookbook_sync_failed" do let(:cookbooks) { double("cookbooks") } let(:error_mapper) { double("error_mapper") } let(:exception) { double("exception") } it "updates the error description" do expect(Chef::Formatters::ErrorMapper).to receive(:cookbook_sync_failed).with( cookbooks, exception ).and_return(error_mapper) expect(error_mapper).to receive(:for_json).and_return("error_description") expect(reporter).to receive(:update_error_description).with("error_description") reporter.cookbook_sync_failed(cookbooks, exception) end end describe "#disable_reporter_on_error" do context "when no exception is raise by the block" do it "does not disable the reporter" do expect(reporter).not_to receive(:disable_data_collector_reporter) reporter.send(:disable_reporter_on_error) { true } end it "does not raise an exception" do expect { reporter.send(:disable_reporter_on_error) { true } }.not_to raise_error end end context "when an unexpected exception is raised by the block" do it "re-raises the exception" do expect { reporter.send(:disable_reporter_on_error) { raise "bummer" } }.to raise_error(RuntimeError) end end [ Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, OpenSSL::SSL::SSLError, Errno::EHOSTDOWN ].each do |exception_class| context "when the block raises a #{exception_class} exception" do it "disables the reporter" do expect(reporter).to receive(:disable_data_collector_reporter) reporter.send(:disable_reporter_on_error) { raise exception_class.new("bummer") } end context "when raise-on-failure is enabled" do it "logs an error and raises" do Chef::Config[:data_collector][:raise_on_failure] = true expect(Chef::Log).to receive(:error) expect { reporter.send(:disable_reporter_on_error) { raise exception_class.new("bummer") } }.to raise_error(exception_class) end end context "when raise-on-failure is disabled" do it "logs a warning and does not raise an exception" do Chef::Config[:data_collector][:raise_on_failure] = false expect(Chef::Log).to receive(:warn) expect { reporter.send(:disable_reporter_on_error) { raise exception_class.new("bummer") } }.not_to raise_error end end end end end describe "#validate_data_collector_server_url!" do context "when server_url is empty" do it "raises an exception" do Chef::Config[:data_collector][:server_url] = "" expect { reporter.send(:validate_data_collector_server_url!) }.to raise_error(Chef::Exceptions::ConfigurationError) end end context "when server_url is not empty" do context "when server_url is an invalid URI" do it "raises an exception" do Chef::Config[:data_collector][:server_url] = "this is not a URI" expect { reporter.send(:validate_data_collector_server_url!) }.to raise_error(Chef::Exceptions::ConfigurationError) end end context "when server_url is a valid URI" do context "when server_url is a URI with no host" do it "raises an exception" do Chef::Config[:data_collector][:server_url] = "/file/uri.txt" expect { reporter.send(:validate_data_collector_server_url!) }.to raise_error(Chef::Exceptions::ConfigurationError) end end context "when server_url is a URI with a valid host" do it "does not an exception" do Chef::Config[:data_collector][:server_url] = "http://www.google.com/data-collector" expect { reporter.send(:validate_data_collector_server_url!) }.not_to raise_error end end end end end end