# # Author:: AJ Christensen () # Author:: Mark Mzyk (mmzyk@chef.io) # Copyright:: Copyright 2008-2019, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require "spec_helper" describe Chef::Application do before do @original_argv = ARGV.dup ARGV.clear Chef::Log.logger = Logger.new(StringIO.new) @app = Chef::Application.new allow(@app).to receive(:trap) allow(Dir).to receive(:chdir).and_return(0) allow(@app).to receive(:reconfigure) Chef::Log.init(STDERR) end after do ARGV.replace(@original_argv) end context "when there are no configuration errors" do before do expect(Chef::Log).to_not receive(:fatal) expect(Chef::Application).to_not receive(:fatal!) end describe "reconfigure" do before do @app = Chef::Application.new allow(@app).to receive(:configure_chef).and_return(true) allow(@app).to receive(:configure_logging).and_return(true) end it "should configure chef" do expect(@app).to receive(:configure_chef).and_return(true) @app.reconfigure end it "should configure logging" do expect(@app).to receive(:configure_logging).and_return(true) @app.reconfigure end it "should not receive set_specific_recipes" do expect(@app).to_not receive(:set_specific_recipes) @app.reconfigure end end describe Chef::Application do before do @app = Chef::Application.new end describe "run" do before do allow(@app).to receive(:setup_application).and_return(true) allow(@app).to receive(:run_application).and_return(true) allow(@app).to receive(:configure_chef).and_return(true) allow(@app).to receive(:configure_logging).and_return(true) end it "should reconfigure the application before running" do expect(@app).to receive(:reconfigure).and_return(true) @app.run end it "should setup the application before running it" do expect(@app).to receive(:setup_application).and_return(true) @app.run end describe "when enforce_license is set to true" do it "should check the license acceptance" do expect(@app).to receive(:check_license_acceptance) @app.run(enforce_license: true) end end it "should run the actual application" do expect(@app).to receive(:run_application).and_return(true) @app.run end end end describe "configure_chef" do before do # Silence warnings when no config file exists allow(Chef::Log).to receive(:warn) @app = Chef::Application.new allow(@app).to receive(:parse_options).and_return(true) allow(::File).to receive(:read).with("/proc/sys/crypto/fips_enabled").and_call_original expect(Chef::Config).to receive(:export_proxies).and_return(true) end it "should parse the commandline options" do expect(@app).to receive(:parse_options).and_return(true) @app.config[:config_file] = "/etc/chef/default.rb" # have a config file set, to prevent triggering error block @app.configure_chef end describe "when a config_file is present" do let(:config_content) { "rspec_ran('true')" } let(:config_location) { "/etc/chef/default.rb" } let(:config_location_pathname) do p = Pathname.new(config_location) allow(p).to receive(:realpath).and_return(config_location) p end before do @app.config[:config_file] = config_location # force let binding to get evaluated or else we stub Pathname.new before we try to use it. config_location_pathname allow(Pathname).to receive(:new).with(config_location).and_return(config_location_pathname) expect(File).to receive(:read) .with(config_location) .and_return(config_content) end it "should configure chef::config from a file" do expect(Chef::Config).to receive(:from_string).with(config_content, File.expand_path(config_location)) @app.configure_chef end it "should merge the local config hash into chef::config" do # File.should_receive(:open).with("/etc/chef/default.rb").and_yield(@config_file) @app.configure_chef expect(Chef::Config.rspec_ran).to eq("true") end context "when openssl fips" do before do allow(Chef::Config).to receive(:fips).and_return(true) end it "sets openssl in fips mode" do expect(Chef::Config).to receive(:enable_fips_mode) @app.configure_chef end end end describe "when there is no config_file defined" do before do @app.config[:config_file] = nil end it "should emit a warning" do expect(Chef::Config).not_to receive(:from_file).with("/etc/chef/default.rb") expect(Chef::Log).to receive(:warn).with("No config file found or specified on command line. Using command line options instead.") @app.configure_chef end end describe "when the config file is set and not found" do before do @app.config[:config_file] = "/etc/chef/notfound" end it "should use the passed in command line options and defaults" do expect(Chef::Config).to receive(:merge!).at_least(:once) @app.configure_chef end end end describe "when configuring the logger" do before do @app = Chef::Application.new allow(Chef::Log).to receive(:init) end it "should initialise the chef logger" do allow(Chef::Log).to receive(:level=) @monologger = double("Monologger") expect(MonoLogger).to receive(:new).with(Chef::Config[:log_location]).and_return(@monologger) allow(MonoLogger).to receive(:new).with(STDOUT).and_return(@monologger) allow(@monologger).to receive(:formatter=).with(Chef::Log.logger.formatter) expect(Chef::Log).to receive(:init).with(@monologger) @app.configure_logging end shared_examples_for "log_level_is_auto" do before do allow(STDOUT).to receive(:tty?).and_return(true) end it "configures the log level to :warn" do @app.configure_logging expect(Chef::Log.level).to eq(:warn) end context "when force_formater is configured" do before do Chef::Config[:force_formatter] = true end it "configures the log level to warn" do @app.configure_logging expect(Chef::Log.level).to eq(:warn) end end context "when force_logger is configured" do before do Chef::Config[:force_logger] = true end it "configures the log level to info" do @app.configure_logging expect(Chef::Log.level).to eq(:info) end end context "when both are is configured" do before do Chef::Config[:force_logger] = true Chef::Config[:force_formatter] = true end it "configures the log level to warn" do @app.configure_logging expect(Chef::Log.level).to eq(:warn) end end end context "when log_level is not set" do it_behaves_like "log_level_is_auto" end context "when log_level is :auto" do before do Chef::Config[:log_level] = :auto end it_behaves_like "log_level_is_auto" end describe "log_location" do shared_examples("sets log_location") do |config_value, expected_class| context "when the configured value is #{config_value.inspect}" do let(:logger_instance) { instance_double(expected_class).as_null_object } before do allow(expected_class).to receive(:new).and_return(logger_instance) Chef::Config[:log_location] = config_value end it "it sets log_location to an instance of #{expected_class}" do expect(expected_class).to receive(:new).with no_args @app.configure_logging expect(Chef::Config[:log_location]).to be logger_instance end end end if ChefUtils.windows? it_behaves_like "sets log_location", :win_evt, Chef::Log::WinEvt it_behaves_like "sets log_location", "win_evt", Chef::Log::WinEvt else it_behaves_like "sets log_location", :syslog, Chef::Log::Syslog it_behaves_like "sets log_location", "syslog", Chef::Log::Syslog end end end end context "with an invalid log location" do it "logs a fatal error and exits" do Chef::Config[:log_location] = "/tmp/non-existing-dir/logfile" expect(Chef::Log).to receive(:fatal).at_least(:once) expect(Process).to receive(:exit) @app.configure_logging end end describe "class method: fatal!" do before do allow(STDERR).to receive(:puts).with("FATAL: blah").and_return(true) allow(Chef::Log).to receive(:fatal).and_return(true) allow(Process).to receive(:exit).and_return(true) end it "should log an error message to the logger" do expect(Chef::Log).to receive(:fatal).with("blah").and_return(true) Chef::Application.fatal! "blah" end describe "when a standard exit code is supplied" do it "should exit with the given exit code" do expect(Process).to receive(:exit).with(41).and_return(true) Chef::Application.fatal! "blah", 41 end end describe "when a non-standard exit code is supplied" do it "should exit with the default exit code" do expect(Process).to receive(:exit).with(1).and_return(true) Chef::Application.fatal! "blah", -100 end end describe "when an exit code is not supplied" do it "should exit with the default exit code" do expect(Process).to receive(:exit).with(1).and_return(true) Chef::Application.fatal! "blah" end end end describe "setup_application" do before do @app = Chef::Application.new end it "should raise an error" do expect { @app.setup_application }.to raise_error(Chef::Exceptions::Application) end end describe "run_application" do before do @app = Chef::Application.new end it "should raise an error" do expect { @app.run_application }.to raise_error(Chef::Exceptions::Application) end end context "when the config file is not available" do it "should warn for bad config file path" do @app.config[:config_file] = "/tmp/non-existing-dir/file" config_file_regexp = Regexp.new @app.config[:config_file] expect(Chef::Log).to receive(:warn).at_least(:once).with(config_file_regexp).and_return(true) allow(Chef::Log).to receive(:warn).and_return(true) @app.configure_chef end end describe "run_chef_client" do context "with an application" do let(:app) { Chef::Application.new } context "when called with an invalid argument" do before do allow(app).to receive(:fork_chef_client).and_return(true) allow(app).to receive(:run_with_graceful_exit_option).and_return(true) end it "should raise an argument error detailing the problem" do specific_recipes_regexp = Regexp.new "received non-Array like specific_recipes argument" expect { app.run_chef_client(nil) }.to raise_error(ArgumentError, specific_recipes_regexp) end end context "when called with an Array-like argument (#size)" do before do allow(app).to receive(:fork_chef_client).and_return(true) allow(app).to receive(:run_with_graceful_exit_option).and_return(true) end it "should be cool" do expect { app.run_chef_client([]) }.not_to raise_error end end end end describe "#set_specific_recipes" do let(:app) { Chef::Application.new } context "when cli arguments does not contain any values" do before do allow(app).to receive(:cli_arguments).and_return([]) end it "returns an empty array" do app.set_specific_recipes expect(Chef::Config[:specific_recipes]).to eq([]) end end context "when cli arguments contain valid recipe file path" do let(:tempfile) { Tempfile.new("default.rb").path } before do allow(app).to receive(:cli_arguments).and_return([tempfile]) end it "sets the specific recipes to config" do app.set_specific_recipes expect(Chef::Config[:specific_recipes]).to eq([tempfile]) end end context "when cli arguments contain invalid recipe file path" do let(:fatal) { false } before do tempfile = "/root/default.rb" allow(app).to receive(:cli_arguments).and_return([tempfile]) allow(Chef::Application).to receive(:fatal!).and_return(fatal) end it "raises an error with application exit" do expect(app.set_specific_recipes).to eq(fatal) end end context "when cli arguments contain empty string" do let(:fatal) { false } before do allow(app).to receive(:cli_arguments).and_return([""]) allow(Chef::Application).to receive(:fatal!).and_return(fatal) end it "raises an arguments error" do expect(app.set_specific_recipes).to eq(fatal) end end context "when cli arguments contain any string" do let(:fatal) { false } before do allow(app).to receive(:cli_arguments).and_return(["test"]) allow(Chef::Application).to receive(:fatal!).and_return(fatal) end it "raises an arguments error" do expect(app.set_specific_recipes).to eq(fatal) end end context "when cli arguments contain multiple invalid strings" do let(:fatal) { false } before do allow(app).to receive(:cli_arguments).and_return(["", "test"]) allow(Chef::Application).to receive(:fatal!).and_return(fatal) end it "raises an arguments error" do expect(app.set_specific_recipes).to eq(fatal) end end context "when cli arguments contain valid recipe file path and invalid string" do let(:fatal) { false } before do tempfile = Tempfile.new("default.rb").path allow(app).to receive(:cli_arguments).and_return([tempfile, "test"]) allow(Chef::Application).to receive(:fatal!).and_return(fatal) end it "raises an arguments error" do expect(app.set_specific_recipes).to eq(fatal) end end end describe "configuration errors" do before do expect(Process).to receive(:exit) end def raises_informative_fatals_on_configure_chef config_file_regexp = Regexp.new @app.config[:config_file] expect(Chef::Log).to receive(:fatal) .with(/Configuration error/) expect(Chef::Log).to receive(:fatal) .with(config_file_regexp) .at_least(1).times @app.configure_chef end describe "when config file exists but contains errors" do def create_config_file(text) @config_file = Tempfile.new("rspec-chef-config") @config_file.write(text) @config_file.close @app.config[:config_file] = @config_file.path end after(:each) do @config_file.unlink end it "should raise informative fatals for badly written config" do create_config_file("text that should break the config parsing") raises_informative_fatals_on_configure_chef end end end describe "merged config" do class MyTestConfig < Chef::Config extend Mixlib::Config default :test_config_1, "config default" default :test_config_2, "config default" end class MyAppClass < Chef::Application # there's an implicit test here that mixlib-cli's separate_default_options is being inherited option :test_config_2, long: "--test-config2 CONFIG", default: "cli default" end before(:each) do MyTestConfig.reset @original_argv = ARGV.dup ARGV.clear @app = MyAppClass.new expect(@app).to receive(:chef_config).at_least(:once).and_return(MyTestConfig) expect(Chef::ConfigFetcher).to receive(:new).and_return(fake_config_fetcher) allow(@app).to receive(:log).and_return(instance_double(Mixlib::Log, warn: nil)) # ignorken end after(:each) do ARGV.replace(@original_argv) end let(:fake_config_fetcher) { instance_double(Chef::ConfigFetcher, expanded_path: "/thisbetternotexist", "config_missing?": false, read_config: "" ) } it "reading a mixlib-config default works" do @app.parse_options @app.load_config_file expect(MyTestConfig[:test_config_1]).to eql("config default") end it "a mixlib-cli default overrides a mixlib-config default" do @app.parse_options @app.load_config_file expect(MyTestConfig[:test_config_2]).to eql("cli default") end it "a set mixlib-config value overrides a mixlib-config default" do expect(fake_config_fetcher).to receive(:read_config).and_return(%q{test_config_1 "config setting"}) @app.parse_options @app.load_config_file expect(MyTestConfig[:test_config_1]).to eql("config setting") end it "a set mixlib-config value overrides a mixlib-cli default" do expect(fake_config_fetcher).to receive(:read_config).and_return(%q{test_config_2 "config setting"}) @app.parse_options @app.load_config_file expect(MyTestConfig[:test_config_2]).to eql("config setting") end it "a set mixlib-cli value overrides everything else" do expect(fake_config_fetcher).to receive(:read_config).and_return(%q{test_config_2 "config setting"}) ARGV.replace("--test-config2 cli-setting".split) @app.parse_options @app.load_config_file expect(MyTestConfig[:test_config_2]).to eql("cli-setting") end end end