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