require 'chef_helper'

RSpec.describe 'geo postgresql' do
  let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(runit_service)).converge('gitlab-ee::default') }
  let(:postgresql_conf) { '/var/opt/gitlab/geo-postgresql/data/postgresql.conf' }
  let(:runtime_conf) { '/var/opt/gitlab/geo-postgresql/data/runtime.conf' }
  let(:pg_hba_conf) { '/var/opt/gitlab/geo-postgresql/data/pg_hba.conf' }

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

    # Workaround for Chef reloading instances across different examples
    allow_any_instance_of(GeoPgHelper).to receive(:bootstrapped?).and_return(true)
    allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).and_call_original
    allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).with('geo-postgresql').and_return(true)
  end

  context 'when geo postgres is enabled' do
    before do
      stub_gitlab_rb(geo_postgresql: { enable: true })
    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 the 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(GeoPgHelper).to receive(:is_running?).and_return(true)
      expect(chef_run).not_to run_ruby_block('warn pending geo-postgresql restart')
    end

    it 'notifies restarts postgresql when the postgresql runit run file changes' do
      allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).and_call_original
      allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).with('geo-postgresql').and_return(true)

      psql_service = chef_run.service('geo-postgresql')
      expect(psql_service).not_to subscribe_to('template[/opt/gitlab/sv/geo-postgresql/run]').on(:restart).delayed
    end

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

    it 'creates the gitlab_geo role in the geo-postgresql database, without a password' do
      expect(chef_run).to create_postgresql_user('gitlab_geo').with(password: nil)
    end

    it 'creates gitlabhq_geo_production database' do
      params = {
        owner: 'gitlab_geo'
      }
      expect(chef_run).to create_postgresql_database('gitlabhq_geo_production').with(params)
    end

    context 'with default settings' do
      it_behaves_like 'enabled runit service', 'geo-postgresql', 'root', 'root'

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

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

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

        it 'sets the max_replication_slots setting' do
          expect(chef_run.node['gitlab']['geo_postgresql']['max_replication_slots']).to eq(0)

          expect(chef_run).to render_file(
            postgresql_conf
          ).with_content(/max_replication_slots = 0/)
        end

        it 'sets the synchronous_commit setting' do
          expect(chef_run.node['gitlab']['geo_postgresql']['synchronous_standby_names']).to eq('')

          expect(chef_run).to render_file(
            postgresql_conf
          ).with_content(/synchronous_standby_names = ''/)
        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['gitlab']['geo_postgresql']['max_locks_per_transaction'])
            .to eq(128)

          expect(chef_run).to render_file(
            postgresql_conf
          ).with_content(/max_locks_per_transaction = 128/)
        end
      end
      context 'when rendering runtime.conf' do
        it 'correctly sets the log_line_prefix default setting' do
          expect(chef_run.node['gitlab']['geo_postgresql']['log_line_prefix']).to be_nil

          expect(chef_run).to render_file(runtime_conf)
            .with_content(/log_line_prefix = ''/)
        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 the synchronous_commit setting' do
          expect(chef_run.node['gitlab']['geo_postgresql']['synchronous_commit']).to eq('on')

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

        it 'sets the hot_standby_feedback setting' do
          expect(chef_run.node['gitlab']['geo_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['gitlab']['geo_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['gitlab']['geo_postgresql']['log_temp_files'])
            .to eq(-1)

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

        it 'sets the log_checkpoints setting' do
          expect(chef_run.node['gitlab']['geo_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['gitlab']['geo_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['gitlab']['geo_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['gitlab']['geo_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['gitlab']['geo_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['gitlab']['geo_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['gitlab']['geo_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['gitlab']['geo_postgresql']['deadlock_timeout'])
            .to eq('5s')

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

        it 'sets track_io_timing' do
          expect(chef_run.node['gitlab']['geo_postgresql']['track_io_timing'])
            .to eq('off')

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

    context 'with user specified settings' do
      before do
        stub_gitlab_rb(geo_postgresql: {
                         enable: true,
                         dynamic_shared_memory_type: 'none',
                         custom_pg_hba_entries: {
                           foo: [
                             type: 'host',
                             database: 'foo',
                             user: 'bar',
                             cidr: '127.0.0.1/32',
                             method: 'trust'
                           ]
                         },
                         sql_user_password: 'fakepasswordhash',
                         shared_preload_libraries: 'pg_stat_statements',
                         log_line_prefix: '%a',
                         max_standby_archive_delay: '60s',
                         max_standby_streaming_delay: '120s',
                         archive_mode: 'on',
                         archive_command: 'command',
                         archive_timeout: '120',
                       },
                       postgresql: {
                         username: 'foo',
                         group: 'bar'
                       }
                      )
      end

      it 'notifies geo-postgresql reload' do
        runtime_resource = chef_run.template(runtime_conf)
        expect(runtime_resource).to notify('execute[reload geo-postgresql]').to(:run).immediately
      end

      it 'creates the gitlab_geo role in the geo-postgresql database with the specified password' do
        expect(chef_run).to create_postgresql_user('gitlab_geo').with(password: 'md5fakepasswordhash')
      end

      context 'when rendering postgresql.conf' do
        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 'correctly sets the shared_preload_libraries setting' do
          expect(chef_run.node['gitlab']['geo_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 'sets archive settings' do
          expect(chef_run).to render_file(
            postgresql_conf
          ).with_content(/archive_mode = on/)
          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
      end

      context 'when rendering runtime.conf' do
        it 'correctly sets the log_line_prefix setting' do
          expect(chef_run.node['gitlab']['geo_postgresql']['log_line_prefix']).to eql('%a')

          expect(chef_run).to render_file(runtime_conf)
            .with_content(/log_line_prefix = '%a'/)
        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

        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 '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
        end
      end
    end

    context 'when geo postgres is disabled' do
      before do
        stub_gitlab_rb(geo_postgresql: { enable: false })
      end

      it_behaves_like 'disabled runit service', 'geo-postgresql'
    end
  end
end

RSpec.describe 'geo postgresql when version mismatches occur' do
  let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(runit_service)).converge('gitlab-ee::default') }
  let(:postgresql_conf) { '/var/opt/gitlab/geo-postgresql/data/postgresql.conf' }
  let(:runtime_conf) { '/var/opt/gitlab/geo-postgresql/data/runtime.conf' }

  before do
    allow(Gitlab).to receive(:[]).and_call_original
    stub_gitlab_rb(geo_postgresql: { enable: true })
  end

  context 'running version differs from installed version' do
    before do
      allow_any_instance_of(GeoPgHelper).to receive(:version).and_return(PGVersion.new('expectation'))
      allow_any_instance_of(GeoPgHelper).to receive(:running_version).and_return(PGVersion.new('reality'))
    end

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

    context 'when auto_restart_on_version_change is set to false' do
      before do
        stub_gitlab_rb(
          geo_postgresql: {
            enable: true,
            auto_restart_on_version_change: false
          }
        )
      end

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

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

  context 'running version differs from data version' do
    before do
      allow_any_instance_of(GeoPgHelper).to receive(:version).and_return(PGVersion.new('16'))
      allow_any_instance_of(GeoPgHelper).to receive(:running_version).and_return(PGVersion.new('16'))
      allow_any_instance_of(GeoPgHelper).to receive(:database_version).and_return(PGVersion.new('14'))
    end

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

    it 'creates the runit log/run file using chpst to run with the pinned postgres version' do
      allow_any_instance_of(GeoPgHelper).to receive(:pinned_postgresql_version).and_return(PGVersion.new('13'))

      expect(chef_run).to render_file("/opt/gitlab/sv/geo-postgresql/run").with_content { |content|
        expect(content).to match(/opt\/gitlab\/embedded\/postgresql\/13\/bin\/postgres/)
      }
    end

    it 'creates the runit log/run file using chpst to run with the real database version' do
      expect(chef_run).to render_file("/opt/gitlab/sv/geo-postgresql/run").with_content { |content|
        expect(content).to match(/opt\/gitlab\/embedded\/postgresql\/14\/bin\/postgres/)
      }
    end
  end

  context 'log directory and runit group' do
    context 'default values' do
      before do
        stub_gitlab_rb(geo_postgresql: { enable: true })
      end
      it_behaves_like 'enabled logged service', 'geo-postgresql', true, { log_directory_owner: 'gitlab-psql' }
    end

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