# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::OmniauthInitializer do
  let(:devise_config) { class_double(Devise) }

  subject(:initializer) { described_class.new(devise_config) }

  describe '.arguments_for' do
    let(:devise_config) { nil }

    let(:arguments) { initializer.send(:arguments_for, provider) }

    context 'when there are no args at all' do
      let(:provider) { { 'name' => 'unknown' } }

      it 'returns an empty array' do
        expect(arguments).to eq []
      end
    end

    context 'when there is an app_id and an app_secret' do
      let(:provider) { { 'name' => 'unknown', 'app_id' => 1, 'app_secret' => 2 } }

      it 'includes both of them, in positional order' do
        expect(arguments).to eq [1, 2]
      end
    end

    context 'when there is an app_id and an app_secret, and an array of args' do
      let(:provider) do
        {
          'name' =>  'unknown',
          'app_id' => 1,
          'app_secret' => 2,
          'args' => %w[one two three]
        }
      end

      it 'concatenates the args on the end' do
        expect(arguments).to eq [1, 2, 'one', 'two', 'three']
      end
    end

    context 'when there is an app_id and an app_secret, and an array of args, and default values' do
      let(:provider) do
        {
          'name' =>  'unknown',
          'app_id' => 1,
          'app_secret' => 2,
          'args' => %w[one two three]
        }
      end

      before do
        expect(described_class)
          .to receive(:default_arguments_for).with('unknown')
          .and_return({ default_arg: :some_value })
      end

      it 'concatenates the args on the end' do
        expect(arguments)
          .to eq [1, 2, 'one', 'two', 'three', { default_arg: :some_value }]
      end
    end

    context 'when there is an app_id and an app_secret, and a hash of args' do
      let(:provider) do
        {
          'name' =>  'unknown',
          'app_id' => 1,
          'app_secret' => 2,
          'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } }
        }
      end

      it 'concatenates the args on the end' do
        expect(arguments)
          .to eq [1, 2, { foo: 100, bar: 200, nested: { value: 300 } }]
      end
    end

    context 'when there is an app_id and an app_secret, and a hash of args, and default arguments' do
      let(:provider) do
        {
          'name' =>  'unknown',
          'app_id' => 1,
          'app_secret' => 2,
          'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } }
        }
      end

      before do
        expect(described_class)
          .to receive(:default_arguments_for).with('unknown')
          .and_return({ default_arg: :some_value })
      end

      it 'concatenates the args on the end' do
        expect(arguments)
          .to eq [1, 2, { default_arg: :some_value, foo: 100, bar: 200, nested: { value: 300 } }]
      end
    end

    context 'when there is an app_id and an app_secret, no args, and default values' do
      let(:provider) do
        {
          'name' =>  'unknown',
          'app_id' => 1,
          'app_secret' => 2
        }
      end

      before do
        expect(described_class)
          .to receive(:default_arguments_for).with('unknown')
          .and_return({ default_arg: :some_value })
      end

      it 'concatenates the args on the end' do
        expect(arguments)
          .to eq [1, 2, { default_arg: :some_value }]
      end
    end

    context 'when there are args, of an unsupported type' do
      let(:provider) do
        {
          'name' =>  'unknown',
          'args' => 1
        }
      end

      context 'when there are default arguments' do
        before do
          expect(described_class)
            .to receive(:default_arguments_for).with('unknown')
            .and_return({ default_arg: :some_value })
        end

        it 'tracks a configuration error' do
          expect(::Gitlab::ErrorTracking)
            .to receive(:track_and_raise_for_dev_exception)
            .with(described_class::ConfigurationError, provider_name: 'unknown', arguments_type: 'Integer')

          expect(arguments)
            .to eq [{ default_arg: :some_value }]
        end
      end

      context 'when there are no default arguments' do
        it 'tracks a configuration error' do
          expect(::Gitlab::ErrorTracking)
            .to receive(:track_and_raise_for_dev_exception)
            .with(described_class::ConfigurationError, provider_name: 'unknown', arguments_type: 'Integer')

          expect(arguments).to be_empty
        end
      end
    end
  end

  describe '#execute' do
    it 'configures providers from array' do
      generic_config = { 'name' => 'generic' }

      expect(devise_config).to receive(:omniauth).with(:generic)

      subject.execute([generic_config])
    end

    it 'allows "args" array for app_id and app_secret' do
      legacy_config = { 'name' => 'legacy', 'args' => %w(123 abc) }

      expect(devise_config).to receive(:omniauth).with(:legacy, '123', 'abc')

      subject.execute([legacy_config])
    end

    it 'passes app_id and app_secret as additional arguments' do
      twitter_config = { 'name' => 'twitter', 'app_id' => '123', 'app_secret' => 'abc' }

      expect(devise_config).to receive(:omniauth).with(:twitter, '123', 'abc')

      subject.execute([twitter_config])
    end

    it 'passes "args" hash as symbolized hash argument' do
      hash_config = { 'name' => 'hash', 'args' => { 'custom' => 'format' } }

      expect(devise_config).to receive(:omniauth).with(:hash, custom: 'format')

      subject.execute([hash_config])
    end

    it 'normalizes a String strategy_class' do
      hash_config = { 'name' => 'hash', 'args' => { strategy_class: 'OmniAuth::Strategies::OAuth2Generic' } }

      expect(devise_config).to receive(:omniauth).with(:hash, strategy_class: OmniAuth::Strategies::OAuth2Generic)

      subject.execute([hash_config])
    end

    it 'allows a class to be specified in strategy_class' do
      hash_config = { 'name' => 'hash', 'args' => { strategy_class: OmniAuth::Strategies::OAuth2Generic } }

      expect(devise_config).to receive(:omniauth).with(:hash, strategy_class: OmniAuth::Strategies::OAuth2Generic)

      subject.execute([hash_config])
    end

    it 'throws an error for an invalid strategy_class' do
      hash_config = { 'name' => 'hash', 'args' => { strategy_class: 'OmniAuth::Strategies::Bogus' } }

      expect { subject.execute([hash_config]) }.to raise_error(NameError)
    end

    it 'configures fail_with_empty_uid for shibboleth' do
      shibboleth_config = { 'name' => 'shibboleth', 'args' => {} }

      expect(devise_config).to receive(:omniauth).with(:shibboleth, fail_with_empty_uid: true)

      subject.execute([shibboleth_config])
    end

    it 'configures remote_sign_out_handler proc for authentiq' do
      authentiq_config = { 'name' => 'authentiq', 'args' => {} }

      expect(devise_config).to receive(:omniauth).with(:authentiq, remote_sign_out_handler: an_instance_of(Proc))

      subject.execute([authentiq_config])
    end

    it 'configures on_single_sign_out proc for cas3' do
      cas3_config = { 'name' => 'cas3', 'args' => {} }

      expect(devise_config).to receive(:omniauth).with(:cas3, on_single_sign_out: an_instance_of(Proc))

      subject.execute([cas3_config])
    end

    it 'configures defaults for google_oauth2' do
      google_config = {
        'name' => 'google_oauth2',
        "args" => { "access_type" => "offline", "approval_prompt" => '' }
      }

      expect(devise_config).to receive(:omniauth).with(
        :google_oauth2,
        access_type: "offline",
        approval_prompt: "",
        client_options: { connection_opts: { request: { timeout: Gitlab::OmniauthInitializer::OAUTH2_TIMEOUT_SECONDS } } }
      )

      subject.execute([google_config])
    end

    it 'configures defaults for gitlab' do
      conf = {
        'name' => 'gitlab',
        "args" => { 'client_options' => { 'site' => generate(:url) } }
      }

      expect(devise_config).to receive(:omniauth).with(
        :gitlab,
        client_options: { site: conf.dig('args', 'client_options', 'site') },
        authorize_params: { gl_auth_type: 'login' }
      )

      subject.execute([conf])
    end

    it 'configures defaults for gitlab, when arguments are not provided' do
      conf = { 'name' => 'gitlab' }

      expect(devise_config).to receive(:omniauth).with(
        :gitlab,
        authorize_params: { gl_auth_type: 'login' }
      )

      subject.execute([conf])
    end

    it 'configures defaults for gitlab, when array arguments are provided' do
      conf = { 'name' => 'gitlab', 'args' => ['a'] }

      expect(devise_config).to receive(:omniauth).with(
        :gitlab,
        'a',
        authorize_params: { gl_auth_type: 'login' }
      )

      subject.execute([conf])
    end

    it 'tracks a configuration error if the arguments are neither a hash nor an array' do
      conf = { 'name' => 'gitlab', 'args' => 17 }

      expect(::Gitlab::ErrorTracking)
        .to receive(:track_and_raise_for_dev_exception)
        .with(described_class::ConfigurationError, provider_name: 'gitlab', arguments_type: 'Integer')

      expect(devise_config).to receive(:omniauth).with(
        :gitlab,
        authorize_params: { gl_auth_type: 'login' }
      )

      subject.execute([conf])
    end
  end

  describe '.full_host' do
    subject { described_class.full_host.call({}) }

    let(:base_url) { 'http://localhost/test' }

    before do
      allow(Settings).to receive(:gitlab).and_return({ 'base_url' => base_url })
    end

    it { is_expected.to eq(base_url) }
  end
end