summaryrefslogtreecommitdiff
path: root/spec/unit/client_spec.rb
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2019-03-11 11:49:31 -0700
committerLamont Granquist <lamont@scriptkiddie.org>2019-03-11 11:49:31 -0700
commit66015ba654469f4dacfd78d40b02aafee52bbf1b (patch)
treeb00d0de111d18980f446b006ac63ef599eea8108 /spec/unit/client_spec.rb
parent4037976199b728d4bdc18fd428e8d40a84c97e2b (diff)
downloadchef-66015ba654469f4dacfd78d40b02aafee52bbf1b.tar.gz
Extract Action Collection from Data Collector
See the PR for details on this change. Signed-off-by: Lamont Granquist <lamont@scriptkiddie.org>
Diffstat (limited to 'spec/unit/client_spec.rb')
-rw-r--r--spec/unit/client_spec.rb343
1 files changed, 339 insertions, 4 deletions
diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb
index 54bd7eab55..370d5d34e7 100644
--- a/spec/unit/client_spec.rb
+++ b/spec/unit/client_spec.rb
@@ -2,7 +2,7 @@
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
-# Copyright:: Copyright 2008-2017, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,9 +19,6 @@
#
require "spec_helper"
-require "spec/support/shared/context/client"
-require "spec/support/shared/examples/client"
-
require "chef/run_context"
require "chef/server_api"
require "rbconfig"
@@ -29,6 +26,344 @@ require "rbconfig"
class FooError < RuntimeError
end
+#
+# SHARED CONTEXTS
+#
+
+# Stubs a basic client object
+shared_context "client" do
+ let(:fqdn) { "hostname.example.org" }
+ let(:hostname) { "hostname" }
+ let(:machinename) { "machinename.example.org" }
+ let(:platform) { "example-platform" }
+ let(:platform_version) { "example-platform-1.0" }
+
+ let(:ohai_data) do
+ {
+ fqdn: fqdn,
+ hostname: hostname,
+ machinename: machinename,
+ platform: platform,
+ platform_version: platform_version,
+ }
+ end
+
+ let(:ohai_system) do
+ ohai = instance_double("Ohai::System", all_plugins: true, data: ohai_data, logger: logger)
+ allow(ohai).to receive(:[]) do |k|
+ ohai_data[k]
+ end
+ ohai
+ 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(:stdout) { STDOUT }
+ let(:stderr) { STDERR }
+
+ let(:client) do
+ Chef::Config[:event_loggers] = []
+ allow(Ohai::System).to receive(:new).and_return(ohai_system)
+ opts = client_opts.merge({ logger: logger })
+ Chef::Client.new(json_attribs, opts).tap do |c|
+ c.node = node
+ end
+ end
+
+ let(:logger) { instance_double("Mixlib::Log::Child", trace: nil, debug: nil, warn: nil, info: nil, error: nil, fatal: nil) }
+
+ before do
+ stub_const("Chef::Client::STDOUT_FD", stdout)
+ stub_const("Chef::Client::STDERR_FD", stderr)
+ allow(client).to receive(:logger).and_return(logger)
+ end
+end
+
+# Stubs a client for a client run.
+# Requires a client object be defined in the scope of this included context.
+# e.g.:
+# describe "some functionality" do
+# include_context "client"
+# include_context "a client run"
+# ...
+# end
+shared_context "a client run" do
+ let(:stdout) { StringIO.new }
+ let(:stderr) { StringIO.new }
+
+ let(:api_client_exists?) { false }
+ let(:enable_fork) { false }
+
+ let(:http_data_collector) { double("Chef::ServerAPI (data collector)") }
+ let(:http_cookbook_sync) { double("Chef::ServerAPI (cookbook sync)") }
+ let(:http_node_load) { double("Chef::ServerAPI (node)") }
+ let(:http_node_save) { double("Chef::ServerAPI (node save)") }
+ let(:reporting_rest_client) { double("Chef::ServerAPI (reporting client)") }
+
+ let(:runner) { instance_double("Chef::Runner") }
+ let(:audit_runner) { instance_double("Chef::Audit::Runner", failed?: 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.
+ allow(File).to receive(:exists?).and_call_original
+ expect(File).to receive(:exists?)
+ .with(Chef::Config[:client_key])
+ .exactly(:once)
+ .and_return(api_client_exists?)
+
+ unless api_client_exists?
+ # Client.register will register with the validation client name.
+ expect_any_instance_of(Chef::ApiClient::Registration).to receive(:run)
+ end
+ end
+
+ def stub_for_event_registration
+ expect(client.events).to receive(:register).with(instance_of(Chef::DataCollector::Reporter))
+ expect(client.events).to receive(:register).with(instance_of(Chef::ResourceReporter))
+ expect(client.events).to receive(:register).with(instance_of(Chef::ActionCollection))
+ expect(client.events).to receive(:register).with(instance_of(Chef::Audit::AuditReporter))
+ end
+
+ def stub_for_node_load
+ # Client.register will then turn around create another
+ # Chef::ServerAPI object, this time with the client key it got from the
+ # previous step.
+ expect(Chef::ServerAPI).to receive(:new)
+ .with(Chef::Config[:chef_server_url], client_name: fqdn,
+ signing_key_filename: Chef::Config[:client_key])
+ .exactly(:once)
+ .and_return(http_node_load)
+
+ # --Client#build_node
+ # looks up the node, which we will return, then later saves it.
+ expect(Chef::Node).to receive(:find_or_create).with(fqdn).and_return(node)
+ end
+
+ def stub_rest_clean
+ allow(client).to receive(:rest_clean).and_return(reporting_rest_client)
+ end
+
+ def stub_for_sync_cookbooks
+ # --Client#setup_run_context
+ # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync
+ #
+ expect_any_instance_of(Chef::CookbookSynchronizer).to receive(:sync_cookbooks)
+ expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_url], version_class: Chef::CookbookManifestVersions).and_return(http_cookbook_sync)
+ expect(http_cookbook_sync).to receive(:post)
+ .with("environments/_default/cookbook_versions", { run_list: [] })
+ .and_return({})
+ end
+
+ def stub_for_required_recipe
+ response = Net::HTTPNotFound.new("1.1", "404", "Not Found")
+ exception = Net::HTTPClientException.new('404 "Not Found"', response)
+ expect(http_node_load).to receive(:get).with("required_recipe").and_raise(exception)
+ end
+
+ def stub_for_converge
+ # define me
+ end
+
+ def stub_for_audit
+ # define me
+ end
+
+ def stub_for_node_save
+ # define me
+ end
+
+ def stub_for_run
+ # define me
+ end
+
+ before do
+ Chef::Config[:client_fork] = enable_fork
+ Chef::Config[:cache_path] = windows? ? 'C:\chef' : "/var/chef"
+ Chef::Config[:why_run] = false
+ Chef::Config[:audit_mode] = :enabled
+ Chef::Config[:chef_guid] = "default-guid"
+
+ stub_rest_clean
+ stub_for_register
+ stub_for_event_registration
+ stub_for_node_load
+ stub_for_sync_cookbooks
+ stub_for_required_recipe
+ stub_for_converge
+ stub_for_audit
+ stub_for_node_save
+
+ expect_any_instance_of(Chef::RunLock).to receive(:acquire)
+ expect_any_instance_of(Chef::RunLock).to receive(:save_pid)
+ expect_any_instance_of(Chef::RunLock).to receive(:release)
+
+ # Post conditions: check that node has been filled in correctly
+ expect(client).to receive(:run_started)
+
+ stub_for_run
+ end
+end
+
+shared_context "converge completed" do
+ def stub_for_converge
+ # --Client#converge
+ expect(Chef::Runner).to receive(:new).and_return(runner)
+ expect(runner).to receive(:converge).and_return(true)
+ end
+
+ def stub_for_node_save
+ allow(node).to receive(:data_for_save).and_return(node.for_json)
+
+ # --Client#save_updated_node
+ expect(Chef::ServerAPI).to receive(:new).with(Chef::Config[:chef_server_url], client_name: fqdn,
+ signing_key_filename: Chef::Config[:client_key], validate_utf8: false).and_return(http_node_save)
+ expect(http_node_save).to receive(:put).with("nodes/#{fqdn}", node.for_json).and_return(true)
+ end
+end
+
+shared_context "converge failed" do
+ let(:converge_error) do
+ err = Chef::Exceptions::UnsupportedAction.new("Action unsupported")
+ err.set_backtrace([ "/path/recipe.rb:15", "/path/recipe.rb:12" ])
+ err
+ end
+
+ def stub_for_converge
+ expect(Chef::Runner).to receive(:new).and_return(runner)
+ expect(runner).to receive(:converge).and_raise(converge_error)
+ end
+
+ def stub_for_node_save
+ expect(client).to_not receive(:save_updated_node)
+ end
+end
+
+shared_context "audit phase completed" do
+ def stub_for_audit
+ # -- Client#run_audits
+ expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
+ expect(audit_runner).to receive(:run).and_return(true)
+ expect(client.events).to receive(:audit_phase_complete)
+ end
+end
+
+shared_context "audit phase failed with error" do
+ let(:audit_error) do
+ err = RuntimeError.new("Unexpected audit error")
+ err.set_backtrace([ "/path/recipe.rb:57", "/path/recipe.rb:55" ])
+ err
+ end
+
+ def stub_for_audit
+ expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
+ expect(Chef::Audit::Logger).to receive(:read_buffer).and_return("Audit mode output!")
+ expect(audit_runner).to receive(:run).and_raise(audit_error)
+ expect(client.events).to receive(:audit_phase_failed).with(audit_error, "Audit mode output!")
+ end
+end
+
+shared_context "audit phase completed with failed controls" do
+ let(:audit_runner) do
+ instance_double("Chef::Audit::Runner", failed?: true,
+ num_failed: 1, num_total: 3) end
+
+ let(:audit_error) do
+ err = Chef::Exceptions::AuditsFailed.new(audit_runner.num_failed, audit_runner.num_total)
+ err.set_backtrace([ "/path/recipe.rb:108", "/path/recipe.rb:103" ])
+ err
+ end
+
+ def stub_for_audit
+ expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
+ expect(Chef::Audit::Logger).to receive(:read_buffer).and_return("Audit mode output!")
+ expect(audit_runner).to receive(:run)
+ expect(Chef::Exceptions::AuditsFailed).to receive(:new).with(
+ audit_runner.num_failed, audit_runner.num_total
+ ).and_return(audit_error)
+ expect(client.events).to receive(:audit_phase_failed).with(audit_error, "Audit mode output!")
+ end
+end
+
+shared_context "run completed" do
+ def stub_for_run
+ expect(client).to receive(:run_completed_successfully)
+ end
+end
+
+shared_context "run failed" do
+ def stub_for_run
+ expect(client).to receive(:run_failed)
+ end
+
+ before do
+ expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError)
+ end
+end
+
+#
+# SHARED EXAMPLES
+#
+
+# requires platform and platform_version be defined
+shared_examples "a completed run" do
+ include_context "run completed"
+
+ it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges, and runs audits" do
+ # This is what we're testing.
+ expect(client.run).to be true
+
+ # fork is stubbed, so we can see the outcome of the run
+ expect(node.automatic_attrs[:platform]).to eq(platform)
+ expect(node.automatic_attrs[:platform_version]).to eq(platform_version)
+ end
+end
+
+shared_examples "a completed run with audit failure" do
+ include_context "run completed"
+
+ before do
+ expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError)
+ end
+
+ it "converges, runs audits, saves the node and raises the error in a wrapping error" do
+ expect { client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error|
+ expect(error.wrapped_errors.size).to eq(run_errors.size)
+ run_errors.each do |run_error|
+ expect(error.wrapped_errors).to include(run_error)
+ expect(error.backtrace).to include(*run_error.backtrace)
+ end
+ end
+
+ # fork is stubbed, so we can see the outcome of the run
+ expect(node.automatic_attrs[:platform]).to eq(platform)
+ expect(node.automatic_attrs[:platform_version]).to eq(platform_version)
+ end
+end
+
+shared_examples "a failed run" do
+ include_context "run failed"
+
+ it "skips node save and raises the error in a wrapping error" do
+ expect { client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error|
+ expect(error.wrapped_errors.size).to eq(run_errors.size)
+ run_errors.each do |run_error|
+ expect(error.wrapped_errors).to include(run_error)
+ expect(error.backtrace).to include(*run_error.backtrace)
+ end
+ end
+ end
+end
+
describe Chef::Client do
include_context "client"