require 'chef_helper'

RSpec.describe 'redis' do
  let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(redis_service runit_service)).converge('gitlab::default') }
  let(:redis_conf) { '/var/opt/gitlab/redis/redis.conf' }

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

  context 'by default' do
    let(:gitlab_redis_cli_rc) do
      <<-EOF
redis_dir='/var/opt/gitlab/redis'
redis_host=''
redis_port='0'
redis_tls_port=''
redis_tls_auth_clients='optional'
redis_tls_cacert_file='/opt/gitlab/embedded/ssl/certs/cacert.pem'
redis_tls_cacert_dir='/opt/gitlab/embedded/ssl/certs/'
redis_tls_cert_file=''
redis_tls_key_file=''
redis_socket='/var/opt/gitlab/redis/redis.socket'
      EOF
    end

    it 'enables the redis service' do
      expect(chef_run).to create_redis_service('redis')
    end

    it 'creates redis config with default values' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content { |content|
          expect(content).to match(/client-output-buffer-limit normal 0 0 0/)
          expect(content).to match(/client-output-buffer-limit replica 256mb 64mb 60/)
          expect(content).to match(/client-output-buffer-limit pubsub 32mb 8mb 60/)
          expect(content).to match(/^hz 10/)
          expect(content).to match(/^save 900 1/)
          expect(content).to match(/^save 300 10/)
          expect(content).to match(/^save 60 10000/)
          expect(content).to match(/^maxmemory 0/)
          expect(content).to match(/^maxmemory-policy noeviction/)
          expect(content).to match(/^maxmemory-samples 5/)
          expect(content).to match(/^tcp-backlog 511/)
          expect(content).to match(/^rename-command KEYS ""$/)
          expect(content).to match(/^lazyfree-lazy-eviction no$/)
          expect(content).to match(/^lazyfree-lazy-expire no$/)
          expect(content).to match(/^io-threads 1$/)
          expect(content).to match(/^io-threads-do-reads no$/)
          expect(content).to match(/^stop-writes-on-bgsave-error yes$/)
          expect(content).not_to match(/^replicaof/)
          expect(content).not_to match(/^tls-/)
        }
    end

    it 'creates redis user and group' do
      expect(chef_run).to create_account('user and group for redis').with(username: 'gitlab-redis', groupname: 'gitlab-redis')
    end

    it_behaves_like 'enabled runit service', 'redis', 'root', 'root'
    it_behaves_like 'a service with proper supervise directories', 'redis'

    it 'uses a 0 second startup delay' do
      expect(chef_run).to render_file('/opt/gitlab/sv/redis/run')
    .with_content { |content|
      expect(content).to match(/^sleep 0$/)
    }
    end

    it 'creates gitlab-redis-cli-rc' do
      expect(chef_run).to render_file('/opt/gitlab/etc/gitlab-redis-cli-rc')
        .with_content(gitlab_redis_cli_rc)
    end

    describe 'pending restart check' do
      context 'when running version is same as installed version' do
        before do
          allow_any_instance_of(RedisHelper::Server).to receive(:running_version).and_return('3.2.12')
          allow_any_instance_of(RedisHelper::Server).to receive(:installed_version).and_return('3.2.12')
        end

        it 'does not raise a warning' do
          expect(chef_run).not_to run_ruby_block('warn pending redis restart')
        end
      end

      context 'when running version is different than installed version' do
        before do
          allow_any_instance_of(RedisHelper::Server).to receive(:running_version).and_return('3.2.12')
          allow_any_instance_of(RedisHelper::Server).to receive(:installed_version).and_return('5.0.9')
        end

        it 'raises a warning' do
          expect(chef_run).to run_ruby_block('warn pending redis restart')
        end
      end
    end
  end

  context 'with user specified values' do
    before do
      stub_gitlab_rb(
        redis: {
          client_output_buffer_limit_normal: "5 5 5",
          client_output_buffer_limit_replica: "512mb 128mb 120",
          client_output_buffer_limit_pubsub: "64mb 16mb 120",
          save: ["10 15000"],
          maxmemory: "32gb",
          maxmemory_policy: "allkeys-url",
          maxmemory_samples: 10,
          tcp_backlog: 1024,
          hz: 100,
          username: 'foo',
          group: 'bar',
          rename_commands: {
            "FAKE_COMMAND" => "RENAMED_FAKE_COMMAND",
            "DISABLED_FAKE_COMMAND" => ""
          },
          'startup_delay': 10,
          open_files_ulimit: 60000
        }
      )
    end

    it 'creates redis config with custom values' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/client-output-buffer-limit normal 5 5 5/)
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/client-output-buffer-limit replica 512mb 128mb 120/)
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/client-output-buffer-limit pubsub 64mb 16mb 120/)
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/^save 10 15000/)
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/^maxmemory 32gb/)
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/^maxmemory-policy allkeys-url/)
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/^maxmemory-samples 10/)
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/^tcp-backlog 1024/)
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/^hz 100/)
    end

    it 'does not include the default renamed keys in redis.conf' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content { |content|
          expect(content).not_to match(/^rename-command KEYS ""$/)
          expect(content).to match(/^rename-command FAKE_COMMAND "RENAMED_FAKE_COMMAND"$/)
          expect(content).to match(/^rename-command DISABLED_FAKE_COMMAND ""$/)
        }
    end

    it 'creates redis user and group' do
      expect(chef_run).to create_account('user and group for redis').with(username: 'foo', groupname: 'bar')
    end

    it_behaves_like 'enabled runit service', 'redis', 'root', 'root'
    it_behaves_like 'a service with proper supervise directories', 'redis'

    it 'uses a 10 second startup delay' do
      expect(chef_run).to render_file('/opt/gitlab/sv/redis/run')
    .with_content { |content|
      expect(content).to match(/^sleep 10$/)
    }
    end

    it 'sets a filehandle limit' do
      expect(chef_run).to render_file('/opt/gitlab/sv/redis/run')
    .with_content { |content|
      expect(content).to match(/^ulimit -n 60000$/)
    }
    end
  end

  context 'with snapshotting disabled' do
    before do
      stub_gitlab_rb(
        redis: {
          save: []
        }
      )
    end
    it 'creates redis config without save setting' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
      expect(chef_run).not_to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/^save/)
    end
  end

  context 'with snapshotting cleared' do
    before do
      stub_gitlab_rb(
        redis: {
          save: [""]
        }
      )
    end
    it 'creates redis config without save setting' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/^save ""/)
    end
  end

  context 'with multiple bind addresses' do
    let(:redis_host) { '1.2.3.4 5.6.7.8' }
    let(:redis_port) { 6370 }
    let(:master_ip) { '10.0.0.0' }
    let(:master_port) { 6371 }

    let(:gitlab_redis_cli_rc) do
      <<-EOF
redis_dir='/var/opt/gitlab/redis'
redis_host='1.2.3.4'
redis_port='6370'
redis_tls_port=''
redis_tls_auth_clients='optional'
redis_tls_cacert_file='/opt/gitlab/embedded/ssl/certs/cacert.pem'
redis_tls_cacert_dir='/opt/gitlab/embedded/ssl/certs/'
redis_tls_cert_file=''
redis_tls_key_file=''
redis_socket=''
      EOF
    end

    before do
      stub_gitlab_rb(
        redis: {
          bind: redis_host,
          port: redis_port,
          master_ip: master_ip,
          master_port: master_port,
          master_password: 'password',
          master: false
        }
      )
    end

    it 'creates gitlab-redis-cli-rc' do
      expect(chef_run).to render_file('/opt/gitlab/etc/gitlab-redis-cli-rc')
        .with_content(gitlab_redis_cli_rc)
    end
  end

  context 'with a replica configured' do
    let(:redis_host) { '1.2.3.4' }
    let(:redis_port) { 6370 }
    let(:master_ip) { '10.0.0.0' }
    let(:master_port) { 6371 }

    let(:gitlab_redis_cli_rc) do
      <<-EOF
redis_dir='/var/opt/gitlab/redis'
redis_host='1.2.3.4'
redis_port='6370'
redis_tls_port=''
redis_tls_auth_clients='optional'
redis_tls_cacert_file='/opt/gitlab/embedded/ssl/certs/cacert.pem'
redis_tls_cacert_dir='/opt/gitlab/embedded/ssl/certs/'
redis_tls_cert_file=''
redis_tls_key_file=''
redis_socket=''
      EOF
    end

    before do
      stub_gitlab_rb(
        redis: {
          bind: redis_host,
          port: redis_port,
          master_ip: master_ip,
          master_port: master_port,
          master_password: 'password',
          master: false
        }
      )
    end

    it 'includes replicaof' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/^replicaof #{master_ip} #{master_port}/)
    end

    it 'creates gitlab-redis-cli-rc' do
      expect(chef_run).to render_file('/opt/gitlab/etc/gitlab-redis-cli-rc')
        .with_content(gitlab_redis_cli_rc)
    end
  end

  context 'in HA mode with Sentinels' do
    let(:redis_host) { '1.2.3.4' }
    let(:redis_port) { 6370 }
    let(:master_ip) { '10.0.0.0' }
    let(:master_port) { 6371 }

    let(:gitlab_redis_cli_rc) do
      <<-EOF
redis_dir='/var/opt/gitlab/redis'
redis_host='1.2.3.4'
redis_port='6370'
redis_tls_port=''
redis_tls_auth_clients='optional'
redis_tls_cacert_file='/opt/gitlab/embedded/ssl/certs/cacert.pem'
redis_tls_cacert_dir='/opt/gitlab/embedded/ssl/certs/'
redis_tls_cert_file=''
redis_tls_key_file=''
redis_socket=''
      EOF
    end

    before do
      stub_gitlab_rb(
        redis: {
          bind: redis_host,
          port: redis_port,
          ha: true,
          master_ip: master_ip,
          master_port: master_port,
          master_password: 'password',
          master: false
        }
      )
    end

    it 'omits replicaof' do
      expect(chef_run).not_to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content(/^replicaof/)
    end

    it_behaves_like 'started down runit service', 'redis'

    it 'creates gitlab-redis-cli-rc' do
      expect(chef_run).to render_file('/opt/gitlab/etc/gitlab-redis-cli-rc')
        .with_content(gitlab_redis_cli_rc)
    end

    context 'with Sentinels configured' do
      before do
        stub_gitlab_rb(
          redis: {
            bind: redis_host,
            port: redis_port,
            ha: true,
            master_ip: master_ip,
            master_port: master_port,
            master_password: 'password',
            master: false
          },
          gitlab_rails: {
            redis_sentinels: [
              { 'host' => '127.0.0.1', 'port' => 2637 }
            ]
          }
        )
      end

      it_behaves_like 'started down runit service', 'redis'

      it 'creates gitlab-redis-cli-rc' do
        expect(chef_run).to render_file('/opt/gitlab/etc/gitlab-redis-cli-rc')
          .with_content(gitlab_redis_cli_rc)
      end
    end
  end

  context 'with rename_commands disabled' do
    before do
      stub_gitlab_rb(
        redis: {
          rename_commands: {}
        }
      )
    end

    it 'should not rename any commands' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content { |content|
          expect(content).not_to match(/^rename-command/)
        }
    end
  end

  context 'with lazy eviction enabled' do
    before do
      stub_gitlab_rb(
        redis: {
          lazyfree_lazy_eviction: true
        }
      )
    end

    it 'creates redis config with lazyfree-lazy-eviction yes' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content { |content|
          expect(content).to match(/^lazyfree-lazy-eviction yes$/)
          expect(content).to match(/^lazyfree-lazy-expire no$/)
        }
    end
  end

  context 'with lazy eviction enabled' do
    before do
      stub_gitlab_rb(
        redis: {
          io_threads: 4,
          io_threads_do_reads: true
        }
      )
    end

    it 'creates redis config with lazyfree-lazy-eviction yes' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content { |content|
          expect(content).to match(/^io-threads 4$/)
          expect(content).to match(/^io-threads-do-reads yes$/)
        }
    end
  end

  context 'with stop writes on bgsave error disabled' do
    before do
      stub_gitlab_rb(
        redis: {
          stop_writes_on_bgsave_error: false
        }
      )
    end

    it 'creates redis config with stop-writes-on-bgsave-error no' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content { |content|
          expect(content).to match(/^stop-writes-on-bgsave-error no$/)
        }
    end
  end

  context 'with tls settings specified' do
    let(:gitlab_redis_cli_rc) do
      <<-EOF
redis_dir='/var/opt/gitlab/redis'
redis_host='127.0.0.1'
redis_port='0'
redis_tls_port='6380'
redis_tls_auth_clients='no'
redis_tls_cacert_file='/etc/gitlab/ssl/redis-ca.crt'
redis_tls_cacert_dir='/opt/gitlab/embedded/ssl/certs'
redis_tls_cert_file='/etc/gitlab/ssl/redis.crt'
redis_tls_key_file='/etc/gitlab/ssl/redis.key'
redis_socket=''
      EOF
    end

    before do
      stub_gitlab_rb(
        redis: {
          tls_port: 6380,
          tls_cert_file: '/etc/gitlab/ssl/redis.crt',
          tls_key_file: '/etc/gitlab/ssl/redis.key',
          tls_dh_params_file: '/etc/gitlab/ssl/redis-dhparams',
          tls_ca_cert_file: '/etc/gitlab/ssl/redis-ca.crt',
          tls_ca_cert_dir: '/opt/gitlab/embedded/ssl/certs',
          tls_auth_clients: 'no',
          tls_replication: 'yes',
          tls_cluster: 'yes',
          tls_protocols: 'TLSv1.2 TLSv1.3',
          tls_ciphers: 'DEFAULT:!MEDIUM',
          tls_ciphersuites: 'TLS_CHACHA20_POLY1305_SHA256',
          tls_prefer_server_ciphers: 'yes',
          tls_session_caching: 'no',
          tls_session_cache_size: 10000,
          tls_session_cache_timeout: 120
        }
      )
    end

    it 'renders redis config with tls settings' do
      expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
        .with_content { |content|
          expect(content).to match(%r{^tls-port 6380$})
          expect(content).to match(%r{^tls-cert-file /etc/gitlab/ssl/redis.crt$})
          expect(content).to match(%r{^tls-key-file /etc/gitlab/ssl/redis.key$})
          expect(content).to match(%r{^tls-dh-params-file /etc/gitlab/ssl/redis-dhparams$})
          expect(content).to match(%r{^tls-ca-cert-file /etc/gitlab/ssl/redis-ca.crt$})
          expect(content).to match(%r{^tls-ca-cert-dir /opt/gitlab/embedded/ssl/certs$})
          expect(content).to match(%r{^tls-auth-clients no$})
          expect(content).to match(%r{^tls-replication yes$})
          expect(content).to match(%r{^tls-cluster yes$})
          expect(content).to match(%r{^tls-protocols "TLSv1.2 TLSv1.3"$})
          expect(content).to match(%r{^tls-ciphers DEFAULT:!MEDIUM$})
          expect(content).to match(%r{^tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256$})
          expect(content).to match(%r{^tls-prefer-server-ciphers yes$})
          expect(content).to match(%r{^tls-session-caching no$})
          expect(content).to match(%r{^tls-session-cache-size 10000$})
          expect(content).to match(%r{^tls-session-cache-timeout 120$})
        }
    end

    it 'creates gitlab-redis-cli-rc' do
      expect(chef_run).to render_file('/opt/gitlab/etc/gitlab-redis-cli-rc')
        .with_content(gitlab_redis_cli_rc)
    end
  end

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

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

  context 'extra config command' do
    context 'when extra_config_command points to a file that does not exist' do
      before do
        stub_gitlab_rb(
          redis: {
            extra_config_command: '/tmp/a-file-that-does-not-exist'
          }
        )
      end

      it 'raises error' do
        expect { chef_run }.to raise_error(Redis::CommandExecutionError).with_message("Redis: Execution of `/tmp/a-file-that-does-not-exist` failed. File does not exist.")
      end
    end

    context 'extra_config_command exits with a non-zero code' do
      before do
        stub_gitlab_rb(
          redis: {
            extra_config_command: 'bash /tmp/a-file-that-does-not-exist'
          }
        )
      end

      it 'raises error' do
        expect { chef_run }.to raise_error(Redis::CommandExecutionError).with_message(/Redis.*exit code 127.*No such file or directory/)
      end
    end

    context 'extra_config_command runs successfully' do
      let(:code_file) { Tempfile.new(['redis-code', '.sh']) }

      before do
        file_content = <<~MSG
          #!/usr/bin/env bash
          echo 'requirepass "toomanysecrets"'
        MSG

        File.write(code_file.path, file_content)

        stub_gitlab_rb(
          redis: {
            extra_config_command: "bash #{code_file.path}"
          }
        )
      end

      after do
        code_file.close
        code_file.unlink
      end

      it 'populates redis.conf with output from command' do
        expect(chef_run).to render_file('/var/opt/gitlab/redis/redis.conf')
          .with_content { |content|
            expect(content).to match(/requirepass "toomanysecrets"/)
          }
      end
    end
  end

  context 'with redis disabled' do
    before do
      stub_gitlab_rb(redis: { enable: false })
    end

    it_behaves_like 'disabled runit service', 'redis', 'root', 'root'
  end
end
