require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
describe 'Blather::Stream::Client' do
class MockServer; end
module ServerMock
def receive_data(data)
@server ||= MockServer.new
@server.receive_data data, self
end
end
def mocked_server(times = nil, &block)
@client ||= mock()
@client.stubs(:stopped) unless @client.respond_to?(:stopped)
@client.stubs(:jid=) unless @client.respond_to?(:jid=)
MockServer.any_instance.expects(:receive_data).send(*(times ? [:times, times] : [:at_least, 1])).with &block
EventMachine::run {
# Mocked server
EventMachine::start_server '127.0.0.1', 12345, ServerMock
# Stream connection
EM.connect('127.0.0.1', 12345, Stream::Client, @client, @jid || JID.new('n@d/r'), 'pass') { |c| @stream = c }
}
end
it 'can be started' do
client = mock()
params = [client, 'n@d/r', 'pass', 'host', 1234]
EM.expects(:connect).with do |*parms|
parms[0] == 'host' &&
parms[1] == 1234 &&
parms[3] == client &&
parms[5] == 'pass' &&
parms[4] == JID.new('n@d/r')
end
Stream::Client.start *(params)
end
it 'can figure out the host to use based on the jid' do
client = mock()
params = [client, 'n@d/r', 'pass', 'd', 5222]
EM.expects(:connect).with do |*parms|
parms[0] == 'd' &&
parms[1] == 5222 &&
parms[3] == client &&
parms[5] == 'pass' &&
parms[4] == JID.new('n@d/r')
end
Stream::Client.start client, 'n@d/r', 'pass'
end
it 'starts the stream once the connection is complete' do
mocked_server(1) { |val, _| EM.stop; val.must_match(/stream:stream/) }
end
it 'sends stanzas to the client when the stream is ready' do
@client = mock()
@client.expects(:call).with do |n|
EM.stop
n.kind_of?(Stanza::Message) && @stream.ready?.must_equal(true)
end
mocked_server(1) do |val, server|
val.must_match(/stream:stream/)
server.send_data ""
server.send_data "Message!"
end
end
it 'puts itself in the stopped state and calls @client.stopped when stopped' do
@client = mock()
@client.expects(:stopped).at_least_once
started = false
mocked_server(2) do |val, server|
if !started
started = true
server.send_data ""
server.send_data ""
val.must_match(/stream:stream/)
else
EM.stop
@stream.stopped?.must_equal false
@stream.unbind
@stream.stopped?.must_equal true
end
end
end
it 'will be in the negotiating state during feature negotiations' do
state = nil
@client = mock()
@client.stubs(:stream_started)
@client.expects(:call).with do |n|
EM.stop
state.must_equal(:negotiated) && @stream.negotiating?.must_equal(false)
end
mocked_server(2) do |val, server|
case state
when nil
state = :started
server.send_data ""
server.send_data ""
true
when :started
state = :negotiated
@stream.negotiating?.must_equal(true)
server.send_data ""
server.send_data "Message!"
true
else
EM.stop
false
end
end
end
it 'stops when sent ' do
state = nil
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :stopped
server.send_data ''
@stream.stopped?.must_equal false
when :stopped
EM.stop
@stream.stopped?.must_equal true
val.must_equal ''
else
EM.stop
false
end
end
end
it 'sends client an error on stream:error' do
@client = mock()
@client.expects(:call).with do |v|
v.must_be_instance_of(StreamError::Conflict)
v.text.must_equal 'Already signed in'
v.to_s.must_equal "Stream Error (conflict): #{v.text}"
end
state = nil
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :stopped
server.send_data ""
server.send_data "Already signed in"
when :stopped
EM.stop
val.must_equal ""
else
EM.stop
false
end
end
end
it 'starts TLS when asked' do
state = nil
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :tls
@stream.expects(:start_tls)
server.send_data ""
val.must_match(/starttls/)
when :tls
EM.stop
true
else
EM.stop
false
end
end
end
it 'will fail if TLS negotiation fails' do
state = nil
@client = mock()
@client.expects(:call).with { |v| v.must_be_kind_of TLSFailure }
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :tls
@stream.expects(:start_tls).never
server.send_data ""
val.must_match(/starttls/)
when :tls
EM.stop
val.must_equal ""
else
EM.stop
false
end
end
end
it 'will fail if a bad node comes through TLS negotiations' do
state = nil
@client = mock()
@client.expects(:call).with do |v|
v.must_be_kind_of UnknownResponse
v.node.element_name.must_equal 'foo-bar'
end
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :tls
@stream.expects(:start_tls).never
server.send_data ""
val.must_match(/starttls/)
when :tls
EM.stop
val.must_equal ""
else
EM.stop
false
end
end
end
it 'connects via SASL MD5 when asked' do
Time.any_instance.stubs(:to_f).returns(1.1)
state = nil
mocked_server(5) do |val, server|
case state
when nil
state = :started
server.send_data "DIGEST-MD5"
val.must_match(/stream:stream/)
when :started
state = :auth_sent
server.send_data "cmVhbG09InNvbWVyZWFsbSIsbm9uY2U9Ik9BNk1HOXRFUUdtMmhoIixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNzCg=="
val.must_match(/auth.*DIGEST\-MD5/)
when :auth_sent
state = :response1_sent
server.send_data "cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZAo="
val.must_equal('bm9uY2U9Ik9BNk1HOXRFUUdtMmhoIixjaGFyc2V0PXV0Zi04LHVzZXJuYW1lPSJuIixyZWFsbT0ic29tZXJlYWxtIixjbm9uY2U9Ijc3N2Q0NWJiYmNkZjUwZDQ5YzQyYzcwYWQ3YWNmNWZlIixuYz0wMDAwMDAwMSxxb3A9YXV0aCxkaWdlc3QtdXJpPSJ4bXBwL2QiLHJlc3BvbnNlPTZiNTlhY2Q1ZWJmZjhjZTA0NTYzMGFiMDU2Zjg3MTdm')
when :response1_sent
state = :response2_sent
server.send_data ""
val.must_match(%r{})
when :response2_sent
EM.stop
state = :complete
val.must_match(/stream:stream/)
else
EM.stop
false
end
end
end
it 'will connect via SSL PLAIN when asked' do
state = nil
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data "PLAIN"
val.must_match(/stream:stream/)
when :started
state = :auth_sent
server.send_data ""
val.must_equal('bkBkAG4AcGFzcw==')
when :auth_sent
EM.stop
state = :complete
val.must_match(/stream:stream/)
else
EM.stop
false
end
end
end
it 'will connect via SSL ANONYMOUS when asked' do
state = nil
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data "ANONYMOUS"
val.must_match(/stream:stream/)
when :started
state = :auth_sent
server.send_data ""
val.must_equal('bg==')
when :auth_sent
EM.stop
state = :complete
val.must_match(/stream:stream/)
else
EM.stop
false
end
end
end
it 'tried each possible mechanism until it fails completely' do
state = nil
@client = mock()
@client.expects(:call).with do |n|
n.must_be_kind_of(SASLError)
n.must_be_instance_of SASLError::NotAuthorized
end
mocked_server(5) do |val, server|
case state
when nil
state = :started
server.send_data "DIGEST-MD5PLAINANONYMOUS"
val.must_match(/stream:stream/)
when :started
state = :failed_md5
server.send_data ""
val.must_match(/mechanism="DIGEST-MD5"/)
when :failed_md5
state = :failed_plain
server.send_data ""
val.must_match(/mechanism="PLAIN"/)
when :failed_plain
state = :failed_anon
server.send_data ""
val.must_match(/mechanism="ANONYMOUS"/)
when :failed_anon
EM.stop
state = :complete
val.must_match(/\/stream:stream/)
else
EM.stop
false
end
end
end
it 'tries each mechanism until it succeeds' do
state = nil
mocked_server(4) do |val, server|
case state
when nil
state = :started
server.send_data "DIGEST-MD5PLAINANONYMOUS"
val.must_match(/stream:stream/)
when :started
state = :failed_md5
server.send_data ""
val.must_match(/mechanism="DIGEST-MD5"/)
when :failed_md5
state = :plain_sent
server.send_data ""
val.must_match(/mechanism="PLAIN"/)
when :plain_sent
EM.stop
val.must_match(/stream:stream/)
else
EM.stop
false
end
end
end
it 'sends client an error when an unknown mechanism is sent' do
@client = mock()
@client.expects(:call).with { |v| v.must_be_kind_of(Stream::SASL::UnknownMechanism) }
started = false
mocked_server(2) do |val, server|
if !started
started = true
server.send_data ""
server.send_data "UNKNOWN"
val.must_match(/stream:stream/)
else
EM.stop
val.must_match(/failure(.*)invalid\-mechanism/)
end
end
end
%w[ aborted
incorrect-encoding
invalid-authzid
invalid-mechanism
mechanism-too-weak
not-authorized
temporary-auth-failure
].each do |error_type|
it "fails on #{error_type}" do
@client = mock()
@client.expects(:call).with do |n|
n.must_be_instance_of SASLError.class_from_registration(error_type)
end
state = nil
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data "PLAIN"
val.must_match(/stream:stream/)
when :started
state = :auth_sent
server.send_data "<#{error_type} />"
val.must_equal('bkBkAG4AcGFzcw==')
when :auth_sent
EM.stop
state = :complete
val.must_match(/\/stream:stream/)
else
EM.stop
false
end
end
end
end
it 'fails when an unkown node comes through during SASL negotiation' do
@client = mock()
@client.expects(:call).with do |n|
n.must_be_instance_of UnknownResponse
n.node.element_name.must_equal 'foo-bar'
end
state = nil
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data "PLAIN"
val.must_match(/stream:stream/)
when :started
state = :auth_sent
server.send_data ""
val.must_equal('bkBkAG4AcGFzcw==')
when :auth_sent
EM.stop
state = :complete
val.must_match(/\/stream:stream/)
else
EM.stop
false
end
end
end
it 'will bind to a resource set by the server' do
state = nil
class Client; attr_accessor :jid; end
@client = Client.new
@jid = JID.new('n@d')
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :complete
val =~ %r{]+id="([^"]+)"}
server.send_data "#{@jid}/server_resource"
server.send_data ""
val.must_match(%r{})
when :complete
EM.stop
@client.jid.must_equal JID.new('n@d/server_resource')
else
EM.stop
false
end
end
end
it 'will bind to a resource set by the client' do
state = nil
class Client; attr_accessor :jid; end
@client = Client.new
@jid = JID.new('n@d/r')
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :complete
val =~ %r{]+id="([^"]+)"}
server.send_data "#{@jid}"
server.send_data ""
val.must_match(%r{r})
when :complete
EM.stop
@client.jid.must_equal JID.new('n@d/r')
else
EM.stop
false
end
end
end
it 'will return an error if resource binding errors out' do
state = nil
@client = mock()
@client.expects(:call).with do |n|
n.must_be_instance_of StanzaError::BadRequest
end
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :complete
val =~ %r{]+id="([^"]+)"}
server.send_data "r"
val.must_match(%r{r})
when :complete
EM.stop
val.must_match(/\/stream:stream/)
else
EM.stop
false
end
end
end
it 'will return an error if an unkown node comes through during resouce binding' do
state = nil
@client = mock()
@client.expects(:call).with do |n|
n.must_be_instance_of UnknownResponse
n.node.element_name.must_equal 'foo-bar'
end
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :complete
val =~ %r{]+id="([^"]+)"}
server.send_data ""
val.must_match(%r{r})
when :complete
EM.stop
val.must_match(/\/stream:stream/)
else
EM.stop
false
end
end
end
it 'will establish a session if requested' do
state = nil
@client = mock()
@client.expects(:stream_started)
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :completed
server.send_data ""
server.send_data ""
val.must_match(%r{})
when :completed
EM.stop
true
else
EM.stop
false
end
end
end
it 'will return an error if session establishment errors out' do
state = nil
@client = mock()
@client.expects(:call).with do |n|
n.must_be_instance_of StanzaError::InternalServerError
end
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :completed
server.send_data ""
val.must_match(%r{})
when :completed
EM.stop
val.must_match(/\/stream:stream/)
else
EM.stop
false
end
end
end
it 'will return an error if an unknown node come through during session establishment' do
state = nil
@client = mock()
@client.expects(:call).with do |n|
n.must_be_instance_of UnknownResponse
n.node.element_name.must_equal 'foo-bar'
end
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :completed
server.send_data ''
val.must_match(%r{})
when :completed
EM.stop
val.must_match(/\/stream:stream/)
else
EM.stop
false
end
end
end
it 'sends client an error on parse error' do
@client = mock()
@client.expects(:call).with do |v|
v.must_be_kind_of ParseError
v.message.must_match(/generate\-parse\-error/)
end
state = nil
mocked_server(3) do |val, server|
case state
when nil
state = :started
server.send_data ""
server.send_data ""
val.must_match(/stream:stream/)
when :started
state = :parse_error
server.send_data ""
when :parse_error
EM.stop
val.must_equal ""
else
EM.stop
false
end
end
end
end