require "minitest/autorun"
require "openid/association"
require "openid/protocolerror"

module OpenID
  class AssociationTestCase < Minitest::Test
    def setup
      # Use this funny way of getting a time so that it does not have
      # fractional seconds, and so can be serialized exactly using our
      # standard code.
      issued = Time.at(Time.now.to_i)
      lifetime = 600

      @assoc = Association.new('handle', 'secret', issued,
                               lifetime, 'HMAC-SHA1')
    end

    def test_round_trip
      assoc2 = Association.deserialize(@assoc.serialize())
      [:handle, :secret, :lifetime, :assoc_type].each do |attr|
        assert_equal(@assoc.send(attr), assoc2.send(attr))
      end
    end

    def test_deserialize_failure
      field_list = Util.kv_to_seq(@assoc.serialize)
      kv = Util.seq_to_kv(field_list + [['monkeys', 'funny']])
      assert_raises(ProtocolError) {
        Association.deserialize(kv)
      }

      bad_version_list = field_list.dup
      bad_version_list[0] = ['version', 'moon']
      bad_version_kv = Util.seq_to_kv(bad_version_list)
      assert_raises(ProtocolError) {
        Association.deserialize(bad_version_kv)
      }
    end

    def test_serialization_identity
      assoc2 = Association.deserialize(@assoc.serialize)
      assert_equal(@assoc, assoc2)
    end

    def test_expires_in
      # Allow one second of slop
      assert(@assoc.expires_in.between?(599,600))
      assert(@assoc.expires_in(Time.now.to_i).between?(599,600))
      assert_equal(0,@assoc.expires_in(Time.now.to_i + 10000),"negative expires_in")
    end

    def test_from_expires_in
      start_time = Time.now
      expires_in = @assoc.expires_in
      assoc = Association.from_expires_in(expires_in,
                                          @assoc.handle,
                                          @assoc.secret,
                                          @assoc.assoc_type)

      # Allow one second of slop here for code execution time
      assert_in_delta(1, assoc.expires_in, @assoc.expires_in)
      [:handle, :secret, :assoc_type].each do |attr|
        assert_equal(@assoc.send(attr), assoc.send(attr))
      end

      # Make sure the issued time is near the start
      assert(assoc.issued >= start_time)
      assert_in_delta(1, assoc.issued.to_f, start_time.to_f)
    end

    def test_sign_sha1
      pairs = [['key1', 'value1'],
               ['key2', 'value2']]

      [['HMAC-SHA256', "\xfd\xaa\xfe;\xac\xfc*\x988\xad\x05d6-\xeaVy\xd5\xa5Z.<\xa9\xed\x18\x82\\$\x95x\x1c&"],
       ['HMAC-SHA1', "\xe0\x1bv\x04\xf1G\xc0\xbb\x7f\x9a\x8b\xe9\xbc\xee}\\\xe5\xbb7*"],
      ].each do |assoc_type, expected|
        assoc = Association.from_expires_in(3600, "handle", 'very_secret', assoc_type)
        sig = assoc.sign(pairs)
        assert_equal(expected.force_encoding("UTF-8"), sig.force_encoding("UTF-8"))

        m = Message.new(OPENID2_NS)
        pairs.each { |k, v|
          m.set_arg(OPENID_NS, k, v)
        }
        m.set_arg(BARE_NS, "not_an_openid_arg", "bogus")

        signed_m = assoc.sign_message(m)
        assert(signed_m.has_key?(OPENID_NS, 'sig'))
        assert_equal(signed_m.get_arg(OPENID_NS, 'signed'),
                     'assoc_handle,key1,key2,ns,signed')
      end
    end

    def test_sign_message_with_sig
      assoc = Association.from_expires_in(3600, "handle", "very_secret",
                                          "HMAC-SHA1")
      m = Message.new(OPENID2_NS)
      m.set_arg(OPENID_NS, 'sig', 'noise')
      assert_raises(ArgumentError) {
        assoc.sign_message(m)
      }
    end

    def test_sign_message_with_signed
      assoc = Association.from_expires_in(3600, "handle", "very_secret",
                                          "HMAC-SHA1")
      m = Message.new(OPENID2_NS)
      m.set_arg(OPENID_NS, 'signed', 'fields')
      assert_raises(ArgumentError) {
        assoc.sign_message(m)
      }
    end

    def test_sign_different_assoc_handle
      assoc = Association.from_expires_in(3600, "handle", "very_secret",
                                          "HMAC-SHA1")
      m = Message.new(OPENID2_NS)
      m.set_arg(OPENID_NS, 'assoc_handle', 'different')
      assert_raises(ArgumentError) {
        assoc.sign_message(m)
      }
    end

    def test_sign_bad_assoc_type
      @assoc.instance_eval { @assoc_type = 'Cookies' }
      assert_raises(ProtocolError) {
        @assoc.sign([])
      }
    end

    def test_make_pairs
      msg = Message.new(OPENID2_NS)
      msg.update_args(OPENID2_NS, {
                        'mode' => 'id_res',
                        'identifier' => '=example',
                        'signed' => 'identifier,mode',
                        'sig' => 'cephalopod',
                      })
      msg.update_args(BARE_NS, {'xey' => 'value'})
      assoc = Association.from_expires_in(3600, '{sha1}', 'very_secret',
                                          "HMAC-SHA1")
      pairs = assoc.make_pairs(msg)
      assert_equal([['identifier', '=example'],
                    ['mode', 'id_res']], pairs)
    end

    def test_check_message_signature_no_signed
      m = Message.new(OPENID2_NS)
      m.update_args(OPENID2_NS, {'mode' => 'id_res',
                      'identifier' => '=example',
                      'sig' => 'coyote',
                    })
      assoc = Association.from_expires_in(3600, '{sha1}', 'very_secret',
                                          "HMAC-SHA1")
      assert_raises(ProtocolError) {
        assoc.check_message_signature(m)
      }
    end

    def test_check_message_signature_no_sig
      m = Message.new(OPENID2_NS)
      m.update_args(OPENID2_NS, {'mode' => 'id_res',
                      'identifier' => '=example',
                      'signed' => 'mode',
                    })
      assoc = Association.from_expires_in(3600, '{sha1}', 'very_secret',
                                          "HMAC-SHA1")
      assert_raises(ProtocolError) {
        assoc.check_message_signature(m)
      }
    end

    def test_check_message_signature_bad_sig
      m = Message.new(OPENID2_NS)
      m.update_args(OPENID2_NS, {'mode' => 'id_res',
                      'identifier' => '=example',
                      'signed' => 'mode',
                      'sig' => Util.to_base64('coyote'),
                    })
      assoc = Association.from_expires_in(3600, '{sha1}', 'very_secret',
                                          "HMAC-SHA1")
      assert(!assoc.check_message_signature(m))
    end

    def test_check_message_signature_good_sig
      m = Message.new(OPENID2_NS)
      m.update_args(OPENID2_NS, {'mode' => 'id_res',
                      'identifier' => '=example',
                      'signed' => 'mode',
                      'sig' => Util.to_base64('coyote'),
                    })
      assoc = Association.from_expires_in(3600, '{sha1}', 'very_secret',
                                          "HMAC-SHA1")
      class << assoc
        # Override sign, because it's already tested elsewhere
        def sign(pairs)
          "coyote"
        end
      end

      assert(assoc.check_message_signature(m))
    end
  end

  class AssociationNegotiatorTestCase < Minitest::Test
    def assert_equal_under(item1, item2)
      val1 = yield(item1)
      val2 = yield(item2)
      assert_equal(val1, val2)
    end

    def test_copy
      neg = AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1']])
      neg2 = neg.copy
      assert_equal_under(neg, neg2) {|n| n.instance_eval{@allowed_types} }
      assert(neg.object_id != neg2.object_id)
    end

    def test_add_allowed
      neg = AssociationNegotiator.new([])
      assert(!neg.allowed?('HMAC-SHA1', 'DH-SHA1'))
      assert(!neg.allowed?('HMAC-SHA1', 'no-encryption'))
      assert(!neg.allowed?('HMAC-SHA256', 'DH-SHA256'))
      assert(!neg.allowed?('HMAC-SHA256', 'no-encryption'))
      neg.add_allowed_type('HMAC-SHA1')
      assert(neg.allowed?('HMAC-SHA1', 'DH-SHA1'))
      assert(neg.allowed?('HMAC-SHA1', 'no-encryption'))
      assert(!neg.allowed?('HMAC-SHA256', 'DH-SHA256'))
      assert(!neg.allowed?('HMAC-SHA256', 'no-encryption'))
      neg.add_allowed_type('HMAC-SHA256', 'DH-SHA256')
      assert(neg.allowed?('HMAC-SHA1', 'DH-SHA1'))
      assert(neg.allowed?('HMAC-SHA1', 'no-encryption'))
      assert(neg.allowed?('HMAC-SHA256', 'DH-SHA256'))
      assert(!neg.allowed?('HMAC-SHA256', 'no-encryption'))
      assert_equal(neg.get_allowed_type, ['HMAC-SHA1', 'DH-SHA1'])
    end

    def test_bad_assoc_type
      assert_raises(ProtocolError) {
        AssociationNegotiator.new([['OMG', 'Ponies']])
      }
    end

    def test_bad_session_type
      assert_raises(ProtocolError) {
        AssociationNegotiator.new([['HMAC-SHA1', 'OMG-Ponies']])
      }
    end

    def test_default_negotiator
      assert_equal(DefaultNegotiator.get_allowed_type,
                   ['HMAC-SHA1', 'DH-SHA1'])
      assert(DefaultNegotiator.allowed?('HMAC-SHA256', 'no-encryption'))
    end

    def test_encrypted_negotiator
      assert_equal(EncryptedNegotiator.get_allowed_type,
                   ['HMAC-SHA1', 'DH-SHA1'])
      assert(!EncryptedNegotiator.allowed?('HMAC-SHA256', 'no-encryption'))
    end
  end
end
