require 'spec_helper'
require 'blather/client/client'
describe Blather::Client do
let(:jid) { Blather::JID.new 'n@d/r' }
let(:stream) { mock 'Stream' }
before do
stream.stubs :send
end
it 'provides a Blather::JID reader' do
subject.post_init stream, jid
subject.should respond_to :jid
subject.jid.should == jid
end
it 'provides a reader for the roster' do
subject.should respond_to :roster
subject.roster.should be_kind_of Blather::Roster
end
it 'provides a status reader' do
subject.post_init stream, jid
subject.should respond_to :status
subject.status = :away
subject.status.should == :away
end
it 'should have a caps handler' do
subject.should respond_to :caps
subject.caps.should be_kind_of Blather::Stanza::Capabilities
end
it 'can be setup' do
subject.should respond_to :setup
subject.setup('me@me.com', 'pass').should == subject
end
it 'knows if it has been setup' do
subject.should respond_to :setup?
subject.should_not be_setup
subject.setup 'me@me.com', 'pass'
subject.should be_setup
end
it 'cannot be run before being setup' do
lambda { subject.run }.should raise_error RuntimeError
end
it 'starts up a Component connection when setup without a node' do
setup = 'pubsub.jabber.local', 'secret'
subject.setup *setup
Blather::Stream::Component.expects(:start).with subject, *setup + [nil, nil, nil, nil]
subject.run
end
it 'starts up a Client connection when setup with a node' do
setup = 'test@jabber.local', 'secret'
subject.setup *setup
Blather::Stream::Client.expects(:start).with subject, *setup + [nil, nil, nil, nil]
subject.run
end
it 'knows if it is disconnected' do
subject.should respond_to :connected?
subject.should_not be_connected
end
it 'knows if it is connected' do
stream.expects(:stopped?).returns false
subject.setup 'me.com', 'secret'
subject.post_init stream, Blather::JID.new('me.com')
subject.should be_connected
end
describe 'if it has been setup but not connected yet' do
it 'should consider itself disconnected' do
subject.setup 'me.com', 'secret'
subject.should_not be_connected
end
end
it 'writes to the connection the closes when #close is called' do
stream.expects(:close_connection_after_writing)
EM.stubs(:next_tick).yields
subject.setup 'me.com', 'secret'
subject.post_init stream, Blather::JID.new('me.com')
subject.close
end
it 'shuts down EM when #unbind is called if it is running' do
EM.expects(:reactor_running?).returns true
EM.expects(:stop)
subject.unbind
end
it 'does nothing when #unbind is called and EM is not running' do
EM.expects(:reactor_running?).returns false
EM.expects(:stop).never
subject.unbind
end
it 'calls the :disconnected handler with #unbind is called' do
EM.expects(:reactor_running?).returns false
disconnected = mock
disconnected.expects(:call)
subject.register_handler(:disconnected) { disconnected.call }
subject.unbind
end
it 'does not call EM.stop on #unbind if a handler returns positive' do
EM.expects(:reactor_running?).never
EM.expects(:stop).never
disconnected = mock
disconnected.expects(:call).returns true
subject.register_handler(:disconnected) { disconnected.call }
subject.unbind
end
it 'calls EM.stop on #unbind if a handler returns negative' do
EM.expects(:reactor_running?).returns true
EM.expects(:stop)
disconnected = mock
disconnected.expects(:call).returns false
subject.register_handler(:disconnected) { disconnected.call }
subject.unbind
end
it 'can register a temporary handler based on stanza ID' do
stanza = Blather::Stanza::Iq.new
response = mock
response.expects(:call)
subject.register_tmp_handler(stanza.id) { |_| response.call }
subject.receive_data stanza
end
it 'removes a tmp handler as soon as it is used' do
stanza = Blather::Stanza::Iq.new
response = mock
response.expects(:call)
subject.register_tmp_handler(stanza.id) { |_| response.call }
subject.receive_data stanza
subject.receive_data stanza
end
it 'will create a handler then write the stanza' do
stanza = Blather::Stanza::Iq.new
response = mock
response.expects(:call)
subject.expects(:write).with do |s|
subject.receive_data stanza
s.should == stanza
end
subject.write_with_handler(stanza) { |_| response.call }
end
it 'can register a handler' do
stanza = Blather::Stanza::Iq.new
response = mock
response.expects(:call).times(2)
subject.register_handler(:iq) { |_| response.call }
subject.receive_data stanza
subject.receive_data stanza
end
it 'allows for breaking out of handlers' do
stanza = Blather::Stanza::Iq.new
response = mock(:iq => nil)
subject.register_handler(:iq) do |_|
response.iq
throw :halt
response.fail
end
subject.receive_data stanza
end
it 'allows for passing to the next handler of the same type' do
stanza = Blather::Stanza::Iq.new
response = mock(:iq1 => nil, :iq2 => nil)
subject.register_handler(:iq) do |_|
response.iq1
throw :pass
response.fail
end
subject.register_handler(:iq) do |_|
response.iq2
end
subject.receive_data stanza
end
it 'allows for passing to the next handler in the hierarchy' do
stanza = Blather::Stanza::Iq::Query.new
response = mock(:query => nil, :iq => nil)
subject.register_handler(:query) do |_|
response.query
throw :pass
response.fail
end
subject.register_handler(:iq) { |_| response.iq }
subject.receive_data stanza
end
it 'can clear handlers' do
stanza = Blather::Stanza::Message.new
stanza.expects(:chat?).returns true
response = mock
response.expects(:call).once
subject.register_handler(:message, :chat?) { |_| response.call }
subject.receive_data stanza
subject.clear_handlers :message, :chat?
subject.receive_data stanza
end
describe '#write' do
it 'writes to the stream' do
stanza = Blather::Stanza::Iq.new
stream.expects(:send).with stanza
subject.setup 'me@me.com', 'me'
subject.post_init stream, Blather::JID.new('me.com')
subject.write stanza
end
end
describe '#status=' do
before do
subject.post_init stream, Blather::JID.new('n@d/r')
end
it 'updates the state when not sending to a Blather::JID' do
stream.stubs(:write)
subject.status.should_not equal :away
subject.status = :away, 'message'
subject.status.should == :away
end
it 'does not update the state when sending to a Blather::JID' do
stream.stubs(:write)
subject.status.should_not equal :away
subject.status = :away, 'message', 'me@me.com'
subject.status.should_not equal :away
end
it 'writes the new status to the stream' do
Blather::Stanza::Presence::Status.stubs(:next_id).returns 0
status = [:away, 'message']
stream.expects(:send).with do |s|
s.should be_kind_of Blather::Stanza::Presence::Status
s.to_s.should == Blather::Stanza::Presence::Status.new(*status).to_s
end
subject.status = status
end
end
describe 'default handlers' do
it 're-raises errors' do
err = Blather::BlatherError.new
lambda { subject.receive_data err }.should raise_error Blather::BlatherError
end
# it 'responds to iq:get with a "service-unavailable" error' do
# get = Blather::Stanza::Iq.new :get
# err = Blather::StanzaError.new(get, 'service-unavailable', :cancel).to_node
# subject.expects(:write).with err
# subject.receive_data get
# end
# it 'responds to iq:get with a "service-unavailable" error' do
# get = Blather::Stanza::Iq.new :get
# err = Blather::StanzaError.new(get, 'service-unavailable', :cancel).to_node
# subject.expects(:write).with { |n| n.to_s.should == err.to_s }
# subject.receive_data get
# end
# it 'responds to iq:set with a "service-unavailable" error' do
# get = Blather::Stanza::Iq.new :set
# err = Blather::StanzaError.new(get, 'service-unavailable', :cancel).to_node
# subject.expects(:write).with { |n| n.to_s.should == err.to_s }
# subject.receive_data get
# end
it 'responds to s2c pings with a pong' do
ping = Blather::Stanza::Iq::Ping.new :get
pong = ping.reply
subject.expects(:write).with { |n| n.to_s.should == pong.to_s }
subject.receive_data ping
end
it 'handles status changes by updating the roster if the status is from a Blather::JID in the roster' do
jid = 'friend@jabber.local'
status = Blather::Stanza::Presence::Status.new :away
status.stubs(:from).returns jid
roster_item = mock
roster_item.expects(:status=).with status
subject.stubs(:roster).returns({status.from => roster_item})
subject.receive_data status
end
it 'lets status stanzas fall through to other handlers' do
jid = 'friend@jabber.local'
status = Blather::Stanza::Presence::Status.new :away
status.stubs(:from).returns jid
roster_item = mock
roster_item.expects(:status=).with status
subject.stubs(:roster).returns({status.from => roster_item})
response = mock
response.expects(:call).with jid
subject.register_handler(:status) { |s| response.call s.from.to_s }
subject.receive_data status
end
it 'handles an incoming roster node by processing it through the roster' do
roster = Blather::Stanza::Iq::Roster.new
client_roster = mock
client_roster.expects(:process).with roster
subject.stubs(:roster).returns client_roster
subject.receive_data roster
end
it 'handles an incoming roster node by processing it through the roster' do
roster = Blather::Stanza::Iq::Roster.new
client_roster = mock
client_roster.expects(:process).with roster
subject.stubs(:roster).returns client_roster
response = mock
response.expects(:call)
subject.register_handler(:roster) { |_| response.call }
subject.receive_data roster
end
end
describe 'with a Component stream' do
before do
class MockComponent < Blather::Stream::Component; def initialize(); end; end
stream = MockComponent.new('')
stream.stubs(:send_data)
subject.setup 'me.com', 'secret'
end
it 'calls the ready handler when sent post_init' do
ready = mock
ready.expects(:call)
subject.register_handler(:ready) { ready.call }
subject.post_init stream
end
end
describe 'with a Client stream' do
before do
class MockClientStream < Blather::Stream::Client; def initialize(); end; end
stream = MockClientStream.new('')
Blather::Stream::Client.stubs(:start).returns stream
subject.setup('me@me.com', 'secret').run
end
it 'sends a request for the roster when post_init is called' do
stream.expects(:send).with { |stanza| stanza.should be_kind_of Blather::Stanza::Iq::Roster }
subject.post_init stream, Blather::JID.new('n@d/r')
end
it 'calls the ready handler after post_init and roster is received' do
result_roster = Blather::Stanza::Iq::Roster.new :result
stream.stubs(:send).with do |s|
result_roster.id = s.id
subject.receive_data result_roster
true
end
ready = mock
ready.expects(:call)
subject.register_handler(:ready) { ready.call }
subject.post_init stream, Blather::JID.new('n@d/r')
end
end
describe 'filters' do
it 'raises an error when an invalid filter type is registered' do
lambda { subject.register_filter(:invalid) {} }.should raise_error RuntimeError
end
it 'can be guarded' do
stanza = Blather::Stanza::Iq.new
ready = mock
ready.expects(:call).once
subject.register_filter(:before, :iq, :id => stanza.id) { |_| ready.call }
subject.register_filter(:before, :iq, :id => 'not-id') { |_| ready.call }
subject.receive_data stanza
end
it 'can pass to the next handler' do
stanza = Blather::Stanza::Iq.new
ready = mock
ready.expects(:call).once
subject.register_filter(:before) { |_| throw :pass; ready.call }
subject.register_filter(:before) { |_| ready.call }
subject.receive_data stanza
end
it 'runs them in order' do
stanza = Blather::Stanza::Iq.new
count = 0
subject.register_filter(:before) { |_| count.should == 0; count = 1 }
subject.register_filter(:before) { |_| count.should == 1; count = 2 }
subject.register_handler(:iq) { |_| count.should == 2; count = 3 }
subject.register_filter(:after) { |_| count.should == 3; count = 4 }
subject.register_filter(:after) { |_| count.should == 4 }
subject.receive_data stanza
end
it 'can modify the stanza' do
stanza = Blather::Stanza::Iq.new
stanza.from = 'from@test.local'
new_jid = 'before@filter.local'
ready = mock
ready.expects(:call).with new_jid
subject.register_filter(:before) { |s| s.from = new_jid }
subject.register_handler(:iq) { |s| ready.call s.from.to_s }
subject.receive_data stanza
end
it 'can halt the handler chain' do
stanza = Blather::Stanza::Iq.new
ready = mock
ready.expects(:call).never
subject.register_filter(:before) { |_| throw :halt }
subject.register_handler(:iq) { |_| ready.call }
subject.receive_data stanza
end
it 'can be specific to a handler' do
stanza = Blather::Stanza::Iq.new
ready = mock
ready.expects(:call).once
subject.register_filter(:before, :iq) { |_| ready.call }
subject.register_filter(:before, :message) { |_| ready.call }
subject.receive_data stanza
end
end
describe 'guards' do
let(:stanza) { Blather::Stanza::Iq.new }
let(:response) { mock }
it 'can be a symbol' do
response.expects :call
subject.register_handler(:iq, :chat?) { |_| response.call }
stanza.expects(:chat?).returns true
subject.receive_data stanza
stanza.expects(:chat?).returns false
subject.receive_data stanza
end
it 'can be a hash with string match' do
response.expects :call
subject.register_handler(:iq, :body => 'exit') { |_| response.call }
stanza.expects(:body).returns 'exit'
subject.receive_data stanza
stanza.expects(:body).returns 'not-exit'
subject.receive_data stanza
end
it 'can be a hash with a value' do
response.expects :call
subject.register_handler(:iq, :number => 0) { |_| response.call }
stanza.expects(:number).returns 0
subject.receive_data stanza
stanza.expects(:number).returns 1
subject.receive_data stanza
end
it 'can be a hash with a regexp' do
response.expects :call
subject.register_handler(:iq, :body => /exit/) { |_| response.call }
stanza.expects(:body).returns 'more than just exit, but exit still'
subject.receive_data stanza
stanza.expects(:body).returns 'keyword not found'
subject.receive_data stanza
stanza.expects(:body).returns nil
subject.receive_data stanza
end
it 'can be a hash with an array' do
response.expects(:call).times(2)
subject.register_handler(:iq, :type => [:result, :error]) { |_| response.call }
stanza = Blather::Stanza::Iq.new
stanza.expects(:type).at_least_once.returns :result
subject.receive_data stanza
stanza = Blather::Stanza::Iq.new
stanza.expects(:type).at_least_once.returns :error
subject.receive_data stanza
stanza = Blather::Stanza::Iq.new
stanza.expects(:type).at_least_once.returns :get
subject.receive_data stanza
end
it 'chained are treated like andand (short circuited)' do
response.expects :call
subject.register_handler(:iq, :type => :get, :body => 'test') { |_| response.call }
stanza = Blather::Stanza::Iq.new
stanza.expects(:type).at_least_once.returns :get
stanza.expects(:body).returns 'test'
subject.receive_data stanza
stanza = Blather::Stanza::Iq.new
stanza.expects(:type).at_least_once.returns :set
stanza.expects(:body).never
subject.receive_data stanza
end
it 'within an Array are treated as oror (short circuited)' do
response.expects(:call).times 2
subject.register_handler(:iq, [{:type => :get}, {:body => 'test'}]) { |_| response.call }
stanza = Blather::Stanza::Iq.new
stanza.expects(:type).at_least_once.returns :set
stanza.expects(:body).returns 'test'
subject.receive_data stanza
stanza = Blather::Stanza::Iq.new
stanza.stubs(:type).at_least_once.returns :get
stanza.expects(:body).never
subject.receive_data stanza
end
it 'can be a lambda' do
response.expects :call
subject.register_handler(:iq, lambda { |s| s.number % 3 == 0 }) { |_| response.call }
stanza.expects(:number).at_least_once.returns 3
subject.receive_data stanza
stanza.expects(:number).at_least_once.returns 2
subject.receive_data stanza
end
it 'can be an xpath and will send the result to the handler' do
response.expects(:call).with do |stanza, xpath|
xpath.should be_instance_of Nokogiri::XML::NodeSet
xpath.should_not be_empty
stanza.should == stanza
end
subject.register_handler(:iq, "/iq[@id='#{stanza.id}']") { |stanza, xpath| response.call stanza, xpath }
subject.receive_data stanza
end
it 'can be an xpath with namespaces and will send the result to the handler' do
stanza = Blather::Stanza.parse('')
response.expects(:call).with do |stanza, xpath|
xpath.should be_instance_of Nokogiri::XML::NodeSet
xpath.should_not be_empty
stanza.should == stanza
end
subject.register_handler(:message, "/message/bar:foo", :bar => 'http://bar.com') { |stanza, xpath| response.call stanza, xpath }
subject.receive_data stanza
end
it 'raises an error when a bad guard is tried' do
lambda { subject.register_handler(:iq, 0) {} }.should raise_error RuntimeError
end
end
describe '#caps' do
let(:caps) { subject.caps }
it 'must be of type result' do
caps.should respond_to :type
caps.type.should == :result
end
it 'can have a client node set' do
caps.should respond_to :node=
caps.node = "somenode"
end
it 'provides a client node reader' do
caps.should respond_to :node
caps.node = "somenode"
caps.node.should == "somenode##{caps.ver}"
end
it 'can have identities set' do
caps.should respond_to :identities=
caps.identities = [{:name => "name", :type => "type", :category => "cat"}]
end
it 'provides an identities reader' do
caps.should respond_to :identities
caps.identities = [{:name => "name", :type => "type", :category => "cat"}]
caps.identities.should == [Blather::Stanza::Iq::DiscoInfo::Identity.new({:name => "name", :type => "type", :category => "cat"})]
end
it 'can have features set' do
caps.should respond_to :features=
caps.features.size.should == 0
caps.features = ["feature1"]
caps.features.size.should == 1
caps.features += [Blather::Stanza::Iq::DiscoInfo::Feature.new("feature2")]
caps.features.size.should == 2
caps.features = nil
caps.features.size.should == 0
end
it 'provides a features reader' do
caps.should respond_to :features
caps.features = %w{feature1 feature2}
caps.features.should == [Blather::Stanza::Iq::DiscoInfo::Feature.new("feature1"), Blather::Stanza::Iq::DiscoInfo::Feature.new("feature2")]
end
it 'provides a client ver reader' do
caps.should respond_to :ver
caps.node = 'http://code.google.com/p/exodus'
caps.identities = [Blather::Stanza::Iq::DiscoInfo::Identity.new({:name => 'Exodus 0.9.1', :type => 'pc', :category => 'client'})]
caps.features = %w{
http://jabber.org/protocol/caps
http://jabber.org/protocol/disco#info
http://jabber.org/protocol/disco#items
http://jabber.org/protocol/muc
}
caps.ver.should == 'QgayPKawpkPSDYmwT/WM94uAlu0='
caps.node.should == "http://code.google.com/p/exodus#QgayPKawpkPSDYmwT/WM94uAlu0="
end
it 'can construct caps presence correctly' do
caps.should respond_to :c
caps.node = 'http://code.google.com/p/exodus'
caps.identities = [Blather::Stanza::Iq::DiscoInfo::Identity.new({:name => 'Exodus 0.9.1', :type => 'pc', :category => 'client'})]
caps.features = %w{
http://jabber.org/protocol/caps
http://jabber.org/protocol/disco#info
http://jabber.org/protocol/disco#items
http://jabber.org/protocol/muc
}
Nokogiri::XML(caps.c.to_xml).to_s.should == Nokogiri::XML("").to_s
end
end
end