require 'minitest/autorun'
require 'testutil'
require 'util'
require 'uri'
require 'openid/server'
require 'openid/cryptutil'
require 'openid/association'
require 'openid/util'
require 'openid/message'
require 'openid/store/memory'
require 'openid/dh'
require 'openid/consumer/associationmanager'

# In general, if you edit or add tests here, try to move in the
# direction of testing smaller units.  For testing the external
# interfaces, we'll be developing an implementation-agnostic testing
# suite.

# for more, see /etc/ssh/moduli

module OpenID

  ALT_MODULUS = 0xCAADDDEC1667FC68B5FA15D53C4E1532DD24561A1A2D47A12C01ABEA1E00731F6921AAC40742311FDF9E634BB7131BEE1AF240261554389A910425E044E88C8359B010F5AD2B80E29CB1A5B027B19D9E01A6F63A6F45E5D7ED2FF6A2A0085050A7D0CF307C3DB51D2490355907B4427C23A98DF1EB8ABEF2BA209BB7AFFE86A7
  ALT_GEN = 5

  class CatchLogs
    def catchlogs_setup
      @old_logger = Util.logger
      Util.logger = self.method('got_log_message')
      @messages = []
    end

    def got_log_message(message)
      @messages << message
    end

    def teardown
      Util.logger = @old_logger
    end
  end

  class TestProtocolError < Minitest::Test
    def test_browserWithReturnTo
      return_to = "http://rp.unittest/consumer"
      # will be a ProtocolError raised by Decode or
      # CheckIDRequest.answer
      args = Message.from_post_args({
                                      'openid.mode' => 'monkeydance',
                                      'openid.identity' => 'http://wagu.unittest/',
                                      'openid.return_to' => return_to,
                                    })
      e = Server::ProtocolError.new(args, "plucky")
      assert(e.has_return_to)
      expected_args = {
        'openid.mode' => 'error',
        'openid.error' => 'plucky',
      }

      _, result_args = e.encode_to_url.split('?', 2)
      result_args = Util.parse_query(result_args)
      assert_equal(result_args, expected_args)
    end

    def test_browserWithReturnTo_OpenID2_GET
      return_to = "http://rp.unittest/consumer"
      # will be a ProtocolError raised by Decode or
      # CheckIDRequest.answer
      args = Message.from_post_args({
                                      'openid.ns' => OPENID2_NS,
                                      'openid.mode' => 'monkeydance',
                                      'openid.identity' => 'http://wagu.unittest/',
                                      'openid.claimed_id' => 'http://wagu.unittest/',
                                      'openid.return_to' => return_to,
                                    })
      e = Server::ProtocolError.new(args, "plucky")
      assert(e.has_return_to)
      expected_args = {
        'openid.ns' => OPENID2_NS,
        'openid.mode' => 'error',
        'openid.error' => 'plucky',
      }

      _, result_args = e.encode_to_url.split('?', 2)
      result_args = Util.parse_query(result_args)
      assert_equal(result_args, expected_args)
    end

    def test_browserWithReturnTo_OpenID2_POST
      return_to = "http://rp.unittest/consumer" + ('x' * OPENID1_URL_LIMIT)
      # will be a ProtocolError raised by Decode or
      # CheckIDRequest.answer
      args = Message.from_post_args({
                                      'openid.ns' => OPENID2_NS,
                                      'openid.mode' => 'monkeydance',
                                      'openid.identity' => 'http://wagu.unittest/',
                                      'openid.claimed_id' => 'http://wagu.unittest/',
                                      'openid.return_to' => return_to,
                                    })
      e = Server::ProtocolError.new(args, "plucky")
      assert(e.has_return_to)
      assert(e.which_encoding == Server::ENCODE_HTML_FORM)
      assert(e.to_form_markup == e.to_message.to_form_markup(
                                                             args.get_arg(OPENID_NS, 'return_to')))
    end

    def test_browserWithReturnTo_OpenID1_exceeds_limit
      return_to = "http://rp.unittest/consumer" + ('x' * OPENID1_URL_LIMIT)
      # will be a ProtocolError raised by Decode or
      # CheckIDRequest.answer
      args = Message.from_post_args({
                                      'openid.mode' => 'monkeydance',
                                      'openid.identity' => 'http://wagu.unittest/',
                                      'openid.return_to' => return_to,
                                    })
      e = Server::ProtocolError.new(args, "plucky")
      assert(e.has_return_to)
      expected_args = {
        'openid.mode' => 'error',
        'openid.error' => 'plucky',
      }

      assert(e.which_encoding == Server::ENCODE_URL)

      _, result_args = e.encode_to_url.split('?', 2)
      result_args = Util.parse_query(result_args)
      assert_equal(result_args, expected_args)
    end

    def test_noReturnTo
      # will be a ProtocolError raised by Decode or
      # CheckIDRequest.answer
      args = Message.from_post_args({
                                      'openid.mode' => 'zebradance',
                                      'openid.identity' => 'http://wagu.unittest/',
                                    })
      e = Server::ProtocolError.new(args, "waffles")
      assert(!e.has_return_to)
      expected = "error:waffles\nmode:error\n"
      assert_equal(e.encode_to_kvform, expected)
    end

    def test_no_message
      e = Server::ProtocolError.new(nil, "no message")
      assert(e.get_return_to.nil?)
      assert_equal(e.which_encoding, nil)
    end

    def test_which_encoding_no_message
      e = Server::ProtocolError.new(nil, "no message")
      assert(e.which_encoding.nil?)
    end
  end

  class TestDecode < Minitest::Test
    def setup
      @claimed_id = 'http://de.legating.de.coder.unittest/'
      @id_url = "http://decoder.am.unittest/"
      @rt_url = "http://rp.unittest/foobot/?qux=zam"
      @tr_url = "http://rp.unittest/"
      @assoc_handle = "{assoc}{handle}"
      @op_endpoint = 'http://endpoint.unittest/encode'
      @store = Store::Memory.new()
      @server = Server::Server.new(@store, @op_endpoint)
      @decode = Server::Decoder.new(@server).method('decode')
    end

    def test_none
      args = {}
      r = @decode.call(args)
      assert_equal(r, nil)
    end

    def test_irrelevant
      args = {
        'pony' => 'spotted',
        'sreg.mutant_power' => 'decaffinator',
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_bad
      args = {
        'openid.mode' => 'twos-compliment',
        'openid.pants' => 'zippered',
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_dictOfLists
      args = {
        'openid.mode' => ['checkid_setup'],
        'openid.identity' => @id_url,
        'openid.assoc_handle' => @assoc_handle,
        'openid.return_to' => @rt_url,
        'openid.trust_root' => @tr_url,
      }
      begin
        result = @decode.call(args)
      rescue ArgumentError => err
        assert !err.to_s.index('values').nil?
      else
        flunk("Expected ArgumentError, but got result #{result}")
      end
    end

    def test_checkidImmediate
      args = {
        'openid.mode' => 'checkid_immediate',
        'openid.identity' => @id_url,
        'openid.assoc_handle' => @assoc_handle,
        'openid.return_to' => @rt_url,
        'openid.trust_root' => @tr_url,
        # should be ignored
        'openid.some.extension' => 'junk',
      }
      r = @decode.call(args)
      assert(r.is_a?(Server::CheckIDRequest))
      assert_equal(r.mode, "checkid_immediate")
      assert_equal(r.immediate, true)
      assert_equal(r.identity, @id_url)
      assert_equal(r.trust_root, @tr_url)
      assert_equal(r.return_to, @rt_url)
      assert_equal(r.assoc_handle, @assoc_handle)
    end

    def test_checkidImmediate_constructor
      r = Server::CheckIDRequest.new(@id_url, @rt_url, nil,
                                     @rt_url, true, @assoc_handle)
      assert(r.mode == 'checkid_immediate')
      assert(r.immediate)
    end

    def test_checkid_missing_return_to_and_trust_root
      args = {
        'openid.ns' => OPENID2_NS,
        'openid.mode' => 'checkid_setup',
        'openid.identity' => @id_url,
        'openid.claimed_id' => @id_url,
        'openid.assoc_handle' => @assoc_handle,
      }
      assert_raises(Server::ProtocolError) {
        m = Message.from_post_args(args)
        Server::CheckIDRequest.from_message(m, @op_endpoint)
      }
    end

    def test_checkid_id_select
      args = {
        'openid.ns' => OPENID2_NS,
        'openid.mode' => 'checkid_setup',
        'openid.identity' => IDENTIFIER_SELECT,
        'openid.claimed_id' => IDENTIFIER_SELECT,
        'openid.assoc_handle' => @assoc_handle,
        'openid.return_to' => @rt_url,
        'openid.realm' => @tr_url,
      }
      m = Message.from_post_args(args)
      req = Server::CheckIDRequest.from_message(m, @op_endpoint)
      assert(req.id_select)
    end

    def test_checkid_not_id_select
      args = {
        'openid.ns' => OPENID2_NS,
        'openid.mode' => 'checkid_setup',
        'openid.assoc_handle' => @assoc_handle,
        'openid.return_to' => @rt_url,
        'openid.realm' => @tr_url,
      }

      id_args = [
                 {'openid.claimed_id' => IDENTIFIER_SELECT,
                   'openid.identity' => 'http://bogus.com/'},

                 {'openid.claimed_id' => 'http://bogus.com/',
                   'openid.identity' => 'http://bogus.com/'},
                ]

      id_args.each { |id|
        m = Message.from_post_args(args.merge(id))
        req = Server::CheckIDRequest.from_message(m, @op_endpoint)
        assert(!req.id_select)
      }
    end

    def test_checkidSetup
      args = {
        'openid.mode' => 'checkid_setup',
        'openid.identity' => @id_url,
        'openid.assoc_handle' => @assoc_handle,
        'openid.return_to' => @rt_url,
        'openid.trust_root' => @tr_url,
      }
      r = @decode.call(args)
      assert(r.is_a?(Server::CheckIDRequest))
      assert_equal(r.mode, "checkid_setup")
      assert_equal(r.immediate, false)
      assert_equal(r.identity, @id_url)
      assert_equal(r.trust_root, @tr_url)
      assert_equal(r.return_to, @rt_url)
    end

    def test_checkidSetupOpenID2
      args = {
        'openid.ns' => OPENID2_NS,
        'openid.mode' => 'checkid_setup',
        'openid.identity' => @id_url,
        'openid.claimed_id' => @claimed_id,
        'openid.assoc_handle' => @assoc_handle,
        'openid.return_to' => @rt_url,
        'openid.realm' => @tr_url,
      }
      r = @decode.call(args)
      assert(r.is_a?(Server::CheckIDRequest))
      assert_equal(r.mode, "checkid_setup")
      assert_equal(r.immediate, false)
      assert_equal(r.identity, @id_url)
      assert_equal(r.claimed_id, @claimed_id)
      assert_equal(r.trust_root, @tr_url)
      assert_equal(r.return_to, @rt_url)
    end

    def test_checkidSetupNoClaimedIDOpenID2
      args = {
        'openid.ns' => OPENID2_NS,
        'openid.mode' => 'checkid_setup',
        'openid.identity' => @id_url,
        'openid.assoc_handle' => @assoc_handle,
        'openid.return_to' => @rt_url,
        'openid.realm' => @tr_url,
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_checkidSetupNoIdentityOpenID2
      args = {
        'openid.ns' => OPENID2_NS,
        'openid.mode' => 'checkid_setup',
        'openid.assoc_handle' => @assoc_handle,
        'openid.return_to' => @rt_url,
        'openid.realm' => @tr_url,
      }
      r = @decode.call(args)
      assert(r.is_a?(Server::CheckIDRequest))
      assert_equal(r.mode, "checkid_setup")
      assert_equal(r.immediate, false)
      assert_equal(r.identity, nil)
      assert_equal(r.trust_root, @tr_url)
      assert_equal(r.return_to, @rt_url)
    end

    def test_checkidSetupNoReturnOpenID1
      # Make sure an OpenID 1 request cannot be decoded if it lacks a
      # return_to.
      args = {
        'openid.mode' => 'checkid_setup',
        'openid.identity' => @id_url,
        'openid.assoc_handle' => @assoc_handle,
        'openid.trust_root' => @tr_url,
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_checkidSetupNoReturnOpenID2
      # Make sure an OpenID 2 request with no return_to can be decoded,
      # and make sure a response to such a request raises
      # NoReturnToError.
      args = {
        'openid.ns' => OPENID2_NS,
        'openid.mode' => 'checkid_setup',
        'openid.identity' => @id_url,
        'openid.claimed_id' => @id_url,
        'openid.assoc_handle' => @assoc_handle,
        'openid.realm' => @tr_url,
      }

      req = @decode.call(args)
      assert(req.is_a?(Server::CheckIDRequest))

      assert_raises(Server::NoReturnToError) {
        req.answer(false)
      }

      assert_raises(Server::NoReturnToError) {
        req.encode_to_url('bogus')
      }

      assert_raises(Server::NoReturnToError) {
        req.cancel_url
      }
    end

    def test_checkidSetupRealmRequiredOpenID2
      # Make sure that an OpenID 2 request which lacks return_to cannot
      # be decoded if it lacks a realm.  Spec => This value
      # (openid.realm) MUST be sent if openid.return_to is omitted.
      args = {
        'openid.ns' => OPENID2_NS,
        'openid.mode' => 'checkid_setup',
        'openid.identity' => @id_url,
        'openid.assoc_handle' => @assoc_handle,
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_checkidSetupBadReturn
      args = {
        'openid.mode' => 'checkid_setup',
        'openid.identity' => @id_url,
        'openid.assoc_handle' => @assoc_handle,
        'openid.return_to' => 'not a url',
      }
      begin
        result = @decode.call(args)
      rescue Server::ProtocolError => err
        assert(err.openid_message)
      else
        flunk("Expected ProtocolError, instead returned with #{result}")
      end
    end

    def test_checkidSetupUntrustedReturn
      args = {
        'openid.mode' => 'checkid_setup',
        'openid.identity' => @id_url,
        'openid.assoc_handle' => @assoc_handle,
        'openid.return_to' => @rt_url,
        'openid.trust_root' => 'http://not-the-return-place.unittest/',
      }
      begin
        result = @decode.call(args)
      rescue Server::UntrustedReturnURL => err
        assert(err.openid_message, err.to_s)
      else
        flunk("Expected UntrustedReturnURL, instead returned with #{result}")
      end
    end

    def test_checkidSetupUntrustedReturn_Constructor
      assert_raises(Server::UntrustedReturnURL) {
        Server::CheckIDRequest.new(@id_url, @rt_url, nil,
                                   'http://not-the-return-place.unittest/',
                                   false, @assoc_handle)
      }
    end

    def test_checkidSetupMalformedReturnURL_Constructor
      assert_raises(Server::MalformedReturnURL) {
        Server::CheckIDRequest.new(@id_url, 'bogus://return.url', nil,
                                   'http://trustroot.com/',
                                   false, @assoc_handle)
      }
    end

    def test_checkAuth
      args = {
        'openid.mode' => 'check_authentication',
        'openid.assoc_handle' => '{dumb}{handle}',
        'openid.sig' => 'sigblob',
        'openid.signed' => 'identity,return_to,response_nonce,mode',
        'openid.identity' => 'signedval1',
        'openid.return_to' => 'signedval2',
        'openid.response_nonce' => 'signedval3',
        'openid.baz' => 'unsigned',
      }
      r = @decode.call(args)
      assert(r.is_a?(Server::CheckAuthRequest))
      assert_equal(r.mode, 'check_authentication')
      assert_equal(r.sig, 'sigblob')
    end

    def test_checkAuthMissingSignature
      args = {
        'openid.mode' => 'check_authentication',
        'openid.assoc_handle' => '{dumb}{handle}',
        'openid.signed' => 'foo,bar,mode',
        'openid.foo' => 'signedval1',
        'openid.bar' => 'signedval2',
        'openid.baz' => 'unsigned',
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_checkAuthAndInvalidate
      args = {
        'openid.mode' => 'check_authentication',
        'openid.assoc_handle' => '{dumb}{handle}',
        'openid.invalidate_handle' => '[[SMART_handle]]',
        'openid.sig' => 'sigblob',
        'openid.signed' => 'identity,return_to,response_nonce,mode',
        'openid.identity' => 'signedval1',
        'openid.return_to' => 'signedval2',
        'openid.response_nonce' => 'signedval3',
        'openid.baz' => 'unsigned',
      }
      r = @decode.call(args)
      assert(r.is_a?(Server::CheckAuthRequest))
      assert_equal(r.invalidate_handle, '[[SMART_handle]]')
    end

    def test_associateDH
      args = {
        'openid.mode' => 'associate',
        'openid.session_type' => 'DH-SHA1',
        'openid.dh_consumer_public' => "Rzup9265tw==",
      }
      r = @decode.call(args)
      assert(r.is_a?(Server::AssociateRequest))
      assert_equal(r.mode, "associate")
      assert_equal(r.session.session_type, "DH-SHA1")
      assert_equal(r.assoc_type, "HMAC-SHA1")
      assert(r.session.consumer_pubkey)
    end

    def test_associateDHMissingKey
      # Trying DH assoc w/o public key
      args = {
        'openid.mode' => 'associate',
        'openid.session_type' => 'DH-SHA1',
      }
      # Using DH-SHA1 without supplying dh_consumer_public is an error.
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_associateDHpubKeyNotB64
      args = {
        'openid.mode' => 'associate',
        'openid.session_type' => 'DH-SHA1',
        'openid.dh_consumer_public' => "donkeydonkeydonkey",
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_associateDHModGen
      # test dh with non-default but valid values for dh_modulus and
      # dh_gen
      args = {
        'openid.mode' => 'associate',
        'openid.session_type' => 'DH-SHA1',
        'openid.dh_consumer_public' => "Rzup9265tw==",
        'openid.dh_modulus' => CryptUtil.num_to_base64(ALT_MODULUS),
        'openid.dh_gen' => CryptUtil.num_to_base64(ALT_GEN) ,
      }
      r = @decode.call(args)
      assert(r.is_a?(Server::AssociateRequest))
      assert_equal(r.mode, "associate")
      assert_equal(r.session.session_type, "DH-SHA1")
      assert_equal(r.assoc_type, "HMAC-SHA1")
      assert_equal(r.session.dh.modulus, ALT_MODULUS)
      assert_equal(r.session.dh.generator, ALT_GEN)
      assert(r.session.consumer_pubkey)
    end

    def test_associateDHCorruptModGen
      # test dh with non-default but valid values for dh_modulus and
      # dh_gen
      args = {
        'openid.mode' => 'associate',
        'openid.session_type' => 'DH-SHA1',
        'openid.dh_consumer_public' => "Rzup9265tw==",
        'openid.dh_modulus' => 'pizza',
        'openid.dh_gen' => 'gnocchi',
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_associateDHMissingGen
      args = {
        'openid.mode' => 'associate',
        'openid.session_type' => 'DH-SHA1',
        'openid.dh_consumer_public' => "Rzup9265tw==",
        'openid.dh_modulus' => 'pizza',
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_associateDHMissingMod
      args = {
        'openid.mode' => 'associate',
        'openid.session_type' => 'DH-SHA1',
        'openid.dh_consumer_public' => "Rzup9265tw==",
        'openid.dh_gen' => 'pizza',
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    #     def test_associateDHInvalidModGen(self):
    #         # test dh with properly encoded values that are not a valid
    #         #   modulus/generator combination.
    #         args = {
    #             'openid.mode': 'associate',
    #             'openid.session_type': 'DH-SHA1',
    #             'openid.dh_consumer_public': "Rzup9265tw==",
    #             'openid.dh_modulus': cryptutil.longToBase64(9),
    #             'openid.dh_gen': cryptutil.longToBase64(27) ,
    #             }
    #         self.failUnlessRaises(server.ProtocolError, self.decode, args)
    #     test_associateDHInvalidModGen.todo = "low-priority feature"

    def test_associateWeirdSession
      args = {
        'openid.mode' => 'associate',
        'openid.session_type' => 'FLCL6',
        'openid.dh_consumer_public' => "YQ==\n",
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_associatePlain
      args = {
        'openid.mode' => 'associate',
      }
      r = @decode.call(args)
      assert(r.is_a?(Server::AssociateRequest))
      assert_equal(r.mode, "associate")
      assert_equal(r.session.session_type, "no-encryption")
      assert_equal(r.assoc_type, "HMAC-SHA1")
    end

    def test_nomode
      args = {
        'openid.session_type' => 'DH-SHA1',
        'openid.dh_consumer_public' => "my public keeey",
      }
      assert_raises(Server::ProtocolError) {
        @decode.call(args)
      }
    end

    def test_invalidns
      args = {'openid.ns' => 'Vegetables',
              'openid.mode' => 'associate'}
      begin
        @decode.call(args)
      rescue Server::ProtocolError => err
        assert(err.openid_message)
        assert(err.to_s.index('Vegetables'))
      end
    end
  end

  class BogusEncoder < Server::Encoder
    def encode(response)
      return "BOGUS"
    end
  end

  class BogusDecoder < Server::Decoder
    def decode(query)
      return "BOGUS"
    end
  end

  class TestEncode < Minitest::Test
    def setup
      @encoder = Server::Encoder.new
      @encode = @encoder.method('encode')
      @op_endpoint = 'http://endpoint.unittest/encode'
      @store = Store::Memory.new
      @server = Server::Server.new(@store, @op_endpoint)
    end

    def test_id_res_OpenID2_GET
      # Check that when an OpenID 2 response does not exceed the OpenID
      # 1 message size, a GET response (i.e., redirect) is issued.
      request = Server::CheckIDRequest.new(
                                   'http://bombom.unittest/',
                                   'http://burr.unittest/999',
                                   @server.op_endpoint,
                                   'http://burr.unittest/',
                                   false,
                                   nil)
      request.message = Message.new(OPENID2_NS)
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'ns' => OPENID2_NS,
                                                   'mode' => 'id_res',
                                                   'identity' => request.identity,
                                                   'claimed_id' => request.identity,
                                                   'return_to' => request.return_to,
                                                 })

      assert(!response.render_as_form)
      assert(response.which_encoding == Server::ENCODE_URL)
      webresponse = @encode.call(response)
      assert(webresponse.headers.member?('location'))
    end

    def test_id_res_OpenID2_POST
      # Check that when an OpenID 2 response exceeds the OpenID 1
      # message size, a POST response (i.e., an HTML form) is returned.
      request = Server::CheckIDRequest.new(
                                   'http://bombom.unittest/',
                                   'http://burr.unittest/999',
                                   @server.op_endpoint,
                                   'http://burr.unittest/',
                                   false,
                                   nil)
      request.message = Message.new(OPENID2_NS)
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'ns' => OPENID2_NS,
                                                   'mode' => 'id_res',
                                                   'identity' => request.identity,
                                                   'claimed_id' => request.identity,
                                                   'return_to' => 'x' * OPENID1_URL_LIMIT,
                                                 })

      assert(response.render_as_form)
      assert(response.encode_to_url.length > OPENID1_URL_LIMIT)
      assert(response.which_encoding == Server::ENCODE_HTML_FORM)
      webresponse = @encode.call(response)
      assert_equal(webresponse.body, response.to_form_markup)
    end

    def test_to_form_markup
       request = Server::CheckIDRequest.new(
                                   'http://bombom.unittest/',
                                   'http://burr.unittest/999',
                                   @server.op_endpoint,
                                   'http://burr.unittest/',
                                   false,
                                   nil)
      request.message = Message.new(OPENID2_NS)
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'ns' => OPENID2_NS,
                                                   'mode' => 'id_res',
                                                   'identity' => request.identity,
                                                   'claimed_id' => request.identity,
                                                   'return_to' => 'x' * OPENID1_URL_LIMIT,
                                                 })
      form_markup = response.to_form_markup({'foo'=>'bar'})
      assert(/ foo="bar"/ =~ form_markup, form_markup)
    end

    def test_to_html
       request = Server::CheckIDRequest.new(
                                   'http://bombom.unittest/',
                                   'http://burr.unittest/999',
                                   @server.op_endpoint,
                                   'http://burr.unittest/',
                                   false,
                                   nil)
      request.message = Message.new(OPENID2_NS)
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'ns' => OPENID2_NS,
                                                   'mode' => 'id_res',
                                                   'identity' => request.identity,
                                                   'claimed_id' => request.identity,
                                                   'return_to' => 'x' * OPENID1_URL_LIMIT,
                                                 })
      html = response.to_html
      assert(html)
    end

    def test_id_res_OpenID1_exceeds_limit
      # Check that when an OpenID 1 response exceeds the OpenID 1
      # message size, a GET response is issued.  Technically, this
      # shouldn't be permitted by the library, but this test is in place
      # to preserve the status quo for OpenID 1.
      request = Server::CheckIDRequest.new(
                                   'http://bombom.unittest/',
                                   'http://burr.unittest/999',
                                   @server.op_endpoint,
                                   'http://burr.unittest/',
                                   false,
                                   nil)
      request.message = Message.new(OPENID1_NS)

      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'mode' => 'id_res',
                                                   'identity' => request.identity,
                                                   'return_to' => 'x' * OPENID1_URL_LIMIT,
                                                 })

      assert(!response.render_as_form)
      assert(response.encode_to_url.length > OPENID1_URL_LIMIT)
      assert(response.which_encoding == Server::ENCODE_URL)
      webresponse = @encode.call(response)
      assert_equal(webresponse.headers['location'], response.encode_to_url)
    end

    def test_id_res
      request = Server::CheckIDRequest.new(
                                   'http://bombom.unittest/',
                                   'http://burr.unittest/999',
                                   @server.op_endpoint,
                                   'http://burr.unittest/',
                                   false, nil)
      request.message = Message.new(OPENID1_NS)
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'mode' => 'id_res',
                                                   'identity' => request.identity,
                                                   'return_to' => request.return_to,
                                                 })
      webresponse = @encode.call(response)
      assert_equal(webresponse.code, Server::HTTP_REDIRECT)
      assert(webresponse.headers.member?('location'))

      location = webresponse.headers['location']
      assert(location.start_with?(request.return_to),
             sprintf("%s does not start with %s",
                     location, request.return_to))
      # argh.
      q2 = Util.parse_query(URI::parse(location).query)
      expected = response.fields.to_post_args
      assert_equal(q2, expected)
    end

    def test_cancel
      request = Server::CheckIDRequest.new(
                                   'http://bombom.unittest/',
                                   'http://burr.unittest/999',
                                   @server.op_endpoint,
                                   'http://burr.unittest/',
                                   false, nil)
      request.message = Message.new(OPENID2_NS)
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'mode' => 'cancel',
                                                 })
      webresponse = @encode.call(response)
      assert_equal(webresponse.code, Server::HTTP_REDIRECT)
      assert(webresponse.headers.member?('location'))
    end

    def test_cancel_to_form
      request = Server::CheckIDRequest.new(
                                   'http://bombom.unittest/',
                                   'http://burr.unittest/999',
                                   @server.op_endpoint,
                                   'http://burr.unittest/',
                                   false, nil)
      request.message = Message.new(OPENID2_NS)
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'mode' => 'cancel',
                                                 })
      form = response.to_form_markup
      assert(form.index(request.return_to))
    end

    def test_assocReply
      msg = Message.new(OPENID2_NS)
      msg.set_arg(OPENID2_NS, 'session_type', 'no-encryption')
      request = Server::AssociateRequest.from_message(msg)
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_post_args(
                                               {'openid.assoc_handle' => "every-zig"})
      webresponse = @encode.call(response)
      body = "assoc_handle:every-zig\n"

      assert_equal(webresponse.code, Server::HTTP_OK)
      assert_equal(webresponse.headers, {})
      assert_equal(webresponse.body, body)
    end

    def test_checkauthReply
      request = Server::CheckAuthRequest.new('a_sock_monkey',
                                     'siggggg',
                                     [])
      request.message = Message.new(OPENID2_NS)
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'is_valid' => 'true',
                                                   'invalidate_handle' => 'xXxX:xXXx'
                                                 })
      body = "invalidate_handle:xXxX:xXXx\nis_valid:true\n"

      webresponse = @encode.call(response)
      assert_equal(webresponse.code, Server::HTTP_OK)
      assert_equal(webresponse.headers, {})
      assert_equal(webresponse.body, body)
    end

    def test_unencodableError
      args = Message.from_post_args({
                                      'openid.identity' => 'http://limu.unittest/',
                                    })
      e = Server::ProtocolError.new(args, "wet paint")
      assert_raises(Server::EncodingError) {
        @encode.call(e)
      }
    end

    def test_encodableError
      args = Message.from_post_args({
                                      'openid.mode' => 'associate',
                                      'openid.identity' => 'http://limu.unittest/',
                                    })
      body="error:snoot\nmode:error\n"
      webresponse = @encode.call(Server::ProtocolError.new(args, "snoot"))
      assert_equal(webresponse.code, Server::HTTP_ERROR)
      assert_equal(webresponse.headers, {})
      assert_equal(webresponse.body, body)
    end
  end

  class TestSigningEncode < Minitest::Test
    def setup
      @_dumb_key = Server::Signatory._dumb_key
      @_normal_key = Server::Signatory._normal_key
      @store = Store::Memory.new()
      @server = Server::Server.new(@store, "http://signing.unittest/enc")
      @request = Server::CheckIDRequest.new(
                                    'http://bombom.unittest/',
                                    'http://burr.unittest/999',
                                    @server.op_endpoint,
                                    'http://burr.unittest/',
                                    false, nil)
      @request.message = Message.new(OPENID2_NS)

      @response = Server::OpenIDResponse.new(@request)
      @response.fields = Message.from_openid_args({
                                                    'mode' => 'id_res',
                                                    'identity' => @request.identity,
                                                    'return_to' => @request.return_to,
                                                  })
      @signatory = Server::Signatory.new(@store)
      @encoder = Server::SigningEncoder.new(@signatory)
      @encode = @encoder.method('encode')
    end

    def test_idres
      assoc_handle = '{bicycle}{shed}'
      @store.store_association(
                               @_normal_key,
                               Association.from_expires_in(60, assoc_handle,
                                                           'sekrit', 'HMAC-SHA1'))
      @request.assoc_handle = assoc_handle
      webresponse = @encode.call(@response)
      assert_equal(webresponse.code, Server::HTTP_REDIRECT)
      assert(webresponse.headers.member?('location'))

      location = webresponse.headers['location']
      query = Util.parse_query(URI::parse(location).query)
      assert(query.member?('openid.sig'))
      assert(query.member?('openid.assoc_handle'))
      assert(query.member?('openid.signed'))
    end

    def test_idresDumb
      webresponse = @encode.call(@response)
      assert_equal(webresponse.code, Server::HTTP_REDIRECT)
      assert(webresponse.headers.has_key?('location'))

      location = webresponse.headers['location']
      query = Util.parse_query(URI::parse(location).query)
      assert(query.member?('openid.sig'))
      assert(query.member?('openid.assoc_handle'))
      assert(query.member?('openid.signed'))
    end

    def test_forgotStore
      @encoder.signatory = nil
      assert_raises(ArgumentError) {
        @encode.call(@response)
      }
    end

    def test_cancel
      request = Server::CheckIDRequest.new(
                                   'http://bombom.unittest/',
                                   'http://burr.unittest/999',
                                   @server.op_endpoint,
                                   'http://burr.unittest/',
                                   false, nil)
      request.message = Message.new(OPENID2_NS)
      response = Server::OpenIDResponse.new(request)
      response.fields.set_arg(OPENID_NS, 'mode', 'cancel')
      webresponse = @encode.call(response)
      assert_equal(webresponse.code, Server::HTTP_REDIRECT)
      assert(webresponse.headers.has_key?('location'))
      location = webresponse.headers['location']
      query = Util.parse_query(URI::parse(location).query)
      assert !query.has_key?('openid.sig')
    end

    def test_assocReply
      msg = Message.new(OPENID2_NS)
      msg.set_arg(OPENID2_NS, 'session_type', 'no-encryption')
      request = Server::AssociateRequest.from_message(msg)
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({'assoc_handle' => "every-zig"})
      webresponse = @encode.call(response)
      body = "assoc_handle:every-zig\n"
      assert_equal(webresponse.code, Server::HTTP_OK)
      assert_equal(webresponse.headers, {})
      assert_equal(webresponse.body, body)
    end

    def test_alreadySigned
      @response.fields.set_arg(OPENID_NS, 'sig', 'priorSig==')
      assert_raises(Server::AlreadySigned) {
        @encode.call(@response)
      }
    end
  end

  class TestCheckID < Minitest::Test
    def setup
      @op_endpoint = 'http://endpoint.unittest/'
      @store = Store::Memory.new()
      @server = Server::Server.new(@store, @op_endpoint)
      @request = Server::CheckIDRequest.new(
                                    'http://bambam.unittest/',
                                    'http://bar.unittest/999',
                                    @server.op_endpoint,
                                    'http://bar.unittest/',
                                    false)
      @request.message = Message.new(OPENID2_NS)
    end

    def test_trustRootInvalid
      @request.trust_root = "http://foo.unittest/17"
      @request.return_to = "http://foo.unittest/39"
      assert(!@request.trust_root_valid())
    end

    def test_trustRootInvalid_modified
      @request.trust_root = "does://not.parse/"
      @request.message = :sentinel
      begin
        result = @request.trust_root_valid
      rescue Server::MalformedTrustRoot => why
        assert_equal(:sentinel, why.openid_message)
      else
        flunk("Expected MalformedTrustRoot, got #{result.inspect}")
      end
    end

    def test_trustRootvalid_absent_trust_root
      @request.trust_root = nil
      assert(@request.trust_root_valid())
    end

    def test_trustRootValid
      @request.trust_root = "http://foo.unittest/"
      @request.return_to = "http://foo.unittest/39"
      assert(@request.trust_root_valid())
    end

    def test_trustRootValidNoReturnTo
      request = Server::CheckIDRequest.new(
                                   'http://bambam.unittest/',
                                   nil,
                                   @server.op_endpoint,
                                   'http://bar.unittest/',
                                   false)

      assert(request.trust_root_valid())
    end

    def test_returnToVerified_callsVerify
      # Make sure that verifyReturnTo is calling the trustroot
      # function verifyReturnTo
      # Ensure that exceptions are passed through
      sentinel = Exception.new()

      __req = @request
      tc = self

      vrfyExc = Proc.new { |trust_root, return_to|
        tc.assert_equal(__req.trust_root, trust_root)
        tc.assert_equal(__req.return_to, return_to)
        raise sentinel
      }

      TrustRoot.extend(OverrideMethodMixin)

      TrustRoot.with_method_overridden(:verify_return_to, vrfyExc) do
        begin
          @request.return_to_verified()
          flunk("Expected sentinel to be raised, got success")
        rescue Exception => e
          assert(e.equal?(sentinel), [e, sentinel].inspect)
        end
      end

      # Ensure that True and False are passed through unchanged
      constVerify = Proc.new { |val|
        verify = Proc.new { |trust_root, return_to|
          tc.assert_equal(__req.trust_root, trust_root)
          tc.assert_equal(__req.request.return_to, return_to)
          return val
        }

        return verify
      }

      [true, false].each { |val|
        verifier = constVerify.call(val)

        TrustRoot.with_method_overridden(:verify_return_to, verifier) do
          assert_equal(val, @request.return_to_verified())
        end
      }
    end

    def _expectAnswer(answer, identity=nil, claimed_id=nil)
      expected_list = [
                       ['mode', 'id_res'],
                       ['return_to', @request.return_to],
                       ['op_endpoint', @op_endpoint],
                      ]
      if identity
        expected_list << ['identity', identity]
        if claimed_id
          expected_list << ['claimed_id', claimed_id]
        else
          expected_list << ['claimed_id', identity]
        end
      end

      expected_list.each { |k, expected|
        actual = answer.fields.get_arg(OPENID_NS, k)
        assert_equal(expected, actual,
                     sprintf("%s: expected %s, got %s",
                             k, expected, actual))
      }

      assert(answer.fields.has_key?(OPENID_NS, 'response_nonce'))
      assert(answer.fields.get_openid_namespace() == OPENID2_NS)

      # One for nonce, one for ns
      assert_equal(answer.fields.to_post_args.length,
                   expected_list.length + 2,
                   answer.fields.to_post_args.inspect)
    end

    def test_answerAllow
      # Check the fields specified by "Positive Assertions"
      #
      # including mode=id_res, identity, claimed_id, op_endpoint,
      # return_to
      answer = @request.answer(true)
      assert_equal(answer.request, @request)
      _expectAnswer(answer, @request.identity)
    end

    def test_answerAllowDelegatedIdentity
      @request.claimed_id = 'http://delegating.unittest/'
      answer = @request.answer(true)
      _expectAnswer(answer, @request.identity,
                    @request.claimed_id)
    end

    def test_answerAllowWithoutIdentityReally
      @request.identity = nil
      answer = @request.answer(true)
      assert_equal(answer.request, @request)
      _expectAnswer(answer)
    end

    def test_answerAllowAnonymousFail
      @request.identity = nil
      # XXX - Check on this, I think this behavior is legal in OpenID
      # 2.0?
      assert_raises(ArgumentError) {
        @request.answer(true, nil, "=V")
      }
    end

    def test_answerAllowWithIdentity
      @request.identity = IDENTIFIER_SELECT
      selected_id = 'http://anon.unittest/9861'
      answer = @request.answer(true, nil, selected_id)
      _expectAnswer(answer, selected_id)
    end

    def test_answerAllowWithNoIdentity
      @request.identity = IDENTIFIER_SELECT
      assert_raises(ArgumentError) {
        @request.answer(true, nil, nil)
      }
    end

    def test_immediate_openid1_no_identity
      @request.message = Message.new(OPENID1_NS)
      @request.immediate = true
      @request.mode = 'checkid_immediate'
      resp = @request.answer(false)
      assert(resp.fields.get_arg(OPENID_NS, 'mode') == 'id_res')
    end

    def test_checkid_setup_openid1_no_identity
      @request.message = Message.new(OPENID1_NS)
      @request.immediate = false
      @request.mode = 'checkid_setup'
      resp = @request.answer(false)
      assert(resp.fields.get_arg(OPENID_NS, 'mode') == 'cancel')
    end

    def test_immediate_openid1_no_server_url
      @request.message = Message.new(OPENID1_NS)
      @request.immediate = true
      @request.mode = 'checkid_immediate'
      @request.op_endpoint = nil

      assert_raises(ArgumentError) {
        @request.answer(false)
      }
    end

    def test_immediate_encode_to_url
      @request.message = Message.new(OPENID1_NS)
      @request.immediate = true
      @request.mode = 'checkid_immediate'
      @request.trust_root = "BOGUS"
      @request.assoc_handle = "ASSOC"

      server_url = "http://server.com/server"

      url = @request.encode_to_url(server_url)
      assert(url.start_with?(server_url))

      _, query = url.split("?", 2)
      args = Util.parse_query(query)

      m = Message.from_post_args(args)
      assert(m.get_arg(OPENID_NS, 'trust_root') == "BOGUS")
      assert(m.get_arg(OPENID_NS, 'assoc_handle') == "ASSOC")
      assert(m.get_arg(OPENID_NS, 'mode'), "checkid_immediate")
      assert(m.get_arg(OPENID_NS, 'identity') == @request.identity)
      assert(m.get_arg(OPENID_NS, 'claimed_id') == @request.claimed_id)
      assert(m.get_arg(OPENID_NS, 'return_to') == @request.return_to)
    end

    def test_answerAllowWithDelegatedIdentityOpenID2
      # Answer an IDENTIFIER_SELECT case with a delegated identifier.

      # claimed_id delegates to selected_id here.
      @request.identity = IDENTIFIER_SELECT
      selected_id = 'http://anon.unittest/9861'
      claimed_id = 'http://monkeyhat.unittest/'
      answer = @request.answer(true, nil, selected_id, claimed_id)
      _expectAnswer(answer, selected_id, claimed_id)
    end

    def test_answerAllowWithDelegatedIdentityOpenID1
      # claimed_id parameter doesn't exist in OpenID 1.
      @request.message = Message.new(OPENID1_NS)
      # claimed_id delegates to selected_id here.
      @request.identity = IDENTIFIER_SELECT
      selected_id = 'http://anon.unittest/9861'
      claimed_id = 'http://monkeyhat.unittest/'
      assert_raises(Server::VersionError) {
        @request.answer(true, nil, selected_id, claimed_id)
      }
    end

    def test_answerAllowWithAnotherIdentity
      # XXX - Check on this, I think this behavior is legal in OpenID
      # 2.0?
      assert_raises(ArgumentError){
        @request.answer(true, nil, "http://pebbles.unittest/")
      }
    end

    def test_answerAllowNoIdentityOpenID1
      @request.message = Message.new(OPENID1_NS)
      @request.identity = nil
      assert_raises(ArgumentError) {
        @request.answer(true, nil, nil)
      }
    end

    def test_answerAllowForgotEndpoint
      @request.op_endpoint = nil
      assert_raises(RuntimeError) {
        @request.answer(true)
      }
    end

    def test_checkIDWithNoIdentityOpenID1
      msg = Message.new(OPENID1_NS)
      msg.set_arg(OPENID_NS, 'return_to', 'bogus')
      msg.set_arg(OPENID_NS, 'trust_root', 'bogus')
      msg.set_arg(OPENID_NS, 'mode', 'checkid_setup')
      msg.set_arg(OPENID_NS, 'assoc_handle', 'bogus')

      assert_raises(Server::ProtocolError) {
        Server::CheckIDRequest.from_message(msg, @server)
      }
    end

    def test_fromMessageClaimedIDWithoutIdentityOpenID2
      msg = Message.new(OPENID2_NS)
      msg.set_arg(OPENID_NS, 'mode', 'checkid_setup')
      msg.set_arg(OPENID_NS, 'return_to', 'http://invalid:8000/rt')
      msg.set_arg(OPENID_NS, 'claimed_id', 'https://example.myopenid.com')

      assert_raises(Server::ProtocolError) {
        Server::CheckIDRequest.from_message(msg, @server)
      }
    end

    def test_fromMessageIdentityWithoutClaimedIDOpenID2
      msg = Message.new(OPENID2_NS)
      msg.set_arg(OPENID_NS, 'mode', 'checkid_setup')
      msg.set_arg(OPENID_NS, 'return_to', 'http://invalid:8000/rt')
      msg.set_arg(OPENID_NS, 'identity', 'https://example.myopenid.com')

      assert_raises(Server::ProtocolError) {
        Server::CheckIDRequest.from_message(msg, @server)
      }
    end

    def test_fromMessageWithEmptyTrustRoot
      return_to = 'http://some.url/foo?bar=baz'
      msg = Message.from_post_args({
              'openid.assoc_handle' => '{blah}{blah}{OZivdQ==}',
              'openid.claimed_id' => 'http://delegated.invalid/',
              'openid.identity' => 'http://op-local.example.com/',
              'openid.mode' => 'checkid_setup',
              'openid.ns' => 'http://openid.net/signon/1.0',
              'openid.return_to' => return_to,
              'openid.trust_root' => ''
              });
      result = Server::CheckIDRequest.from_message(msg, @server)
      assert_equal(return_to, result.trust_root)
    end

    def test_trustRootOpenID1
      # Ignore openid.realm in OpenID 1
      msg = Message.new(OPENID1_NS)
      msg.set_arg(OPENID_NS, 'mode', 'checkid_setup')
      msg.set_arg(OPENID_NS, 'trust_root', 'http://trustroot.com/')
      msg.set_arg(OPENID_NS, 'realm', 'http://fake_trust_root/')
      msg.set_arg(OPENID_NS, 'return_to', 'http://trustroot.com/foo')
      msg.set_arg(OPENID_NS, 'assoc_handle', 'bogus')
      msg.set_arg(OPENID_NS, 'identity', 'george')

      result = Server::CheckIDRequest.from_message(msg, @server.op_endpoint)

      assert(result.trust_root == 'http://trustroot.com/')
    end

    def test_trustRootOpenID2
      # Ignore openid.trust_root in OpenID 2
      msg = Message.new(OPENID2_NS)
      msg.set_arg(OPENID_NS, 'mode', 'checkid_setup')
      msg.set_arg(OPENID_NS, 'realm', 'http://trustroot.com/')
      msg.set_arg(OPENID_NS, 'trust_root', 'http://fake_trust_root/')
      msg.set_arg(OPENID_NS, 'return_to', 'http://trustroot.com/foo')
      msg.set_arg(OPENID_NS, 'assoc_handle', 'bogus')
      msg.set_arg(OPENID_NS, 'identity', 'george')
      msg.set_arg(OPENID_NS, 'claimed_id', 'george')

      result = Server::CheckIDRequest.from_message(msg, @server.op_endpoint)

      assert(result.trust_root == 'http://trustroot.com/')
    end

    def test_answerAllowNoTrustRoot
      @request.trust_root = nil
      answer = @request.answer(true)
      assert_equal(answer.request, @request)
      _expectAnswer(answer, @request.identity)
    end

    def test_answerImmediateDenyOpenID2
      # Look for mode=setup_needed in checkid_immediate negative
      # response in OpenID 2 case.
      #
      # See specification Responding to Authentication Requests /
      # Negative Assertions / In Response to Immediate Requests.
      @request.mode = 'checkid_immediate'
      @request.immediate = true

      server_url = "http://setup-url.unittest/"
      # crappiting setup_url, you dirty my interface with your presence!
      answer = @request.answer(false, server_url)
      assert_equal(answer.request, @request)
      assert_equal(answer.fields.to_post_args.length, 3, answer.fields)
      assert_equal(answer.fields.get_openid_namespace, OPENID2_NS)
      assert_equal(answer.fields.get_arg(OPENID_NS, 'mode'),
                   'setup_needed')
      # user_setup_url no longer required.
    end

    def test_answerImmediateDenyOpenID1
      # Look for user_setup_url in checkid_immediate negative response
      # in OpenID 1 case.
      @request.message = Message.new(OPENID1_NS)
      @request.mode = 'checkid_immediate'
      @request.immediate = true
      @request.claimed_id = 'http://claimed-id.test/'
      server_url = "http://setup-url.unittest/"
      # crappiting setup_url, you dirty my interface with your presence!
      answer = @request.answer(false, server_url)
      assert_equal(answer.request, @request)
      assert_equal(2, answer.fields.to_post_args.length, answer.fields)
      assert_equal(OPENID1_NS, answer.fields.get_openid_namespace)
      assert_equal('id_res', answer.fields.get_arg(OPENID_NS, 'mode'))

      usu = answer.fields.get_arg(OPENID_NS, 'user_setup_url', '')
      assert(usu.start_with?(server_url))
      expected_substr = 'openid.claimed_id=http%3A%2F%2Fclaimed-id.test%2F'
      assert(!usu.index(expected_substr).nil?, usu)
    end

    def test_answerSetupDeny
      answer = @request.answer(false)
      assert_equal(answer.fields.get_args(OPENID_NS), {
                     'mode' => 'cancel',
                   })
    end

    def test_encodeToURL
      server_url = 'http://openid-server.unittest/'
      result = @request.encode_to_url(server_url)

      # How to check?  How about a round-trip test.
      _, result_args = result.split('?', 2)
      result_args = Util.parse_query(result_args)
      message = Message.from_post_args(result_args)
      rebuilt_request = Server::CheckIDRequest.from_message(message,
                                                    @server.op_endpoint)

      @request.message = message

      @request.instance_variables.each { |var|
        assert_equal(@request.instance_variable_get(var),
                     rebuilt_request.instance_variable_get(var), var)
      }
    end

    def test_getCancelURL
      url = @request.cancel_url
      rt, query_string = url.split('?', -1)
      assert_equal(@request.return_to, rt)
      query = Util.parse_query(query_string)
      assert_equal(query, {'openid.mode' => 'cancel',
                     'openid.ns' => OPENID2_NS})
    end

    def test_getCancelURLimmed
      @request.mode = 'checkid_immediate'
      @request.immediate = true
      assert_raises(ArgumentError) {
        @request.cancel_url
      }
    end

    def test_fromMessageWithoutTrustRoot
        msg = Message.new(OPENID2_NS)
        msg.set_arg(OPENID_NS, 'mode', 'checkid_setup')
        msg.set_arg(OPENID_NS, 'return_to', 'http://real.trust.root/foo')
        msg.set_arg(OPENID_NS, 'assoc_handle', 'bogus')
        msg.set_arg(OPENID_NS, 'identity', 'george')
        msg.set_arg(OPENID_NS, 'claimed_id', 'george')

        result = Server::CheckIDRequest.from_message(msg, @server.op_endpoint)

        assert_equal(result.trust_root, 'http://real.trust.root/foo')
    end

    def test_fromMessageWithoutTrustRootOrReturnTo
        msg = Message.new(OPENID2_NS)
        msg.set_arg(OPENID_NS, 'mode', 'checkid_setup')
        msg.set_arg(OPENID_NS, 'assoc_handle', 'bogus')
        msg.set_arg(OPENID_NS, 'identity', 'george')
        msg.set_arg(OPENID_NS, 'claimed_id', 'george')

        assert_raises(Server::ProtocolError) {
          Server::CheckIDRequest.from_message(msg, @server.op_endpoint)
        }
    end
  end

  class TestCheckIDExtension < Minitest::Test

    def setup
      @op_endpoint = 'http://endpoint.unittest/ext'
      @store = Store::Memory.new()
      @server = Server::Server.new(@store, @op_endpoint)
      @request = Server::CheckIDRequest.new(
                                    'http://bambam.unittest/',
                                    'http://bar.unittest/999',
                                    @server.op_endpoint,
                                    'http://bar.unittest/',
                                    false)
      @request.message = Message.new(OPENID2_NS)
      @response = Server::OpenIDResponse.new(@request)
      @response.fields.set_arg(OPENID_NS, 'mode', 'id_res')
      @response.fields.set_arg(OPENID_NS, 'blue', 'star')
    end

    def test_addField
      namespace = 'something:'
      @response.fields.set_arg(namespace, 'bright', 'potato')
      assert_equal(@response.fields.get_args(OPENID_NS),
                   {'blue' => 'star',
                     'mode' => 'id_res',
                   })

      assert_equal(@response.fields.get_args(namespace),
                   {'bright' => 'potato'})
    end

    def test_addFields
      namespace = 'mi5:'
      args =  {'tangy' => 'suspenders',
        'bravo' => 'inclusion'}
      @response.fields.update_args(namespace, args)
      assert_equal(@response.fields.get_args(OPENID_NS),
                   {'blue' => 'star',
                     'mode' => 'id_res',
                   })
      assert_equal(@response.fields.get_args(namespace), args)
    end
  end

  class MockSignatory
    attr_accessor :isValid, :assocs

    def initialize(assoc)
      @isValid = true
      @assocs = [assoc]
    end

    def verify(assoc_handle, message)
      Util.assert(message.has_key?(OPENID_NS, "sig"))
      if self.assocs.member?([true, assoc_handle])
        return @isValid
      else
        return false
      end
    end

    def get_association(assoc_handle, dumb)
      if self.assocs.member?([dumb, assoc_handle])
        # This isn't a valid implementation for many uses of this
        # function, mind you.
        return true
      else
        return nil
      end
    end

    def invalidate(assoc_handle, dumb)
      if self.assocs.member?([dumb, assoc_handle])
        @assocs.delete([dumb, assoc_handle])
      end
    end
  end

  class TestCheckAuth < Minitest::Test
    def setup
      @assoc_handle = 'mooooooooo'
      @message = Message.from_post_args({
                                          'openid.sig' => 'signarture',
                                          'one' => 'alpha',
                                          'two' => 'beta',
                                        })
      @request = Server::CheckAuthRequest.new(
                                      @assoc_handle, @message)
      @request.message = Message.new(OPENID2_NS)

      @signatory = MockSignatory.new([true, @assoc_handle])
    end

    def test_to_s
      @request.to_s
    end

    def test_valid
      r = @request.answer(@signatory)
      assert_equal({'is_valid' => 'true'},
                   r.fields.get_args(OPENID_NS))
      assert_equal(r.request, @request)
    end

    def test_invalid
      @signatory.isValid = false
      r = @request.answer(@signatory)
      assert_equal({'is_valid' => 'false'},
                   r.fields.get_args(OPENID_NS))

    end

    def test_replay
      # Don't validate the same response twice.
      #
      # From "Checking the Nonce"::
      #
      #   When using "check_authentication", the OP MUST ensure that an
      #   assertion has not yet been accepted with the same value for
      #   "openid.response_nonce".
      #
      # In this implementation, the assoc_handle is only valid once.
      # And nonces are a signed component of the message, so they can't
      # be used with another handle without breaking the sig.
      r = @request.answer(@signatory)
      r = @request.answer(@signatory)
      assert_equal({'is_valid' => 'false'},
                   r.fields.get_args(OPENID_NS))
    end

    def test_invalidatehandle
      @request.invalidate_handle = "bogusHandle"
      r = @request.answer(@signatory)
      assert_equal(r.fields.get_args(OPENID_NS),
                   {'is_valid' => 'true',
                     'invalidate_handle' => "bogusHandle"})
      assert_equal(r.request, @request)
    end

    def test_invalidatehandleNo
      assoc_handle = 'goodhandle'
      @signatory.assocs << [false, 'goodhandle']
      @request.invalidate_handle = assoc_handle
      r = @request.answer(@signatory)
      assert_equal(r.fields.get_args(OPENID_NS), {'is_valid' => 'true'})
    end
  end

  class TestAssociate < Minitest::Test
    # TODO: test DH with non-default values for modulus and gen.
    # (important to do because we actually had it broken for a while.)

    def setup
      @request = Server::AssociateRequest.from_message(Message.from_post_args({}))
      @store = Store::Memory.new()
      @signatory = Server::Signatory.new(@store)
    end

    def test_dhSHA1
      @assoc = @signatory.create_association(false, 'HMAC-SHA1')
      consumer_dh = DiffieHellman.from_defaults()
      cpub = consumer_dh.public
      server_dh = DiffieHellman.from_defaults()
      session = Server::DiffieHellmanSHA1ServerSession.new(server_dh, cpub)
      @request = Server::AssociateRequest.new(session, 'HMAC-SHA1')
      @request.message = Message.new(OPENID2_NS)
      response = @request.answer(@assoc)
      rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) }
      assert_equal(rfg.call("assoc_type"), "HMAC-SHA1")
      assert_equal(rfg.call("assoc_handle"), @assoc.handle)
      assert(!rfg.call("mac_key"))
      assert_equal(rfg.call("session_type"), "DH-SHA1")
      assert(rfg.call("enc_mac_key"))
      assert(rfg.call("dh_server_public"))

      enc_key = Util.from_base64(rfg.call("enc_mac_key"))
      spub = CryptUtil.base64_to_num(rfg.call("dh_server_public"))
      secret = consumer_dh.xor_secret(CryptUtil.method('sha1'),
                                      spub, enc_key)
      assert_equal(secret, @assoc.secret)
    end

    def test_dhSHA256
      @assoc = @signatory.create_association(false, 'HMAC-SHA256')
      consumer_dh = DiffieHellman.from_defaults()
      cpub = consumer_dh.public
      server_dh = DiffieHellman.from_defaults()
      session = Server::DiffieHellmanSHA256ServerSession.new(server_dh, cpub)
      @request = Server::AssociateRequest.new(session, 'HMAC-SHA256')
      @request.message = Message.new(OPENID2_NS)
      response = @request.answer(@assoc)
      rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) }
      assert_equal(rfg.call("assoc_type"), "HMAC-SHA256")
      assert_equal(rfg.call("assoc_handle"), @assoc.handle)
      assert(!rfg.call("mac_key"))
      assert_equal(rfg.call("session_type"), "DH-SHA256")
      assert(rfg.call("enc_mac_key"))
      assert(rfg.call("dh_server_public"))

      enc_key = Util.from_base64(rfg.call("enc_mac_key"))
      spub = CryptUtil.base64_to_num(rfg.call("dh_server_public"))
      secret = consumer_dh.xor_secret(CryptUtil.method('sha256'),
                                      spub, enc_key)
      assert_equal(secret, @assoc.secret)
    end

    def test_protoError256
      s256_session = Consumer::DiffieHellmanSHA256Session.new()

      invalid_s256 = {'openid.assoc_type' => 'HMAC-SHA1',
        'openid.session_type' => 'DH-SHA256',}
      invalid_s256.merge!(s256_session.get_request())

      invalid_s256_2 = {'openid.assoc_type' => 'MONKEY-PIRATE',
        'openid.session_type' => 'DH-SHA256',}
      invalid_s256_2.merge!(s256_session.get_request())

      bad_request_argss = [
                           invalid_s256,
                           invalid_s256_2,
                          ]

      bad_request_argss.each { |request_args|
        message = Message.from_post_args(request_args)
        assert_raises(Server::ProtocolError) {
          Server::AssociateRequest.from_message(message)
        }
      }
    end

    def test_protoError
      s1_session = Consumer::DiffieHellmanSHA1Session.new()

      invalid_s1 = {'openid.assoc_type' => 'HMAC-SHA256',
        'openid.session_type' => 'DH-SHA1',}
      invalid_s1.merge!(s1_session.get_request())

      invalid_s1_2 = {'openid.assoc_type' => 'ROBOT-NINJA',
        'openid.session_type' => 'DH-SHA1',}
      invalid_s1_2.merge!(s1_session.get_request())

      bad_request_argss = [
                           {'openid.assoc_type' => 'Wha?'},
                           invalid_s1,
                           invalid_s1_2,
                          ]

      bad_request_argss.each { |request_args|
        message = Message.from_post_args(request_args)
        assert_raises(Server::ProtocolError) {
          Server::AssociateRequest.from_message(message)
        }
      }
    end

    def test_protoErrorFields

      contact = 'user@example.invalid'
      reference = 'Trac ticket number MAX_INT'
      error = 'poltergeist'

      openid1_args = {
        'openid.identitiy' => 'invalid',
        'openid.mode' => 'checkid_setup',
      }

      openid2_args = openid1_args.dup
      openid2_args.merge!({'openid.ns' => OPENID2_NS})

      # Check presence of optional fields in both protocol versions

      openid1_msg = Message.from_post_args(openid1_args)
      p = Server::ProtocolError.new(openid1_msg, error,
                                    reference, contact)
      reply = p.to_message()

      assert_equal(reply.get_arg(OPENID_NS, 'reference'), reference)
      assert_equal(reply.get_arg(OPENID_NS, 'contact'), contact)

      openid2_msg = Message.from_post_args(openid2_args)
      p = Server::ProtocolError.new(openid2_msg, error,
                                    reference, contact)
      reply = p.to_message()

      assert_equal(reply.get_arg(OPENID_NS, 'reference'), reference)
      assert_equal(reply.get_arg(OPENID_NS, 'contact'), contact)
    end

    def failUnlessExpiresInMatches(msg, expected_expires_in)
      expires_in_str = msg.get_arg(OPENID_NS, 'expires_in', NO_DEFAULT)
      expires_in = expires_in_str.to_i

      # Slop is necessary because the tests can sometimes get run
      # right on a second boundary
      slop = 1 # second
      difference = expected_expires_in - expires_in

      error_message = sprintf('"expires_in" value not within %s of expected: ' +
                              'expected=%s, actual=%s', slop, expected_expires_in,
                              expires_in)
      assert((0 <= difference and difference <= slop), error_message)
    end

    def test_plaintext
      @assoc = @signatory.create_association(false, 'HMAC-SHA1')
      response = @request.answer(@assoc)
      rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) }

      assert_equal(rfg.call("assoc_type"), "HMAC-SHA1")
      assert_equal(rfg.call("assoc_handle"), @assoc.handle)

      failUnlessExpiresInMatches(response.fields,
                                 @signatory.secret_lifetime)

      assert_equal(
                   rfg.call("mac_key"), Util.to_base64(@assoc.secret))
      assert(!rfg.call("session_type"))
      assert(!rfg.call("enc_mac_key"))
      assert(!rfg.call("dh_server_public"))
    end

    def test_plaintext_v2
        # The main difference between this and the v1 test is that
        # session_type is always returned in v2.
        args = {
            'openid.ns' => OPENID2_NS,
            'openid.mode' => 'associate',
            'openid.assoc_type' => 'HMAC-SHA1',
            'openid.session_type' => 'no-encryption',
            }
        @request = Server::AssociateRequest.from_message(
          Message.from_post_args(args))

        assert(!@request.message.is_openid1())

        @assoc = @signatory.create_association(false, 'HMAC-SHA1')
        response = @request.answer(@assoc)
        rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) }

        assert_equal(rfg.call("assoc_type"), "HMAC-SHA1")
        assert_equal(rfg.call("assoc_handle"), @assoc.handle)

        failUnlessExpiresInMatches(
            response.fields, @signatory.secret_lifetime)

        assert_equal(
            rfg.call("mac_key"), Util.to_base64(@assoc.secret))

        assert_equal(rfg.call("session_type"), "no-encryption")
        assert(!rfg.call("enc_mac_key"))
        assert(!rfg.call("dh_server_public"))
    end

    def test_plaintext256
      @assoc = @signatory.create_association(false, 'HMAC-SHA256')
      response = @request.answer(@assoc)
      rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) }

      assert_equal(rfg.call("assoc_type"), "HMAC-SHA1")
      assert_equal(rfg.call("assoc_handle"), @assoc.handle)

      failUnlessExpiresInMatches(
                                 response.fields, @signatory.secret_lifetime)

      assert_equal(
                   rfg.call("mac_key"), Util.to_base64(@assoc.secret))
      assert(!rfg.call("session_type"))
      assert(!rfg.call("enc_mac_key"))
      assert(!rfg.call("dh_server_public"))
    end

    def test_unsupportedPrefer
      allowed_assoc = 'COLD-PET-RAT'
      allowed_sess = 'FROG-BONES'
      message = 'This is a unit test'

      # Set an OpenID 2 message so answerUnsupported doesn't raise
      # ProtocolError.
      @request.message = Message.new(OPENID2_NS)

      response = @request.answer_unsupported(message,
                                             allowed_assoc,
                                             allowed_sess)
      rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) }
      assert_equal(rfg.call('error_code'), 'unsupported-type')
      assert_equal(rfg.call('assoc_type'), allowed_assoc)
      assert_equal(rfg.call('error'), message)
      assert_equal(rfg.call('session_type'), allowed_sess)
    end

    def test_unsupported
      message = 'This is a unit test'

      # Set an OpenID 2 message so answerUnsupported doesn't raise
      # ProtocolError.
      @request.message = Message.new(OPENID2_NS)

      response = @request.answer_unsupported(message)
      rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) }
      assert_equal(rfg.call('error_code'), 'unsupported-type')
      assert_equal(rfg.call('assoc_type'), nil)
      assert_equal(rfg.call('error'), message)
      assert_equal(rfg.call('session_type'), nil)
    end

    def test_openid1_unsupported_explode
      # answer_unsupported on an associate request should explode if
      # the request was an OpenID 1 request.
      m = Message.new(OPENID1_NS)

      assert_raises(Server::ProtocolError) {
        @request.answer_unsupported(m)
      }
    end
  end

  class UnhandledError < Exception
  end

  class TestServer < Minitest::Test
    include TestUtil

    def setup
      @store = Store::Memory.new()
      @server = Server::Server.new(@store, "http://server.unittest/endpt")
      # catchlogs_setup()
    end

    def test_failed_dispatch
      request = Server::OpenIDRequest.new()
      request.mode = "monkeymode"
      request.message = Message.new(OPENID1_NS)
      assert_raises(RuntimeError) {
        @server.handle_request(request)
      }
    end

    def test_decode_request
      @server.decoder = BogusDecoder.new(@server)
      assert(@server.decode_request({}) == "BOGUS")
    end

    def test_encode_response
      @server.encoder = BogusEncoder.new
      assert(@server.encode_response(nil) == "BOGUS")
    end

    def test_dispatch
      @server.extend(InstanceDefExtension)
      @server.instance_def(:openid_monkeymode) do |request|
        raise UnhandledError
      end

      request = Server::OpenIDRequest.new()
      request.mode = "monkeymode"
      request.message = Message.new(OPENID1_NS)
      assert_raises(UnhandledError) {
        @server.handle_request(request)
      }
    end

    def test_associate
      request = Server::AssociateRequest.from_message(Message.from_post_args({}))
      response = @server.openid_associate(request)
      assert(response.fields.has_key?(OPENID_NS, "assoc_handle"),
             sprintf("No assoc_handle here: %s", response.fields.inspect))
    end

    def test_associate2
      # Associate when the server has no allowed association types
      #
      # Gives back an error with error_code and no fallback session or
      # assoc types.
      @server.negotiator.allowed_types = []

      # Set an OpenID 2 message so answerUnsupported doesn't raise
      # ProtocolError.
      msg = Message.from_post_args({
                                     'openid.ns' => OPENID2_NS,
                                     'openid.session_type' => 'no-encryption',
                                   })

      request = Server::AssociateRequest.from_message(msg)

      response = @server.openid_associate(request)
      assert(response.fields.has_key?(OPENID_NS, "error"))
      assert(response.fields.has_key?(OPENID_NS, "error_code"))
      assert(!response.fields.has_key?(OPENID_NS, "assoc_handle"))
      assert(!response.fields.has_key?(OPENID_NS, "assoc_type"))
      assert(!response.fields.has_key?(OPENID_NS, "session_type"))
    end

    def test_associate3
      # Request an assoc type that is not supported when there are
      # supported types.
      #
      # Should give back an error message with a fallback type.
      @server.negotiator.allowed_types = [['HMAC-SHA256', 'DH-SHA256']]

      msg = Message.from_post_args({
                                     'openid.ns' => OPENID2_NS,
                                     'openid.session_type' => 'no-encryption',
                                   })

      request = Server::AssociateRequest.from_message(msg)
      response = @server.openid_associate(request)

      assert(response.fields.has_key?(OPENID_NS, "error"))
      assert(response.fields.has_key?(OPENID_NS, "error_code"))
      assert(!response.fields.has_key?(OPENID_NS, "assoc_handle"))

      assert_equal(response.fields.get_arg(OPENID_NS, "assoc_type"),
                   'HMAC-SHA256')
      assert_equal(response.fields.get_arg(OPENID_NS, "session_type"),
                   'DH-SHA256')
    end

    def test_associate4
      # DH-SHA256 association session
      @server.negotiator.allowed_types = [['HMAC-SHA256', 'DH-SHA256']]

      query = {
        'openid.dh_consumer_public' =>
        'ALZgnx8N5Lgd7pCj8K86T/DDMFjJXSss1SKoLmxE72kJTzOtG6I2PaYrHX' +
        'xku4jMQWSsGfLJxwCZ6280uYjUST/9NWmuAfcrBfmDHIBc3H8xh6RBnlXJ' +
        '1WxJY3jHd5k1/ZReyRZOxZTKdF/dnIqwF8ZXUwI6peV0TyS/K1fOfF/s',

        'openid.assoc_type' => 'HMAC-SHA256',
        'openid.session_type' => 'DH-SHA256',
      }

      message = Message.from_post_args(query)
      request = Server::AssociateRequest.from_message(message)
      response = @server.openid_associate(request)
      assert(response.fields.has_key?(OPENID_NS, "assoc_handle"))
    end

    def test_no_encryption_openid1
      # Make sure no-encryption associate requests for OpenID 1 are
      # logged.
      assert_log_matches(/Continuing anyway./) {
        m = Message.from_openid_args({
                                       'session_type' => 'no-encryption',
                                     })

        Server::AssociateRequest.from_message(m)
      }
    end

    def test_missingSessionTypeOpenID2
      # Make sure session_type is required in OpenID 2
      msg = Message.from_post_args({
                                     'openid.ns' => OPENID2_NS,
                                   })

      assert_raises(Server::ProtocolError) {
        Server::AssociateRequest.from_message(msg)
      }
    end

    def test_checkAuth
      request = Server::CheckAuthRequest.new('arrrrrf', '0x3999', [])
      request.message = Message.new(OPENID2_NS)
      response = nil
      silence_logging {
        response = @server.openid_check_authentication(request)
      }
      assert(response.fields.has_key?(OPENID_NS, "is_valid"))
    end
  end

  class TestingRequest < Server::OpenIDRequest
    attr_accessor :assoc_handle, :namespace
  end

  class TestSignatory < Minitest::Test
    include TestUtil

    def setup
      @store = Store::Memory.new()
      @signatory = Server::Signatory.new(@store)
      @_dumb_key = @signatory.class._dumb_key
      @_normal_key = @signatory.class._normal_key
      # CatchLogs.setUp(self)
    end

    def test_get_association_nil
      assert_raises(ArgumentError) {
        @signatory.get_association(nil, false)
      }
    end

    def test_sign
      request = TestingRequest.new()
      assoc_handle = '{assoc}{lookatme}'
      @store.store_association(
                               @_normal_key,
                               Association.from_expires_in(60, assoc_handle,
                                                           'sekrit', 'HMAC-SHA1'))
      request.assoc_handle = assoc_handle
      request.namespace = OPENID1_NS
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'foo' => 'amsigned',
                                                   'bar' => 'notsigned',
                                                   'azu' => 'alsosigned',
                                                 })
      sresponse = @signatory.sign(response)
      assert_equal(
            sresponse.fields.get_arg(OPENID_NS, 'assoc_handle'),
            assoc_handle)
      assert_equal(sresponse.fields.get_arg(OPENID_NS, 'signed'),
                   'assoc_handle,azu,bar,foo,signed')
      assert(sresponse.fields.get_arg(OPENID_NS, 'sig'))
      # assert(!@messages, @messages)
    end

    def test_signDumb
      request = TestingRequest.new()
      request.assoc_handle = nil
      request.namespace = OPENID2_NS
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'foo' => 'amsigned',
                                                   'bar' => 'notsigned',
                                                   'azu' => 'alsosigned',
                                                   'ns' => OPENID2_NS,
                                                 })
      sresponse = @signatory.sign(response)
      assoc_handle = sresponse.fields.get_arg(OPENID_NS, 'assoc_handle')
      assert(assoc_handle)
      assoc = @signatory.get_association(assoc_handle, true)
      assert(assoc)
      assert_equal(sresponse.fields.get_arg(OPENID_NS, 'signed'),
                   'assoc_handle,azu,bar,foo,ns,signed')
      assert(sresponse.fields.get_arg(OPENID_NS, 'sig'))
      # assert(!@messages, @messages)
    end

    def test_signExpired
      # Sign a response to a message with an expired handle (using
      # invalidate_handle).
      #
      # From "Verifying with an Association":
      #
      #   If an authentication request included an association handle
      #   for an association between the OP and the Relying party, and
      #   the OP no longer wishes to use that handle (because it has
      #   expired or the secret has been compromised, for instance),
      #   the OP will send a response that must be verified directly
      #   with the OP, as specified in Section 11.3.2. In that
      #   instance, the OP will include the field
      #   "openid.invalidate_handle" set to the association handle
      #   that the Relying Party included with the original request.
      request = TestingRequest.new()
      request.namespace = OPENID2_NS
      assoc_handle = '{assoc}{lookatme}'
      @store.store_association(
                               @_normal_key,
                               Association.from_expires_in(-10, assoc_handle,
                                                           'sekrit', 'HMAC-SHA1'))
      assert(@store.get_association(@_normal_key, assoc_handle))

      request.assoc_handle = assoc_handle
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'foo' => 'amsigned',
                                                   'bar' => 'notsigned',
                                                   'azu' => 'alsosigned',
                                                 })
      sresponse = nil
      silence_logging {
        sresponse = @signatory.sign(response)
      }

      new_assoc_handle = sresponse.fields.get_arg(OPENID_NS, 'assoc_handle')
      assert(new_assoc_handle)
      assert(new_assoc_handle != assoc_handle)

      assert_equal(
            sresponse.fields.get_arg(OPENID_NS, 'invalidate_handle'),
            assoc_handle)

      assert_equal(sresponse.fields.get_arg(OPENID_NS, 'signed'),
                   'assoc_handle,azu,bar,foo,invalidate_handle,signed')
      assert(sresponse.fields.get_arg(OPENID_NS, 'sig'))

      # make sure the expired association is gone
      assert(!@store.get_association(@_normal_key, assoc_handle),
             "expired association is still retrievable.")

      # make sure the new key is a dumb mode association
      assert(@store.get_association(@_dumb_key, new_assoc_handle))
      assert(!@store.get_association(@_normal_key, new_assoc_handle))
      # assert(@messages)
    end

    def test_signInvalidHandle
      request = TestingRequest.new()
      request.namespace = OPENID2_NS
      assoc_handle = '{bogus-assoc}{notvalid}'

      request.assoc_handle = assoc_handle
      response = Server::OpenIDResponse.new(request)
      response.fields = Message.from_openid_args({
                                                   'foo' => 'amsigned',
                                                   'bar' => 'notsigned',
                                                   'azu' => 'alsosigned',
                                                 })
      sresponse = @signatory.sign(response)

      new_assoc_handle = sresponse.fields.get_arg(OPENID_NS, 'assoc_handle')
      assert(new_assoc_handle)
      assert(new_assoc_handle != assoc_handle)

      assert_equal(
            sresponse.fields.get_arg(OPENID_NS, 'invalidate_handle'),
            assoc_handle)

      assert_equal(
            sresponse.fields.get_arg(OPENID_NS, 'signed'),
                   'assoc_handle,azu,bar,foo,invalidate_handle,signed')
      assert(sresponse.fields.get_arg(OPENID_NS, 'sig'))

      # make sure the new key is a dumb mode association
      assert(@store.get_association(@_dumb_key, new_assoc_handle))
      assert(!@store.get_association(@_normal_key, new_assoc_handle))
      # @failIf(@messages, @messages)
    end

    def test_verify
      assoc_handle = '{vroom}{zoom}'
      assoc = Association.from_expires_in(
            60, assoc_handle, 'sekrit', 'HMAC-SHA1')

      @store.store_association(@_dumb_key, assoc)

      signed = Message.from_post_args({
                                        'openid.foo' => 'bar',
                                        'openid.apple' => 'orange',
                                        'openid.assoc_handle' => assoc_handle,
                                        'openid.signed' => 'apple,assoc_handle,foo,signed',
                                        'openid.sig' => 'uXoT1qm62/BB09Xbj98TQ8mlBco=',
                                      })

      verified = @signatory.verify(assoc_handle, signed)
      assert(verified)
      # assert(!@messages, @messages)
    end

    def test_verifyBadSig
      assoc_handle = '{vroom}{zoom}'
      assoc = Association.from_expires_in(
            60, assoc_handle, 'sekrit', 'HMAC-SHA1')

      @store.store_association(@_dumb_key, assoc)

      signed = Message.from_post_args({
            'openid.foo' => 'bar',
            'openid.apple' => 'orange',
            'openid.assoc_handle' => assoc_handle,
            'openid.signed' => 'apple,assoc_handle,foo,signed',
            'openid.sig' => 'uXoT1qm62/BB09Xbj98TQ8mlBco=BOGUS'
            })

      verified = @signatory.verify(assoc_handle, signed)
      # @failIf(@messages, @messages)
      assert(!verified)
    end

    def test_verifyBadHandle
      assoc_handle = '{vroom}{zoom}'
      signed = Message.from_post_args({
            'foo' => 'bar',
            'apple' => 'orange',
            'openid.sig' => "Ylu0KcIR7PvNegB/K41KpnRgJl0=",
            })

      verified = nil
      silence_logging {
        verified = @signatory.verify(assoc_handle, signed)
      }

      assert !verified
    end

    def test_verifyAssocMismatch
      # Attempt to validate sign-all message with a signed-list assoc.
      assoc_handle = '{vroom}{zoom}'
      assoc = Association.from_expires_in(
            60, assoc_handle, 'sekrit', 'HMAC-SHA1')

      @store.store_association(@_dumb_key, assoc)

      signed = Message.from_post_args({
            'foo' => 'bar',
            'apple' => 'orange',
            'openid.sig' => "d71xlHtqnq98DonoSgoK/nD+QRM=",
            })

      verified = nil
      silence_logging {
        verified = @signatory.verify(assoc_handle, signed)
      }

      assert !verified
    end

    def test_getAssoc
      assoc_handle = makeAssoc(true)
      assoc = @signatory.get_association(assoc_handle, true)
      assert assoc
      assert_equal assoc.handle, assoc_handle
    end

    def test_getAssocExpired
      assoc_handle = makeAssoc(true, -10)
      assoc = nil
      silence_logging {
        assoc = @signatory.get_association(assoc_handle, true)
      }
      assert !assoc
    end

    def test_getAssocInvalid
      ah = 'no-such-handle'
      silence_logging {
        assert_equal(
                     @signatory.get_association(ah, false), nil)
      }
      # assert(!@messages, @messages)
    end

    def test_getAssocDumbVsNormal
      # getAssociation(dumb=False) cannot get a dumb assoc
      assoc_handle = makeAssoc(true)
      silence_logging {
        assert_equal(
                     @signatory.get_association(assoc_handle, false), nil)
      }
      # @failIf(@messages, @messages)
    end

    def test_getAssocNormalVsDumb
      # getAssociation(dumb=True) cannot get a shared assoc
      #
      # From "Verifying Directly with the OpenID Provider"::
      #
      #   An OP MUST NOT verify signatures for associations that have shared
      #   MAC keys.
      assoc_handle = makeAssoc(false)
      silence_logging {
        assert_equal(
                     @signatory.get_association(assoc_handle, true), nil)
      }
      # @failIf(@messages, @messages)
    end

    def test_createAssociation
      assoc = @signatory.create_association(false)
      silence_logging {
        assert(@signatory.get_association(assoc.handle, false))
      }
      # @failIf(@messages, @messages)
    end

    def makeAssoc(dumb, lifetime=60)
      assoc_handle = '{bling}'
      assoc = Association.from_expires_in(lifetime, assoc_handle,
                                          'sekrit', 'HMAC-SHA1')

      silence_logging {
        @store.store_association(((dumb and @_dumb_key) or @_normal_key), assoc)
      }

      return assoc_handle
    end

    def test_invalidate
      assoc_handle = '-squash-'
      assoc = Association.from_expires_in(60, assoc_handle,
                                          'sekrit', 'HMAC-SHA1')

      silence_logging {
        @store.store_association(@_dumb_key, assoc)
        assoc = @signatory.get_association(assoc_handle, true)
        assert(assoc)
        assoc = @signatory.get_association(assoc_handle, true)
        assert(assoc)
        @signatory.invalidate(assoc_handle, true)
        assoc = @signatory.get_association(assoc_handle, true)
        assert(!assoc)
      }
      # @failIf(@messages, @messages)
    end
  end

  class RunthroughTestCase < Minitest::Test
    def setup
      @store = Store::Memory.new
      @server = Server::Server.new(@store, "http://example.com/openid/server")
    end

    def test_openid1_assoc_checkid
      assoc_args = {'openid.mode' => 'associate',
                    'openid.assoc_type' => 'HMAC-SHA1'}
      areq = @server.decode_request(assoc_args)
      aresp = @server.handle_request(areq)

      amess = aresp.fields
      assert(amess.is_openid1)
      ahandle = amess.get_arg(OPENID_NS, 'assoc_handle')
      assert(ahandle)
      assoc = @store.get_association('http://localhost/|normal', ahandle)
      assert(assoc.is_a?(Association))


      checkid_args = {'openid.mode' => 'checkid_setup',
                      'openid.return_to' => 'http://example.com/openid/consumer',
                      'openid.assoc_handle' => ahandle,
                      'openid.identity' => 'http://foo.com/'}

      cireq = @server.decode_request(checkid_args)
      ciresp = cireq.answer(true)

      signed_resp = @server.signatory.sign(ciresp)

      assert_equal(assoc.get_message_signature(signed_resp.fields),
                   signed_resp.fields.get_arg(OPENID_NS, 'sig'))

      assert(assoc.check_message_signature(signed_resp.fields))
    end

  end
end
