RSpec.describe Metasploit::Credential::SSHKey, type: :model do
  it_should_behave_like 'Metasploit::Concern.run'

  #
  # lets
  #

  let(:key_size) do
    # key size tuned for speed.  DO NOT use for production, it is below current recommended key size of 2048
    512
  end

  context 'factories' do
    context 'metasploit_credential_dsa_key' do
      subject(:metasploit_credential_dsa_key) do
        FactoryGirl.build(:metasploit_credential_dsa_key)
      end

      it { is_expected.to be_valid }

      it 'has DSA key type' do
        expect(metasploit_credential_dsa_key.data).to match(/-----BEGIN DSA PRIVATE KEY-----/)
        expect(metasploit_credential_dsa_key.send(:openssl_pkey_pkey)).to be_a OpenSSL::PKey::DSA
      end
    end

    context 'metasploit_credential_rsa_key' do
      subject(:metasploit_credential_rsa_key) do
        FactoryGirl.build(:metasploit_credential_rsa_key)
      end

      it { is_expected.to be_valid }

      it 'has RSA key type' do
        expect(metasploit_credential_rsa_key.data).to match(/-----BEGIN RSA PRIVATE KEY-----/)
        expect(metasploit_credential_rsa_key.send(:openssl_pkey_pkey)).to be_a OpenSSL::PKey::RSA
      end
    end

    context 'metasploit_credential_ssh_key' do
      subject(:metasploit_credential_ssh_key) do
        FactoryGirl.build(:metasploit_credential_ssh_key)
      end

      it { is_expected.to be_valid }
    end
  end

  context 'validations' do
    it { is_expected.to validate_presence_of :data }

    context 'on #data' do
      subject(:data_errors) do
        ssh_key.errors[:data]
      end

      let(:ssh_key) do
        FactoryGirl.build(:metasploit_credential_ssh_key)
      end

      context '#private' do
        #
        # lets
        #

        let(:error) do
          I18n.translate!('activerecord.errors.models.metasploit/credential/ssh_key.attributes.data.not_private')
        end

        #
        # Callbacks
        #

        before(:example) do
          expect(ssh_key).to receive(:private?).and_return(private)

          ssh_key.valid?
        end

        context 'with #private?' do
          let(:private) do
            true
          end

          it { is_expected.not_to include(error) }
        end

        context 'without #private?' do
          let(:private) do
            false
          end

          it { is_expected.to include(error) }
        end
      end

      context '#readable' do
        context 'with #data' do
          context 'with error' do
            #
            # Shared Examples
            #

            shared_examples_for 'exception' do
              it 'includes error class' do
                exception_class_name = exception.class.to_s
                expect(
                    data_errors.any? { |error|
                      error.include? exception_class_name
                    }
                ).to be true
              end

              it 'includes error message' do
                exception_message = exception.to_s

                expect(
                    data_errors.any? { |error|
                      error.include? exception_message
                    }
                ).to be true
              end
            end

            #
            # Callbacks
            #

            before(:example) do
              expect(ssh_key).to receive(:openssl_pkey_pkey).and_raise(exception)

              ssh_key.valid?
            end

            context 'with ArgumentError' do
              let(:exception) do
                ArgumentError.new("Bad Argument")
              end

              it_should_behave_like 'exception'
            end

            context 'with OpenSSL::PKey::PKeyError' do
              let(:exception) do
                OpenSSL::PKey::PKeyError.new("Bad Public/Private Key")
              end

              it_should_behave_like 'exception'
            end
          end

          context 'without error' do
            before(:example) do
              ssh_key.valid?
            end

            it { is_expected.to be_empty }
          end
        end

        context 'without #data' do
          let(:error) do
            I18n.translate!('errors.messages.blank')
          end

          #
          # Callbacks
          #

          before(:example) do
            ssh_key.data = nil

            ssh_key.valid?
          end

          it { is_expected.to include(error) }
        end
      end

      context '#unencrypted' do
        #
        # lets
        #

        let(:error) do
          I18n.translate!('activerecord.errors.models.metasploit/credential/ssh_key.attributes.data.encrypted')
        end

        #
        # Callbacks
        #

        before(:example) do
          expect(ssh_key).to receive(:encrypted?).and_return(encrypted)

          ssh_key.valid?
        end

        context 'with #encrypted?' do
          let(:encrypted) do
            true
          end

          it { is_expected.to include(error) }
        end

        context 'without #encrypted' do
          let(:encrypted) do
            false
          end

          it { is_expected.not_to include(error) }
        end
      end
    end
  end

  context '#encrypted?' do
    subject(:ssh_key) do
      FactoryGirl.build(
          :metasploit_credential_ssh_key,
          data: data
      )
    end

    context 'with #data' do
      #
      # Shared examples
      #

      shared_examples_for 'key type' do |key_type|
        context "with #{key_type} key" do
          let(:unencrypted_key) do
            OpenSSL::PKey.const_get(key_type).new(key_size)
          end

          context 'with encrypted' do
            let(:cipher) do
              OpenSSL::Cipher.new('AES-128-CBC')
            end

            let(:data) do
              begin
                unencrypted_key.to_pem(cipher, password)
              # TODO This error is occasionally thrown, unsure of cause:
              rescue OpenSSL::PKey::RSAError, OpenSSL::PKey::DSAError => e
                puts "#{key_type} key error encountered (cipher: #{cipher.name}, password: #{password.inspect}): #{e.backtrace}"
                raise e
              end
            end

            let(:password) do
              cipher.random_key
            end

            it { is_expected.to be_encrypted }
          end

          context 'without encrypted' do
            let(:data) do
              unencrypted_key.to_pem
            end

            it { is_expected.not_to be_encrypted }
          end
        end
      end

      it_should_behave_like 'key type', 'DSA'
      it_should_behave_like 'key type', 'RSA'
    end

    context 'without #data' do
      let(:data) do
        nil
      end

      it { is_expected.not_to be_encrypted }
    end
  end

  context '#openssl_pkey_pkey' do
    subject(:openssl_pkey_pkey) do
      ssh_key.send(:openssl_pkey_pkey)
    end

    #
    # lets
    #

    let(:ssh_key) do
      FactoryGirl.build(
          :metasploit_credential_ssh_key,
          data: data
      )
    end

    context 'with data' do
      #
      # Shared examples
      #

      shared_examples_for 'key type' do |key_type|
        context "with #{key_type} key" do
          let(:unencrypted_key) do
            OpenSSL::PKey.const_get(key_type).new(key_size)
          end

          context 'with encrypted' do
            let(:cipher) do
              OpenSSL::Cipher.new('AES-128-CBC')
            end

            let(:data) do
              unencrypted_key.to_pem(cipher, password)
            end

            let(:password) do
              cipher.random_key
            end

            specify {
              expect {
                openssl_pkey_pkey
              }.to raise_error(ArgumentError)
            }
          end

          context 'without encrypted' do
            let(:data) do
              unencrypted_key.to_pem
            end

            it { is_expected.to be_a OpenSSL::PKey.const_get(key_type) }
          end
        end
      end

      it_should_behave_like 'key type', 'DSA'
      it_should_behave_like 'key type', 'RSA'

      context 'with non-key data' do
        let(:data) do
          'Not a key'
        end

        specify {
          expect {
            openssl_pkey_pkey
          }.to raise_error(ArgumentError)
        }
      end
    end

    context 'without data' do
      let(:data) do
        nil
      end

      it { is_expected.to be_nil }
    end

    context 'with DSA key' do
      let(:ssh_key) do
        FactoryGirl.build(:metasploit_credential_dsa_key)
      end

      it { is_expected.to be_a OpenSSL::PKey::DSA }
    end

    context 'with RSA key' do
      let(:ssh_key) do
        FactoryGirl.build(:metasploit_credential_rsa_key)
      end

      it { is_expected.to be_a OpenSSL::PKey::RSA }
    end

    context 'with nil' do

    end
  end

  context '#private?' do
    subject(:ssh_key) do
      FactoryGirl.build(
          :metasploit_credential_ssh_key,
          data: data
      )
    end

    context 'with #data' do
      #
      # Shared examples
      #

      shared_examples_for 'key type' do |key_type|
        context "with #{key_type} key" do
          let(:data) do
            key.to_pem
          end

          let(:private_key) do
            OpenSSL::PKey.const_get(key_type).new(key_size)
          end

          context 'with public' do
            let(:key) do
              private_key.public_key
            end

            it { is_expected.not_to be_private }
          end

          context 'with private' do
            let(:key) do
              private_key
            end

            it { is_expected.to be_private }
          end
        end
      end

      it_should_behave_like 'key type', 'DSA'
      it_should_behave_like 'key type', 'RSA'
    end

    context 'without #data' do
      let(:data) do
        nil
      end

      it { is_expected.not_to be_private }
    end
  end

  context 'human name' do
    it 'properly determines the model\'s human name' do
      expect(Metasploit::Credential::SSHKey.model_name.human).to eq('SSH key')
    end
  end
end
