require 'support/spec_support'
require 'cheffish/rspec/chef_run_support'
require 'support/key_support'

repo_path = Dir.mktmpdir('chef_repo')

describe Chef::Resource::PrivateKey do
  extend Cheffish::RSpec::ChefRunSupport

  before :each do
    FileUtils.remove_entry_secure(repo_path)
    Dir.mkdir(repo_path)
  end

  context 'with a recipe with a private_key' do
    it 'the private_key is created in pem format' do
      expect_recipe {
        private_key "#{repo_path}/blah"
      }.to have_updated "private_key[#{repo_path}/blah]", :create
      expect(IO.read("#{repo_path}/blah")).to start_with('-----BEGIN')
      expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA)
    end
  end

  context 'with a private_key "blah" resource' do
    before :each do
      Dir.mkdir("#{repo_path}/other_keys")
      Chef::Config.private_key_paths = [ repo_path, "#{repo_path}/other_keys" ]
    end

    it 'the private key is created in the private_key_write_path' do
      expect_recipe {
        private_key 'blah'
      }.to have_updated "private_key[blah]", :create
      expect(Chef::Config.private_key_write_path).to eq(repo_path)
      expect(File.exist?("#{repo_path}/blah")).to be true
      expect(File.exist?("#{repo_path}/other_keys/blah")).to be false
      expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA)
      expect(OpenSSL::PKey.read(Cheffish.get_private_key('blah'))).to be_kind_of(OpenSSL::PKey::RSA)
    end

    context 'and the private key already exists somewhere not in the write path' do
      before :each do
        recipe { private_key "#{repo_path}/other_keys/blah" }.converge
      end

      it 'the private expect(key).to not update' do
        expect_recipe {
          private_key 'blah'
        }.not_to have_updated "private_key[blah]", :create

        expect(File.exist?("#{repo_path}/blah")).to be false
        expect(File.exist?("#{repo_path}/other_keys/blah")).to be true
      end
    end
  end

  context 'with a private key' do
    before :each do
      Cheffish::BasicChefClient.converge_block do
        private_key "#{repo_path}/blah"
      end
    end

    context 'and a private_key that copies it in der format' do
      it 'the private_key is copied in der format and is identical' do
        expect_recipe {
          private_key "#{repo_path}/blah.der" do
            source_key_path "#{repo_path}/blah"
            format :der
          end
        }.to have_updated "private_key[#{repo_path}/blah.der]", :create
        key_str = IO.read("#{repo_path}/blah.der")
        expect(key_str).not_to start_with('-----BEGIN')
        expect(key_str).not_to start_with('ssh-')
        expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah")
      end
    end

    it 'a private_key that copies it from in-memory as a string succeeds' do
      expect_recipe {
        private_key "#{repo_path}/blah.der" do
          source_key IO.read("#{repo_path}/blah")
          format :der
        end
      }.to have_updated "private_key[#{repo_path}/blah.der]", :create
      key_str = IO.read("#{repo_path}/blah.der")
      expect(key_str).not_to start_with('-----BEGIN')
      expect(key_str).not_to start_with('ssh-')
      expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah")
    end

    it 'a private_key that copies it from in-memory as a key succeeds' do
      key = OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))
      expect_recipe {
        private_key "#{repo_path}/blah.der" do
          source_key key
          format :der
        end
      }.to have_updated "private_key[#{repo_path}/blah.der]", :create
      key_str = IO.read("#{repo_path}/blah.der")
      expect(key_str).not_to start_with('-----BEGIN')
      expect(key_str).not_to start_with('ssh-')
      expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah")
    end

    context 'and a public_key recipe' do
      it 'the public_key is created' do
        expect_recipe {
          public_key "#{repo_path}/blah.pub" do
            source_key_path "#{repo_path}/blah"
          end
        }.to have_updated "public_key[#{repo_path}/blah.pub]", :create
        expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ')
        expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah"
      end
    end

    context 'and a public key' do
      before :each do
        Cheffish::BasicChefClient.converge_block do
          public_key "#{repo_path}/blah.pub" do
            source_key_path "#{repo_path}/blah"
          end
        end
      end

      context 'and public_key resource based off the public key file' do
        it 'the second public_key is created' do
          expect_recipe {
            public_key "#{repo_path}/blah.pub2" do
              source_key_path "#{repo_path}/blah.pub"
            end
          }.to have_updated "public_key[#{repo_path}/blah.pub2]", :create
          expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ')
          expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah"
        end
      end

      context 'and another public_key based off the first public_key in-memory in a string' do
        it 'the second public_key is created' do
          expect_recipe {
            public_key "#{repo_path}/blah.pub2" do
              source_key IO.read("#{repo_path}/blah.pub")
            end
          }.to have_updated "public_key[#{repo_path}/blah.pub2]", :create
          expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ')
          expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah"
        end
      end

      it 'and another public_key based off the first public_key in-memory in a key, the second public_key is created' do
        key, format = Cheffish::KeyFormatter.decode(IO.read("#{repo_path}/blah.pub"))

        expect_recipe {
          public_key "#{repo_path}/blah.pub2" do
            source_key key
          end
        }.to have_updated "public_key[#{repo_path}/blah.pub2]", :create
        expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ')
        expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah"
      end

      context 'and another public_key in :pem format based off the first public_key' do
        it 'the second public_key is created' do
          expect_recipe {
            public_key "#{repo_path}/blah.pub2" do
              source_key_path "#{repo_path}/blah.pub"
              format :pem
            end
          }.to have_updated "public_key[#{repo_path}/blah.pub2]", :create
          expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ')
          expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah"
        end
      end

      context 'and another public_key in :der format based off the first public_key' do
        it 'the second public_key is created' do
          expect_recipe {
            public_key "#{repo_path}/blah.pub2" do
              source_key_path "#{repo_path}/blah.pub"
              format :pem
            end
          }.to have_updated "public_key[#{repo_path}/blah.pub2]", :create
          expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ')
          expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah"
        end
      end
    end

    context 'and a public_key resource in pem format' do
      it 'the public_key is created' do
        expect_recipe {
          public_key "#{repo_path}/blah.pub" do
            source_key_path "#{repo_path}/blah"
            format :pem
          end
        }.to have_updated "public_key[#{repo_path}/blah.pub]", :create
        expect(IO.read("#{repo_path}/blah.pub")).to start_with('-----BEGIN')
        expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah"
      end
    end

    context 'and a public_key resource in der format' do
      it 'the public_key is created in openssh format' do
        expect_recipe {
          public_key "#{repo_path}/blah.pub" do
            source_key_path "#{repo_path}/blah"
            format :der
          end
        }.to have_updated "public_key[#{repo_path}/blah.pub]", :create
        expect(IO.read("#{repo_path}/blah.pub")).not_to start_with('-----BEGIN')
        expect(IO.read("#{repo_path}/blah.pub")).not_to start_with('ssh-rsa')
        expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah"
      end
    end
  end

  context 'with a recipe with a private_key in der format' do
    it 'the private_key is created' do
      expect_recipe {
        private_key "#{repo_path}/blah" do
          format :der
        end
      }.to have_updated "private_key[#{repo_path}/blah]", :create
      expect(IO.read("#{repo_path}/blah")).not_to start_with('-----BEGIN')
      expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA)
    end
  end

  context 'with a private key in der format' do
    before :each do
      Cheffish::BasicChefClient.converge_block do
        private_key "#{repo_path}/blah" do
          format :der
        end
      end
    end

    context 'and a public_key' do
      it 'the public_key is created in openssh format' do
        expect_recipe {
          public_key "#{repo_path}/blah.pub" do
            source_key_path "#{repo_path}/blah"
          end
        }.to have_updated "public_key[#{repo_path}/blah.pub]", :create
        expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ')
        expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah"
      end
    end
  end

  context 'with a recipe with a private_key with a pass_phrase' do
    it 'the private_key is created' do
      expect_recipe {
        private_key "#{repo_path}/blah" do
          pass_phrase 'hello'
        end
      }.to have_updated "private_key[#{repo_path}/blah]", :create
      expect(IO.read("#{repo_path}/blah")).to start_with('-----BEGIN')
      expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"), 'hello')).to be_kind_of(OpenSSL::PKey::RSA)
    end
  end

  context 'with a private key with a pass phrase' do
    before :each do
      Cheffish::BasicChefClient.converge_block do
        private_key "#{repo_path}/blah" do
          pass_phrase 'hello'
        end
      end
    end

    context 'and a private_key that copies it in der format' do
      it 'the private_key is copied in der format and is identical' do
        expect_recipe {
          private_key "#{repo_path}/blah.der" do
            source_key_path "#{repo_path}/blah"
            source_key_pass_phrase 'hello'
            format :der
          end
        }.to have_updated "private_key[#{repo_path}/blah.der]", :create
        key_str = IO.read("#{repo_path}/blah.der")
        expect(key_str).not_to start_with('-----BEGIN')
        expect(key_str).not_to start_with('ssh-')
        expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah", 'hello')
      end
    end

    context 'and a private_key resource pointing at it without a pass_phrase' do
      it 'the run fails with an exception' do
        expect {
          converge {
            private_key "#{repo_path}/blah"
          }
        }.to raise_error /missing pass phrase?/
      end
    end

    context 'and a private_key resource with no pass phrase and regenerate_if_different' do
      it 'the private_key is regenerated' do
        expect_recipe {
          private_key "#{repo_path}/blah" do
            regenerate_if_different true
          end
        }.to have_updated "private_key[#{repo_path}/blah]", :create
        expect(IO.read("#{repo_path}/blah")).to start_with('-----BEGIN')
        expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA)
      end
    end

    it 'a private_key resource that copies it from in-memory as a string succeeds' do
      expect_recipe {
        private_key "#{repo_path}/blah.der" do
          source_key IO.read("#{repo_path}/blah")
          source_key_pass_phrase 'hello'
          format :der
        end
      }.to have_updated "private_key[#{repo_path}/blah.der]", :create
      key_str = IO.read("#{repo_path}/blah.der")
      expect(key_str).not_to start_with('-----BEGIN')
      expect(key_str).not_to start_with('ssh-')
      expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah", 'hello')
    end

    context 'and a public_key' do
      it 'the public_key is created in openssh format' do
        expect_recipe {
          public_key "#{repo_path}/blah.pub" do
            source_key_path "#{repo_path}/blah"
            source_key_pass_phrase 'hello'
          end
        }.to have_updated "public_key[#{repo_path}/blah.pub]", :create
        expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ')
        expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah", 'hello'
      end
    end

    context 'and a public_key derived from the private key in an in-memory string' do
      it 'the public_key is created in openssh format' do
        expect_recipe {
          public_key "#{repo_path}/blah.pub" do
            source_key IO.read("#{repo_path}/blah")
            source_key_pass_phrase 'hello'
          end
        }.to have_updated "public_key[#{repo_path}/blah.pub]", :create
        expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ')
        expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah", 'hello'
      end
    end
  end

  context 'with a recipe with a private_key and public_key_path' do
    it 'the private_key and public_key are created' do
      expect_recipe {
        private_key "#{repo_path}/blah" do
          public_key_path "#{repo_path}/blah.pub"
        end
      }.to have_updated "private_key[#{repo_path}/blah]", :create
      expect(IO.read("#{repo_path}/blah")).to start_with('-----BEGIN')
      expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA)
      expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ')
      expect("#{repo_path}/blah.pub").to be_public_key_for("#{repo_path}/blah")
    end
  end

  context 'with a recipe with a private_key and public_key_path and public_key_format' do
    it 'the private_key and public_key are created' do
      expect_recipe {
        private_key "#{repo_path}/blah" do
          public_key_path "#{repo_path}/blah.pub.der"
          public_key_format :der
        end
      }.to have_updated "private_key[#{repo_path}/blah]", :create
      expect(IO.read("#{repo_path}/blah")).to start_with('-----BEGIN')
      expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA)
      expect(IO.read("#{repo_path}/blah.pub.der")).not_to start_with('ssh-rsa ')
      expect("#{repo_path}/blah.pub.der").to be_public_key_for("#{repo_path}/blah")
    end
  end

  context 'with a recipe with a private_key with path :none' do
    it 'the private_key is created' do
      got_private_key = nil
      expect_recipe {
        private_key 'in_memory' do
          path :none
          after { |resource, private_key| got_private_key = private_key }
        end
      }.to have_updated "private_key[in_memory]", :create
      expect(got_private_key).to be_kind_of(OpenSSL::PKey::RSA)
    end
  end

end
