#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/configurer'

describe Puppet::Configurer do
  before do
    Puppet.settings.stubs(:use).returns(true)
    @agent = Puppet::Configurer.new
    @agent.stubs(:init_storage)
    Puppet::Util::Storage.stubs(:store)
    Puppet[:server] = "puppetmaster"
    Puppet[:report] = true
  end

  it "should include the Fact Handler module" do
    expect(Puppet::Configurer.ancestors).to be_include(Puppet::Configurer::FactHandler)
  end

  describe "when executing a pre-run hook" do
    it "should do nothing if the hook is set to an empty string" do
      Puppet.settings[:prerun_command] = ""
      Puppet::Util.expects(:exec).never

      @agent.execute_prerun_command
    end

    it "should execute any pre-run command provided via the 'prerun_command' setting" do
      Puppet.settings[:prerun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")

      @agent.execute_prerun_command
    end

    it "should fail if the command fails" do
      Puppet.settings[:prerun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")

      expect(@agent.execute_prerun_command).to be_falsey
    end
  end

  describe "when executing a post-run hook" do
    it "should do nothing if the hook is set to an empty string" do
      Puppet.settings[:postrun_command] = ""
      Puppet::Util.expects(:exec).never

      @agent.execute_postrun_command
    end

    it "should execute any post-run command provided via the 'postrun_command' setting" do
      Puppet.settings[:postrun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")

      @agent.execute_postrun_command
    end

    it "should fail if the command fails" do
      Puppet.settings[:postrun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")

      expect(@agent.execute_postrun_command).to be_falsey
    end
  end

  describe "when executing a catalog run" do
    before do
      Puppet.settings.stubs(:use).returns(true)
      @agent.stubs(:download_plugins)
      Puppet::Node::Facts.indirection.terminus_class = :memory
      @facts = Puppet::Node::Facts.new(Puppet[:node_name_value])
      Puppet::Node::Facts.indirection.save(@facts)

      @catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote(Puppet[:environment].to_sym))
      @catalog.stubs(:to_ral).returns(@catalog)
      Puppet::Resource::Catalog.indirection.terminus_class = :rest
      Puppet::Resource::Catalog.indirection.stubs(:find).returns(@catalog)
      @agent.stubs(:send_report)
      @agent.stubs(:save_last_run_summary)

      Puppet::Util::Log.stubs(:close_all)
    end

    after :all do
      Puppet::Node::Facts.indirection.reset_terminus_class
      Puppet::Resource::Catalog.indirection.reset_terminus_class
    end

    it "should initialize storage" do
      Puppet::Util::Storage.expects(:load)
      @agent.run
    end

    it "downloads plugins when told" do
      @agent.expects(:download_plugins)
      @agent.run(:pluginsync => true)
    end

    it "does not download plugins when told" do
      @agent.expects(:download_plugins).never
      @agent.run(:pluginsync => false)
    end

    it "should carry on when it can't fetch its node definition" do
      error = Net::HTTPError.new(400, 'dummy server communication error')
      Puppet::Node.indirection.expects(:find).raises(error)
      expect(@agent.run).to eq(0)
    end

    it "applies a cached catalog when it can't connect to the master" do
      error = Errno::ECONNREFUSED.new('Connection refused - connect(2)')

      Puppet::Node.indirection.expects(:find).raises(error)
      Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entry(:ignore_cache => true)).raises(error)
      Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entry(:ignore_terminus => true)).returns(@catalog)

      expect(@agent.run).to eq(0)
    end

    it "should initialize a transaction report if one is not provided" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.expects(:new).returns report

      @agent.run
    end

    it "should respect node_name_fact when setting the host on a report" do
      Puppet[:node_name_fact] = 'my_name_fact'
      @facts.values = {'my_name_fact' => 'node_name_from_fact'}

      report = Puppet::Transaction::Report.new

      @agent.run(:report => report)
      expect(report.host).to eq('node_name_from_fact')
    end

    it "should pass the new report to the catalog" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.stubs(:new).returns report
      @catalog.expects(:apply).with{|options| options[:report] == report}

      @agent.run
    end

    it "should use the provided report if it was passed one" do
      report = Puppet::Transaction::Report.new
      @catalog.expects(:apply).with {|options| options[:report] == report}

      @agent.run(:report => report)
    end

    it "should set the report as a log destination" do
      report = Puppet::Transaction::Report.new

      report.expects(:<<).with(instance_of(Puppet::Util::Log)).at_least_once

      @agent.run(:report => report)
    end

    it "should retrieve the catalog" do
      @agent.expects(:retrieve_catalog)

      @agent.run
    end

    it "should log a failure and do nothing if no catalog can be retrieved" do
      @agent.expects(:retrieve_catalog).returns nil

      Puppet.expects(:err).with "Could not retrieve catalog; skipping run"

      @agent.run
    end

    it "should apply the catalog with all options to :run" do
      @agent.expects(:retrieve_catalog).returns @catalog

      @catalog.expects(:apply).with { |args| args[:one] == true }
      @agent.run :one => true
    end

    it "should accept a catalog and use it instead of retrieving a different one" do
      @agent.expects(:retrieve_catalog).never

      @catalog.expects(:apply)
      @agent.run :one => true, :catalog => @catalog
    end

    it "should benchmark how long it takes to apply the catalog" do
      @agent.expects(:benchmark).with(:notice, instance_of(String))

      @agent.expects(:retrieve_catalog).returns @catalog

      @catalog.expects(:apply).never # because we're not yielding
      @agent.run
    end

    it "should execute post-run hooks after the run" do
      @agent.expects(:execute_postrun_command)

      @agent.run
    end

    it "should create report with passed transaction_uuid and job_id" do
      @agent = Puppet::Configurer.new("test_tuuid", "test_jid")
      @agent.stubs(:init_storage)

      report = Puppet::Transaction::Report.new(nil, "test", "aaaa")
      Puppet::Transaction::Report.expects(:new).with(anything, anything, 'test_tuuid', 'test_jid').returns(report)
      @agent.expects(:send_report).with(report)

      @agent.run
    end

    it "should send the report" do
      report = Puppet::Transaction::Report.new(nil, "test", "aaaa")
      Puppet::Transaction::Report.expects(:new).returns(report)
      @agent.expects(:send_report).with(report)

      expect(report.environment).to eq("test")
      expect(report.transaction_uuid).to eq("aaaa")

      @agent.run
    end

    it "should send the transaction report even if the catalog could not be retrieved" do
      @agent.expects(:retrieve_catalog).returns nil

      report = Puppet::Transaction::Report.new(nil, "test", "aaaa")
      Puppet::Transaction::Report.expects(:new).returns(report)
      @agent.expects(:send_report).with(report)

      expect(report.environment).to eq("test")
      expect(report.transaction_uuid).to eq("aaaa")

      @agent.run
    end

    it "should send the transaction report even if there is a failure" do
      @agent.expects(:retrieve_catalog).raises "whatever"

      report = Puppet::Transaction::Report.new(nil, "test", "aaaa")
      Puppet::Transaction::Report.expects(:new).returns(report)
      @agent.expects(:send_report).with(report)

      expect(report.environment).to eq("test")
      expect(report.transaction_uuid).to eq("aaaa")

      expect(@agent.run).to be_nil
    end

    it "should remove the report as a log destination when the run is finished" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.expects(:new).returns(report)

      @agent.run

      expect(Puppet::Util::Log.destinations).not_to include(report)
    end

    it "should return the report exit_status as the result of the run" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.expects(:new).returns(report)
      report.expects(:exit_status).returns(1234)

      expect(@agent.run).to eq(1234)
    end

    it "should return nil if catalog application fails" do
      @catalog.expects(:apply).raises(Puppet::Error, 'One or more resource dependency cycles detected in graph')
      report = Puppet::Transaction::Report.new
      expect(@agent.run(catalog: @catalog, report: report)).to be_nil
    end

    it "should send the transaction report even if the pre-run command fails" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.expects(:new).returns(report)

      Puppet.settings[:prerun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
      @agent.expects(:send_report).with(report)

      expect(@agent.run).to be_nil
    end

    it "should include the pre-run command failure in the report" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.expects(:new).returns(report)

      Puppet.settings[:prerun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")

      expect(@agent.run).to be_nil
      expect(report.logs.find { |x| x.message =~ /Could not run command from prerun_command/ }).to be
    end

    it "should send the transaction report even if the post-run command fails" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.expects(:new).returns(report)

      Puppet.settings[:postrun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
      @agent.expects(:send_report).with(report)

      expect(@agent.run).to be_nil
    end

    it "should include the post-run command failure in the report" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.expects(:new).returns(report)

      Puppet.settings[:postrun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")

      report.expects(:<<).with { |log| log.message.include?("Could not run command from postrun_command") }

      expect(@agent.run).to be_nil
    end

    it "should execute post-run command even if the pre-run command fails" do
      Puppet.settings[:prerun_command] = "/my/precommand"
      Puppet.settings[:postrun_command] = "/my/postcommand"
      Puppet::Util::Execution.expects(:execute).with(["/my/precommand"]).raises(Puppet::ExecutionFailure, "Failed")
      Puppet::Util::Execution.expects(:execute).with(["/my/postcommand"])

      expect(@agent.run).to be_nil
    end

    it "should finalize the report" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.expects(:new).returns(report)

      report.expects(:finalize_report)
      @agent.run
    end

    it "should not apply the catalog if the pre-run command fails" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.expects(:new).returns(report)

      Puppet.settings[:prerun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")

      @catalog.expects(:apply).never()
      @agent.expects(:send_report)

      expect(@agent.run).to be_nil
    end

    it "should apply the catalog, send the report, and return nil if the post-run command fails" do
      report = Puppet::Transaction::Report.new
      Puppet::Transaction::Report.expects(:new).returns(report)

      Puppet.settings[:postrun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")

      @catalog.expects(:apply)
      @agent.expects(:send_report)

      expect(@agent.run).to be_nil
    end

    it 'includes total time metrics in the report after successfully applying the catalog' do
      report = Puppet::Transaction::Report.new
      @catalog.stubs(:apply).with(:report => report)
      @agent.run(report: report)

      expect(report.metrics['time']).to be
      expect(report.metrics['time']['total']).to be_a_kind_of(Numeric)
    end

    it 'includes total time metrics in the report even if prerun fails' do
      Puppet.settings[:prerun_command] = "/my/command"
      Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")

      report = Puppet::Transaction::Report.new
      @agent.run(report: report)

      expect(report.metrics['time']).to be
      expect(report.metrics['time']['total']).to be_a_kind_of(Numeric)
    end

    it 'includes total time metrics in the report even if catalog retrieval fails' do
      report = Puppet::Transaction::Report.new
      @agent.stubs(:prepare_and_retrieve_catalog_from_cache).raises
      @agent.run(:report => report)

      expect(report.metrics['time']).to be
      expect(report.metrics['time']['total']).to be_a_kind_of(Numeric)
    end

    it "should refetch the catalog if the server specifies a new environment in the catalog" do
      catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote('second_env'))
      @agent.expects(:retrieve_catalog).returns(catalog).twice

      @agent.run
    end

    it "should change the environment setting if the server specifies a new environment in the catalog" do
      @catalog.stubs(:environment).returns("second_env")

      @agent.run

      expect(@agent.environment).to eq("second_env")
    end

    it "should fix the report if the server specifies a new environment in the catalog" do
      report = Puppet::Transaction::Report.new(nil, "test", "aaaa")
      Puppet::Transaction::Report.expects(:new).returns(report)
      @agent.expects(:send_report).with(report)

      @catalog.stubs(:environment).returns("second_env")
      @agent.stubs(:retrieve_catalog).returns(@catalog)

      @agent.run

      expect(report.environment).to eq("second_env")
    end

    it "sends the transaction uuid in a catalog request" do
      @agent.instance_variable_set(:@transaction_uuid, 'aaa')
      Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entries(:transaction_uuid => 'aaa'))
      @agent.run
    end

    it "sends the transaction uuid in a catalog request" do
      @agent.instance_variable_set(:@job_id, 'aaa')
      Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entries(:job_id => 'aaa'))
      @agent.run
    end

    it "sets the static_catalog query param to true in a catalog request" do
      Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entries(:static_catalog => true))
      @agent.run
    end

    it "sets the checksum_type query param to the default supported_checksum_types in a catalog request" do
      Puppet::Resource::Catalog.indirection.expects(:find).with(anything,
        has_entries(:checksum_type => 'md5.sha256.sha384.sha512.sha224'))
      @agent.run
    end

    it "sets the checksum_type query param to the supported_checksum_types setting in a catalog request" do
      # Regenerate the agent to pick up the new setting
      Puppet[:supported_checksum_types] = ['sha256']
      @agent = Puppet::Configurer.new
      @agent.stubs(:init_storage)
      @agent.stubs(:download_plugins)
      @agent.stubs(:send_report)
      @agent.stubs(:save_last_run_summary)

      Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entries(:checksum_type => 'sha256'))
      @agent.run
    end

    describe "when not using a REST terminus for catalogs" do
      it "should not pass any facts when retrieving the catalog" do
        Puppet::Resource::Catalog.indirection.terminus_class = :compiler
        @agent.expects(:facts_for_uploading).never
        Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options|
          options[:facts].nil?
        }.returns @catalog

        @agent.run
      end
    end

    describe "when using a REST terminus for catalogs" do
      it "should pass the prepared facts and the facts format as arguments when retrieving the catalog" do
        Puppet::Resource::Catalog.indirection.terminus_class = :rest
        @agent.expects(:facts_for_uploading).returns(:facts => "myfacts", :facts_format => :foo)
        Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options|
          options[:facts] == "myfacts" and options[:facts_format] == :foo
        }.returns @catalog

        @agent.run
      end
    end
  end

  describe "when initialized with a transaction_uuid" do
    it "stores it" do
      SecureRandom.expects(:uuid).never
      configurer = Puppet::Configurer.new('foo')
      expect(configurer.instance_variable_get(:@transaction_uuid) == 'foo')
    end
  end

  describe "when sending a report" do
    include PuppetSpec::Files

    before do
      Puppet.settings.stubs(:use).returns(true)
      @configurer = Puppet::Configurer.new
      Puppet[:lastrunfile] = tmpfile('last_run_file')

      @report = Puppet::Transaction::Report.new
      Puppet[:reports] = "none"
    end

    it "should print a report summary if configured to do so" do
      Puppet.settings[:summarize] = true

      @report.expects(:summary).returns "stuff"

      @configurer.expects(:puts).with("stuff")
      @configurer.send_report(@report)
    end

    it "should not print a report summary if not configured to do so" do
      Puppet.settings[:summarize] = false

      @configurer.expects(:puts).never
      @configurer.send_report(@report)
    end

    it "should save the report if reporting is enabled" do
      Puppet.settings[:report] = true

      Puppet::Transaction::Report.indirection.expects(:save).with(@report, nil, instance_of(Hash))
      @configurer.send_report(@report)
    end

    it "should not save the report if reporting is disabled" do
      Puppet.settings[:report] = false

      Puppet::Transaction::Report.indirection.expects(:save).with(@report, nil, instance_of(Hash)).never
      @configurer.send_report(@report)
    end

    it "should save the last run summary if reporting is enabled" do
      Puppet.settings[:report] = true

      @configurer.expects(:save_last_run_summary).with(@report)
      @configurer.send_report(@report)
    end

    it "should save the last run summary if reporting is disabled" do
      Puppet.settings[:report] = false

      @configurer.expects(:save_last_run_summary).with(@report)
      @configurer.send_report(@report)
    end

    it "should log but not fail if saving the report fails" do
      Puppet.settings[:report] = true

      Puppet::Transaction::Report.indirection.expects(:save).raises("whatever")

      Puppet.expects(:err)
      expect { @configurer.send_report(@report) }.not_to raise_error
    end
  end

  describe "when saving the summary report file" do
    include PuppetSpec::Files

    before do
      Puppet.settings.stubs(:use).returns(true)
      @configurer = Puppet::Configurer.new

      @report = stub 'report', :raw_summary => {}

      Puppet[:lastrunfile] = tmpfile('last_run_file')
    end

    it "should write the last run file" do
      @configurer.save_last_run_summary(@report)
      expect(Puppet::FileSystem.exist?(Puppet[:lastrunfile])).to be_truthy
    end

    it "should write the raw summary as yaml" do
      @report.expects(:raw_summary).returns("summary")
      @configurer.save_last_run_summary(@report)
      expect(File.read(Puppet[:lastrunfile])).to eq(YAML.dump("summary"))
    end

    it "should log but not fail if saving the last run summary fails" do
      # The mock will raise an exception on any method used.  This should
      # simulate a nice hard failure from the underlying OS for us.
      fh = Class.new(Object) do
        def method_missing(*args)
          raise "failed to do #{args[0]}"
        end
      end.new

      Puppet::Util.expects(:replace_file).yields(fh)

      Puppet.expects(:err)
      expect { @configurer.save_last_run_summary(@report) }.to_not raise_error
    end

    it "should create the last run file with the correct mode" do
      Puppet.settings.setting(:lastrunfile).expects(:mode).returns('664')
      @configurer.save_last_run_summary(@report)

      if Puppet::Util::Platform.windows?
        require 'puppet/util/windows/security'
        mode = Puppet::Util::Windows::Security.get_mode(Puppet[:lastrunfile])
      else
        mode = Puppet::FileSystem.stat(Puppet[:lastrunfile]).mode
      end
      expect(mode & 0777).to eq(0664)
    end

    it "should report invalid last run file permissions" do
      Puppet.settings.setting(:lastrunfile).expects(:mode).returns('892')
      Puppet.expects(:err).with(regexp_matches(/Could not save last run local report.*892 is invalid/))
      @configurer.save_last_run_summary(@report)
    end
  end

  describe "when requesting a node" do
    it "uses the transaction uuid in the request" do
      Puppet::Node.indirection.expects(:find).with(anything, has_entries(:transaction_uuid => anything)).twice
      @agent.run
    end

    it "sends an explicitly configured environment request" do
      Puppet.settings.expects(:set_by_config?).with(:environment).returns(true)
      Puppet::Node.indirection.expects(:find).with(anything, has_entries(:configured_environment => Puppet[:environment])).twice
      @agent.run
    end

    it "does not send a configured_environment when using the default" do
      Puppet::Node.indirection.expects(:find).with(anything, has_entries(:configured_environment => nil)).twice
      @agent.run
    end
  end

  def expects_new_catalog_only(catalog)
    Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns catalog
    Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.never
  end

  def expects_cached_catalog_only(catalog)
    Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns catalog
    Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.never
  end

  def expects_fallback_to_cached_catalog(catalog)
    Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil
    Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns catalog
  end

  def expects_fallback_to_new_catalog(catalog)
    Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns nil
    Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns catalog
  end

  def expects_neither_new_or_cached_catalog
    Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil
    Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns nil
  end

  describe "when retrieving a catalog" do
    before do
      Puppet.settings.stubs(:use).returns(true)
      @agent.stubs(:facts_for_uploading).returns({})
      @agent.stubs(:download_plugins)

      # retrieve a catalog in the current environment, so we don't try to converge unexpectedly
      @catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote(Puppet[:environment].to_sym))

      # this is the default when using a Configurer instance
      Puppet::Resource::Catalog.indirection.stubs(:terminus_class).returns :rest
    end

    describe "and configured to only retrieve a catalog from the cache" do
      before do
        Puppet.settings[:use_cached_catalog] = true
      end

      it "should first look in the cache for a catalog" do
        expects_cached_catalog_only(@catalog)

        expect(@agent.retrieve_catalog({})).to eq(@catalog)
      end

      it "should not make a node request or pluginsync when a cached catalog is successfully retrieved" do
        Puppet::Node.indirection.expects(:find).never
        expects_cached_catalog_only(@catalog)
        @agent.expects(:download_plugins).never

        @agent.run
      end

      it "should make a node request and pluginsync when a cached catalog cannot be retrieved" do
        Puppet::Node.indirection.expects(:find).returns nil
        expects_fallback_to_new_catalog(@catalog)
        @agent.expects(:download_plugins)

        @agent.run
      end

      it "should set its cached_catalog_status to 'explicitly_requested'" do
        expects_cached_catalog_only(@catalog)

        @agent.retrieve_catalog({})
        expect(@agent.instance_variable_get(:@cached_catalog_status)).to eq('explicitly_requested')
      end

      it "should set its cached_catalog_status to 'explicitly requested' if the cached catalog is from a different environment" do
        cached_catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote('second_env'))
        expects_cached_catalog_only(cached_catalog)

        @agent.retrieve_catalog({})
        expect(@agent.instance_variable_get(:@cached_catalog_status)).to eq('explicitly_requested')
      end

      it "should compile a new catalog if none is found in the cache" do
        expects_fallback_to_new_catalog(@catalog)

        expect(@agent.retrieve_catalog({})).to eq(@catalog)
      end

      it "should set its cached_catalog_status to 'not_used' if no catalog is found in the cache" do
        expects_fallback_to_new_catalog(@catalog)

        @agent.retrieve_catalog({})
        expect(@agent.instance_variable_get(:@cached_catalog_status)).to eq('not_used')
      end

      it "should not attempt to retrieve a cached catalog again if the first attempt failed" do
        Puppet::Node.indirection.expects(:find).returns(nil)
        expects_neither_new_or_cached_catalog

        @agent.run
      end

      it "should return the cached catalog when the environment doesn't match" do
        cached_catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote('second_env'))
        expects_cached_catalog_only(cached_catalog)

        Puppet.expects(:info).with("Using cached catalog from environment 'second_env'")
        expect(@agent.retrieve_catalog({})).to eq(cached_catalog)
      end
    end

    describe "and strict environment mode is set" do
      before do
        @catalog.stubs(:to_ral).returns(@catalog)
        @catalog.stubs(:write_class_file)
        @catalog.stubs(:write_resource_file)
        @agent.stubs(:send_report)
        @agent.stubs(:save_last_run_summary)
        Puppet.settings[:strict_environment_mode] = true
      end

      it "should not make a node request" do
        Puppet::Node.indirection.expects(:find).never

        @agent.run
      end

      it "should return nil when the catalog's environment doesn't match the agent specified environment" do
        @agent.instance_variable_set(:@environment, 'second_env')
        expects_new_catalog_only(@catalog)

        Puppet.expects(:err).with("Not using catalog because its environment 'production' does not match agent specified environment 'second_env' and strict_environment_mode is set")
        expect(@agent.run).to be_nil
      end

      it "should not return nil when the catalog's environment matches the agent specified environment" do
        @agent.instance_variable_set(:@environment, 'production')
        expects_new_catalog_only(@catalog)

        expect(@agent.run).to eq(0)
      end

      describe "and a cached catalog is explicitly requested" do
        before do
          Puppet.settings[:use_cached_catalog] = true
        end

        it "should return nil when the cached catalog's environment doesn't match the agent specified environment" do
          @agent.instance_variable_set(:@environment, 'second_env')
          expects_cached_catalog_only(@catalog)

          Puppet.expects(:err).with("Not using catalog because its environment 'production' does not match agent specified environment 'second_env' and strict_environment_mode is set")
          expect(@agent.run).to be_nil
        end

        it "should proceed with the cached catalog if its environment matchs the local environment" do
          Puppet.settings[:use_cached_catalog] = true
          @agent.instance_variable_set(:@environment, 'production')
          expects_cached_catalog_only(@catalog)

          expect(@agent.run).to eq(0)
        end
      end
    end

    it "should use the Catalog class to get its catalog" do
      Puppet::Resource::Catalog.indirection.expects(:find).returns @catalog

      @agent.retrieve_catalog({})
    end

    it "should set its cached_catalog_status to 'not_used' when downloading a new catalog" do
      Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog

      @agent.retrieve_catalog({})
      expect(@agent.instance_variable_get(:@cached_catalog_status)).to eq('not_used')
    end

    it "should use its node_name_value to retrieve the catalog" do
      Facter.stubs(:value).returns "eh"
      Puppet.settings[:node_name_value] = "myhost.domain.com"
      Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| name == "myhost.domain.com" }.returns @catalog

      @agent.retrieve_catalog({})
    end

    it "should default to returning a catalog retrieved directly from the server, skipping the cache" do
      Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog

      expect(@agent.retrieve_catalog({})).to eq(@catalog)
    end

    it "should log and return the cached catalog when no catalog can be retrieved from the server" do
      expects_fallback_to_cached_catalog(@catalog)

      Puppet.expects(:info).with("Using cached catalog from environment 'production'")
      expect(@agent.retrieve_catalog({})).to eq(@catalog)
    end

    it "should set its cached_catalog_status to 'on_failure' when no catalog can be retrieved from the server" do
      expects_fallback_to_cached_catalog(@catalog)

      @agent.retrieve_catalog({})
      expect(@agent.instance_variable_get(:@cached_catalog_status)).to eq('on_failure')
    end

    it "should not look in the cache for a catalog if one is returned from the server" do
      expects_new_catalog_only(@catalog)

      expect(@agent.retrieve_catalog({})).to eq(@catalog)
    end

    it "should return the cached catalog when retrieving the remote catalog throws an exception" do
      Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.raises "eh"
      Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog

      expect(@agent.retrieve_catalog({})).to eq(@catalog)
    end

    it "should set its cached_catalog_status to 'on_failure' when retrieving the remote catalog throws an exception" do
      Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.raises "eh"
      Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog

      @agent.retrieve_catalog({})
      expect(@agent.instance_variable_get(:@cached_catalog_status)).to eq('on_failure')
    end

    it "should log and return nil if no catalog can be retrieved from the server and :usecacheonfailure is disabled" do
      Puppet[:usecacheonfailure] = false
      Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil

      Puppet.expects(:warning).with('Not using cache on failed catalog')

      expect(@agent.retrieve_catalog({})).to be_nil
    end

    it "should set its cached_catalog_status to 'not_used' if no catalog can be retrieved from the server and :usecacheonfailure is disabled or fails to retrieve a catalog" do
      Puppet[:usecacheonfailure] = false
      Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil

      @agent.retrieve_catalog({})
      expect(@agent.instance_variable_get(:@cached_catalog_status)).to eq('not_used')
    end

    it "should return nil if no cached catalog is available and no catalog can be retrieved from the server" do
      expects_neither_new_or_cached_catalog

      expect(@agent.retrieve_catalog({})).to be_nil
    end

    it "should return nil if its cached catalog environment doesn't match server-specified environment" do
      cached_catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote('second_env'))
      @agent.instance_variable_set(:@node_environment, 'production')

      expects_fallback_to_cached_catalog(cached_catalog)

      Puppet.expects(:err).with("Not using cached catalog because its environment 'second_env' does not match 'production'")
      expect(@agent.retrieve_catalog({})).to be_nil
    end

    it "should set its cached_catalog_status to 'not_used' if the cached catalog environment doesn't match server-specified environment" do
      cached_catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote('second_env'))
      @agent.instance_variable_set(:@node_environment, 'production')

      expects_fallback_to_cached_catalog(cached_catalog)

      @agent.retrieve_catalog({})
      expect(@agent.instance_variable_get(:@cached_catalog_status)).to eq('not_used')
    end

    it "should return its cached catalog if the environment matches the server-specified environment" do
      cached_catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote(Puppet[:environment]))
      @agent.instance_variable_set(:@node_environment, cached_catalog.environment)

      expects_fallback_to_cached_catalog(cached_catalog)

      expect(@agent.retrieve_catalog({})).to eq(cached_catalog)
    end

    it "should set its cached_catalog_status to 'on_failure' if the cached catalog environment matches server-specified environment" do
      cached_catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote(Puppet[:environment]))
      @agent.instance_variable_set(:@node_environment, cached_catalog.environment)

      expects_fallback_to_cached_catalog(cached_catalog)

      @agent.retrieve_catalog({})
      expect(@agent.instance_variable_get(:@cached_catalog_status)).to eq('on_failure')
    end

    it "should not update the cached catalog in noop mode" do
      Puppet[:noop] = true
      Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entries(:ignore_cache => true, :ignore_cache_save => true)).returns(@catalog)

      @agent.retrieve_catalog({})
    end

    it "should update the cached catalog when not in noop mode" do
      Puppet[:noop] = false
      Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entries(:ignore_cache => true, :ignore_cache_save => false)).returns(@catalog)

      @agent.retrieve_catalog({})
    end
  end

  describe "when converting the catalog" do
    before do
      Puppet.settings.stubs(:use).returns(true)

      catalog.stubs(:to_ral).returns ral_catalog
    end

    let (:catalog) { Puppet::Resource::Catalog.new('tester', Puppet::Node::Environment.remote(Puppet[:environment].to_sym)) }
    let (:ral_catalog) { Puppet::Resource::Catalog.new('tester', Puppet::Node::Environment.remote(Puppet[:environment].to_sym)) }

    it "should convert the catalog to a RAL-formed catalog" do
      expect(@agent.convert_catalog(catalog, 10)).to equal(ral_catalog)
    end

    it "should finalize the catalog" do
      ral_catalog.expects(:finalize)

      @agent.convert_catalog(catalog, 10)
    end

    it "should record the passed retrieval time with the RAL catalog" do
      ral_catalog.expects(:retrieval_duration=).with 10

      @agent.convert_catalog(catalog, 10)
    end

    it "should write the RAL catalog's class file" do
      ral_catalog.expects(:write_class_file)

      @agent.convert_catalog(catalog, 10)
    end

    it "should write the RAL catalog's resource file" do
      ral_catalog.expects(:write_resource_file)

      @agent.convert_catalog(catalog, 10)
    end

    it "should set catalog conversion time on the report" do
      report = Puppet::Transaction::Report.new

      report.expects(:add_times).with(:convert_catalog, kind_of(Numeric))
      @agent.convert_catalog(catalog, 10, {:report => report})
    end
  end

  describe "when determining whether to pluginsync" do
    it "should default to Puppet[:pluginsync] when explicitly set by the commandline" do
      Puppet.settings[:pluginsync] = false
      Puppet.settings.expects(:set_by_cli?).returns(true)

      expect(described_class).not_to be_should_pluginsync
    end

    it "should default to Puppet[:pluginsync] when explicitly set by config" do
      Puppet.settings[:pluginsync] = false
      Puppet.settings.expects(:set_by_config?).returns(true)

      expect(described_class).not_to be_should_pluginsync
    end

    it "should be true if use_cached_catalog is false" do
      Puppet.settings[:use_cached_catalog] = false

      expect(described_class).to be_should_pluginsync
    end

    it "should be false if use_cached_catalog is true" do
      Puppet.settings[:use_cached_catalog] = true

      expect(described_class).not_to be_should_pluginsync
    end
  end

  describe "when attempting failover" do
    it "should not failover if server_list is not set" do
      Puppet.settings[:server_list] = []
      @agent.expects(:find_functional_server).never
      @agent.run
    end

    it "should not failover during an apply run" do
      Puppet.settings[:server_list] = ["myserver:123"]
      @agent.expects(:find_functional_server).never
      catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote(Puppet[:environment].to_sym))
      @agent.run :catalog => catalog
    end

    it "should select a server when provided" do
      Puppet.settings[:server_list] = ["myserver:123"]
      pool = Puppet::Network::HTTP::Pool.new(Puppet[:http_keepalive_timeout])
      Puppet::Network::HTTP::Pool.expects(:new).returns(pool)
      Puppet.expects(:override).with({:http_pool => pool}).yields
      Puppet.expects(:override).with({:server => "myserver", :serverport => '123'}).twice.yields
      Puppet::Node.indirection.expects(:find).returns(nil)
      @agent.expects(:run_internal).returns(nil)
      @agent.run
    end

    it "should fallback to an empty server when failover fails" do
      Puppet.settings[:server_list] = ["myserver:123"]
      pool = Puppet::Network::HTTP::Pool.new(Puppet[:http_keepalive_timeout])
      Puppet::Network::HTTP::Pool.expects(:new).returns(pool)
      Puppet.expects(:override).with({:http_pool => pool}).yields
      Puppet.expects(:override).with({:server => "myserver", :serverport => '123'}).yields
      Puppet.expects(:override).with({:server => nil, :serverport => nil}).yields
      error = Net::HTTPError.new(400, 'dummy server communication error')
      Puppet::Node.indirection.expects(:find).raises(error)
      @agent.expects(:run_internal).returns(nil)
      @agent.run
    end

    it "should not make multiple node requets when the server is found" do
      Puppet.settings[:server_list] = ["myserver:123"]
      Puppet::Node.indirection.expects(:find).returns("mynode").once
      @agent.expects(:prepare_and_retrieve_catalog).returns(nil)
      @agent.run
    end
  end
end
