
require "spec_helper"

# 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)
    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(:client) do
    Chef::Config[:event_loggers] = []
    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
    #Chef::Config[:node_name] = fqdn
    allow(Ohai::System).to receive(:new).and_return(ohai_system)
  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_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_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)

    # --ResourceReporter#node_load_completed
    #   gets a run id from the server for storing resource history
    #   (has its own tests, so stubbing it here.)
    expect_any_instance_of(Chef::ResourceReporter).to receive(:node_load_completed)
  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]).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_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

    stub_const("Chef::Client::STDOUT_FD", stdout)
    stub_const("Chef::Client::STDERR_FD", stderr)

    stub_rest_clean
    stub_for_register
    stub_for_node_load
    stub_for_sync_cookbooks
    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)

    # --ResourceReporter#run_completed
    #   updates the server with the resource history
    #   (has its own tests, so stubbing it here.)
    expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed)
    # --AuditReporter#run_completed
    #   posts the audit data to server.
    #   (has its own tests, so stubbing it here.)
    expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_completed)
  end
end

shared_context "run failed" do
  def stub_for_run
    expect(client).to receive(:run_failed)

    # --ResourceReporter#run_completed
    #   updates the server with the resource history
    #   (has its own tests, so stubbing it here.)
    expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed)
    # --AuditReporter#run_completed
    #   posts the audit data to server.
    #   (has its own tests, so stubbing it here.)
    expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed)
  end

  before do
    expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError)
  end
end
