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('16.0'))
    allow_any_instance_of(PgHelper).to receive(:database_version).and_return(PGVersion.new('16.0'))
    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
    it_behaves_like 'a service with proper supervise directories', 'postgresql'

    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 'sets max_slot_wal_keep_size to unlimited' do
        expect(chef_run.node['postgresql']['max_slot_wal_keep_size'])
          .to eq(-1)
      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
    it_behaves_like 'a service with proper supervise directories', 'postgresql'

    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',
                         max_slot_wal_keep_size: '2GB'
                       })
      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

      it 'sets max_slot_wal_keep_size' do
        expect(chef_run).to render_file(
          runtime_conf
        ).with_content(/^max_slot_wal_keep_size = 2GB/)
      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 amcheck extension when it is possible' do
      allow_any_instance_of(PgHelper).to receive(:extension_can_be_enabled?).with('amcheck', 'gitlabhq_production').and_return(true)
      expect(chef_run).to enable_postgresql_extension('amcheck')
    end

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

    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 'registry database objects' do
    context 'when auto_create is true' do
      before do
        stub_gitlab_rb(
          postgresql: {
            registry: {
              auto_create: true
            }
          }
        )
      end

      it 'includes the registry::database_objects recipe' do
        expect(chef_run).to include_recipe('registry::database_objects')
      end
    end

    context 'when auto_create is false' do
      before do
        stub_gitlab_rb(
          postgresql: {
            registry: {
              auto_create: false
            }
          }
        )
      end

      it 'does not include the registry::database_objects recipe' do
        expect(chef_run).not_to include_recipe('registry::database_objects')
      end

      it 'shows a warning about the temporary config' do
        expect(chef_run).to run_ruby_block('warn registry auto_create is disabled')
      end
    end

    context 'when auto_create is not set (default)' do
      it 'does includes the registry::database_objects recipe' do
        expect(chef_run).to include_recipe('registry::database_objects')
      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 '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
