# frozen_string_literal: true

require 'spec_helper'
require 'json'
require 'omniauth-google-oauth2'
require 'stringio'

describe OmniAuth::Strategies::GoogleOauth2 do
  let(:request) { double('Request', params: {}, cookies: {}, env: {}) }
  let(:app) do
    lambda do
      [200, {}, ['Hello.']]
    end
  end

  subject do
    OmniAuth::Strategies::GoogleOauth2.new(app, 'appid', 'secret', @options || {}).tap do |strategy|
      allow(strategy).to receive(:request) do
        request
      end
    end
  end

  before do
    OmniAuth.config.test_mode = true
  end

  after do
    OmniAuth.config.test_mode = false
  end

  describe '#client_options' do
    it 'has correct site' do
      expect(subject.client.site).to eq('https://oauth2.googleapis.com')
    end

    it 'has correct authorize_url' do
      expect(subject.client.options[:authorize_url]).to eq('https://accounts.google.com/o/oauth2/auth')
    end

    it 'has correct token_url' do
      expect(subject.client.options[:token_url]).to eq('/token')
    end

    describe 'overrides' do
      context 'as strings' do
        it 'should allow overriding the site' do
          @options = { client_options: { 'site' => 'https://example.com' } }
          expect(subject.client.site).to eq('https://example.com')
        end

        it 'should allow overriding the authorize_url' do
          @options = { client_options: { 'authorize_url' => 'https://example.com' } }
          expect(subject.client.options[:authorize_url]).to eq('https://example.com')
        end

        it 'should allow overriding the token_url' do
          @options = { client_options: { 'token_url' => 'https://example.com' } }
          expect(subject.client.options[:token_url]).to eq('https://example.com')
        end
      end

      context 'as symbols' do
        it 'should allow overriding the site' do
          @options = { client_options: { site: 'https://example.com' } }
          expect(subject.client.site).to eq('https://example.com')
        end

        it 'should allow overriding the authorize_url' do
          @options = { client_options: { authorize_url: 'https://example.com' } }
          expect(subject.client.options[:authorize_url]).to eq('https://example.com')
        end

        it 'should allow overriding the token_url' do
          @options = { client_options: { token_url: 'https://example.com' } }
          expect(subject.client.options[:token_url]).to eq('https://example.com')
        end
      end
    end
  end

  describe '#authorize_options' do
    %i[access_type hd login_hint prompt scope state device_id device_name].each do |k|
      it "should support #{k}" do
        @options = { k => 'http://someval' }
        expect(subject.authorize_params[k.to_s]).to eq('http://someval')
      end
    end

    describe 'redirect_uri' do
      it 'should default to nil' do
        @options = {}
        expect(subject.authorize_params['redirect_uri']).to eq(nil)
      end

      it 'should set the redirect_uri parameter if present' do
        @options = { redirect_uri: 'https://example.com' }
        expect(subject.authorize_params['redirect_uri']).to eq('https://example.com')
      end
    end

    describe 'access_type' do
      it 'should default to "offline"' do
        @options = {}
        expect(subject.authorize_params['access_type']).to eq('offline')
      end

      it 'should set the access_type parameter if present' do
        @options = { access_type: 'online' }
        expect(subject.authorize_params['access_type']).to eq('online')
      end
    end

    describe 'hd' do
      it 'should default to nil' do
        expect(subject.authorize_params['hd']).to eq(nil)
      end

      it 'should set the hd (hosted domain) parameter if present' do
        @options = { hd: 'example.com' }
        expect(subject.authorize_params['hd']).to eq('example.com')
      end

      it 'should set the hd parameter and work with nil hd (gmail)' do
        @options = { hd: nil }
        expect(subject.authorize_params['hd']).to eq(nil)
      end

      it 'should set the hd parameter to * if set (only allows G Suite emails)' do
        @options = { hd: '*' }
        expect(subject.authorize_params['hd']).to eq('*')
      end
    end

    describe 'login_hint' do
      it 'should default to nil' do
        expect(subject.authorize_params['login_hint']).to eq(nil)
      end

      it 'should set the login_hint parameter if present' do
        @options = { login_hint: 'john@example.com' }
        expect(subject.authorize_params['login_hint']).to eq('john@example.com')
      end
    end

    describe 'prompt' do
      it 'should default to nil' do
        expect(subject.authorize_params['prompt']).to eq(nil)
      end

      it 'should set the prompt parameter if present' do
        @options = { prompt: 'consent select_account' }
        expect(subject.authorize_params['prompt']).to eq('consent select_account')
      end
    end

    describe 'request_visible_actions' do
      it 'should default to nil' do
        expect(subject.authorize_params['request_visible_actions']).to eq(nil)
      end

      it 'should set the request_visible_actions parameter if present' do
        @options = { request_visible_actions: 'something' }
        expect(subject.authorize_params['request_visible_actions']).to eq('something')
      end
    end

    describe 'include_granted_scopes' do
      it 'should default to nil' do
        expect(subject.authorize_params['include_granted_scopes']).to eq(nil)
      end

      it 'should set the include_granted_scopes parameter if present' do
        @options = { include_granted_scopes: 'true' }
        expect(subject.authorize_params['include_granted_scopes']).to eq('true')
      end
    end

    describe 'enable_granular_consent' do
      it 'should default to nil' do
        expect(subject.authorize_params['enable_granular_consent']).to eq(nil)
      end

      it 'should set the enable_granular_consent parameter if present' do
        @options = { enable_granular_consent: 'true' }
        expect(subject.authorize_params['enable_granular_consent']).to eq('true')
      end
    end

    describe 'scope' do
      it 'should expand scope shortcuts' do
        @options = { scope: 'calendar' }
        expect(subject.authorize_params['scope']).to eq('https://www.googleapis.com/auth/calendar')
      end

      it 'should leave base scopes as is' do
        @options = { scope: 'profile' }
        expect(subject.authorize_params['scope']).to eq('profile')
      end

      it 'should join scopes' do
        @options = { scope: 'profile,email' }
        expect(subject.authorize_params['scope']).to eq('profile email')
      end

      it 'should deal with whitespace when joining scopes' do
        @options = { scope: 'profile, email' }
        expect(subject.authorize_params['scope']).to eq('profile email')
      end

      it 'should set default scope to email,profile' do
        expect(subject.authorize_params['scope']).to eq('email profile')
      end

      it 'should support space delimited scopes' do
        @options = { scope: 'profile email' }
        expect(subject.authorize_params['scope']).to eq('profile email')
      end

      it 'should support extremely badly formed scopes' do
        @options = { scope: 'profile email,foo,steve yeah http://example.com' }
        expect(subject.authorize_params['scope']).to eq('profile email https://www.googleapis.com/auth/foo https://www.googleapis.com/auth/steve https://www.googleapis.com/auth/yeah http://example.com')
      end
    end

    describe 'state' do
      it 'should set the state parameter' do
        @options = { state: 'some_state' }
        expect(subject.authorize_params['state']).to eq('some_state')
        expect(subject.authorize_params[:state]).to eq('some_state')
        expect(subject.session['omniauth.state']).to eq('some_state')
      end

      it 'should set the omniauth.state dynamically' do
        allow(subject).to receive(:request) { double('Request', params: { 'state' => 'some_state' }, env: {}) }
        expect(subject.authorize_params['state']).to eq('some_state')
        expect(subject.authorize_params[:state]).to eq('some_state')
        expect(subject.session['omniauth.state']).to eq('some_state')
      end
    end

    describe 'overrides' do
      it 'should include top-level options that are marked as :authorize_options' do
        @options = { authorize_options: %i[scope foo request_visible_actions], scope: 'http://bar', foo: 'baz', hd: 'wow', request_visible_actions: 'something' }
        expect(subject.authorize_params['scope']).to eq('http://bar')
        expect(subject.authorize_params['foo']).to eq('baz')
        expect(subject.authorize_params['hd']).to eq(nil)
        expect(subject.authorize_params['request_visible_actions']).to eq('something')
      end

      describe 'request overrides' do
        %i[access_type hd login_hint prompt scope state].each do |k|
          context "authorize option #{k}" do
            let(:request) { double('Request', params: { k.to_s => 'http://example.com' }, cookies: {}, env: {}) }

            context 'when overridable_authorize_options is default' do
              it "should set the #{k} authorize option dynamically in the request" do
                @options = { k: '' }
                expect(subject.authorize_params[k.to_s]).to eq('http://example.com')
              end
            end

            context 'when overridable_authorize_options is empty' do
              it "should not set the #{k} authorize option dynamically in the request" do
                @options = { k: '', overridable_authorize_options: [] }
                expect(subject.authorize_params[k.to_s]).not_to eq('http://example.com')
              end
            end
          end
        end

        describe 'custom authorize_options' do
          let(:request) { double('Request', params: { 'foo' => 'something' }, cookies: {}, env: {}) }

          context 'when overridable_authorize_options is default' do
            it 'should not support request overrides from custom authorize_options' do
              @options = { authorize_options: [:foo], foo: '' }
              expect(subject.authorize_params['foo']).not_to eq('something')
            end
          end

          context 'when overridable_authorize_options is customized' do
            it 'should support request overrides from custom authorize_options' do
              @options = { authorize_options: [:foo], overridable_authorize_options: [:foo], foo: '' }
              expect(subject.authorize_params['foo']).to eq('something')
            end
          end
        end
      end
    end
  end

  describe '#authorize_params' do
    it 'should include any authorize params passed in the :authorize_params option' do
      @options = { authorize_params: { request_visible_actions: 'something', foo: 'bar', baz: 'zip' }, hd: 'wow', bad: 'not_included' }
      expect(subject.authorize_params['request_visible_actions']).to eq('something')
      expect(subject.authorize_params['foo']).to eq('bar')
      expect(subject.authorize_params['baz']).to eq('zip')
      expect(subject.authorize_params['hd']).to eq('wow')
      expect(subject.authorize_params['bad']).to eq(nil)
    end
  end

  describe '#token_params' do
    it 'should include any token params passed in the :token_params option' do
      @options = { token_params: { foo: 'bar', baz: 'zip' } }
      expect(subject.token_params['foo']).to eq('bar')
      expect(subject.token_params['baz']).to eq('zip')
    end
  end

  describe '#token_options' do
    it 'should include top-level options that are marked as :token_options' do
      @options = { token_options: %i[scope foo], scope: 'bar', foo: 'baz', bad: 'not_included' }
      expect(subject.token_params['scope']).to eq('bar')
      expect(subject.token_params['foo']).to eq('baz')
      expect(subject.token_params['bad']).to eq(nil)
    end
  end

  describe '#callback_url' do
    let(:base_url) { 'https://example.com' }

    it 'has the correct default callback path' do
      allow(subject).to receive(:full_host) { base_url }
      allow(subject).to receive(:script_name) { '' }
      expect(subject.send(:callback_url)).to eq("#{base_url}/auth/google_oauth2/callback")
    end

    it 'should set the callback path with script_name if present' do
      allow(subject).to receive(:full_host) { base_url }
      allow(subject).to receive(:script_name) { '/v1' }
      expect(subject.send(:callback_url)).to eq("#{base_url}/v1/auth/google_oauth2/callback")
    end

    it 'should set the callback_path parameter if present' do
      @options = { callback_path: '/auth/foo/callback' }
      allow(subject).to receive(:full_host) { base_url }
      allow(subject).to receive(:script_name) { '' }
      expect(subject.send(:callback_url)).to eq("#{base_url}/auth/foo/callback")
    end
  end

  describe '#info' do
    let(:client) do
      OAuth2::Client.new('abc', 'def') do |builder|
        builder.request :url_encoded
        builder.adapter :test do |stub|
          stub.get('/oauth2/v3/userinfo') { [200, { 'content-type' => 'application/json' }, response_hash.to_json] }
        end
      end
    end
    let(:access_token) { OAuth2::AccessToken.from_hash(client, { 'access_token' => 'a' }) }
    before { allow(subject).to receive(:access_token).and_return(access_token) }

    context 'with verified email' do
      let(:response_hash) do
        { email: 'something@domain.invalid', email_verified: true }
      end

      it 'should return equal email and unverified_email' do
        expect(subject.info[:email]).to eq('something@domain.invalid')
        expect(subject.info[:unverified_email]).to eq('something@domain.invalid')
      end
    end

    context 'with unverified email' do
      let(:response_hash) do
        { email: 'something@domain.invalid', email_verified: false }
      end

      it 'should return nil email, and correct unverified email' do
        expect(subject.info[:email]).to eq(nil)
        expect(subject.info[:unverified_email]).to eq('something@domain.invalid')
      end
    end
  end

  describe '#credentials' do
    let(:client) { OAuth2::Client.new('abc', 'def') }
    let(:access_token) { OAuth2::AccessToken.from_hash(client, access_token: 'valid_access_token', expires_at: 123_456_789, refresh_token: 'valid_refresh_token') }
    before(:each) do
      allow(subject).to receive(:access_token).and_return(access_token)
      subject.options.client_options[:connection_build] = proc do |builder|
        builder.request :url_encoded
        builder.adapter :test do |stub|
          stub.post('/oauth2/v3/tokeninfo', 'access_token=valid_access_token') do
            [200, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump(
              aud: '000000000000.apps.googleusercontent.com',
              sub: '123456789',
              scope: 'profile email'
            )]
          end
        end
      end
    end

    it 'should return access token and (optionally) refresh token' do
      expect(subject.credentials.to_h).to \
        match(hash_including(
                'token' => 'valid_access_token',
                'refresh_token' => 'valid_refresh_token',
                'scope' => 'profile email',
                'expires_at' => 123_456_789,
                'expires' => true
              ))
    end
  end

  describe '#extra' do
    let(:client) do
      OAuth2::Client.new('abc', 'def') do |builder|
        builder.request :url_encoded
        builder.adapter :test do |stub|
          stub.get('/oauth2/v3/userinfo') { [200, { 'content-type' => 'application/json' }, '{"sub": "12345"}'] }
        end
      end
    end
    before { allow(subject).to receive(:access_token).and_return(access_token) }

    describe 'id_token' do
      shared_examples 'id_token issued by valid issuer' do |issuer|
        context 'when the id_token is passed into the access token' do
          let(:token_info) do
            {
              'abc' => 'xyz',
              'exp' => Time.now.to_i + 3600,
              'nbf' => Time.now.to_i - 60,
              'iat' => Time.now.to_i,
              'aud' => 'appid',
              'iss' => issuer
            }
          end
          let(:id_token) { JWT.encode(token_info, 'secret') }
          let(:access_token) { OAuth2::AccessToken.from_hash(client, 'id_token' => id_token) }

          it 'should include id_token when set on the access_token' do
            expect(subject.extra).to include(id_token: id_token)
          end

          it 'should include id_info when id_token is set on the access_token and skip_jwt is false' do
            subject.options[:skip_jwt] = false
            expect(subject.extra).to include(id_info: token_info)
          end

          it 'should not include id_info when id_token is set on the access_token and skip_jwt is true' do
            subject.options[:skip_jwt] = true
            expect(subject.extra).not_to have_key(:id_info)
          end

          it 'should include id_info when id_token is set on the access_token by default' do
            expect(subject.extra).to include(id_info: token_info)
          end
        end
      end

      it_behaves_like 'id_token issued by valid issuer', 'accounts.google.com'
      it_behaves_like 'id_token issued by valid issuer', 'https://accounts.google.com'

      context 'when the id_token is issued by an invalid issuer' do
        let(:token_info) do
          {
            'abc' => 'xyz',
            'exp' => Time.now.to_i + 3600,
            'nbf' => Time.now.to_i - 60,
            'iat' => Time.now.to_i,
            'aud' => 'appid',
            'iss' => 'fake.google.com'
          }
        end
        let(:id_token) { JWT.encode(token_info, 'secret') }
        let(:access_token) { OAuth2::AccessToken.from_hash(client, 'id_token' => id_token) }

        it 'raises JWT::InvalidIssuerError' do
          expect { subject.extra }.to raise_error(JWT::InvalidIssuerError)
        end
      end

      context 'when the access token is empty or nil' do
        let(:access_token) { OAuth2::AccessToken.new(client, nil, { 'refresh_token' => 'foo' }) }
        before { allow(subject.extra).to receive(:access_token).and_return(access_token) }

        it 'should not include id_token' do
          expect(subject.extra).not_to have_key(:id_token)
        end

        it 'should not include id_info' do
          expect(subject.extra).not_to have_key(:id_info)
        end
      end
    end

    describe 'raw_info' do
      let(:token_info) do
        {
          'abc' => 'xyz',
          'exp' => Time.now.to_i + 3600,
          'nbf' => Time.now.to_i - 60,
          'iat' => Time.now.to_i,
          'aud' => 'appid',
          'iss' => 'accounts.google.com'
        }
      end
      let(:id_token) { JWT.encode(token_info, 'secret') }
      let(:access_token) { OAuth2::AccessToken.from_hash(client, 'id_token' => id_token) }

      context 'when skip_info is true' do
        before { subject.options[:skip_info] = true }

        it 'should not include raw_info' do
          expect(subject.extra).not_to have_key(:raw_info)
        end
      end

      context 'when skip_info is false' do
        before { subject.options[:skip_info] = false }

        it 'should include raw_info' do
          expect(subject.extra[:raw_info]).to eq('sub' => '12345')
        end
      end
    end
  end

  describe 'populate auth hash urls' do
    it 'should populate url map in auth hash if link present in raw_info' do
      allow(subject).to receive(:raw_info) { { 'name' => 'Foo', 'profile' => 'https://plus.google.com/123456' } }
      expect(subject.info[:urls][:google]).to eq('https://plus.google.com/123456')
    end

    it 'should not populate url map in auth hash if no link present in raw_info' do
      allow(subject).to receive(:raw_info) { { 'name' => 'Foo' } }
      expect(subject.info).not_to have_key(:urls)
    end
  end

  describe 'image options' do
    it 'should have no image if a picture is not present' do
      @options = { image_aspect_ratio: 'square' }
      allow(subject).to receive(:raw_info) { { 'name' => 'User Without Pic' } }
      expect(subject.info[:image]).to be_nil
    end

    describe 'when a picture is returned from google' do
      it 'should return the image with size specified in the `image_size` option' do
        @options = { image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50')
      end

      it 'should return the image with size specified in the `image_size` option when sizing is in the picture' do
        @options = { image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s96' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50')
      end

      it 'should return the image with size specified in the `image_size` option when sizing is in the picture and cropped' do
        @options = { image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s96-c' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50')
      end

      it 'should handle a picture with too many slashes' do
        @options = { image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a//ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50')
      end

      it 'should handle a picture with a size query parameter' do
        @options = { image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0?sz=96' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50')
      end

      it 'should handle a picture with a size query parameter and sizing is in the picture' do
        @options = { image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s96-c?sz=96' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50')
      end

      it 'should handle a picture with a size query parameter and other valid query parameters correctly' do
        @options = { image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0?sz=50&hello=true&life=42' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50?hello=true&life=42')
      end

      it 'should handle a picture with a size query parameter, other valid query parameters and sizing is in the picture correctly' do
        @options = { image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s96-c?sz=50&hello=true&life=42' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50?hello=true&life=42')
      end

      it 'should handle a picture with other valid query parameters correctly' do
        @options = { image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0?hello=true&life=42' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50?hello=true&life=42')
      end

      it 'should return the image with width and height specified in the `image_size` option' do
        @options = { image_size: { width: 50, height: 40 } }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=w50-h40')
      end

      it 'should return the image with width and height specified in the `image_size` option when sizing is in the picture' do
        @options = { image_size: { width: 50, height: 40 } }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=w100-h80-c' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=w50-h40')
      end

      it 'should return square image when square `image_aspect_ratio` is specified' do
        @options = { image_aspect_ratio: 'square' }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=c')
      end

      it 'should return square image when square `image_aspect_ratio` is specified and sizing is in the picture' do
        @options = { image_aspect_ratio: 'square' }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50-c' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=c')
      end

      it 'should return smart image when smart `image_aspect_ratio` is specified' do
        @options = { image_aspect_ratio: 'smart' }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=p')
      end

      it 'should return smart image when smart `image_aspect_ratio` is specified and sizing is in the picture' do
        @options = { image_aspect_ratio: 'smart' }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50-c' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=p')
      end

      it 'should return square sized image when square `image_aspect_ratio` and `image_size` is set' do
        @options = { image_aspect_ratio: 'square', image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50-c')
      end

      it 'should return square sized image when square `image_aspect_ratio` and `image_size` is set and sizing is in the picture' do
        @options = { image_aspect_ratio: 'square', image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s90' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50-c')
      end

      it 'should return smart sized image when smart `image_aspect_ratio` and `image_size` is set' do
        @options = { image_aspect_ratio: 'smart', image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50-p')
      end

      it 'should return smart sized image when smart `image_aspect_ratio` and `image_size` is set and sizing is in the picture' do
        @options = { image_aspect_ratio: 'smart', image_size: 50 }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s90' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=s50-p')
      end

      it 'should return square sized image when square `image_aspect_ratio` and `image_size` has height and width' do
        @options = { image_aspect_ratio: 'square', image_size: { width: 50, height: 40 } }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=w50-h40-c')
      end

      it 'should return square sized image when square `image_aspect_ratio` and `image_size` has height and width and sizing is in the picture' do
        @options = { image_aspect_ratio: 'square', image_size: { width: 50, height: 40 } }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=w100-h80-c' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=w50-h40-c')
      end

      it 'should return smart sized image when smart `image_aspect_ratio` and `image_size` has height and width' do
        @options = { image_aspect_ratio: 'smart', image_size: { width: 50, height: 40 } }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=w50-h40-p')
      end

      it 'should return smart sized image when smart `image_aspect_ratio` and `image_size` has height and width and sizing is in the picture' do
        @options = { image_aspect_ratio: 'smart', image_size: { width: 50, height: 40 } }
        allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=w100-h80-c' } }
        expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0=w50-h40-p')
      end
    end

    it 'should return original image if no options are provided' do
      allow(subject).to receive(:raw_info) { { 'picture' => 'https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0' } }
      expect(subject.info[:image]).to eq('https://lh3.googleusercontent.com/a/ACg8ocKN8F32STvmW-LG0Rl_9re5-Pv2cCn0ayodas6BQFPGEArMOtn0')
    end
  end

  describe 'build_access_token' do
    it 'should use a hybrid authorization request_uri if this is an AJAX request with a code parameter' do
      allow(request).to receive(:xhr?).and_return(true)
      allow(request).to receive(:params).and_return('code' => 'valid_code')

      client = double(:client)
      auth_code = double(:auth_code)
      allow(client).to receive(:auth_code).and_return(auth_code)
      expect(subject).to receive(:client).and_return(client)
      expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'postmessage' }, {})

      expect(subject).not_to receive(:orig_build_access_token)
      subject.build_access_token
    end

    it 'should use a hybrid authorization request_uri if this is an AJAX request (mobile) with a code parameter' do
      allow(request).to receive(:xhr?).and_return(true)
      allow(request).to receive(:params).and_return('code' => 'valid_code', 'redirect_uri' => '')

      client = double(:client)
      auth_code = double(:auth_code)
      allow(client).to receive(:auth_code).and_return(auth_code)
      expect(subject).to receive(:client).and_return(client)
      expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: '' }, {})

      expect(subject).not_to receive(:orig_build_access_token)
      subject.build_access_token
    end

    it 'should use the request_uri from params if this not an AJAX request (request from installed app) with a code parameter' do
      allow(request).to receive(:xhr?).and_return(false)
      allow(request).to receive(:params).and_return('code' => 'valid_code', 'redirect_uri' => 'redirect_uri')

      client = double(:client)
      auth_code = double(:auth_code)
      allow(client).to receive(:auth_code).and_return(auth_code)
      expect(subject).to receive(:client).and_return(client)
      expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'redirect_uri' }, {})

      expect(subject).not_to receive(:orig_build_access_token)
      subject.build_access_token
    end

    it 'should read access_token from hash if this is not an AJAX request with a code parameter' do
      client = OAuth2::Client.new('abc', 'def') do |builder|
        builder.request :url_encoded
        builder.adapter :test do |stub|
          stub.get('/oauth2/v3/userinfo') { [200, { 'content-type' => 'application/json' }, '{"sub": "12345"}'] }
        end
      end

      allow(request).to receive(:xhr?).and_return(false)
      allow(request).to receive(:params).and_return('access_token' => 'valid_access_token')
      expect(subject).to receive(:verify_token).with('valid_access_token').and_return true
      expect(subject).to receive(:client).and_return(client)

      token = subject.build_access_token
      expect(token).to be_instance_of(::OAuth2::AccessToken)
      expect(token.token).to eq('valid_access_token')
      expect(token.client).to eq(client)
    end

    it 'reads the code from a json request body' do
      body = StringIO.new(%({"code":"json_access_token"}))
      client = double(:client)
      auth_code = double(:auth_code)

      allow(request).to receive(:xhr?).and_return(false)
      allow(request).to receive(:content_type).and_return('application/json')
      allow(request).to receive(:body).and_return(body)
      allow(client).to receive(:auth_code).and_return(auth_code)
      expect(subject).to receive(:client).and_return(client)

      expect(auth_code).to receive(:get_token).with('json_access_token', { redirect_uri: 'postmessage' }, {})

      subject.build_access_token
    end

    it 'reads the redirect uri from a json request body' do
      body = StringIO.new(%({"code":"json_access_token", "redirect_uri":"sample"}))
      client = double(:client)
      auth_code = double(:auth_code)

      allow(request).to receive(:xhr?).and_return(false)
      allow(request).to receive(:content_type).and_return('application/json')
      allow(request).to receive(:body).and_return(body)
      allow(client).to receive(:auth_code).and_return(auth_code)
      expect(subject).to receive(:client).and_return(client)

      expect(auth_code).to receive(:get_token).with('json_access_token', { redirect_uri: 'sample' }, {})

      subject.build_access_token
    end

    it 'reads the access token from a json request body' do
      body = StringIO.new(%({"access_token":"valid_access_token"}))
      client = OAuth2::Client.new('abc', 'def') do |builder|
        builder.request :url_encoded
        builder.adapter :test do |stub|
          stub.get('/oauth2/v3/userinfo') { [200, { 'content-type' => 'application/json' }, '{"sub": "12345"}'] }
        end
      end

      allow(request).to receive(:xhr?).and_return(false)
      allow(request).to receive(:content_type).and_return('application/json')
      allow(request).to receive(:body).and_return(body)
      expect(subject).to receive(:client).and_return(client)

      expect(subject).to receive(:verify_token).with('valid_access_token').and_return true

      token = subject.build_access_token
      expect(token).to be_instance_of(::OAuth2::AccessToken)
      expect(token.token).to eq('valid_access_token')
      expect(token.client).to eq(client)
    end

    it 'should use callback_url without query_string if this is not an AJAX request' do
      allow(request).to receive(:xhr?).and_return(false)
      allow(request).to receive(:params).and_return('code' => 'valid_code')
      allow(request).to receive(:content_type).and_return('application/x-www-form-urlencoded')

      client = double(:client)
      auth_code = double(:auth_code)
      allow(client).to receive(:auth_code).and_return(auth_code)
      allow(subject).to receive(:callback_url).and_return('redirect_uri_without_query_string')

      expect(subject).to receive(:client).and_return(client)
      expect(auth_code).to receive(:get_token).with('valid_code', { redirect_uri: 'redirect_uri_without_query_string' }, {})
      subject.build_access_token
    end
  end

  describe 'verify_token' do
    before(:each) do
      subject.options.client_options[:connection_build] = proc do |builder|
        builder.request :url_encoded
        builder.adapter :test do |stub|
          stub.post('/oauth2/v3/tokeninfo', 'access_token=valid_access_token') do
            [200, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump(
              aud: '000000000000.apps.googleusercontent.com',
              sub: '123456789',
              email_verified: 'true',
              email: 'example@example.com',
              access_type: 'offline',
              scope: 'profile email',
              expires_in: 436
            )]
          end
          stub.post('/oauth2/v3/tokeninfo', 'access_token=invalid_access_token') do
            [400, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump(error_description: 'Invalid Value')]
          end
        end
      end
    end

    it 'should verify token if access_token is valid and app_id equals' do
      subject.options.client_id = '000000000000.apps.googleusercontent.com'
      expect(subject.send(:verify_token, 'valid_access_token')).to eq(true)
    end

    it 'should verify token if access_token is valid and app_id authorized' do
      subject.options.authorized_client_ids = ['000000000000.apps.googleusercontent.com']
      expect(subject.send(:verify_token, 'valid_access_token')).to eq(true)
    end

    it 'should not verify token if access_token is valid but app_id is false' do
      expect(subject.send(:verify_token, 'valid_access_token')).to eq(false)
    end

    it 'should raise error if access_token is invalid' do
      expect do
        subject.send(:verify_token, 'invalid_access_token')
      end.to raise_error(OAuth2::Error)
    end
  end

  describe 'verify_hd' do
    let(:client) do
      OAuth2::Client.new('abc', 'def') do |builder|
        builder.request :url_encoded
        builder.adapter :test do |stub|
          stub.get('/oauth2/v3/userinfo') do
            [200, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump(
              hd: 'example.com'
            )]
          end
        end
      end
    end
    let(:access_token) { OAuth2::AccessToken.from_hash(client, { 'access_token' => 'foo' }) }

    context 'when domain is nil' do
      let(:client) do
        OAuth2::Client.new('abc', 'def') do |builder|
          builder.request :url_encoded
          builder.adapter :test do |stub|
            stub.get('/oauth2/v3/userinfo') do
              [200, { 'Content-Type' => 'application/json; charset=UTF-8' }, JSON.dump({})]
            end
          end
        end
      end

      it 'should verify hd if options hd is set and correct' do
        subject.options.hd = nil
        expect(subject.send(:verify_hd, access_token)).to eq(true)
      end

      it 'should verify hd if options hd is set as an array and is correct' do
        subject.options.hd = ['example.com', 'example.co', nil]
        expect(subject.send(:verify_hd, access_token)).to eq(true)
      end

      it 'should raise an exception if nil is not included' do
        subject.options.hd = ['example.com', 'example.co']
        expect do
          subject.send(:verify_hd, access_token)
        end.to raise_error(OmniAuth::Strategies::OAuth2::CallbackError)
      end
    end

    it 'should verify hd if options hd is not set' do
      expect(subject.send(:verify_hd, access_token)).to eq(true)
    end

    it 'should verify hd if options hd is set and correct' do
      subject.options.hd = 'example.com'
      expect(subject.send(:verify_hd, access_token)).to eq(true)
    end

    it 'should verify hd if options hd is set as an array and is correct' do
      subject.options.hd = ['example.com', 'example.co', nil]
      expect(subject.send(:verify_hd, access_token)).to eq(true)
    end

    it 'should verify hd if options hd is set as an Proc and is correct' do
      subject.options.hd = proc { 'example.com' }
      expect(subject.send(:verify_hd, access_token)).to eq(true)
    end

    it 'should verify hd if options hd is set as an Proc returning an array and is correct' do
      subject.options.hd = proc { ['example.com', 'example.co'] }
      expect(subject.send(:verify_hd, access_token)).to eq(true)
    end

    it 'should raise error if options hd is set and wrong' do
      subject.options.hd = 'invalid.com'
      expect do
        subject.send(:verify_hd, access_token)
      end.to raise_error(OmniAuth::Strategies::GoogleOauth2::CallbackError)
    end

    it 'should raise error if options hd is set as an array and is not correct' do
      subject.options.hd = ['invalid.com', 'invalid.co']
      expect do
        subject.send(:verify_hd, access_token)
      end.to raise_error(OmniAuth::Strategies::GoogleOauth2::CallbackError)
    end
  end
end
