require 'chef_helper'

RSpec.describe BasePgHelper do
  cached(:chef_run) { converge_config }
  let(:node) { chef_run.node }
  subject { described_class.new(node) }

  before do
    allow(subject).to receive(:service_name) { 'postgresql' }
    allow(subject).to receive(:service_cmd) { 'gitlab-psql' }
  end

  context 'when handling ssl configuration' do
    after do
      chef_run.node.rm_normal('postgresql', 'dir')
      chef_run.node.rm_normal('postgresql', 'ssl_cert_file')
    end

    describe 'ssl_cert_file' do
      it 'is configured with an absolute path' do
        absolute_path = "/my/absolute/path"
        config_path = "MrBoots"
        chef_run.node.normal['postgresql']['ssl_cert_file'] = absolute_path
        chef_run.node.normal['postgresql']['dir'] = config_path
        expect(subject.ssl_cert_file).to eq(absolute_path)
      end

      it 'is configured with a relative path' do
        relative_path = "my/relative/path"
        config_path = "/MrBoots"
        chef_run.node.normal['postgresql']['ssl_cert_file'] = relative_path
        chef_run.node.normal['postgresql']['dir'] = config_path
        expect(subject.ssl_cert_file).to eq(File.join(config_path, 'data', relative_path))
      end
    end

    describe 'ssl_key_file' do
      it 'is configured with an absolute path' do
        absolute_path = "/my/absolute/path"
        config_path = "MrBoots"
        chef_run.node.normal['postgresql']['ssl_key_file'] = absolute_path
        chef_run.node.normal['postgresql']['dir'] = config_path
        expect(subject.ssl_key_file).to eq(absolute_path)
      end

      it 'is configured with a relative path' do
        relative_path = "my/relative/path"
        config_path = "/MrBoots"
        chef_run.node.normal['postgresql']['ssl_key_file'] = relative_path
        chef_run.node.normal['postgresql']['dir'] = config_path
        expect(subject.ssl_key_file).to eq(File.join(config_path, 'data', relative_path))
      end
    end
  end

  describe '#database_exists?' do
    it 'calls out to psql_cmd' do
      expect(subject).to receive(:psql_cmd).with(
        [
          "-d 'template1'",
          "-c 'select datname from pg_database' -A",
          '| grep -x example'
        ])
      subject.database_exists?('example')
    end

    context 'database present' do
      it 'truthy' do
        allow(subject).to receive(:psql_cmd) { true }
        expect(subject.database_exists?('example')).to be_truthy
      end

      it 'falsey' do
        allow(subject).to receive(:psql_cmd) { false }
        expect(subject.database_exists?('example')).to be_falsey
      end
    end
  end

  describe '#user_exists?' do
    it 'calls out to psql_cmd' do
      expect(subject).to receive(:psql_cmd).with(
        [
          "-d 'template1'",
          "-c 'select usename from pg_user' -A",
          '|grep -x testuser'
        ])
      subject.user_exists?('testuser')
    end

    context 'user present' do
      it 'returns true when user exists' do
        allow(subject).to receive(:psql_cmd) { true }
        expect(subject.user_exists?('testuser')).to be_truthy
      end

      it 'returns false when user does not exist' do
        allow(subject).to receive(:psql_cmd) { false }
        expect(subject.user_exists?('testuser')).to be_falsey
      end
    end
  end

  describe '#extension_enabled?' do
    it 'will check with psql_cmd when database present' do
      expect(subject).to receive(:psql_cmd).with(
        [
          "-d 'database'",
          "-c 'select extname from pg_extension' -A",
          '| grep -x extension'
        ])
      subject.extension_enabled?('extension', 'database')
    end

    context 'extension present' do
      it 'truthy' do
        allow(subject).to receive(:psql_cmd) { true }
        expect(subject.extension_enabled?('extension', 'database')).to be_truthy
      end

      it 'falsey' do
        allow(subject).to receive(:psql_cmd) { false }
        expect(subject.extension_enabled?('extension', 'database')).to be_falsey
      end
    end
  end

  describe '#extension_can_be_enabled?' do
    before do
      allow(subject).to receive(:is_running?).and_return(true)
      allow(subject).to receive(:is_standby?).and_return(false)
      allow(subject).to receive(:extension_exists?).and_return(true)
      allow(subject).to receive(:database_exists?).and_return(true)
      allow(subject).to receive(:extension_enabled?).and_return(false)
    end

    it 'can be enabled' do
      expect(subject.extension_can_be_enabled?('extension', 'db')).to be_truthy
    end

    it 'needs to be running' do
      allow(subject).to receive(:is_running?).and_return(false)
      expect(subject.extension_can_be_enabled?('extension', 'db')).to be_falsey
    end

    it 'cannot be done on a slave' do
      allow(subject).to receive(:is_standby?).and_return(true)
      expect(subject.extension_can_be_enabled?('extension', 'db')).to be_falsey
    end

    it 'needs to have the extension available' do
      allow(subject).to receive(:extension_exists?).and_return(false)
      expect(subject.extension_can_be_enabled?('extension', 'db')).to be_falsey
    end

    it 'needs the database to load the extension into' do
      allow(subject).to receive(:database_exists?).and_return(false)
      expect(subject.extension_can_be_enabled?('extension', 'db')).to be_falsey
    end

    it 'should not enable twice' do
      allow(subject).to receive(:extension_enabled?).and_return(true)
      expect(subject.extension_can_be_enabled?('extension', 'db')).to be_falsey
    end
  end

  describe '#user_options' do
    before do
      result = spy('shellout')
      allow(result).to receive(:stdout).and_return("f|f|t|f\n")
      allow(subject).to receive(:do_shell_out).and_return(result)
    end

    it 'returns hash from query' do
      expect(subject.user_options('')).to eq(
        {
          'SUPERUSER' => false,
          'CREATEDB' => false,
          'REPLICATION' => true,
          'BYPASSRLS' => false
        }
      )
    end
  end

  describe '#user_options_set?' do
    let(:default_options) do
      {
        'SUPERUSER' => false,
        'CREATEDB' => false,
        'REPLICATION' => true,
        'BYPASSRLS' => false
      }
    end

    context 'default user options' do
      before do
        allow(subject).to receive(:user_options).and_return(default_options)
      end

      it 'returns true when no options are asked about' do
        expect(subject.user_options_set?('', [])).to be_truthy
      end

      it 'returns true when options are set to their defaults' do
        expect(subject.user_options_set?('', ['NOSUPERUSER'])).to be_truthy
      end

      it 'returns false when options are set away from their defaults' do
        expect(subject.user_options_set?('', ['SUPERUSER'])).to be_falsey
      end
    end

    context 'modified user' do
      before do
        allow(subject).to receive(:user_options).and_return(default_options.merge({ 'SUPERUSER' => true }))
      end

      it 'returns false when options is not what we expect' do
        expect(subject.user_options_set?('', ['NOSUPERUSER'])).to be_falsey
      end
    end
  end

  describe '#user_password_match?' do
    before do
      # user: gitlab pass: test123
      allow(subject).to receive(:user_hashed_password) { 'md5b56573ef0d94cff111898c63ec259f3f' }
    end

    it 'returns true when same password is in plain-text' do
      expect(subject.user_password_match?('gitlab', 'test123')).to be_truthy
    end

    it 'returns true when same password is in MD5 format' do
      expect(subject.user_password_match?('gitlab', 'md5b56573ef0d94cff111898c63ec259f3f')).to be_truthy
    end

    it 'returns false when wrong password is in plain-text' do
      expect(subject.user_password_match?('gitlab', 'wrong')).to be_falsey
    end

    it 'returns false when wrong password is in MD5 format' do
      expect(subject.user_password_match?('gitlab', 'md5b599de4332636c03a60fca13be1edb5f')).to be_falsey
    end

    it 'returns false when password is not supplied' do
      expect(subject.user_password_match?('gitlab', nil)).to be_falsey
    end

    context 'nil password' do
      before do
        # user: gitlab pass: unset
        allow(subject).to receive(:user_hashed_password) { '' }
      end

      it 'returns true when the password is nil' do
        expect(subject.user_password_match?('gitlab', nil)).to be_truthy
      end
    end
  end

  describe '#parse_pghash' do
    let(:payload) { '{host=127.0.0.1,dbname=gitlabhq_production,port=5432,user=gitlab,"password=foo}bar\"zoo\\cat"}' }

    it 'returns a hash' do
      expect(subject.parse_pghash(payload)).to be_a(Hash)
    end

    it 'when content is empty still return a hash' do
      expect(subject.parse_pghash('')).to be_a(Hash)
      expect(subject.parse_pghash('{}')).to be_a(Hash)
    end

    it 'returns hash with expected keys' do
      hash = subject.parse_pghash(payload)

      expect(hash.keys).to contain_exactly(:host, :dbname, :port, :user, :password)
    end

    it 'returns hash with expected values' do
      hash = subject.parse_pghash(payload)

      expect(hash.values).to contain_exactly('127.0.0.1', 'gitlabhq_production', '5432', 'gitlab', 'foo}bar"zoo\cat')
    end
  end

  describe '#is_running?' do
    it 'returns true when postgres is running' do
      stub_service_success_status('postgresql', true)

      expect(subject.is_running?).to be_truthy
    end

    it 'returns false when postgres is not running' do
      stub_service_success_status('postgresql', false)

      expect(subject.is_running?).to be_falsey
    end
  end

  describe '#is_managed_and_offline?' do
    it 'returns true when conditions are met' do
      chef_run.node.normal['postgresql']['enable'] = true
      stub_service_failure_status('postgresql', true)

      expect(subject.is_managed_and_offline?).to be_truthy
    end

    it 'returns false when conditions are not met' do
      chef_run.node.normal['postgresql']['enable'] = true
      stub_service_failure_status('postgresql', false)

      expect(subject.is_managed_and_offline?).to be_falsey

      chef_run.node.normal['postgresql']['enable'] = false
      stub_service_failure_status('postgresql', false)

      expect(subject.is_managed_and_offline?).to be_falsey

      stub_service_failure_status('postgresql', true)
      expect(subject.is_managed_and_offline?).to be_falsey
    end
  end

  describe '#is_standby?' do
    let(:recovery_files) { %w(recovery.conf recovery.signal standby.signal) }

    it 'returns true for a standby instance' do
      recovery_files.each do |f|
        allow(File).to receive(:exist?)
          .with("/var/opt/gitlab/postgresql/data/#{f}").and_return(true)
      end

      expect(subject.is_standby?).to be true
    end

    it 'returns false for a primary instance' do
      recovery_files.each do |f|
        allow(File).to receive(:exist?)
          .with("/var/opt/gitlab/postgresql/data/#{f}").and_return(false)
      end
      expect(subject.is_standby?).to be false
    end
  end

  context 'when handling connection information' do
    describe '#connection_info' do
      it 'raises a not implemented warning' do
        expect { subject.connection_info }.to raise_error(NotImplementedError)
      end
    end

    describe '#build_connection_info' do
      it 'returns a ConnectionInfo object' do
        expect(subject.build_connection_info('dbname', 'dbhost', 'port', 'pguser')).to be_a(described_class::ConnectionInfo)
      end
    end
  end

  context 'when handling database functions' do
    let(:function_stdout) { double(stdout: 'function-owner') }
    let(:empty_stdout) { double(stdout: '') }
    describe '#function_owner' do
      it 'returns the correct owner' do
        allow(subject).to receive(:do_shell_out)
        .with("/opt/gitlab/bin/gitlab-psql -d 'database' -c \"SELECT pg_catalog.pg_get_userbyid(proowner) FROM pg_proc WHERE proname='function';\" -tA")
        .and_return(function_stdout)
        expect(subject.function_owner('database', 'function')).to eq('function-owner')
      end
      it 'is empty if the function does not exist' do
        allow(subject).to receive(:do_shell_out)
        .with("/opt/gitlab/bin/gitlab-psql -d 'database' -c \"SELECT pg_catalog.pg_get_userbyid(proowner) FROM pg_proc WHERE proname='not_a_function';\" -tA")
        .and_return(empty_stdout)
        expect(subject.function_owner('database', 'not_a_function')).to be_empty
      end
    end
  end
end
