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

require 'puppet/ssl/host'

describe Puppet::Face[:certificate, '0.0.1'] do
  include PuppetSpec::Files

  let(:ca) { Puppet::SSL::CertificateAuthority.instance }

  before :each do
    Puppet[:confdir] = tmpdir('conf')
    Puppet::SSL::CertificateAuthority.stubs(:ca?).returns true

    Puppet::SSL::Host.ca_location = :local

    # We can't cache the CA between tests, because each one has its own SSL dir.
    ca = Puppet::SSL::CertificateAuthority.new
    Puppet::SSL::CertificateAuthority.stubs(:new).returns ca
    Puppet::SSL::CertificateAuthority.stubs(:instance).returns ca
  end

  it "should have a ca-location option" do
    expect(subject).to be_option :ca_location
  end

  it "should set the ca location when invoked" do
    Puppet::SSL::Host.expects(:ca_location=).with(:local)
    ca.expects(:sign).with do |name,options|
      name == "hello, friend"
    end

    subject.sign "hello, friend", :ca_location => :local
  end

  it "(#7059) should set the ca location when an inherited action is invoked" do
    Puppet::SSL::Host.expects(:ca_location=).with(:local)
    subject.indirection.expects(:find)
    subject.find "hello, friend", :ca_location => :local
  end

  it "should validate the option as required" do
    expect do
      subject.find 'hello, friend'
    end.to raise_exception ArgumentError, /required/i
  end

  it "should validate the option as a supported value" do
    expect do
      subject.find 'hello, friend', :ca_location => :foo
    end.to raise_exception ArgumentError, /valid values/i
  end

  describe "#generate" do
    let(:options) { {:ca_location => 'local'} }
    let(:host) { Puppet::SSL::Host.new(hostname) }
    let(:csr) { host.certificate_request }

    before :each do
      Puppet[:autosign] = false
    end

    describe "for the current host" do
      let(:hostname) { Puppet[:certname] }

      it "should generate a CSR for this host" do
        subject.generate(hostname, options)

        expect(csr.content.subject.to_s).to eq("/CN=#{Puppet[:certname]}")
        expect(csr.name).to eq(Puppet[:certname])
      end

      it "should add dns_alt_names from the global config if not otherwise specified" do
        Puppet[:dns_alt_names] = 'from,the,config'

        subject.generate(hostname, options)

        expected = %W[DNS:from DNS:the DNS:config DNS:#{hostname}]

        expect(csr.subject_alt_names).to match_array(expected)
      end

      it "should add the provided dns_alt_names if they are specified" do
        Puppet[:dns_alt_names] = 'from,the,config'

        subject.generate(hostname, options.merge(:dns_alt_names => 'explicit,alt,names'))

        expected = %W[DNS:explicit DNS:alt DNS:names DNS:#{hostname}]

        expect(csr.subject_alt_names).to match_array(expected)
      end
    end

    describe "for another host" do
      let(:hostname) { Puppet[:certname] + 'different' }

      it "should generate a CSR for the specified host" do
        subject.generate(hostname, options)

        expect(csr.content.subject.to_s).to eq("/CN=#{hostname}")
        expect(csr.name).to eq(hostname)
      end

      it "should fail if a CSR already exists for the host" do
        subject.generate(hostname, options)

        expect do
          subject.generate(hostname, options)
        end.to raise_error(RuntimeError, /#{hostname} already has a requested certificate; ignoring certificate request/)
      end

      it "should add not dns_alt_names from the config file" do
        Puppet[:dns_alt_names] = 'from,the,config'

        subject.generate(hostname, options)

        expect(csr.subject_alt_names).to be_empty
      end

      it "should add the provided dns_alt_names if they are specified" do
        Puppet[:dns_alt_names] = 'from,the,config'

        subject.generate(hostname, options.merge(:dns_alt_names => 'explicit,alt,names'))

        expected = %W[DNS:explicit DNS:alt DNS:names DNS:#{hostname}]

        expect(csr.subject_alt_names).to match_array(expected)
      end
      
      it "should use the global setting if set by CLI" do
        Puppet.settings.patch_value(:dns_alt_names, 'from,the,cli', :cli)
        
        subject.generate(hostname, options)
        
        expected = %W[DNS:from DNS:the DNS:cli DNS:#{hostname}]
        
        expect(csr.subject_alt_names).to match_array(expected)
      end
      
      it "should generate an error if both set on CLI" do
        Puppet.settings.patch_value(:dns_alt_names, 'from,the,cli', :cli)
        expect do
          subject.generate(hostname, options.merge(:dns_alt_names => 'explicit,alt,names'))
        end.to raise_error ArgumentError, /Can't specify both/ 
      end
    end
  end

  describe "#sign" do
    let(:options) { {:ca_location => 'local'} }
    let(:host) { Puppet::SSL::Host.new(hostname) }
    let(:hostname) { "foobar" }

    it "should sign the certificate request if one is waiting", :unless => Puppet.features.microsoft_windows? do
      subject.generate(hostname, options)

      subject.sign(hostname, options)

      expect(host.certificate_request).to be_nil
      expect(host.certificate).to be_a(Puppet::SSL::Certificate)
      expect(host.state).to eq('signed')
    end

    it "should fail if there is no waiting certificate request" do
      expect do
        subject.sign(hostname, options)
      end.to raise_error(ArgumentError, /Could not find certificate request for #{hostname}/)
    end

    describe "when ca_location is local", :unless => Puppet.features.microsoft_windows? do
      describe "when the request has dns alt names" do
        before :each do
          subject.generate(hostname, options.merge(:dns_alt_names => 'some,alt,names'))
        end

        it "should refuse to sign the request if allow_dns_alt_names is not set" do
          expect do
            subject.sign(hostname, options)
          end.to raise_error(Puppet::SSL::CertificateAuthority::CertificateSigningError,
                             /CSR '#{hostname}' contains subject alternative names \(.*?\), which are disallowed. Use `puppet cert --allow-dns-alt-names sign #{hostname}` to sign this request./i)

          expect(host.state).to eq('requested')
        end

        it "should sign the request if allow_dns_alt_names is set" do
          expect do
            subject.sign(hostname, options.merge(:allow_dns_alt_names => true))
          end.not_to raise_error

          expect(host.state).to eq('signed')
        end
      end

      describe "when the request has no dns alt names" do
        before :each do
          subject.generate(hostname, options)
        end

        it "should sign the request if allow_dns_alt_names is set" do
          expect { subject.sign(hostname, options.merge(:allow_dns_alt_names => true)) }.not_to raise_error

          expect(host.state).to eq('signed')
        end

        it "should sign the request if allow_dns_alt_names is not set" do
          expect { subject.sign(hostname, options) }.not_to raise_error

          expect(host.state).to eq('signed')
        end
      end
    end

    describe "when ca_location is remote" do
      let(:options) { {:ca_location => :remote} }
      it "should fail if allow-dns-alt-names is specified" do
        expect do
          subject.sign(hostname, options.merge(:allow_dns_alt_names => true))
        end
      end
    end
  end
end
