require 'chef_helper'

RSpec.describe 'postgresql' do
  let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(runit_service postgresql_config database_objects)).converge('gitlab::default') }
  let(:postgresql_data_dir) { '/var/opt/gitlab/postgresql/data' }
  let(:postgresql_ssl_cert) { File.join(postgresql_data_dir, 'server.crt') }
  let(:postgresql_ssl_key) { File.join(postgresql_data_dir, 'server.key') }
  let(:postgresql_conf) { File.join(postgresql_data_dir, 'postgresql.conf') }
  let(:runtime_conf) { '/var/opt/gitlab/postgresql/data/runtime.conf' }
  let(:pg_hba_conf) { '/var/opt/gitlab/postgresql/data/pg_hba.conf' }

  before do
    allow(Gitlab).to receive(:[]).and_call_original
    allow_any_instance_of(PgHelper).to receive(:version).and_return(PGVersion.new('best_version'))
    allow_any_instance_of(PgHelper).to receive(:database_version).and_return(PGVersion.new('best_version'))
    allow_any_instance_of(PgHelper).to receive(:running_version).and_return(PGVersion.new('best_version'))
  end

  it 'includes the postgresql::bin recipe' do
    expect(chef_run).to include_recipe('postgresql::bin')
  end

  it 'includes the postgresql::user recipe' do
    expect(chef_run).to include_recipe('postgresql::user')
  end

  it 'includes postgresql::sysctl recipe' do
    expect(chef_run).to include_recipe('postgresql::sysctl')
  end

  it 'does not warn the user that a restart is needed by default' do
    allow_any_instance_of(PgHelper).to receive(:is_running?).and_return(true)
    expect(chef_run).not_to run_ruby_block('warn pending postgresql restart')
  end

  it 'includes runtime.conf in postgresql.conf' do
    expect(chef_run).to render_file(postgresql_conf)
      .with_content(/include 'runtime.conf'/)
  end

  context 'with default settings' do
    it_behaves_like 'enabled runit service', 'postgresql', 'root', 'root', 'gitlab-psql', 'gitlab-psql', true

    context 'when rendering postgresql.conf' do
      it 'correctly sets the shared_preload_libraries default setting' do
        expect(chef_run.node['postgresql']['shared_preload_libraries'])
          .to be_nil

        expect(chef_run).to render_file(postgresql_conf)
          .with_content(/shared_preload_libraries = ''/)
      end

      it 'disables archive mode' do
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/archive_mode = off/)
      end

      it 'enables SSL by default' do
        expect(chef_run.node['postgresql']['ssl'])
          .to eq('on')

        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/ssl = on/)
      end

      it 'sets the default SSL cipher list' do
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(%r{ssl_ciphers = 'HIGH:MEDIUM:\+3DES:!aNULL:!SSLv3:!TLSv1'})
      end

      it 'sets the default locations of SSL certificates' do
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/ssl_cert_file = 'server.crt'/)
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/ssl_key_file = 'server.key'/)
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(%r{ssl_ca_file = '/opt/gitlab/embedded/ssl/certs/cacert.pem'})
      end

      it 'leaves synchronous_standby_names empty' do
        expect(chef_run.node['postgresql']['synchronous_standby_names'])
          .to eq('')

        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/synchronous_standby_names = ''/)
      end

      it 'disables wal_log_hints setting' do
        expect(chef_run.node['postgresql']['wal_log_hints']).to eq('off')

        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/wal_log_hints = off/)
      end

      it 'does not set dynamic_shared_memory_type by default' do
        expect(chef_run).not_to render_file(
          postgresql_conf
        ).with_content(/^dynamic_shared_memory_type = /)
      end

      it 'sets the max_locks_per_transaction setting' do
        expect(chef_run.node['postgresql']['max_locks_per_transaction'])
          .to eq(128)

        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/max_locks_per_transaction = 128/)
      end

      it 'does not include gitlab-geo.conf' do
        expect(chef_run).to render_file(postgresql_conf)
          .with_content { |content|
            expect(content).not_to match(/include_if_exists 'gitlab-geo.conf'/)
          }
      end
    end

    it 'generates a self-signed SSL certificate and key' do
      stub_gitlab_rb(postgresql: { ssl_cert_file: 'certfile', ssl_key_file: 'keyfile' })

      absolute_cert_path = File.join(postgresql_data_dir, 'certfile')
      absolute_key_path = File.join(postgresql_data_dir, 'keyfile')

      expect(chef_run).to create_file(absolute_cert_path).with(
        user: 'gitlab-psql',
        group: 'gitlab-psql',
        mode: 0400
      )

      expect(chef_run).to create_file(absolute_key_path).with(
        user: 'gitlab-psql',
        group: 'gitlab-psql',
        mode: 0400
      )

      expect(chef_run).to render_file(absolute_cert_path)
        .with_content(/-----BEGIN CERTIFICATE-----/)
      expect(chef_run).to render_file(absolute_key_path)
        .with_content(/-----BEGIN RSA PRIVATE KEY-----/)
    end

    context 'when rendering runtime.conf' do
      it 'correctly sets the log_line_prefix default setting' do
        expect(chef_run.node['postgresql']['log_line_prefix'])
          .to be_nil

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/log_line_prefix = ''/)
      end

      it 'does not include log_statement by default' do
        expect(chef_run).not_to render_file(runtime_conf)
          .with_content(/log_statement = /)
      end

      it 'sets max_standby settings' do
        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/max_standby_archive_delay = 30s/)
        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/max_standby_streaming_delay = 30s/)
      end

      it 'sets archive settings' do
        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/archive_command = ''/)
        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/archive_timeout = 0/)
      end

      it 'sets logging directory' do
        expect(chef_run.node['postgresql']['log_directory'])
          .to eq('/var/log/gitlab/postgresql')

        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(%r(^log_directory = '/var/log/gitlab/postgresql'))
      end

      it 'disables hot_standby_feedback' do
        expect(chef_run.node['postgresql']['hot_standby_feedback'])
          .to eq('off')

        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/hot_standby_feedback = off/)
      end

      it 'sets the random_page_cost setting' do
        expect(chef_run.node['postgresql']['random_page_cost'])
          .to eq(2.0)

        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/random_page_cost = 2\.0/)
      end

      it 'sets the log_temp_files setting' do
        expect(chef_run.node['postgresql']['log_temp_files'])
          .to eq(-1)

        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/log_temp_files = -1/)
      end

      it 'disables the log_checkpoints setting' do
        expect(chef_run.node['postgresql']['log_checkpoints'])
          .to eq('off')

        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/log_checkpoints = off/)
      end

      it 'sets idle_in_transaction_session_timeout' do
        expect(chef_run.node['postgresql']['idle_in_transaction_session_timeout'])
          .to eq('60000')

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/idle_in_transaction_session_timeout = 60000/)
      end

      it 'sets effective_io_concurrency' do
        expect(chef_run.node['postgresql']['effective_io_concurrency'])
          .to eq(1)

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/effective_io_concurrency = 1/)
      end

      it 'sets max_worker_processes' do
        expect(chef_run.node['postgresql']['max_worker_processes'])
          .to eq(8)

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/max_worker_processes = 8/)
      end

      it 'sets max_parallel_workers_per_gather' do
        expect(chef_run.node['postgresql']['max_parallel_workers_per_gather'])
          .to eq(0)

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/max_parallel_workers_per_gather = 0/)
      end

      it 'sets log_lock_waits' do
        expect(chef_run.node['postgresql']['log_lock_waits'])
          .to eq(1)

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/log_lock_waits = 1/)
      end

      it 'sets log_min_duration_statement' do
        expect(chef_run.node['postgresql']['log_min_duration_statement'])
          .to eq(1000)

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/log_min_duration_statement = 1000/)
      end

      it 'sets deadlock_timeout' do
        expect(chef_run.node['postgresql']['deadlock_timeout'])
          .to eq('5s')

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/deadlock_timeout = '5s'/)
      end

      it 'disables track_io_timing' do
        expect(chef_run.node['postgresql']['track_io_timing'])
          .to eq('off')

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/track_io_timing = 'off'/)
      end

      it 'sets default_statistics_target' do
        expect(chef_run.node['postgresql']['default_statistics_target'])
          .to eq(1000)

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/default_statistics_target = 1000/)
      end

      it 'enables the synchronous_commit setting' do
        expect(chef_run.node['postgresql']['synchronous_commit'])
          .to eq('on')

        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/synchronous_commit = on/)
      end

      it 'sets log_connections setting' do
        expect(chef_run.node['postgresql']['log_connections'])
          .to eq('off')

        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/log_connections = off/)
      end

      it 'sets log_disconnections setting' do
        expect(chef_run.node['postgresql']['log_disconnections'])
          .to eq('off')

        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/log_disconnections = off/)
      end
    end

    context 'when rendering pg_hba.conf' do
      it 'creates a standard pg_hba.conf' do
        expect(chef_run).to render_file(pg_hba_conf)
          .with_content('local   all         all                               peer map=gitlab')
      end

      it 'cert authentication is disabled by default' do
        expect(chef_run).to render_file(pg_hba_conf).with_content { |content|
          expect(content).to_not match(/cert$/)
        }
      end
    end
  end

  context 'with user specified settings' do
    before do
      stub_gitlab_rb(postgresql: {
                       shared_preload_libraries: 'pg_stat_statements',
                       archive_mode: 'on',
                       username: 'foo',
                       group: 'bar',
                       ssl: 'off',
                       ssl_crl_file: 'revoke.crl',
                       ssl_ciphers: 'ALL',
                       log_destination: 'csvlog',
                       logging_collector: 'on',
                       log_filename: 'test.log',
                       log_file_mode: '0600',
                       log_truncate_on_rotation: 'on',
                       log_rotation_age: '1d',
                       log_rotation_size: '10MB',
                       dynamic_shared_memory_type: 'none',
                       wal_log_hints: 'on'
                     },
                     geo_secondary_role: {
                       enable: true
                     })
    end

    it_behaves_like 'enabled runit service', 'postgresql', 'root', 'root', 'foo', 'bar', true

    context 'when rendering postgresql.conf' do
      it 'correctly sets the shared_preload_libraries setting' do
        expect(chef_run.node['postgresql']['shared_preload_libraries'])
          .to eql('pg_stat_statements')

        expect(chef_run).to render_file(postgresql_conf)
          .with_content(/shared_preload_libraries = 'pg_stat_statements'/)
      end

      it 'enables archive mode' do
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/archive_mode = on/)
      end

      it 'disables SSL' do
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/ssl = off/)

        expect(chef_run).not_to render_file(postgresql_ssl_cert)
        expect(chef_run).not_to render_file(postgresql_ssl_key)
      end

      it 'sets the certificate revocation list' do
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/ssl_crl_file = 'revoke.crl'/)
      end

      it 'sets the SSL cipher list' do
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/ssl_ciphers = 'ALL'/)
      end

      it 'includes gitlab-geo.conf in postgresql.conf' do
        expect(chef_run).to render_file(postgresql_conf)
          .with_content(/include_if_exists 'gitlab-geo.conf'/)
      end

      it 'sets user specified logging parameters' do
        expect(chef_run).to render_file(postgresql_conf).with_content { |content|
          expect(content).to match(/logging_collector = on/)
        }

        expect(chef_run).to render_file(runtime_conf).with_content { |content|
          expect(content).to match(/log_destination = 'csvlog'/)
          expect(content).to match(/log_filename = 'test.log'/)
          expect(content).to match(/log_file_mode = 0600/)
          expect(content).to match(/log_truncate_on_rotation = on/)
          expect(content).to match(/log_rotation_age = 1d/)
          expect(content).to match(/log_rotation_size = 10MB/)
        }
      end

      it 'sets the dynamic_shared_memory_type' do
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/^dynamic_shared_memory_type = none/)
      end

      it 'enables wal_log_hints' do
        expect(chef_run).to render_file(
          postgresql_conf
        ).with_content(/^wal_log_hints = on/)
      end
    end

    context 'when rendering runtime.conf' do
      before do
        stub_gitlab_rb(postgresql: {
                         log_line_prefix: '%a',
                         log_statement: 'all',
                         max_standby_archive_delay: '60s',
                         max_standby_streaming_delay: '120s',
                         archive_command: 'command',
                         archive_timeout: '120',
                         log_connections: 'on',
                         log_disconnections: 'on'
                       })
      end

      it 'correctly sets the log_line_prefix setting' do
        expect(chef_run.node['postgresql']['log_line_prefix'])
          .to eql('%a')

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/log_line_prefix = '%a'/)
      end

      it 'correctly sets the log_statement setting' do
        expect(chef_run.node['postgresql']['log_statement'])
          .to eql('all')

        expect(chef_run).to render_file(runtime_conf)
          .with_content(/log_statement = 'all'/)
      end

      it 'sets max_standby settings' do
        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/max_standby_archive_delay = 60s/)
        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/max_standby_streaming_delay = 120s/)
      end

      it 'sets archive settings' do
        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/archive_command = 'command'/)
        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/archive_timeout = 120/)
      end

      it 'sets log_connections setting' do
        expect(chef_run.node['postgresql']['log_connections'])
          .to eq('on')

        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/log_connections = on/)
      end

      it 'sets log_disconnections setting' do
        expect(chef_run.node['postgresql']['log_disconnections'])
          .to eq('on')

        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/log_disconnections = on/)
      end
    end

    context 'when rendering pg_hba.conf' do
      before do
        stub_gitlab_rb(
          postgresql: {
            hostssl: true,
            trust_auth_cidr_addresses: ['127.0.0.1/32'],
            custom_pg_hba_entries: {
              foo: [
                {
                  type: 'host',
                  database: 'foo',
                  user: 'bar',
                  cidr: '127.0.0.1/32',
                  method: 'trust'
                }
              ]
            },
            cert_auth_addresses: {
              '1.2.3.4/32' => {
                database: 'fakedatabase',
                user: 'fakeuser'
              },
              'fakehostname' => {
                database: 'anotherfakedatabase',
                user: 'anotherfakeuser'
              },
            }
          }
        )
      end

      it 'prefers hostssl when configured in pg_hba.conf' do
        expect(chef_run).to render_file(pg_hba_conf)
          .with_content('hostssl    all         all         127.0.0.1/32           trust')
      end

      it 'adds users custom entries to pg_hba.conf' do
        expect(chef_run).to render_file(pg_hba_conf)
          .with_content('host foo bar 127.0.0.1/32 trust')
      end

      it 'allows cert authentication to be enabled' do
        expect(chef_run).to render_file(pg_hba_conf).with_content('hostssl fakedatabase fakeuser 1.2.3.4/32 cert')
        expect(chef_run).to render_file(pg_hba_conf).with_content('hostssl anotherfakedatabase anotherfakeuser fakehostname cert')
      end
    end
  end

  context 'when postgresql.conf changes' do
    before do
      allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).and_call_original
      allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).with('postgresql').and_return(true)
      allow_any_instance_of(OmnibusHelper).to receive(:service_dir_enabled?).and_call_original
      allow_any_instance_of(OmnibusHelper).to receive(:service_dir_enabled?).with('postgresql').and_return(true)
    end

    it 'notifies reload postgresql task' do
      expect(chef_run).to create_postgresql_config('gitlab')
      postgresql_config = chef_run.postgresql_config('gitlab')
      expect(postgresql_config).to notify('execute[reload postgresql]').to(:run).immediately
      expect(postgresql_config).to notify('execute[start postgresql]').to(:run).immediately
    end
  end

  context 'when enabling extensions' do
    it 'creates the pg_trgm extension when it is possible' do
      allow_any_instance_of(PgHelper).to receive(:extension_can_be_enabled?).with('pg_trgm', 'gitlabhq_production').and_return(true)
      expect(chef_run).to enable_postgresql_extension('pg_trgm')
    end

    it 'does not create the pg_trgm extension if it is not possible' do
      allow_any_instance_of(PgHelper).to receive(:extension_can_be_enabled?).with('pg_trgm', 'gitlabhq_production').and_return(false)
      expect(chef_run).not_to run_execute('enable pg_trgm extension')
    end

    context 'when on a secondary database node' do
      before do
        allow_any_instance_of(PgHelper).to receive(:is_standby?).and_return(true)
        allow_any_instance_of(PgHelper).to receive(:replica?).and_return(true)
      end

      it 'should not activate pg_trgm' do
        expect(chef_run).not_to run_execute('enable pg_trgm extension')
      end
    end

    it 'creates the btree_gist extension when it is possible' do
      allow_any_instance_of(PgHelper).to receive(:extension_can_be_enabled?).with('btree_gist', 'gitlabhq_production').and_return(true)
      expect(chef_run).to enable_postgresql_extension('btree_gist')
    end

    it 'does not create the btree_gist extension if it is not possible' do
      allow_any_instance_of(PgHelper).to receive(:extension_can_be_enabled?).with('btree_gist', 'gitlabhq_production').and_return(false)
      expect(chef_run).not_to run_execute('enable btree_gist extension')
    end
  end

  context 'when configuring postgresql_user resources' do
    before do
      stub_gitlab_rb(
        {
          postgresql: {
            sql_user_password: 'fakepassword',
            sql_replication_password: 'fakepassword'
          }
        }
      )
    end

    context 'when on the primary database node' do
      before do
        allow_any_instance_of(PgHelper).to receive(:is_standby?).and_return(false)
      end

      it 'should set a password for sql_user when sql_user_password is set' do
        expect(chef_run).to create_postgresql_user('gitlab').with(password: 'md5fakepassword')
      end

      it 'should create the gitlab_replicator user with replication permissions' do
        expect(chef_run).to create_postgresql_user('gitlab_replicator').with(
          options: %w(replication),
          password: 'md5fakepassword'
        )
      end
    end

    context 'when on a secondary database node' do
      before do
        allow_any_instance_of(PgHelper).to receive(:is_standby?).and_return(true)
        allow_any_instance_of(PgHelper).to receive(:replica?).and_return(true)
      end

      it 'should not create users' do
        expect(chef_run).not_to create_postgresql_user('gitlab')
        expect(chef_run).not_to create_postgresql_user('gitlab_replicator')
      end
    end
  end

  context 'log directory and runit group' do
    context 'default values' do
      it_behaves_like 'enabled logged service', 'postgresql', true, { log_directory_owner: 'gitlab-psql' }
    end

    context 'custom values' do
      before do
        stub_gitlab_rb(
          postgresql: {
            log_group: 'fugee'
          }
        )
      end
      it_behaves_like 'enabled logged service', 'postgresql', true, { log_directory_owner: 'gitlab-psql', log_group: 'fugee' }
    end
  end
end

RSpec.describe 'postgresql 16' do
  let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(runit_service postgresql_config)).converge('gitlab::default') }
  let(:postgresql_conf) { File.join(postgresql_data_dir, 'postgresql.conf') }
  let(:runtime_conf) { '/var/opt/gitlab/postgresql/data/runtime.conf' }

  before do
    allow_any_instance_of(PgHelper).to receive(:version).and_return(PGVersion.new('16.0'))
    allow_any_instance_of(PgHelper).to receive(:database_version).and_return(PGVersion.new('16.0'))
  end

  it 'configures wal_keep_size instead of wal_keep_segments' do
    expect(chef_run).to render_file(runtime_conf).with_content { |content|
      expect(content).to include("wal_keep_size")
      expect(content).not_to include("wal_keep_segments")
    }
  end
end

RSpec.describe 'postgres when version mismatches occur' do
  let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(runit_service postgresql_config)).converge('gitlab::default') }
  let(:postgresql_conf) { File.join(postgresql_data_dir, 'postgresql.conf') }
  let(:runtime_conf) { '/var/opt/gitlab/postgresql/data/runtime.conf' }

  context 'when data and binary versions differ' do
    before do
      allow_any_instance_of(PgHelper).to receive(:version).and_return(PGVersion.new('expectation'))
      allow_any_instance_of(PgHelper).to receive(:running_version).and_return(PGVersion.new('expectation'))
      allow_any_instance_of(PgHelper).to receive(:database_version).and_return(PGVersion.new('reality'))
      allow(File).to receive(:exists?).and_call_original
      allow(File).to receive(:exists?).with("/var/opt/gitlab/postgresql/data/PG_VERSION").and_return(true)
      allow(Dir).to receive(:glob).and_call_original
      allow(Dir).to receive(:glob).with("/opt/gitlab/embedded/postgresql/reality*").and_return(
        ['/opt/gitlab/embedded/postgresql/reality']
      )
      allow(Dir).to receive(:glob).with("/opt/gitlab/embedded/postgresql/reality/bin/*").and_return(
        %w(
          /opt/gitlab/embedded/postgresql/reality/bin/foo_one
          /opt/gitlab/embedded/postgresql/reality/bin/foo_two
          /opt/gitlab/embedded/postgresql/reality/bin/foo_three
        )
      )
    end

    it 'corrects symlinks to the correct location' do
      allow(FileUtils).to receive(:ln_sf).and_return(true)
      %w(foo_one foo_two foo_three).each do |pg_bin|
        expect(FileUtils).to receive(:ln_sf).with(
          "/opt/gitlab/embedded/postgresql/reality/bin/#{pg_bin}",
          "/opt/gitlab/embedded/bin/#{pg_bin}"
        )
      end
      chef_run.ruby_block('Link postgresql bin files to the correct version').block.call
    end

    it 'does not warn the user that a restart is needed by default' do
      allow_any_instance_of(PgHelper).to receive(:is_running?).and_return(true)
      expect(chef_run).not_to run_ruby_block('warn pending postgresql restart')
    end
  end

  context 'when running version and installed version differ' do
    before do
      allow(Gitlab).to receive(:[]).and_call_original
      allow_any_instance_of(PgHelper).to receive(:version).and_return(PGVersion.new('expectation'))
      allow_any_instance_of(PgHelper).to receive(:running_version).and_return(PGVersion.new('reality'))
    end

    context 'by defaul' do
      it 'does not warns the user that a restart is needed' do
        expect(chef_run).not_to run_ruby_block('warn pending postgresql restart')
      end
    end

    context 'when auto_restart_on_version_change is set to false' do
      before do
        stub_gitlab_rb(
          postgresql: {
            auto_restart_on_version_change: false
          }
        )
      end

      it 'warns the user that a restart is needed' do
        allow_any_instance_of(PgHelper).to receive(:is_running?).and_return(true)
        expect(chef_run).to run_ruby_block('warn pending postgresql restart')
      end

      it 'does not warns the user that a restart is needed when postgres is stopped' do
        expect(chef_run).not_to run_ruby_block('warn pending postgresql restart')
      end
    end
  end

  context 'when an older data version is present and no longer used' do
    before do
      allow(Gitlab).to receive(:[]).and_call_original
      allow_any_instance_of(PgHelper).to receive(:version).and_return(PGVersion.new('new_shiny'))
      allow_any_instance_of(PGVersion).to receive(:major).and_return('new_shiny')
      allow_any_instance_of(PgHelper).to receive(:running_version).and_return(PGVersion.new('new_shiny'))
      allow_any_instance_of(PgHelper).to receive(:database_version).and_return(PGVersion.new('ancient_history'))
      allow(File).to receive(:exists?).and_call_original
      allow(File).to receive(:exists?).with("/var/opt/gitlab/postgresql/data/PG_VERSION").and_return(true)
      allow(Dir).to receive(:glob).and_call_original
      allow(Dir).to receive(:glob).with("/opt/gitlab/embedded/postgresql/ancient_history*").and_return([])
      allow(Dir).to receive(:glob).with("/opt/gitlab/embedded/postgresql/new_shiny*").and_return(
        ['/opt/gitlab/embedded/postgresql/new_shiny']
      )
      allow(Dir).to receive(:glob).with("/opt/gitlab/embedded/postgresql/new_shiny/bin/*").and_return(
        %w(
          /opt/gitlab/embedded/postgresql/new_shiny/bin/foo_one
          /opt/gitlab/embedded/postgresql/new_shiny/bin/foo_two
          /opt/gitlab/embedded/postgresql/new_shiny/bin/foo_three
        )
      )
    end

    it 'corrects symlinks to the correct location' do
      allow(FileUtils).to receive(:ln_sf).and_return(true)
      %w(foo_one foo_two foo_three).each do |pg_bin|
        expect(FileUtils).to receive(:ln_sf).with(
          "/opt/gitlab/embedded/postgresql/new_shiny/bin/#{pg_bin}",
          "/opt/gitlab/embedded/bin/#{pg_bin}"
        )
      end
      chef_run.ruby_block('Link postgresql bin files to the correct version').block.call
    end
  end

  context 'when the expected postgres version is missing' do
    before do
      allow_any_instance_of(PgHelper).to receive(:database_version).and_return(PGVersion.new('how_it_started'))
      allow(File).to receive(:exists?).and_call_original
      allow(File).to receive(:exists?).with("/var/opt/gitlab/postgresql/data/PG_VERSION").and_return(true)
      allow(Dir).to receive(:glob).and_call_original
      allow(Dir).to receive(:glob).with("/opt/gitlab/embedded/postgresql/how_it_started*").and_return([])
      allow(Dir).to receive(:glob).with("/opt/gitlab/embedded/postgresql/how_it_is_going*").and_return([])
    end

    it 'throws an error' do
      expect do
        chef_run.ruby_block('Link postgresql bin files to the correct version').block.call
      end.to raise_error(RuntimeError, /Could not find PostgreSQL binaries/)
    end
  end
end

RSpec.describe 'postgresql::bin' do
  let(:chef_run) { ChefSpec::SoloRunner.converge('gitlab::default') }
  let(:gitlab_psql_rc) do
    <<-EOF
psql_user='gitlab-psql'
psql_group='gitlab-psql'
psql_host='/var/opt/gitlab/postgresql'
psql_port='5432'
    EOF
  end

  before do
    allow(Gitlab). to receive(:[]).and_call_original
  end

  context 'when bundled postgresql is disabled' do
    before do
      stub_gitlab_rb(
        postgresql: {
          enable: false
        }
      )

      allow(File).to receive(:exist?).and_call_original
      allow(File).to receive(:exist?).with('/var/opt/gitlab/postgresql/data/PG_VERSION').and_return(false)

      allow_any_instance_of(PgHelper).to receive(:database_version).and_return(nil)
      version = double("PgHelper", major: 10, minor: 9)
      allow_any_instance_of(PgHelper).to receive(:version).and_return(version)
    end

    it 'still includes the postgresql::bin recipe' do
      expect(chef_run).to include_recipe('postgresql::bin')
    end

    it 'includes postgresql::directory_locations' do
      expect(chef_run).to include_recipe('postgresql::directory_locations')
    end

    it 'creates gitlab-psql-rc' do
      expect(chef_run).to render_file('/opt/gitlab/etc/gitlab-psql-rc')
        .with_content(gitlab_psql_rc)
    end

    # We do expect the ruby block to run, but nothing to be found
    it "doesn't link any files by default" do
      expect(FileUtils).to_not receive(:ln_sf)
    end

    context "with postgresql['version'] set" do
      before do
        stub_gitlab_rb(
          postgresql: {
            enable: false,
            version: '999'
          }
        )
        allow(Dir).to receive(:glob).and_call_original
        allow(Dir).to receive(:glob).with("/opt/gitlab/embedded/postgresql/999*").and_return(
          %w(
            /opt/gitlab/embedded/postgresql/999
          )
        )
        allow(Dir).to receive(:glob).with("/opt/gitlab/embedded/postgresql/999/bin/*").and_return(
          %w(
            /opt/gitlab/embedded/postgresql/999/bin/foo_one
            /opt/gitlab/embedded/postgresql/999/bin/foo_two
            /opt/gitlab/embedded/postgresql/999/bin/foo_three
          )
        )
      end

      it "doesn't print a warning with a valid postgresql version" do
        expect(chef_run).to_not run_ruby_block('check_postgresql_version')
      end

      it 'links the specified version' do
        allow(FileUtils).to receive(:ln_sf).and_return(true)
        %w(foo_one foo_two foo_three).each do |pg_bin|
          expect(FileUtils).to receive(:ln_sf).with(
            "/opt/gitlab/embedded/postgresql/999/bin/#{pg_bin}",
            "/opt/gitlab/embedded/bin/#{pg_bin}"
          )
        end
        chef_run.ruby_block('Link postgresql bin files to the correct version').block.call
      end
    end

    context "with an invalid version in postgresql['version']" do
      before do
        stub_gitlab_rb(
          postgresql: {
            enable: false,
            version: '888'
          }
        )
        allow(Dir).to receive(:glob).and_call_original
        allow(Dir).to receive(:glob).with('/opt/gitlab/embedded/postgresql/888*').and_return([])
      end

      it 'should print a warning' do
        expect(chef_run).to run_ruby_block('check_postgresql_version')
      end
    end
  end
end

RSpec.describe 'default directories' do
  let(:chef_run) { ChefSpec::SoloRunner.converge('gitlab::default') }

  before do
    allow(Gitlab).to receive(:[]).and_call_original
  end

  context 'postgresql directory' do
    context 'with default settings' do
      it 'creates postgresql directory' do
        expect(chef_run).to create_directory('/var/opt/gitlab/postgresql').with(
          owner: 'gitlab-psql',
          group: 'gitlab-psql',
          mode: '2775',
          recursive: true
        )
      end
    end

    context 'with custom settings' do
      before do
        stub_gitlab_rb(
          postgresql: {
            dir: '/mypgdir',
            home: '/mypghomedir'
          })
      end

      it 'creates postgresql directory with custom path' do
        expect(chef_run).to create_directory('/mypgdir').with(
          owner: 'gitlab-psql',
          group: 'gitlab-psql',
          mode: '2775',
          recursive: true
        )
      end
    end
  end
end
