require 'spec_helper' require 'blather/client/client' describe Blather::Client do before do @client = Blather::Client.new @stream = mock @stream.stubs(:send) @jid = Blather::JID.new('n@d/r') end it 'provides a Blather::JID reader' do @client.post_init @stream, @jid @client.should respond_to :jid @client.jid.should == @jid end it 'provides a reader for the roster' do @client.should respond_to :roster @client.roster.should be_kind_of Blather::Roster end it 'provides a status reader' do @client.post_init @stream, @jid @client.should respond_to :status @client.status = :away @client.status.should == :away end it 'should have a caps handler' do @client.should respond_to :caps @client.caps.should be_kind_of Blather::Stanza::Capabilities end it 'can be setup' do @client.should respond_to :setup @client.setup('me@me.com', 'pass').should == @client end it 'knows if it has been setup' do @client.should respond_to :setup? @client.setup?.should == false @client.setup 'me@me.com', 'pass' @client.setup?.should == true end it 'cannot be run before being setup' do lambda { @client.run }.should raise_error RuntimeError end it 'starts up a Component connection when setup without a node' do setup = 'pubsub.jabber.local', 'secret' @client.setup *setup Blather::Stream::Component.expects(:start).with @client, *setup + [nil, nil, nil, nil] @client.run end it 'starts up a Client connection when setup with a node' do setup = 'test@jabber.local', 'secret' @client.setup *setup Blather::Stream::Client.expects(:start).with @client, *setup + [nil, nil, nil, nil] @client.run end it 'knows if it is disconnected' do @client.should respond_to :connected? @client.connected?.should == false end it 'knows if it is connected' do stream = mock stream.expects(:stopped?).returns false @client.setup('me.com', 'secret') @client.post_init stream, Blather::JID.new('me.com') @client.connected?.should == true end describe 'if it has been setup but not connected yet' do it 'should consider itself disconnected' do @client.setup('me.com', 'secret') @client.connected?.should == false end end it 'writes to the connection the closes when #close is called' do stream = mock() stream.expects(:close_connection_after_writing) @client.setup('me.com', 'secret') @client.post_init stream, Blather::JID.new('me.com') @client.close end it 'shuts down EM when #unbind is called if it is running' do EM.expects(:reactor_running?).returns true EM.expects(:stop) @client.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 @client.unbind end it 'calls the :disconnected handler with #unbind is called' do EM.expects(:reactor_running?).returns false disconnected = mock() disconnected.expects(:call) @client.register_handler(:disconnected) { disconnected.call } @client.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 @client.register_handler(:disconnected) { disconnected.call } @client.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 @client.register_handler(:disconnected) { disconnected.call } @client.unbind end it 'can register a temporary handler based on stanza ID' do stanza = Blather::Stanza::Iq.new response = mock() response.expects(:call) @client.register_tmp_handler(stanza.id) { |_| response.call } @client.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) @client.register_tmp_handler(stanza.id) { |_| response.call } @client.receive_data stanza @client.receive_data stanza end it 'will create a handler then write the stanza' do stanza = Blather::Stanza::Iq.new response = mock() response.expects(:call) @client.expects(:write).with do |s| @client.receive_data stanza s.should == stanza end @client.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) @client.register_handler(:iq) { |_| response.call } @client.receive_data stanza @client.receive_data stanza end it 'allows for breaking out of handlers' do stanza = Blather::Stanza::Iq.new response = mock(:iq => nil) @client.register_handler(:iq) do |_| response.iq throw :halt response.fail end @client.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) @client.register_handler(:iq) do |_| response.iq1 throw :pass response.fail end @client.register_handler(:iq) do |_| response.iq2 end @client.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) @client.register_handler(:query) do |_| response.query throw :pass response.fail end @client.register_handler(:iq) { |_| response.iq } @client.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 @client.register_handler(:message, :chat?) { |_| response.call } @client.receive_data stanza @client.clear_handlers(:message, :chat?) @client.receive_data stanza end end describe 'Blather::Client#write' do before do @client = Blather::Client.new end it 'writes to the stream' do stanza = Blather::Stanza::Iq.new stream = mock() stream.expects(:send).with stanza @client.setup('me@me.com', 'me') @client.post_init stream, Blather::JID.new('me.com') @client.write stanza end end describe 'Blather::Client#status=' do before do @client = Blather::Client.new @stream = mock() @stream.stubs(:send) @client.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) @client.status.should_not equal :away @client.status = :away, 'message' @client.status.should == :away end it 'does not update the state when sending to a Blather::JID' do @stream.stubs(:write) @client.status.should_not equal :away @client.status = :away, 'message', 'me@me.com' @client.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 @client.status = status end end describe 'Blather::Client default handlers' do before do @client = Blather::Client.new @stream = mock() @stream.stubs(:send) @client.post_init @stream, Blather::JID.new('n@d/r') end it 're-raises errors' do err = Blather::BlatherError.new lambda { @client.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 # @client.expects(:write).with err # @client.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 # @client.expects(:write).with { |n| n.to_s.should == err.to_s } # @client.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 # @client.expects(:write).with { |n| n.to_s.should == err.to_s } # @client.receive_data get # end it 'responds to s2c pings with a pong' do ping = Blather::Stanza::Iq::Ping.new :get pong = ping.reply @client.expects(:write).with { |n| n.to_s.should == pong.to_s } @client.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 @client.stubs(:roster).returns({status.from => roster_item}) @client.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 @client.stubs(:roster).returns({status.from => roster_item}) response = mock() response.expects(:call).with jid @client.register_handler(:status) { |s| response.call s.from.to_s } @client.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 @client.stubs(:roster).returns client_roster @client.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 @client.stubs(:roster).returns client_roster response = mock() response.expects(:call) @client.register_handler(:roster) { |_| response.call } @client.receive_data roster end end describe 'Blather::Client with a Component stream' do before do class MockComponent < Blather::Stream::Component; def initialize(); end; end @stream = MockComponent.new('') @stream.stubs(:send_data) @client = Blather::Client.new @client.setup('me.com', 'secret') end it 'calls the ready handler when sent post_init' do ready = mock() ready.expects(:call) @client.register_handler(:ready) { ready.call } @client.post_init @stream end end describe 'Blather::Client with a Client stream' do before do class MockClientStream < Blather::Stream::Client; def initialize(); end; end @stream = MockClientStream.new('') @client = Blather::Client.new Blather::Stream::Client.stubs(:start).returns @stream @client.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 } @client.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 { |s| result_roster.id = s.id; @client.receive_data result_roster; true } ready = mock() ready.expects(:call) @client.register_handler(:ready) { ready.call } @client.post_init @stream, Blather::JID.new('n@d/r') end end describe 'Blather::Client filters' do before do @client = Blather::Client.new @stream = mock() @stream.stubs(:send) @client.post_init @stream, Blather::JID.new('n@d/r') end it 'raises an error when an invalid filter type is registered' do lambda { @client.register_filter(:invalid) {} }.should raise_error RuntimeError end it 'can be guarded' do stanza = Blather::Stanza::Iq.new ready = mock() ready.expects(:call).once @client.register_filter(:before, :iq, :id => stanza.id) { |_| ready.call } @client.register_filter(:before, :iq, :id => 'not-id') { |_| ready.call } @client.receive_data stanza end it 'can pass to the next handler' do stanza = Blather::Stanza::Iq.new ready = mock() ready.expects(:call).once @client.register_filter(:before) { |_| throw :pass; ready.call } @client.register_filter(:before) { |_| ready.call } @client.receive_data stanza end it 'runs them in order' do stanza = Blather::Stanza::Iq.new count = 0 @client.register_filter(:before) { |_| count.should == 0; count = 1 } @client.register_filter(:before) { |_| count.should == 1; count = 2 } @client.register_handler(:iq) { |_| count.should == 2; count = 3 } @client.register_filter(:after) { |_| count.should == 3; count = 4 } @client.register_filter(:after) { |_| count.should == 4 } @client.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 @client.register_filter(:before) { |s| s.from = new_jid } @client.register_handler(:iq) { |s| ready.call s.from.to_s } @client.receive_data stanza end it 'can halt the handler chain' do stanza = Blather::Stanza::Iq.new ready = mock() ready.expects(:call).never @client.register_filter(:before) { |_| throw :halt } @client.register_handler(:iq) { |_| ready.call } @client.receive_data stanza end it 'can be specific to a handler' do stanza = Blather::Stanza::Iq.new ready = mock() ready.expects(:call).once @client.register_filter(:before, :iq) { |_| ready.call } @client.register_filter(:before, :message) { |_| ready.call } @client.receive_data stanza end end describe 'Blather::Client guards' do before do stream = mock() stream.stubs(:send) @client = Blather::Client.new @client.post_init stream, Blather::JID.new('n@d/r') @stanza = Blather::Stanza::Iq.new @response = mock() end it 'can be a symbol' do @response.expects :call @client.register_handler(:iq, :chat?) { |_| @response.call } @stanza.expects(:chat?).returns true @client.receive_data @stanza @stanza.expects(:chat?).returns false @client.receive_data @stanza end it 'can be a hash with string match' do @response.expects :call @client.register_handler(:iq, :body => 'exit') { |_| @response.call } @stanza.expects(:body).returns 'exit' @client.receive_data @stanza @stanza.expects(:body).returns 'not-exit' @client.receive_data @stanza end it 'can be a hash with a value' do @response.expects :call @client.register_handler(:iq, :number => 0) { |_| @response.call } @stanza.expects(:number).returns 0 @client.receive_data @stanza @stanza.expects(:number).returns 1 @client.receive_data @stanza end it 'can be a hash with a regexp' do @response.expects :call @client.register_handler(:iq, :body => /exit/) { |_| @response.call } @stanza.expects(:body).returns 'more than just exit, but exit still' @client.receive_data @stanza @stanza.expects(:body).returns 'keyword not found' @client.receive_data @stanza @stanza.expects(:body).returns nil @client.receive_data @stanza end it 'can be a hash with an array' do @response.expects(:call).times(2) @client.register_handler(:iq, :type => [:result, :error]) { |_| @response.call } stanza = Blather::Stanza::Iq.new stanza.expects(:type).at_least_once.returns :result @client.receive_data stanza stanza = Blather::Stanza::Iq.new stanza.expects(:type).at_least_once.returns :error @client.receive_data stanza stanza = Blather::Stanza::Iq.new stanza.expects(:type).at_least_once.returns :get @client.receive_data stanza end it 'chained are treated like andand (short circuited)' do @response.expects :call @client.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' @client.receive_data stanza stanza = Blather::Stanza::Iq.new stanza.expects(:type).at_least_once.returns :set stanza.expects(:body).never @client.receive_data stanza end it 'within an Array are treated as oror (short circuited)' do @response.expects(:call).times 2 @client.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' @client.receive_data stanza stanza = Blather::Stanza::Iq.new stanza.stubs(:type).at_least_once.returns :get stanza.expects(:body).never @client.receive_data stanza end it 'can be a lambda' do @response.expects :call @client.register_handler(:iq, lambda { |s| s.number % 3 == 0 }) { |_| @response.call } @stanza.expects(:number).at_least_once.returns 3 @client.receive_data @stanza @stanza.expects(:number).at_least_once.returns 2 @client.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 @client.register_handler(:iq, "/iq[@id='#{@stanza.id}']") { |stanza, xpath| @response.call stanza, xpath } @client.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 @client.register_handler(:message, "/message/bar:foo", :bar => 'http://bar.com') { |stanza, xpath| @response.call stanza, xpath } @client.receive_data @stanza end it 'raises an error when a bad guard is tried' do lambda { @client.register_handler(:iq, 0) {} }.should raise_error RuntimeError end end describe 'Blather::Client::Caps' do before do @client = Blather::Client.new @stream = mock() @stream.stubs(:send) @client.post_init @stream, Blather::JID.new('n@d/r') @caps = @client.caps end 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 } @caps.c.inspect.should == "\n \n" end end