# encoding: UTF-8

require 'test_helper'

describe Vines::Stream::SASL do
  subject       { Vines::Stream::SASL.new(stream) }
  let(:stream)  { MiniTest::Mock.new }
  let(:storage) { MiniTest::Mock.new }
  let(:romeo)   { Vines::User.new(jid: 'romeo@verona.lit') }

  before do
    def subject.log
      Class.new do
        def method_missing(*args)
          # do nothing
        end
      end.new
    end
  end

  describe '#plain_auth' do
    before do
      stream.expect :domain, 'verona.lit'
    end

    it 'fails with empty input' do
      -> { subject.plain_auth(nil) }.must_raise Vines::SaslErrors::IncorrectEncoding
      -> { subject.plain_auth('') }.must_raise Vines::SaslErrors::NotAuthorized
    end

    it 'fails with plain text' do
      -> { subject.plain_auth('bogus') }.must_raise Vines::SaslErrors::IncorrectEncoding
    end

    it 'fails with incorrectly encoded base64 text' do
      -> { subject.plain_auth('=dmVyb25hLmxpdA==') }.must_raise Vines::SaslErrors::IncorrectEncoding
      -> { subject.plain_auth("dmVyb25hLmxpdA==\n") }.must_raise Vines::SaslErrors::IncorrectEncoding
    end

    it 'fails when authzid does not match authcid username' do
      encoded = Base64.strict_encode64("juliet@verona.lit\x00romeo\x00secr3t")
      -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::InvalidAuthzid
      stream.verify
    end

    it 'fails when authzid does not match authcid domain' do
      encoded = Base64.strict_encode64("romeo@wonderland.lit\x00romeo\x00secr3t")
      -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::InvalidAuthzid
      stream.verify
    end

    it 'fails when username and password are missing' do
      encoded = Base64.strict_encode64("\x00\x00")
      -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
    end

    it 'fails when username is missing' do
      encoded = Base64.strict_encode64("\x00\x00secr3t")
      -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
    end

    it 'fails when password is missing with delimiter' do
      encoded = Base64.strict_encode64("\x00romeo\x00")
      -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
    end

    it 'fails when password is missing' do
      encoded = Base64.strict_encode64("\x00romeo")
      -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
    end

    it 'fails with invalid jid' do
      encoded = Base64.strict_encode64("\x00#{'a' * 1024}\x00secr3t")
      -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
      stream.verify
    end

    it 'fails with invalid password' do
      storage.expect :authenticate, nil, [romeo.jid, 'secr3t']
      stream.expect :storage, storage

      encoded = Base64.strict_encode64("\x00romeo\x00secr3t")
      -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized

      stream.verify
      storage.verify
    end

    it 'passes with valid password' do
      storage.expect :authenticate, romeo, [romeo.jid, 'secr3t']
      stream.expect :storage, storage

      encoded = Base64.strict_encode64("\x00romeo\x00secr3t")
      subject.plain_auth(encoded).must_equal romeo

      stream.verify
      storage.verify
    end

    it 'passes with valid password and unicode jid' do
      user = Vines::User.new(jid: 'piñata@verona.lit')
      storage.expect :authenticate, user, [user.jid, 'secr3t']
      stream.expect :storage, storage

      encoded = Base64.strict_encode64("\x00piñata\x00secr3t")
      subject.plain_auth(encoded).must_equal user

      stream.verify
      storage.verify
    end

    it 'passes with valid password and authzid provided by strophe and blather' do
      storage.expect :authenticate, romeo, [romeo.jid, 'secr3t']
      stream.expect :storage, storage

      encoded = Base64.strict_encode64("romeo@Verona.LIT\x00romeo\x00secr3t")
      subject.plain_auth(encoded).must_equal romeo

      stream.verify
      storage.verify
    end

    it 'passes with valid password and authzid provided by smack' do
      storage.expect :authenticate, romeo, [romeo.jid, 'secr3t']
      stream.expect :storage, storage

      encoded = Base64.strict_encode64("romeo\x00romeo\x00secr3t")
      subject.plain_auth(encoded).must_equal romeo

      stream.verify
      storage.verify
    end

    it 'raises temporary-auth-failure when storage backend fails' do
      storage = Class.new do
        def authenticate(*args)
          raise 'boom'
        end
      end.new

      stream.expect :storage, storage
      encoded = Base64.strict_encode64("\x00romeo\x00secr3t")
      -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::TemporaryAuthFailure
      stream.verify
    end
  end

  describe '#external_auth' do
    it 'fails with empty input' do
      stream.expect :remote_domain, 'verona.lit'
      -> { subject.external_auth(nil) }.must_raise Vines::SaslErrors::IncorrectEncoding
      -> { subject.external_auth('') }.must_raise Vines::SaslErrors::InvalidAuthzid
      stream.verify
    end

    it 'fails with plain text' do
      -> { subject.external_auth('bogus') }.must_raise Vines::SaslErrors::IncorrectEncoding
      stream.verify
    end

    it 'fails with incorrectly encoded base64 text' do
      -> { subject.external_auth('=dmVyb25hLmxpdA==') }.must_raise Vines::SaslErrors::IncorrectEncoding
      -> { subject.external_auth("dmVyb25hLmxpdA==\n") }.must_raise Vines::SaslErrors::IncorrectEncoding
      stream.verify
    end

    it 'passes with empty authzid and matching cert' do
      stream.expect :remote_domain, 'verona.lit'
      stream.expect :cert_domain_matches?, true, ['verona.lit']
      subject.external_auth('=').must_equal true
      stream.verify
    end

    it 'fails with empty authzid and non-matching cert' do
      stream.expect :remote_domain, 'verona.lit'
      stream.expect :cert_domain_matches?, false, ['verona.lit']
      -> { subject.external_auth('=') }.must_raise Vines::SaslErrors::NotAuthorized
      stream.verify
    end

    it 'fails when authzid does not match stream from address' do
      stream.expect :remote_domain, 'not.verona.lit'
      -> { subject.external_auth('dmVyb25hLmxpdA==') }.must_raise Vines::SaslErrors::InvalidAuthzid
      stream.verify
    end

    it 'passes when authzid matches stream from address' do
      stream.expect :remote_domain, 'verona.lit'
      stream.expect :remote_domain, 'verona.lit'
      stream.expect :cert_domain_matches?, true, ['verona.lit']
      subject.external_auth('dmVyb25hLmxpdA==').must_equal true
      stream.verify
    end
  end
end
