require 'spec_helper'
require 'puppet/ssl/host'
require 'puppet/indirector/certificate_status'
require 'tempfile'

describe "Puppet::Indirector::CertificateStatus::File" do
  include PuppetSpec::Files

  before :all do
    Puppet::SSL::Host.configure_indirection(:file)
  end

  before do
    allow(Puppet::SSL::CertificateAuthority).to receive(:ca?).and_return(true)
    @terminus = Puppet::SSL::Host.indirection.terminus(:file)

    @tmpdir = tmpdir("certificate_status_ca_testing")
    Puppet[:confdir] = @tmpdir
    Puppet[:vardir] = @tmpdir

    # localcacert is where each client stores the CA certificate
    # cacert is where the master stores the CA certificate
    # Since we need to play the role of both for testing we need them to be the same and exist
    Puppet[:cacert] = Puppet[:localcacert]
  end

  def generate_csr(host)
    host.generate_key
    csr = Puppet::SSL::CertificateRequest.new(host.name)
    csr.generate(host.key.content)
    Puppet::SSL::CertificateRequest.indirection.save(csr)
  end

  def sign_csr(host)
    host.desired_state = "signed"
    @terminus.save(Puppet::Indirector::Request.new(:certificate_status, :save, host.name, host))
  end

  def generate_signed_cert(host)
    generate_csr(host)
    sign_csr(host)

    @terminus.find(Puppet::Indirector::Request.new(:certificate_status, :find, host.name, host))
  end

  def generate_revoked_cert(host)
    generate_signed_cert(host)

    host.desired_state = "revoked"

    @terminus.save(Puppet::Indirector::Request.new(:certificate_status, :save, host.name, host))
  end

  it "should be a terminus on SSL::Host" do
    expect(@terminus).to be_instance_of(Puppet::Indirector::CertificateStatus::File)
  end

  it "should create a CA instance if none is present" do
    expect(@terminus.ca).to be_instance_of(Puppet::SSL::CertificateAuthority)
  end

  describe "when creating the CA" do
    it "should fail if it is not a valid CA" do
      expect(Puppet::SSL::CertificateAuthority).to receive(:ca?).and_return(false)
      expect { @terminus.ca }.to raise_error(ArgumentError, "This process is not configured as a certificate authority")
    end
  end

  it "should be indirected with the name 'certificate_status'" do
    expect(Puppet::SSL::Host.indirection.name).to eq(:certificate_status)
  end

  describe "when finding" do
    before do
      @host = Puppet::SSL::Host.new("foo")
      Puppet.settings.use(:main)
    end

    it "should return the Puppet::SSL::Host when a CSR exists for the host" do
      generate_csr(@host)
      request = Puppet::Indirector::Request.new(:certificate_status, :find, "foo", @host)

      retrieved_host = @terminus.find(request)

      expect(retrieved_host.name).to eq(@host.name)
      expect(retrieved_host.certificate_request.content.to_s.chomp).to eq(@host.certificate_request.content.to_s.chomp)
    end

    it "should return the Puppet::SSL::Host when a public key exists for the host" do
      generate_signed_cert(@host)
      request = Puppet::Indirector::Request.new(:certificate_status, :find, "foo", @host)

      retrieved_host = @terminus.find(request)

      expect(retrieved_host.name).to eq(@host.name)
      expect(retrieved_host.certificate.content.to_s.chomp).to eq(@host.certificate.content.to_s.chomp)
    end

    it "should return nil when neither a CSR nor public key exist for the host" do
      request = Puppet::Indirector::Request.new(:certificate_status, :find, "foo", @host)
      expect(@terminus.find(request)).to eq(nil)
    end
  end

  describe "when saving" do
    before do
      @host = Puppet::SSL::Host.new("foobar")
      Puppet.settings.use(:main)
    end

    describe "when signing a cert" do
      before do
        @host.desired_state = "signed"
        @request = Puppet::Indirector::Request.new(:certificate_status, :save, "foobar", @host)
      end

      it "should fail if no CSR is on disk" do
        expect { @terminus.save(@request) }.to raise_error(Puppet::Error, /certificate request/)
      end

      it "should sign the on-disk CSR when it is present" do
        signed_host = generate_signed_cert(@host)

        expect(signed_host.state).to eq("signed")
        expect(Puppet::SSL::Certificate.indirection.find("foobar")).to be_instance_of(Puppet::SSL::Certificate)
      end
    end

    describe "when revoking a cert" do
      before do
        @request = Puppet::Indirector::Request.new(:certificate_status, :save, "foobar", @host)
      end

      it "should fail if no certificate is on disk" do
        @host.desired_state = "revoked"
        expect { @terminus.save(@request) }.to raise_error(Puppet::Error, /Cannot revoke/)
      end

      it "should revoke the certificate when it is present" do
        generate_revoked_cert(@host)

        expect(@host.state).to eq('revoked')
      end
    end
  end

  describe "when deleting" do
    before do
      Puppet.settings.use(:main)
    end

    it "should not delete anything if no certificate, request, or key is on disk" do
      host = Puppet::SSL::Host.new("clean_me")
      request = Puppet::Indirector::Request.new(:certificate_status, :delete, "clean_me", host)
      expect(@terminus.destroy(request)).to eq("Nothing was deleted")
    end

    it "should clean certs, cert requests, keys" do
      signed_host = Puppet::SSL::Host.new("clean_signed_cert")
      generate_signed_cert(signed_host)
      signed_request = Puppet::Indirector::Request.new(:certificate_status, :delete, "clean_signed_cert", signed_host)
      expect(@terminus.destroy(signed_request)).to eq("Deleted for clean_signed_cert: Puppet::SSL::Certificate, Puppet::SSL::Key")

      requested_host = Puppet::SSL::Host.new("clean_csr")
      generate_csr(requested_host)
      csr_request = Puppet::Indirector::Request.new(:certificate_status, :delete, "clean_csr", requested_host)
      expect(@terminus.destroy(csr_request)).to eq("Deleted for clean_csr: Puppet::SSL::CertificateRequest, Puppet::SSL::Key")
    end
  end

  describe "when searching" do
    it "should return a list of all hosts with certificate requests, signed certs, or revoked certs" do
      Puppet.settings.use(:main)

      signed_host = Puppet::SSL::Host.new("signed_host")
      generate_signed_cert(signed_host)

      requested_host = Puppet::SSL::Host.new("requested_host")
      generate_csr(requested_host)

      revoked_host = Puppet::SSL::Host.new("revoked_host")
      generate_revoked_cert(revoked_host)

      retrieved_hosts = @terminus.search(Puppet::Indirector::Request.new(:certificate_status, :search, "all", signed_host))

      results = retrieved_hosts.map {|h| [h.name, h.state]}.sort{ |h,i| h[0] <=> i[0] }
      expect(results).to eq([["ca","signed"],["requested_host","requested"],["revoked_host","revoked"],["signed_host","signed"]])
    end
  end
end
