require 'chef_helper'

RSpec.describe 'gitlab::puma with Ubuntu 16.04' do
  let(:chef_run) do
    runner = ChefSpec::SoloRunner.new(
      step_into: %w(runit_service puma_config),
      path: 'spec/chef/fixtures/fauxhai/ubuntu/16.04.json'
    )
    runner.converge('gitlab::default')
  end

  before do
    allow(Gitlab).to receive(:[]).and_call_original
    stub_default_should_notify?(true)
    stub_should_notify?('puma', true)
  end

  context 'when puma is enabled' do
    it_behaves_like 'enabled runit service', 'puma', 'root', 'root'

    it 'creates runtime directories' do
      expect(chef_run).to create_directory('/opt/gitlab/var/puma').with(
        owner: 'git',
        group: nil,
        mode: '0700'
      )
      expect(chef_run).to create_directory('/var/opt/gitlab/gitlab-rails/sockets').with(
        owner: 'git',
        group: 'gitlab-www',
        mode: '0750'
      )
    end

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

      context 'custom values' do
        before do
          stub_gitlab_rb(
            puma: {
              log_group: 'fugee'
            }
          )
        end
        it_behaves_like 'configured logrotate service', 'puma', 'git', 'fugee'
        it_behaves_like 'enabled logged service', 'puma', true, { log_directory_owner: 'git', log_group: 'fugee' }
      end

      it_behaves_like 'a service with proper supervise directories', 'puma'
    end

    it 'renders the runit configuration with expected configuration' do
      expect(chef_run).to render_file('/opt/gitlab/sv/puma/run')
        .with_content { |content|
          expect(content).not_to match(/export prometheus_run_dir=\'\'/)
          expect(content).not_to match(/rm \/run\/gitlab\/puma/)
          expect(content).to match(/-u git:git/)
          expect(content).to match(/-U git:git/)
          expect(content).to match(/mkdir -p \/run\/gitlab\/puma/)
          expect(content).to match(/chmod 0700 \/run\/gitlab\/puma/)
          expect(content).to match(/chown git \/run\/gitlab\/puma/)
          expect(content).to match(/export prometheus_run_dir=\'\/run\/gitlab\/puma\'/)
          expect(content).to match(/rubyopt=\"-W:no-experimental\"/)
          expect(content).to include(%(RUBYOPT="${rubyopt}"))
          expect(content).to match(%r(/opt/gitlab/embedded/bin/bundle exec puma -C /var/opt/gitlab/gitlab-rails/etc/puma.rb))
        }
    end

    it 'renders the puma.rb file' do
      expect(chef_run).to create_puma_config('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with(
        tag: 'gitlab-puma-worker',
        rackup: 'config.ru',
        environment: 'production',
        pid: '/opt/gitlab/var/puma/puma.pid',
        state_path: '/opt/gitlab/var/puma/puma.state',
        listen_socket: '/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket',
        listen_tcp: '127.0.0.1:8080',
        working_directory: '/var/opt/gitlab/gitlab-rails/working',
        worker_processes: 2,
        min_threads: 4,
        max_threads: 4
      )
      expect(chef_run).to render_file('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with_content { |content|
        expect(content).to match(/lowlevel_error_handler/)
        expect(content).to include('require "/opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/puma/error_handler"')
      }
    end

    it 'creates sysctl files' do
      expect(chef_run).to create_gitlab_sysctl('net.core.somaxconn').with_value(2048)
    end
  end

  context 'with custom Puma settings' do
    before do
      stub_gitlab_rb(
        puma: {
          worker_timeout: 120,
          worker_processes: 4,
          min_threads: 5,
          max_threads: 10,
          listen: '10.0.0.1',
          port: 9000,
          socket: '/tmp/puma.socket',
          state_path: '/tmp/puma.state',
          per_worker_max_memory_mb: 1000
        }
      )
    end

    it 'renders the puma.rb file' do
      expect(chef_run).to create_puma_config('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with(
        state_path: '/tmp/puma.state',
        listen_socket: '/tmp/puma.socket',
        listen_tcp: '10.0.0.1:9000',
        worker_processes: 4,
        min_threads: 5,
        max_threads: 10,
        per_worker_max_memory_mb: 1000
      )
      expect(chef_run).to render_file("/opt/gitlab/sv/gitlab-workhorse/run").with_content { |content|
        expect(content).to match(%r(-authSocket /tmp/puma.socket))
      }
      expect(Gitlab['gitlab_workhorse']['auth_socket']).to eq('/tmp/puma.socket')
    end
  end

  context 'with SSL enabled' do
    let(:base_params) do
      {
        puma: {
          worker_timeout: 120,
          worker_processes: 4,
          min_threads: 5,
          max_threads: 10,
          socket: '/tmp/puma.socket',
          listen: '10.0.0.1',
          port: 9000,
          ssl_listen: '192.168.0.1',
          ssl_port: 9999,
          ssl_certificate: '/tmp/test.crt',
          ssl_certificate_key: '/tmp/test.key',
          ssl_key_password_command: 'echo mypassword'
        }
      }
    end
    let(:params) { base_params }

    before do
      stub_gitlab_rb(params)
    end

    it 'renders the puma.rb file' do
      expect(chef_run).to create_puma_config('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with(
        ssl_certificate: '/tmp/test.crt',
        ssl_certificate_key: '/tmp/test.key',
        ssl_key_password_command: 'echo mypassword',
        ssl_listen_host: '192.168.0.1',
        ssl_port: 9999,
        listen_socket: '/tmp/puma.socket',
        listen_tcp: '10.0.0.1:9000',
        worker_processes: 4,
        min_threads: 5,
        max_threads: 10
      )

      expect(chef_run).to render_file('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with_content { |content|
        expect(content).to match(/ssl_bind '192.168.0.1', 9999, {\n  cert: '\/tmp\/test.crt',\n  key: '\/tmp\/test.key',\n  key_password_command: 'echo mypassword',\n  verify_mode: 'none'\n}/m)
      }
    end

    it 'uses HTTPS instead of a UNIX socket for the Workhorse auth backend' do
      expect(chef_run).to render_file("/opt/gitlab/sv/gitlab-workhorse/run").with_content { |content|
        expect(content).to match(%r(-authBackend https://192.168.0.1:9999))
        expect(content).not_to match(%r(-authSocket /tmp/puma.socket))
      }
      expect(Gitlab['gitlab_workhorse']['auth_socket']).to be_nil
      expect(Gitlab['gitlab_workhorse']['auth_backend']).to eq('https://192.168.0.1:9999')
    end

    context 'with UNIX socket and HTTP disabled' do
      let(:params) do
        {
          puma: {
            worker_timeout: 120,
            worker_processes: 4,
            min_threads: 5,
            max_threads: 10,
            socket: '',
            listen: '',
            port: 9000,
            ssl_listen: '192.168.0.1',
            ssl_port: 9999,
            ssl_certificate: '/tmp/test.crt',
            ssl_certificate_key: '/tmp/test.key',
            ssl_key_password_command: nil
          }
        }
      end

      it 'omits the UNIX socket and TCP binds from the Puma config' do
        expect(chef_run).to create_puma_config('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with(
          ssl_certificate: '/tmp/test.crt',
          ssl_certificate_key: '/tmp/test.key',
          ssl_key_password_command: nil,
          ssl_listen_host: '192.168.0.1',
          ssl_port: 9999,
          listen_socket: '',
          listen_tcp: nil,
          worker_processes: 4,
          min_threads: 5,
          max_threads: 10
        )

        expect(chef_run).to render_file('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with_content { |content|
          expect(content).not_to match(%r(bind 'unix://))
          expect(content).not_to match(%r(bind 'tcp://))
          expect(content).to match(/ssl_bind '192.168.0.1', 9999, {\n  cert: '\/tmp\/test.crt',\n  key: '\/tmp\/test.key',\n  verify_mode: 'none'\n}/m)
        }
      end
    end

    context 'with other SSL options configured' do
      let(:filter) { '!aNULL:AES+SHA' }
      let(:client_cert) { '/tmp/client.crt' }

      let(:params) do
        base_params.tap do |config|
          config[:puma][:ssl_client_certificate] = client_cert
          config[:puma][:ssl_cipher_filter] = filter
          config[:puma][:ssl_key_password_command] = nil
          config[:puma][:ssl_verify_mode] = 'peer'
        end
      end

      it 'renders the puma.rb file' do
        expect(chef_run).to create_puma_config('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with(
          ssl_listen_host: '192.168.0.1',
          ssl_port: 9999,
          listen_socket: '/tmp/puma.socket',
          listen_tcp: '10.0.0.1:9000',
          ssl_client_certificate: client_cert,
          ssl_cipher_filter: filter,
          worker_processes: 4,
          min_threads: 5,
          max_threads: 10
        )

        expect(chef_run).to render_file('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with_content { |content|
          expect(content).to match(/ssl_bind '192.168.0.1', 9999, {\n  cert: '\/tmp\/test.crt',\n  key: '\/tmp\/test.key',\n  ca: '#{client_cert}',\n  ssl_cipher_filter: '#{Regexp.escape(filter)}',\n  verify_mode: 'peer'\n}/m)
        }
      end
    end
  end

  context 'with custom user and group' do
    before do
      stub_gitlab_rb(
        user: {
          username: 'foo',
          group: 'bar'
        }
      )
    end

    it_behaves_like 'enabled runit service', 'puma', 'root', 'root'
  end

  context 'with custom runtime_dir' do
    before do
      stub_gitlab_rb(
        runtime_dir: '/tmp/test-dir'
      )
    end

    it 'uses the user-specific runtime_dir' do
      expect(chef_run).to render_file('/opt/gitlab/sv/puma/run')
        .with_content { |content|
          expect(content).to match(%r(export prometheus_run_dir='/tmp/test-dir/gitlab/puma'))
          expect(content).to match(%r(mkdir -p /tmp/test-dir/gitlab/puma))
        }
    end
  end

  context 'with ActionCable enabled' do
    before do
      stub_gitlab_rb(
        actioncable: {
          worker_pool_size: 7
        }
      )
    end

    it 'renders the runit configuration with ActionCable environment variables' do
      expect(chef_run).to render_file('/opt/gitlab/sv/puma/run')
        .with_content { |content|
          expect(content).to match(/ACTION_CABLE_WORKER_POOL_SIZE=7/)
          expect(content).to match(%r(/opt/gitlab/embedded/bin/bundle exec puma -C /var/opt/gitlab/gitlab-rails/etc/puma.rb))
        }
    end
  end

  include_examples "consul service discovery", "puma", "rails"
end

RSpec.describe 'gitlab::puma Ubuntu 16.04 with no tmpfs' do
  let(:chef_run) do
    runner = ChefSpec::SoloRunner.new(
      path: 'spec/chef/fixtures/fauxhai/ubuntu/16.04-no-run-tmpfs.json',
      step_into: %w(runit_service)
    )
    runner.converge('gitlab::default')
  end

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

  context 'when puma is enabled on a node with no /run or /dev/shm tmpfs' do
    it_behaves_like 'enabled runit service', 'puma', 'root', 'root'

    it 'populates the files with expected configuration' do
      expect(chef_run).to render_file('/opt/gitlab/sv/puma/run')
        .with_content { |content|
          expect(content).to match(/export prometheus_run_dir=\'\'/)
          expect(content).not_to match(/mkdir -p \/run\/gitlab\/puma/)
        }
    end
  end
end

RSpec.describe 'gitlab::puma Ubuntu 16.04 Docker' do
  let(:chef_run) do
    runner = ChefSpec::SoloRunner.new(
      path: 'spec/chef/fixtures/fauxhai/ubuntu/16.04-docker.json',
      step_into: %w(runit_service)
    )
    runner.converge('gitlab::default')
  end

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

  context 'when puma is enabled on a node with a /dev/shm tmpfs' do
    it_behaves_like 'enabled runit service', 'puma', 'root', 'root'

    it 'populates the files with expected configuration' do
      expect(chef_run).to render_file('/opt/gitlab/sv/puma/run')
        .with_content { |content|
          expect(content).to match(/export prometheus_run_dir=\'\/dev\/shm\/gitlab\/puma\'/)
          expect(content).to match(/mkdir -p \/dev\/shm\/gitlab\/puma/)
        }
    end
  end
end

RSpec.describe 'gitlab::puma with more CPUs' do
  let(:chef_run) do
    runner = ChefSpec::SoloRunner.new(
      step_into: %w(runit_service),
      path: 'spec/chef/fixtures/fauxhai/ubuntu/16.04-more-cpus.json'
    )
    runner.converge('gitlab::default')
  end

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

  context 'when puma is enabled' do
    it 'renders the puma.rb file' do
      expect(chef_run).to create_puma_config('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with(
        environment: 'production',
        pid: '/opt/gitlab/var/puma/puma.pid',
        state_path: '/opt/gitlab/var/puma/puma.state',
        listen_socket: '/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket',
        listen_tcp: '127.0.0.1:8080',
        working_directory: '/var/opt/gitlab/gitlab-rails/working',
        worker_processes: 16
      )
    end
  end
end

RSpec.describe 'gitlab::puma with no total CPUs' do
  let(:chef_run) do
    runner = ChefSpec::SoloRunner.new(
      step_into: %w(runit_service),
      path: 'spec/chef/fixtures/fauxhai/ubuntu/16.04-no-total-cpus.json'
    )
    runner.converge('gitlab::default')
  end

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

  context 'when puma is enabled' do
    it 'renders the puma.rb file' do
      expect(chef_run).to create_puma_config('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with(
        environment: 'production',
        pid: '/opt/gitlab/var/puma/puma.pid',
        state_path: '/opt/gitlab/var/puma/puma.state',
        listen_socket: '/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket',
        listen_tcp: '127.0.0.1:8080',
        working_directory: '/var/opt/gitlab/gitlab-rails/working',
        worker_processes: 16
      )
    end
  end
end

RSpec.describe 'gitlab::puma with Raspberry Pi 4' do
  let(:chef_run) do
    runner = ChefSpec::SoloRunner.new(
      step_into: %w(runit_service),
      path: 'spec/chef/fixtures/fauxhai/ubuntu/22.04-rpi4.json'
    )
    runner.converge('gitlab::default')
  end

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

  context 'when puma is enabled' do
    it 'renders the puma.rb file' do
      expect(chef_run).to create_puma_config('/var/opt/gitlab/gitlab-rails/etc/puma.rb').with(
        environment: 'production',
        pid: '/opt/gitlab/var/puma/puma.pid',
        state_path: '/opt/gitlab/var/puma/puma.state',
        listen_socket: '/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket',
        listen_tcp: '127.0.0.1:8080',
        working_directory: '/var/opt/gitlab/gitlab-rails/working',
        worker_processes: 2
      )
    end
  end
end
