require "minitest/autorun"
require "testutil"
require "openid/consumer/associationmanager"
require "openid/association"
require "openid/dh"
require "openid/util"
require "openid/cryptutil"
require "openid/message"
require "openid/protocolerror"
require "openid/store/memory"
require "util"
require "time"

module OpenID
  class DHAssocSessionTest < Minitest::Test
    def test_sha1_get_request
      # Initialized without an explicit DH gets defaults
      sess = Consumer::DiffieHellmanSHA1Session.new
      assert_equal(['dh_consumer_public'], sess.get_request.keys)
      Util::from_base64(sess.get_request['dh_consumer_public'])
    end

    def test_sha1_get_request_custom_dh
      dh = DiffieHellman.new(1299721, 2)
      sess = Consumer::DiffieHellmanSHA1Session.new(dh)
      req = sess.get_request
      assert_equal(['dh_consumer_public', 'dh_modulus', 'dh_gen'].sort,
                   req.keys.sort)
      assert_equal(dh.modulus, CryptUtil.base64_to_num(req['dh_modulus']))
      assert_equal(dh.generator, CryptUtil.base64_to_num(req['dh_gen']))
      Util::from_base64(req['dh_consumer_public'])
    end
  end

  module TestDiffieHellmanResponseParametersMixin
    def setup
      session_cls = self.class.session_cls

      # Pre-compute DH with small prime so tests run quickly.
      @server_dh = DiffieHellman.new(100389557, 2)
      @consumer_dh = DiffieHellman.new(100389557, 2)

      # base64(btwoc(g ^ xb mod p))
      @dh_server_public = CryptUtil.num_to_base64(@server_dh.public)

      @secret = CryptUtil.random_string(session_cls.secret_size)

      enc_mac_key_unencoded =
        @server_dh.xor_secret(session_cls.hashfunc,
                              @consumer_dh.public,
                              @secret)

      @enc_mac_key = Util.to_base64(enc_mac_key_unencoded)

      @consumer_session = session_cls.new(@consumer_dh)

      @msg = Message.new(self.class.message_namespace)
    end

    def test_extract_secret
      @msg.set_arg(OPENID_NS, 'dh_server_public', @dh_server_public)
      @msg.set_arg(OPENID_NS, 'enc_mac_key', @enc_mac_key)

      extracted = @consumer_session.extract_secret(@msg)
      assert_equal(extracted, @secret)
    end

    def test_absent_serve_public
      @msg.set_arg(OPENID_NS, 'enc_mac_key', @enc_mac_key)

      assert_raises(Message::KeyNotFound) {
        @consumer_session.extract_secret(@msg)
      }
    end

    def test_absent_mac_key
      @msg.set_arg(OPENID_NS, 'dh_server_public', @dh_server_public)

      assert_raises(Message::KeyNotFound) {
        @consumer_session.extract_secret(@msg)
      }
    end

    def test_invalid_base64_public
      @msg.set_arg(OPENID_NS, 'dh_server_public', 'n o t b a s e 6 4.')
      @msg.set_arg(OPENID_NS, 'enc_mac_key', @enc_mac_key)

      assert_raises(ArgumentError) {
        @consumer_session.extract_secret(@msg)
      }
    end

    def test_invalid_base64_mac_key
      @msg.set_arg(OPENID_NS, 'dh_server_public', @dh_server_public)
      @msg.set_arg(OPENID_NS, 'enc_mac_key', 'n o t base 64')

      assert_raises(ArgumentError) {
        @consumer_session.extract_secret(@msg)
      }
    end
  end

  class TestConsumerOpenID1DHSHA1 < Minitest::Test
    include TestDiffieHellmanResponseParametersMixin
    class << self
      attr_reader :session_cls, :message_namespace
    end

    @session_cls = Consumer::DiffieHellmanSHA1Session
    @message_namespace = OPENID1_NS
  end

  class TestConsumerOpenID2DHSHA1 < Minitest::Test
    include TestDiffieHellmanResponseParametersMixin
    class << self
      attr_reader :session_cls, :message_namespace
    end

    @session_cls = Consumer::DiffieHellmanSHA1Session
    @message_namespace = OPENID2_NS
  end

  class TestConsumerOpenID2DHSHA256 < Minitest::Test
    include TestDiffieHellmanResponseParametersMixin
    class << self
      attr_reader :session_cls, :message_namespace
    end

    @session_cls = Consumer::DiffieHellmanSHA256Session
    @message_namespace = OPENID2_NS
  end

  class TestConsumerNoEncryptionSession < Minitest::Test
    def setup
      @sess = Consumer::NoEncryptionSession.new
    end

    def test_empty_request
      assert_equal(@sess.get_request, {})
    end

    def test_get_secret
      secret = 'shhh!' * 4
      mac_key = Util.to_base64(secret)
      msg = Message.from_openid_args({'mac_key' => mac_key})
      assert_equal(secret, @sess.extract_secret(msg))
    end
  end

  class TestCreateAssociationRequest < Minitest::Test
    def setup
      @server_url = 'http://invalid/'
      @assoc_manager = Consumer::AssociationManager.new(nil, @server_url)
      class << @assoc_manager
        def compatibility_mode=(val)
            @compatibility_mode = val
        end
      end
      @assoc_type = 'HMAC-SHA1'
    end

    def test_no_encryption_sends_type
      session_type = 'no-encryption'
      session, args = @assoc_manager.send(:create_associate_request,
                                          @assoc_type,
                                          session_type)

      assert(session.is_a?(Consumer::NoEncryptionSession))
      expected = Message.from_openid_args(
            {'ns' => OPENID2_NS,
             'session_type' => session_type,
             'mode' => 'associate',
             'assoc_type' => @assoc_type,
             })

      assert_equal(expected, args)
    end

    def test_no_encryption_compatibility
      @assoc_manager.compatibility_mode = true
      session_type = 'no-encryption'
      session, args = @assoc_manager.send(:create_associate_request,
                                          @assoc_type,
                                          session_type)

      assert(session.is_a?(Consumer::NoEncryptionSession))
      assert_equal(Message.from_openid_args({'mode' => 'associate',
                                              'assoc_type' => @assoc_type,
                                            }), args)
    end

    def test_dh_sha1_compatibility
      @assoc_manager.compatibility_mode = true
      session_type = 'DH-SHA1'
      session, args = @assoc_manager.send(:create_associate_request,
                                          @assoc_type,
                                          session_type)


      assert(session.is_a?(Consumer::DiffieHellmanSHA1Session))

      # This is a random base-64 value, so just check that it's
      # present.
      refute_nil(args.get_arg(OPENID1_NS, 'dh_consumer_public'))
      args.del_arg(OPENID1_NS, 'dh_consumer_public')

      # OK, session_type is set here and not for no-encryption
      # compatibility
      expected = Message.from_openid_args({'mode' => 'associate',
                                            'session_type' => 'DH-SHA1',
                                            'assoc_type' => @assoc_type,
                                          })
      assert_equal(expected, args)
    end
  end

  class TestAssociationManagerExpiresIn < Minitest::Test
    def expires_in_msg(val)
      msg = Message.from_openid_args({'expires_in' => val})
      Consumer::AssociationManager.extract_expires_in(msg)
    end

    def test_parse_fail
      ['',
       '-2',
       ' 1',
       ' ',
       '0x00',
       'foosball',
       '1\n',
       '100,000,000,000',
      ].each do |x|
        assert_raises(ProtocolError) {expires_in_msg(x)}
      end
    end

    def test_parse
      ['0',
       '1',
       '1000',
       '9999999',
       '01',
      ].each do |n|
        assert_equal(n.to_i, expires_in_msg(n))
      end
    end
  end

  class TestAssociationManagerCreateSession < Minitest::Test
    def test_invalid
      assert_raises(ArgumentError) {
        Consumer::AssociationManager.create_session('monkeys')
      }
    end

    def test_sha256
      sess = Consumer::AssociationManager.create_session('DH-SHA256')
      assert(sess.is_a?(Consumer::DiffieHellmanSHA256Session))
    end
  end

  module NegotiationTestMixin
    include TestUtil
    def mk_message(args)
      args['ns'] = @openid_ns
      Message.from_openid_args(args)
    end

    def call_negotiate(responses, negotiator=nil)
      store = nil
      compat = self.class::Compat
      assoc_manager = Consumer::AssociationManager.new(store, @server_url,
                                                       compat, negotiator)
      class << assoc_manager
        attr_accessor :responses

        def request_association(assoc_type, session_type)
          m = @responses.shift
          if m.is_a?(Message)
            raise ServerError.from_message(m)
          else
            return m
          end
        end
      end
      assoc_manager.responses = responses
      assoc_manager.negotiate_association
    end
  end

  # Test the session type negotiation behavior of an OpenID 2
  # consumer.
  class TestOpenID2SessionNegotiation < Minitest::Test
    include NegotiationTestMixin

    Compat = false

    def setup
      @server_url = 'http://invalid/'
      @openid_ns = OPENID2_NS
    end

    # Test the case where the response to an associate request is a
    # server error or is otherwise undecipherable.
    def test_bad_response
      assert_log_matches('Server error when requesting an association') {
        assert_nil(call_negotiate([mk_message({})]))
      }
    end

    # Test the case where the association type (assoc_type) returned
    # in an unsupported-type response is absent.
    def test_empty_assoc_type
      msg = mk_message({'error' => 'Unsupported type',
                         'error_code' => 'unsupported-type',
                         'session_type' => 'new-session-type',
                       })

      assert_log_matches('Unsupported association type',
                         "Server #{@server_url} responded with unsupported "\
                         "association session but did not supply a fallback."
                         ) {
        assert_nil(call_negotiate([msg]))
      }

    end

    # Test the case where the session type (session_type) returned
    # in an unsupported-type response is absent.
    def test_empty_session_type
      msg = mk_message({'error' => 'Unsupported type',
                         'error_code' => 'unsupported-type',
                         'assoc_type' => 'new-assoc-type',
                       })

      assert_log_matches('Unsupported association type',
                         "Server #{@server_url} responded with unsupported "\
                         "association session but did not supply a fallback."
                         ) {
        assert_nil(call_negotiate([msg]))
      }
    end

    # Test the case where an unsupported-type response specifies a
    # preferred (assoc_type, session_type) combination that is not
    # allowed by the consumer's SessionNegotiator.
    def test_not_allowed
      negotiator = AssociationNegotiator.new([])
      negotiator.instance_eval{
        @allowed_types = [['assoc_bogus', 'session_bogus']]
      }
      msg = mk_message({'error' => 'Unsupported type',
                         'error_code' => 'unsupported-type',
                         'assoc_type' => 'not-allowed',
                         'session_type' => 'not-allowed',
                       })

      assert_log_matches('Unsupported association type',
                         'Server sent unsupported session/association type:') {
        assert_nil(call_negotiate([msg], negotiator))
      }
    end

    # Test the case where an unsupported-type response triggers a
    # retry to get an association with the new preferred type.
    def test_unsupported_with_retry
      msg = mk_message({'error' => 'Unsupported type',
                         'error_code' => 'unsupported-type',
                         'assoc_type' => 'HMAC-SHA1',
                         'session_type' => 'DH-SHA1',
                       })

      assoc = Association.new('handle', 'secret', Time.now, 10000, 'HMAC-SHA1')

      assert_log_matches('Unsupported association type') {
        assert_equal(assoc, call_negotiate([msg, assoc]))
      }
    end

    # Test the case where an unsupported-typ response triggers a
    # retry, but the retry fails and nil is returned instead.
    def test_unsupported_with_retry_and_fail
      msg = mk_message({'error' => 'Unsupported type',
                         'error_code' => 'unsupported-type',
                         'assoc_type' => 'HMAC-SHA1',
                         'session_type' => 'DH-SHA1',
                       })

      assert_log_matches('Unsupported association type',
                         "Server #{@server_url} refused") {
        assert_nil(call_negotiate([msg, msg]))
      }
    end

    # Test the valid case, wherein an association is returned on the
    # first attempt to get one.
    def test_valid
      assoc = Association.new('handle', 'secret', Time.now, 10000, 'HMAC-SHA1')

      assert_log_matches() {
        assert_equal(call_negotiate([assoc]), assoc)
      }
    end
  end


  # Tests for the OpenID 1 consumer association session behavior.  See
  # the docs for TestOpenID2SessionNegotiation.  Notice that this
  # class is not a subclass of the OpenID 2 tests.  Instead, it uses
  # many of the same inputs but inspects the log messages logged with
  # oidutil.log.  See the calls to self.failUnlessLogMatches.  Some of
  # these tests pass openid2-style messages to the openid 1
  # association processing logic to be sure it ignores the extra data.
  class TestOpenID1SessionNegotiation < Minitest::Test
    include NegotiationTestMixin

    Compat = true

    def setup
      @server_url = 'http://invalid/'
      @openid_ns = OPENID1_NS
    end

    def test_bad_response
      assert_log_matches('Server error when requesting an association') {
        response = call_negotiate([mk_message({})])
        assert_nil(response)
      }
    end

    def test_empty_assoc_type
      msg = mk_message({'error' => 'Unsupported type',
                         'error_code' => 'unsupported-type',
                         'session_type' => 'new-session-type',
                       })

      assert_log_matches('Server error when requesting an association') {
        response = call_negotiate([msg])
        assert_nil(response)
      }
    end

    def test_empty_session_type
      msg = mk_message({'error' => 'Unsupported type',
                         'error_code' => 'unsupported-type',
                         'assoc_type' => 'new-assoc-type',
                       })

      assert_log_matches('Server error when requesting an association') {
        response = call_negotiate([msg])
        assert_nil(response)
      }
    end

    def test_not_allowed
      negotiator = AssociationNegotiator.new([])
      negotiator.instance_eval{
        @allowed_types = [['assoc_bogus', 'session_bogus']]
      }

      msg = mk_message({'error' => 'Unsupported type',
                         'error_code' => 'unsupported-type',
                         'assoc_type' => 'not-allowed',
                         'session_type' => 'not-allowed',
                       })

      assert_log_matches('Server error when requesting an association') {
        response = call_negotiate([msg])
        assert_nil(response)
      }
    end

    def test_unsupported_with_retry
      msg = mk_message({'error' => 'Unsupported type',
                         'error_code' => 'unsupported-type',
                         'assoc_type' => 'HMAC-SHA1',
                         'session_type' => 'DH-SHA1',
                       })

      assoc = Association.new('handle', 'secret', Time.now, 10000, 'HMAC-SHA1')


      assert_log_matches('Server error when requesting an association') {
        response = call_negotiate([msg, assoc])
        assert_nil(response)
      }
    end

    def test_valid
      assoc = Association.new('handle', 'secret', Time.now, 10000, 'HMAC-SHA1')
      assert_log_matches() {
        response = call_negotiate([assoc])
        assert_equal(assoc, response)
      }
    end
  end


  class TestExtractAssociation < Minitest::Test
    include ProtocolErrorMixin

    # An OpenID associate response (without the namespace)
    DEFAULTS = {
      'expires_in' => '1000',
      'assoc_handle' => 'a handle',
      'assoc_type' => 'a type',
      'session_type' => 'a session type',
    }

    def setup
      @assoc_manager = Consumer::AssociationManager.new(nil, nil)
    end

    # Make tests that ensure that an association response that is
    # missing required fields will raise an Message::KeyNotFound.
    #
    # According to 'Association Session Response' subsection 'Common
    # Response Parameters', the following fields are required for
    # OpenID 2.0:
    #
    #  * ns
    #  * session_type
    #  * assoc_handle
    #  * assoc_type
    #  * expires_in
    #
    # In OpenID 1, everything except 'session_type' and 'ns' are
    # required.
    MISSING_FIELD_SETS = ([["no_fields", []]] +
                          (DEFAULTS.keys.map do |f|
                             fields = DEFAULTS.keys
                             fields.delete(f)
                             ["missing_#{f}", fields]
                           end)
                          )

    [OPENID1_NS, OPENID2_NS].each do |ns|
      MISSING_FIELD_SETS.each do |name, fields|
        # OpenID 1 is allowed to be missing session_type
        if ns != OPENID1_NS and name != 'missing_session_type'
          test = lambda do
            msg = Message.new(ns)
            fields.each do |field|
              msg.set_arg(ns, field, DEFAULTS[field])
            end
            assert_raises(Message::KeyNotFound) do
              @assoc_manager.send(:extract_association, msg, nil)
            end
          end
          define_method("test_#{name}", test)
        end
      end
    end

    # assert that extracting a response that contains the given
    # response session type when the request was made for the given
    # request session type will raise a ProtocolError indicating
    # session type mismatch
    def assert_session_mismatch(req_type, resp_type, ns)
      # Create an association session that has "req_type" as its
      # session_type and no allowed_assoc_types
      assoc_session_class = Class.new do
        @session_type = req_type
        def self.session_type
          @session_type
        end
        def self.allowed_assoc_types
          []
        end
      end
      assoc_session = assoc_session_class.new

      # Build an OpenID 1 or 2 association response message that has
      # the specified association session type
      msg = Message.new(ns)
      msg.update_args(ns, DEFAULTS)
      msg.set_arg(ns, 'session_type', resp_type)

      # The request type and response type have been chosen to produce
      # a session type mismatch.
      assert_protocol_error('Session type mismatch') {
        @assoc_manager.send(:extract_association, msg, assoc_session)
      }
    end

    [['no-encryption', '', OPENID2_NS],
     ['DH-SHA1', 'no-encryption', OPENID2_NS],
     ['DH-SHA256', 'no-encryption', OPENID2_NS],
     ['no-encryption', 'DH-SHA1', OPENID2_NS],
     ['DH-SHA1', 'DH-SHA256', OPENID1_NS],
     ['DH-SHA256', 'DH-SHA1', OPENID1_NS],
     ['no-encryption', 'DH-SHA1', OPENID1_NS],
    ].each do |req_type, resp_type, ns|
      test = lambda { assert_session_mismatch(req_type, resp_type, ns) }
      name = "test_mismatch_req_#{req_type}_resp_#{resp_type}_#{ns}"
      define_method(name, test)
    end

    def test_openid1_no_encryption_fallback
      # A DH-SHA1 session
      assoc_session = Consumer::DiffieHellmanSHA1Session.new

      # An OpenID 1 no-encryption association response
      msg = Message.from_openid_args({
                                       'expires_in' => '1000',
                                       'assoc_handle' => 'a handle',
                                       'assoc_type' => 'HMAC-SHA1',
                                       'mac_key' => 'X' * 20,
                                     })

      # Should succeed
      assoc = @assoc_manager.send(:extract_association, msg, assoc_session)
      assert_equal('a handle', assoc.handle)
      assert_equal('HMAC-SHA1', assoc.assoc_type)
      assert(assoc.expires_in.between?(999, 1000))
      assert('X' * 20, assoc.secret)
    end
  end

  class GetOpenIDSessionTypeTest < Minitest::Test
    include TestUtil

    SERVER_URL = 'http://invalid/'

    def do_test(expected_session_type, session_type_value)
      # Create a Message with just 'session_type' in it, since
      # that's all this function will use. 'session_type' may be
      # absent if it's set to None.
      args = {}
      if !session_type_value.nil?
        args['session_type'] = session_type_value
      end
      message = Message.from_openid_args(args)
      assert(message.is_openid1)

      assoc_manager = Consumer::AssociationManager.new(nil, SERVER_URL)
      actual_session_type = assoc_manager.send(:get_openid1_session_type,
                                               message)
      error_message = ("Returned session type parameter #{session_type_value}"\
                       "was expected to yield session type "\
                       "#{expected_session_type}, but yielded "\
                       "#{actual_session_type}")
      assert_equal(expected_session_type, actual_session_type, error_message)
    end


    [['nil', 'no-encryption', nil],
     ['empty', 'no-encryption', ''],
     ['dh_sha1', 'DH-SHA1', 'DH-SHA1'],
     ['dh_sha256', 'DH-SHA256', 'DH-SHA256'],
    ].each {|name, expected, input|
      # Define a test method that will check what session type will be
      # used if the OpenID 1 response to an associate call sets the
      # 'session_type' field to `session_type_value`
      test = lambda {assert_log_matches() { do_test(expected, input) } }
      define_method("test_#{name}", &test)
    }

    # This one's different because it expects log messages
    def test_explicit_no_encryption
      assert_log_matches("WARNING: #{SERVER_URL} sent 'no-encryption'"){
        do_test('no-encryption', 'no-encryption')
      }
    end
  end

  class ExtractAssociationTest < Minitest::Test
    include ProtocolErrorMixin

    SERVER_URL = 'http://invalid/'

    def setup
      @session_type = 'testing-session'

      # This must something that works for Association::from_expires_in
      @assoc_type = 'HMAC-SHA1'

      @assoc_handle = 'testing-assoc-handle'

      # These arguments should all be valid
      @assoc_response =
        Message.from_openid_args({
                                   'expires_in' => '1000',
                                   'assoc_handle' => @assoc_handle,
                                   'assoc_type' => @assoc_type,
                                   'session_type' => @session_type,
                                   'ns' => OPENID2_NS,
                                 })
      assoc_session_cls = Class.new do
        class << self
          attr_accessor :allowed_assoc_types, :session_type
        end

        attr_reader :extract_secret_called, :secret
        def initialize
          @extract_secret_called = false
          @secret = 'shhhhh!'
        end

        def extract_secret(_)
          @extract_secret_called = true
          @secret
        end
      end
      @assoc_session = assoc_session_cls.new
      @assoc_session.class.allowed_assoc_types = [@assoc_type]
      @assoc_session.class.session_type = @session_type

      @assoc_manager = Consumer::AssociationManager.new(nil, SERVER_URL)
    end

    def call_extract
      @assoc_manager.send(:extract_association,
                          @assoc_response, @assoc_session)
    end

    # Handle a full successful association response
    def test_works_with_good_fields
      assoc = call_extract
      assert(@assoc_session.extract_secret_called)
      assert_equal(@assoc_session.secret, assoc.secret)
      assert_equal(1000, assoc.lifetime)
      assert_equal(@assoc_handle, assoc.handle)
      assert_equal(@assoc_type, assoc.assoc_type)
    end

    def test_bad_assoc_type
      # Make sure that the assoc type in the response is not valid
      # for the given session.
      @assoc_session.class.allowed_assoc_types = []
      assert_protocol_error('Unsupported assoc_type for sess') {call_extract}
    end

    def test_bad_expires_in
      # Invalid value for expires_in should cause failure
      @assoc_response.set_arg(OPENID_NS, 'expires_in', 'forever')
      assert_protocol_error('Invalid expires_in') {call_extract}
    end
  end

  class TestExtractAssociationDiffieHellman < Minitest::Test
    include ProtocolErrorMixin

    SECRET = 'x' * 20

    def setup
      @assoc_manager = Consumer::AssociationManager.new(nil, nil)
    end

    def setup_dh
      sess, _ = @assoc_manager.send(:create_associate_request,
                                          'HMAC-SHA1', 'DH-SHA1')

      server_dh = DiffieHellman.new
      cons_dh = sess.instance_variable_get('@dh')

      enc_mac_key = server_dh.xor_secret(CryptUtil.method(:sha1),
                                         cons_dh.public, SECRET)

      server_resp = {
        'dh_server_public' => CryptUtil.num_to_base64(server_dh.public),
        'enc_mac_key' => Util.to_base64(enc_mac_key),
        'assoc_type' => 'HMAC-SHA1',
        'assoc_handle' => 'handle',
        'expires_in' => '1000',
        'session_type' => 'DH-SHA1',
      }
      if @assoc_manager.instance_variable_get(:@compatibility_mode)
        server_resp['ns'] = OPENID2_NS
      end
      return [sess, Message.from_openid_args(server_resp)]
    end

    def test_success
      sess, server_resp = setup_dh
      ret = @assoc_manager.send(:extract_association, server_resp, sess)
      assert(!ret.nil?)
      assert_equal(ret.assoc_type, 'HMAC-SHA1')
      assert_equal(ret.secret, SECRET)
      assert_equal(ret.handle, 'handle')
      assert_equal(ret.lifetime, 1000)
    end

    def test_openid2success
      # Use openid 1 type in endpoint so _setUpDH checks
      # compatibility mode state properly
      @assoc_manager.instance_variable_set('@compatibility_mode', true)
      test_success()
    end

    def test_bad_dh_values
      sess, server_resp = setup_dh
      server_resp.set_arg(OPENID_NS, 'enc_mac_key', '\x00\x00\x00')
      assert_protocol_error('Malformed response for') {
        @assoc_manager.send(:extract_association, server_resp, sess)
      }
    end
  end

  class TestAssocManagerGetAssociation < Minitest::Test
    include FetcherMixin
    include TestUtil

    attr_reader :negotiate_association

    def setup
      @server_url = 'http://invalid/'
      @store = Store::Memory.new
      @assoc_manager = Consumer::AssociationManager.new(@store, @server_url)
      @assoc_manager.extend(Const)
      @assoc = Association.new('handle', 'secret', Time.now, 10000,
                               'HMAC-SHA1')
    end

    def set_negotiate_response(assoc)
      @assoc_manager.const(:negotiate_association, assoc)
    end

    def test_not_in_store_no_response
      set_negotiate_response(nil)
      assert_nil(@assoc_manager.get_association)
    end

    def test_not_in_store_negotiate_assoc
      # Not stored beforehand:
      stored_assoc = @store.get_association(@server_url, @assoc.handle)
      assert_nil(stored_assoc)

      # Returned from associate call:
      set_negotiate_response(@assoc)
      assert_equal(@assoc, @assoc_manager.get_association)

      # It should have been stored:
      stored_assoc = @store.get_association(@server_url, @assoc.handle)
      assert_equal(@assoc, stored_assoc)
    end

    def test_in_store_no_response
      set_negotiate_response(nil)
      @store.store_association(@server_url, @assoc)
      assert_equal(@assoc, @assoc_manager.get_association)
    end

    def test_request_assoc_with_status_error
      fetcher_class = Class.new do
        define_method(:fetch) do |*args|
          MockResponse.new(500, '')
        end
      end
      with_fetcher(fetcher_class.new) do
        assert_log_matches('Got HTTP status error when requesting') {
          result = @assoc_manager.send(:request_association, 'HMAC-SHA1',
                                       'no-encryption')
          assert(result.nil?)
        }
      end
    end
  end

  class TestAssocManagerRequestAssociation < Minitest::Test
    include FetcherMixin
    include TestUtil

    def setup
      @assoc_manager = Consumer::AssociationManager.new(nil, 'http://invalid/')
      @assoc_type = 'HMAC-SHA1'
      @session_type = 'no-encryption'
      @message = Message.new(OPENID2_NS)
      @message.update_args(OPENID_NS, {
                             'assoc_type' => @assoc_type,
                             'session_type' => @session_type,
                             'assoc_handle' => 'kaboodle',
                             'expires_in' => '1000',
                             'mac_key' => 'X' * 20,
                           })
    end

    def make_request
      kv = @message.to_kvform
      fetcher_class = Class.new do
        define_method(:fetch) do |*args|
          MockResponse.new(200, kv)
        end
      end
      with_fetcher(fetcher_class.new) do
        @assoc_manager.send(:request_association, @assoc_type, @session_type)
      end
    end

    # The association we get is from valid processing of our result,
    # and that no errors are raised
    def test_success
      assert_equal('kaboodle', make_request.handle)
    end

    # A missing parameter gets translated into a log message and
    # causes the method to return nil
    def test_missing_fields
      @message.del_arg(OPENID_NS, 'assoc_type')
      assert_log_matches('Missing required par') {
        assert_nil(make_request)
      }
    end

    # A bad value results in a log message and causes the method to
    # return nil
    def test_protocol_error
      @message.set_arg(OPENID_NS, 'expires_in', 'goats')
      assert_log_matches('Protocol error processing') {
        assert_nil(make_request)
      }
    end
  end

end
