require 'spec_helper'
require_relative '../../config/initializers/secret_token'

describe 'create_tokens', lib: true do
  include StubENV

  let(:secrets) { ActiveSupport::OrderedOptions.new }

  HEX_KEY = /\h{128}/
  RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m

  before do
    allow(File).to receive(:write)
    allow(File).to receive(:delete)
    allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets)
    allow(Rails).to receive_message_chain(:root, :join) { |string| string }
    allow(self).to receive(:warn)
    allow(self).to receive(:exit)
  end

  context 'setting secret keys' do
    context 'when none of the secrets exist' do
      before do
        stub_env('SECRET_KEY_BASE', nil)
        allow(File).to receive(:exist?).with('.secret').and_return(false)
        allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false)
        allow(self).to receive(:warn_missing_secret)
      end

      it 'generates different hashes for secret_key_base, otp_key_base, and db_key_base' do
        create_tokens

        keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)

        expect(keys.uniq).to eq(keys)
        expect(keys).to all(match(HEX_KEY))
      end

      it 'generates an RSA key for jws_private_key' do
        create_tokens

        keys = secrets.values_at(:jws_private_key)

        expect(keys.uniq).to eq(keys)
        expect(keys).to all(match(RSA_KEY))
      end

      it 'warns about the secrets to add to secrets.yml' do
        expect(self).to receive(:warn_missing_secret).with('secret_key_base')
        expect(self).to receive(:warn_missing_secret).with('otp_key_base')
        expect(self).to receive(:warn_missing_secret).with('db_key_base')
        expect(self).to receive(:warn_missing_secret).with('jws_private_key')

        create_tokens
      end

      it 'writes the secrets to secrets.yml' do
        expect(File).to receive(:write).with('config/secrets.yml', any_args) do |filename, contents, options|
          new_secrets = YAML.load(contents)[Rails.env]

          expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
          expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
          expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
          expect(new_secrets['jws_private_key']).to eq(secrets.jws_private_key)
        end

        create_tokens
      end

      it 'does not write a .secret file' do
        expect(File).not_to receive(:write).with('.secret')

        create_tokens
      end
    end

    context 'when the other secrets all exist' do
      before do
        secrets.db_key_base = 'db_key_base'
        secrets.jws_private_key = 'jws_private_key'

        allow(File).to receive(:exist?).with('.secret').and_return(true)
        allow(File).to receive(:read).with('.secret').and_return('file_key')
      end

      context 'when secret_key_base exists in the environment and secrets.yml' do
        before do
          stub_env('SECRET_KEY_BASE', 'env_key')
          secrets.secret_key_base = 'secret_key_base'
          secrets.otp_key_base = 'otp_key_base'
          secrets.jws_private_key = 'jws_private_key'
        end

        it 'does not issue a warning' do
          expect(self).not_to receive(:warn)

          create_tokens
        end

        it 'uses the environment variable' do
          create_tokens

          expect(secrets.secret_key_base).to eq('env_key')
        end

        it 'does not update secrets.yml' do
          expect(File).not_to receive(:write)

          create_tokens
        end
      end

      context 'when secret_key_base and otp_key_base exist' do
        before do
          secrets.secret_key_base = 'secret_key_base'
          secrets.otp_key_base = 'otp_key_base'
          secrets.jws_private_key = 'jws_private_key'
        end

        it 'does not write any files' do
          expect(File).not_to receive(:write)

          create_tokens
        end

        it 'sets the the keys to the values from the environment and secrets.yml' do
          create_tokens

          expect(secrets.secret_key_base).to eq('secret_key_base')
          expect(secrets.otp_key_base).to eq('otp_key_base')
          expect(secrets.db_key_base).to eq('db_key_base')
          expect(secrets.jws_private_key).to eq('jws_private_key')
        end

        it 'deletes the .secret file' do
          expect(File).to receive(:delete).with('.secret')

          create_tokens
        end
      end

      context 'when secret_key_base and otp_key_base do not exist' do
        before do
          allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true)
          allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => secrets.to_h.stringify_keys)
          allow(self).to receive(:warn_missing_secret)
        end

        it 'uses the file secret' do
          expect(File).to receive(:write) do |filename, contents, options|
            new_secrets = YAML.load(contents)[Rails.env]

            expect(new_secrets['secret_key_base']).to eq('file_key')
            expect(new_secrets['otp_key_base']).to eq('file_key')
            expect(new_secrets['db_key_base']).to eq('db_key_base')
            expect(new_secrets['jws_private_key']).to eq('jws_private_key')
          end

          create_tokens

          expect(secrets.otp_key_base).to eq('file_key')
        end

        it 'keeps the other secrets as they were' do
          create_tokens

          expect(secrets.db_key_base).to eq('db_key_base')
        end

        it 'warns about the missing secrets' do
          expect(self).to receive(:warn_missing_secret).with('secret_key_base')
          expect(self).to receive(:warn_missing_secret).with('otp_key_base')

          create_tokens
        end

        it 'deletes the .secret file' do
          expect(File).to receive(:delete).with('.secret')

          create_tokens
        end
      end
    end

    context 'when db_key_base is blank but exists in secrets.yml' do
      before do
        secrets.otp_key_base = 'otp_key_base'
        secrets.secret_key_base = 'secret_key_base'
        yaml_secrets = secrets.to_h.stringify_keys.merge('db_key_base' => '<%= an_erb_expression %>')

        allow(File).to receive(:exist?).with('.secret').and_return(false)
        allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true)
        allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => yaml_secrets)
        allow(self).to receive(:warn_missing_secret)
      end

      it 'warns about updating db_key_base' do
        expect(self).to receive(:warn_missing_secret).with('db_key_base')

        create_tokens
      end

      it 'warns about the blank value existing in secrets.yml and exits' do
        expect(self).to receive(:warn) do |warning|
          expect(warning).to include('db_key_base')
          expect(warning).to include('<%= an_erb_expression %>')
        end

        create_tokens
      end

      it 'does not update secrets.yml' do
        expect(self).to receive(:exit).with(1).and_call_original
        expect(File).not_to receive(:write)

        expect { create_tokens }.to raise_error(SystemExit)
      end
    end
  end
end
