require 'spec_helper.rb'

describe Rack::OAuth2::Client do
  let(:client_id) { 'client_id' }
  let(:client_secret) { 'client_secret' }
  let :client do
    Rack::OAuth2::Client.new(
      identifier: client_id,
      secret: client_secret,
      host: 'server.example.com',
      redirect_uri: 'https://client.example.com/callback',
      revocation_endpoint: '/oauth2/revoke'
    )
  end
  subject { client }

  its(:identifier) { should == 'client_id' }
  its(:secret)     { should == 'client_secret' }
  its(:authorization_endpoint) { should == '/oauth2/authorize' }
  its(:token_endpoint)         { should == '/oauth2/token' }
  its(:revocation_endpoint)    { should == '/oauth2/revoke' }

  context 'when identifier is missing' do
    it do
      expect { Rack::OAuth2::Client.new }.to raise_error AttrRequired::AttrMissing
    end
  end

  describe '#authorization_uri' do
    subject { client.authorization_uri }
    it { should include 'https://server.example.com/oauth2/authorize' }
    it { should include 'client_id=client_id' }
    it { should include 'redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback' }
    it { should include 'response_type=code' }

    context 'when endpoints are absolute URIs' do
      before do
        client.authorization_endpoint = 'https://server2.example.com/oauth/authorize'
        client.token_endpoint = 'https://server2.example.com/oauth/token'
      end
      it { should include 'https://server2.example.com/oauth/authorize' }
    end

    context 'when scheme is specified' do
      before { client.scheme = 'http' }
      it { should include 'http://server.example.com/oauth2/authorize' }
    end

    context 'when response_type is token' do
      subject { client.authorization_uri(response_type: :token) }
      it { should include 'response_type=token' }
    end

    context 'when response_type is an Array' do
      subject { client.authorization_uri(response_type: [:token, :code]) }
      it { should include 'response_type=token%20code' }
    end

    context 'when scope is given' do
      subject { client.authorization_uri(scope: [:scope1, :scope2]) }
      it { should include 'scope=scope1%20scope2' }
    end
  end

  describe '#authorization_code=' do
    before  { client.authorization_code = 'code' }
    subject { client.instance_variable_get('@grant') }
    it { should be_instance_of Rack::OAuth2::Client::Grant::AuthorizationCode }
  end

  describe '#resource_owner_credentials=' do
    before  { client.resource_owner_credentials = 'username', 'password' }
    subject { client.instance_variable_get('@grant') }
    it { should be_instance_of Rack::OAuth2::Client::Grant::Password }
  end

  describe '#refresh_token=' do
    before  { client.refresh_token = 'refresh_token' }
    subject { client.instance_variable_get('@grant') }
    it { should be_instance_of Rack::OAuth2::Client::Grant::RefreshToken }
  end

  describe '#access_token!' do
    subject { client.access_token! }

    context '*args handling' do
      describe 'client authentication method' do
        before do
          client.authorization_code = 'code'
        end

        it 'should be Basic auth as default' do
          mock_response(
            :post,
            'https://server.example.com/oauth2/token',
            'tokens/bearer',
            request_header: {
              'Authorization' => 'Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ='
            }
          )
          client.access_token!
        end

        context 'when Basic auth method is used' do
          context 'when client_id is a url' do
            let(:client_id) { 'https://client.example.com'}

            it 'should be encoded in "application/x-www-form-urlencoded"' do
              mock_response(
                :post,
                'https://server.example.com/oauth2/token',
                'tokens/bearer',
                request_header: {
                  'Authorization' => 'Basic aHR0cHMlM0ElMkYlMkZjbGllbnQuZXhhbXBsZS5jb206Y2xpZW50X3NlY3JldA=='
                }
              )
              client.access_token!
            end
          end
        end

        context 'when basic_without_www_form_urlencode method is used' do
          context 'when client_id is a url' do
             let(:client_id) { 'https://client.example.com'}

             it 'should be encoded in "application/x-www-form-urlencoded"' do
               mock_response(
                 :post,
                 'https://server.example.com/oauth2/token',
                 'tokens/bearer',
                 request_header: {
                   'Authorization' => 'Basic aHR0cHM6Ly9jbGllbnQuZXhhbXBsZS5jb206Y2xpZW50X3NlY3JldA=='
                 }
               )
               client.access_token! :basic_without_www_form_urlencode
             end
           end
        end

        context 'when jwt_bearer auth method specified' do
          context 'when client_secret is given' do
            it 'should be JWT bearer client assertion w/ auto-generated HS256-signed JWT assertion' do
              mock_response(
                :post,
                'https://server.example.com/oauth2/token',
                'tokens/bearer',
                params: {
                  client_assertion: /^eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9\..+/, # NOTE: HS256
                  client_assertion_type: Rack::OAuth2::URN::ClientAssertionType::JWT_BEARER,
                  code: 'code',
                  grant_type: 'authorization_code',
                  redirect_uri: 'https://client.example.com/callback'
                }
              )
              client.access_token! :jwt_bearer
            end
          end

          context 'when private_key is given' do
            context 'when RSA key' do
              let :client do
                Rack::OAuth2::Client.new(
                  identifier: 'client_id',
                  private_key: OpenSSL::PKey::RSA.generate(2048),
                  host: 'server.example.com',
                  redirect_uri: 'https://client.example.com/callback'
                )
              end

              it 'should be JWT bearer client assertion w/ auto-generated RS256-signed JWT assertion' do
                mock_response(
                  :post,
                  'https://server.example.com/oauth2/token',
                  'tokens/bearer',
                  params: {
                    client_assertion: /^eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9\..+/, # NOTE: RS256
                    client_assertion_type: Rack::OAuth2::URN::ClientAssertionType::JWT_BEARER,
                    code: 'code',
                    grant_type: 'authorization_code',
                    redirect_uri: 'https://client.example.com/callback'
                  }
                )
                client.access_token! :jwt_bearer
              end
            end

            context 'when EC key' do
              let :client do
                Rack::OAuth2::Client.new(
                  identifier: 'client_id',
                  private_key: OpenSSL::PKey::EC.generate('prime256v1'),
                  host: 'server.example.com',
                  redirect_uri: 'https://client.example.com/callback'
                )
              end

              it 'should be JWT bearer client assertion w/ auto-generated ES256-signed JWT assertion' do
                mock_response(
                  :post,
                  'https://server.example.com/oauth2/token',
                  'tokens/bearer',
                  params: {
                    client_assertion: /^eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9\..+/, # NOTE: ES256
                    client_assertion_type: Rack::OAuth2::URN::ClientAssertionType::JWT_BEARER,
                    code: 'code',
                    grant_type: 'authorization_code',
                    redirect_uri: 'https://client.example.com/callback'
                  }
                )
                client.access_token! :jwt_bearer
              end
            end
          end

          context 'when client_assertion is explicitly given' do
            let :client do
              Rack::OAuth2::Client.new(
                identifier: 'client_id',
                host: 'server.example.com',
                redirect_uri: 'https://client.example.com/callback'
              )
            end

            it 'should be JWT bearer client assertion w/ specified assertion' do
              mock_response(
                :post,
                'https://server.example.com/oauth2/token',
                'tokens/bearer',
                params: {
                  client_assertion: 'any.jwt.assertion',
                  client_assertion_type: Rack::OAuth2::URN::ClientAssertionType::JWT_BEARER,
                  code: 'code',
                  grant_type: 'authorization_code',
                  redirect_uri: 'https://client.example.com/callback'
                }
              )
              client.access_token! :jwt_bearer, client_assertion: 'any.jwt.assertion'
            end
          end
        end

        context 'when other auth method specified' do
          it 'should be body params' do
            mock_response(
              :post,
              'https://server.example.com/oauth2/token',
              'tokens/bearer',
              params: {
                client_id: 'client_id',
                client_secret: 'client_secret',
                code: 'code',
                grant_type: 'authorization_code',
                redirect_uri: 'https://client.example.com/callback'
              }
            )
            client.access_token! :client_auth_body
          end
        end

        context 'when auth method is specified as Hash' do
          it 'should be removed before sending request' do
            mock_response(
              :post,
              'https://server.example.com/oauth2/token',
              'tokens/bearer',
              params: {
                client_id: 'client_id',
                client_secret: 'client_secret',
                code: 'code',
                grant_type: 'authorization_code',
                redirect_uri: 'https://client.example.com/callback'
              }
            )
            client.access_token! client_auth_method: :body
          end
        end
      end

      describe 'scopes' do
        context 'when scope option given' do
          it 'should specify given scope' do
            mock_response(
              :post,
              'https://server.example.com/oauth2/token',
              'tokens/bearer',
              params: {
                grant_type: 'client_credentials',
                scope: 'a b'
              }
            )
            client.access_token! scope: [:a, :b]
          end
        end
      end

      describe 'unknown params' do
        it 'should be included in body params' do
          mock_response(
            :post,
            'https://server.example.com/oauth2/token',
            'tokens/bearer',
            params: {
              grant_type: 'client_credentials',
              resource: 'something'
            }
          )
          client.access_token! resource: :something
        end
      end
    end

    context 'local_http_config handling' do
      it do
        mock_response(
          :post,
          'https://server.example.com/oauth2/token',
          'tokens/bearer',
          request_header: {
            'Authorization' => 'Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=',
            'X-Foo' => 'bar'
          }
        )
        client.access_token! do |request|
          request.headers['X-Foo'] = 'bar'
        end
      end
    end

    context 'when bearer token is given' do
      before do
        client.authorization_code = 'code'
        mock_response(
          :post,
          'https://server.example.com/oauth2/token',
          'tokens/bearer'
        )
      end
      it { should be_instance_of Rack::OAuth2::AccessToken::Bearer }
      its(:token_type) { should == :bearer }
      its(:access_token) { should == 'access_token' }
      its(:refresh_token) { should == 'refresh_token' }
      its(:expires_in) { should == 3600 }

      context 'when token type is "Bearer", not "bearer"' do
        before do
          client.authorization_code = 'code'
          mock_response(
            :post,
            'https://server.example.com/oauth2/token',
            'tokens/_Bearer'
          )
        end
        it { should be_instance_of Rack::OAuth2::AccessToken::Bearer }
        its(:token_type) { should == :bearer }
      end
    end

    context 'when unknown-type token is given' do
      before do
        client.authorization_code = 'code'
        mock_response(
          :post,
          'https://server.example.com/oauth2/token',
          'tokens/unknown'
        )
      end
      it do
        expect { client.access_token! }.to raise_error(StandardError, 'Unknown Token Type')
      end
    end

    context 'when error response is given' do
      before do
        mock_response(
          :post,
          'https://server.example.com/oauth2/token',
          'errors/invalid_request',
          status: 400
        )
      end
      it do
        expect { client.access_token! }.to raise_error Rack::OAuth2::Client::Error
      end
    end

    context 'when no body given' do
      context 'when error given' do
        before do
          mock_response(
            :post,
            'https://server.example.com/oauth2/token',
            'blank',
            format: 'txt',
            status: 400
          )
        end
        it do
          expect { client.access_token! }.to raise_error Rack::OAuth2::Client::Error
        end
      end
    end
  end

  describe '#revoke!' do
    context 'local_http_config handling' do
      it do
        mock_response(
          :post,
          'https://server.example.com/oauth2/revoke',
          'blank',
          format: 'txt',
          status: 200,
          body: {
            token: 'access_token',
            token_type_hint: 'access_token'
          },
          request_header: {
            'Authorization' => 'Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=',
            'X-Foo' => 'bar'
          }
        )
        client.revoke!(access_token: 'access_token') do |request|
          request.headers['X-Foo'] = 'bar'
        end
      end
    end

    context 'when access_token given' do
      before do
        mock_response(
          :post,
          'https://server.example.com/oauth2/revoke',
          'blank',
          format: 'txt',
          status: 200,
          body: {
            token: 'access_token',
            token_type_hint: 'access_token'
          }
        )
      end
      it do
        client.revoke!(access_token: 'access_token').should == :success
      end
    end

    context 'when refresh_token given' do
      before do
        mock_response(
          :post,
          'https://server.example.com/oauth2/revoke',
          'blank',
          format: 'txt',
          status: 200,
          body: {
            token: 'refresh_token',
            token_type_hint: 'refresh_token'
          }
        )
      end

      context 'as argument' do
        it do
          client.revoke!(refresh_token: 'refresh_token').should == :success
        end
      end

      context 'as grant' do
        it do
          client.refresh_token = 'refresh_token'
          client.revoke!
        end
      end
    end

    context 'when error response given' do
      before do
        mock_response(
          :post,
          'https://server.example.com/oauth2/revoke',
          'errors/invalid_request',
          status: 400
        )
      end

      it do
        expect do
          client.revoke! access_token: 'access_token'
        end.to raise_error Rack::OAuth2::Client::Error
      end
    end

    context 'when no token given' do
      it do
        expect do
          client.revoke!
        end.to raise_error ArgumentError
      end
    end
  end

  context 'when no host info' do
    let :client do
      Rack::OAuth2::Client.new(
        identifier: 'client_id',
        secret: 'client_secret',
        redirect_uri: 'https://client.example.com/callback',
        revocation_endpoint: '/oauth2/revoke'
      )
    end

    describe '#authorization_uri' do
      it do
        expect { client.authorization_uri }.to raise_error 'No Host Info'
      end
    end

    describe '#access_token!' do
      it do
        expect { client.access_token! }.to raise_error 'No Host Info'
      end
    end

    describe '#revoke!' do
      it do
        expect { client.revoke! access_token: 'access_token' }.to raise_error 'No Host Info'
      end
    end
  end
end
