blather 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. data/LICENSE +2 -0
  2. data/README.rdoc +54 -29
  3. data/Rakefile +94 -13
  4. data/VERSION.yml +4 -0
  5. data/examples/drb_client.rb +2 -4
  6. data/examples/echo.rb +13 -8
  7. data/examples/pubsub/cli.rb +64 -0
  8. data/examples/pubsub/ping_pong.rb +18 -0
  9. data/examples/pubsub/pubsub_dsl.rb +52 -0
  10. data/examples/pubsub_client.rb +39 -0
  11. data/examples/rosterprint.rb +14 -0
  12. data/examples/xmpp4r/echo.rb +35 -0
  13. data/ext/extconf.rb +65 -0
  14. data/lib/blather.rb +18 -121
  15. data/lib/blather/client.rb +13 -0
  16. data/lib/blather/client/client.rb +165 -0
  17. data/lib/blather/client/dsl.rb +99 -0
  18. data/lib/blather/client/pubsub.rb +53 -0
  19. data/lib/blather/client/pubsub/node.rb +27 -0
  20. data/lib/blather/core_ext/active_support.rb +1 -0
  21. data/lib/blather/core_ext/libxml.rb +7 -1
  22. data/lib/blather/errors.rb +39 -18
  23. data/lib/blather/errors/sasl_error.rb +87 -0
  24. data/lib/blather/errors/stanza_error.rb +262 -0
  25. data/lib/blather/errors/stream_error.rb +253 -0
  26. data/lib/blather/jid.rb +9 -16
  27. data/lib/blather/roster.rb +9 -0
  28. data/lib/blather/roster_item.rb +7 -4
  29. data/lib/blather/stanza.rb +19 -25
  30. data/lib/blather/stanza/disco.rb +9 -0
  31. data/lib/blather/stanza/disco/disco_info.rb +84 -0
  32. data/lib/blather/stanza/disco/disco_items.rb +59 -0
  33. data/lib/blather/stanza/iq.rb +16 -4
  34. data/lib/blather/stanza/iq/query.rb +6 -4
  35. data/lib/blather/stanza/iq/roster.rb +38 -38
  36. data/lib/blather/stanza/pubsub.rb +33 -0
  37. data/lib/blather/stanza/pubsub/affiliations.rb +52 -0
  38. data/lib/blather/stanza/pubsub/errors.rb +9 -0
  39. data/lib/blather/stanza/pubsub/event.rb +21 -0
  40. data/lib/blather/stanza/pubsub/items.rb +59 -0
  41. data/lib/blather/stanza/pubsub/owner.rb +9 -0
  42. data/lib/blather/stanza/pubsub/subscriptions.rb +57 -0
  43. data/lib/blather/stream.rb +125 -57
  44. data/lib/blather/stream/client.rb +26 -0
  45. data/lib/blather/stream/component.rb +34 -0
  46. data/lib/blather/stream/parser.rb +17 -27
  47. data/lib/blather/stream/resource.rb +21 -24
  48. data/lib/blather/stream/sasl.rb +60 -37
  49. data/lib/blather/stream/session.rb +12 -19
  50. data/lib/blather/stream/stream_handler.rb +39 -0
  51. data/lib/blather/stream/tls.rb +22 -18
  52. data/lib/blather/xmpp_node.rb +91 -17
  53. data/spec/blather/core_ext/libxml_spec.rb +58 -0
  54. data/spec/blather/errors/sasl_error_spec.rb +56 -0
  55. data/spec/blather/errors/stanza_error_spec.rb +148 -0
  56. data/spec/blather/errors/stream_error_spec.rb +114 -0
  57. data/spec/blather/errors_spec.rb +40 -0
  58. data/spec/blather/jid_spec.rb +0 -7
  59. data/spec/blather/roster_item_spec.rb +5 -0
  60. data/spec/blather/roster_spec.rb +6 -6
  61. data/spec/blather/stanza/discos/disco_info_spec.rb +207 -0
  62. data/spec/blather/stanza/discos/disco_items_spec.rb +136 -0
  63. data/spec/blather/stanza/iq/query_spec.rb +9 -2
  64. data/spec/blather/stanza/iq/roster_spec.rb +117 -1
  65. data/spec/blather/stanza/iq_spec.rb +29 -0
  66. data/spec/blather/stanza/presence/subscription_spec.rb +12 -1
  67. data/spec/blather/stanza/presence_spec.rb +29 -0
  68. data/spec/blather/stanza/pubsub/affiliations_spec.rb +46 -0
  69. data/spec/blather/stanza/pubsub/items_spec.rb +59 -0
  70. data/spec/blather/stanza/pubsub/subscriptions_spec.rb +63 -0
  71. data/spec/blather/stanza/pubsub_spec.rb +26 -0
  72. data/spec/blather/stanza_spec.rb +13 -1
  73. data/spec/blather/stream/client_spec.rb +787 -0
  74. data/spec/blather/stream/component_spec.rb +86 -0
  75. data/spec/blather/xmpp_node_spec.rb +75 -22
  76. data/spec/fixtures/pubsub.rb +157 -0
  77. data/spec/spec_helper.rb +6 -14
  78. metadata +86 -74
  79. data/CHANGELOG +0 -5
  80. data/Manifest +0 -47
  81. data/blather.gemspec +0 -41
  82. data/lib/blather/stanza/error.rb +0 -31
  83. data/spec/blather/stream_spec.rb +0 -462
  84. data/spec/build_safe.rb +0 -20
@@ -0,0 +1,58 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ describe 'LibXML::XML::Node' do
4
+ it 'aliases #name to #element_name' do
5
+ node = LibXML::XML::Node.new 'foo'
6
+ node.must_respond_to :element_name
7
+ node.element_name.must_equal node.name
8
+ end
9
+
10
+ it 'aliases #name= to #element_name=' do
11
+ node = LibXML::XML::Node.new 'foo'
12
+ node.must_respond_to :element_name=
13
+ node.element_name.must_equal node.name
14
+ node.element_name = 'bar'
15
+ node.element_name.must_equal 'bar'
16
+ end
17
+ end
18
+
19
+ describe 'LibXML::XML::Attributes' do
20
+ it 'provides a helper to remove a specified attribute' do
21
+ attrs = LibXML::XML::Node.new('foo').attributes
22
+ attrs['foo'] = 'bar'
23
+ attrs['foo'].must_equal 'bar'
24
+ attrs.remove 'foo'
25
+ attrs['foo'].must_be_nil
26
+
27
+ attrs['foo'] = 'bar'
28
+ attrs['foo'].must_equal 'bar'
29
+ attrs.remove :foo
30
+ attrs['foo'].must_be_nil
31
+ end
32
+
33
+ it 'allows symbols as hash keys' do
34
+ attrs = LibXML::XML::Node.new('foo').attributes
35
+ attrs['foo'] = 'bar'
36
+
37
+ attrs['foo'].must_equal 'bar'
38
+ attrs[:foo].must_equal 'bar'
39
+ end
40
+
41
+ it 'removes an attribute when set to nil' do
42
+ attrs = LibXML::XML::Node.new('foo').attributes
43
+ attrs['foo'] = 'bar'
44
+
45
+ attrs['foo'].must_equal 'bar'
46
+ attrs['foo'] = nil
47
+ attrs['foo'].must_be_nil
48
+ end
49
+
50
+ it 'allows attribute values to change' do
51
+ attrs = LibXML::XML::Node.new('foo').attributes
52
+ attrs['foo'] = 'bar'
53
+
54
+ attrs['foo'].must_equal 'bar'
55
+ attrs['foo'] = 'baz'
56
+ attrs['foo'].must_equal 'baz'
57
+ end
58
+ end
@@ -0,0 +1,56 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ def sasl_error_node(err_name = 'aborted')
4
+ node = XMPPNode.new 'failure'
5
+ node.namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
6
+
7
+ node << XMPPNode.new(err_name)
8
+ node
9
+ end
10
+
11
+ describe 'Blather::SASLError' do
12
+ it 'can import a node' do
13
+ SASLError.must_respond_to :import
14
+ e = SASLError.import sasl_error_node
15
+ e.must_be_kind_of SASLError
16
+ end
17
+
18
+ it 'knows what class to instantiate' do
19
+ e = SASLError.import sasl_error_node
20
+ e.must_be_instance_of SASLError::Aborted
21
+ end
22
+
23
+ describe 'when instantiated' do
24
+ before do
25
+ @err_name = 'mechanism-too-weak'
26
+ @err = SASLError.import sasl_error_node(@err_name)
27
+ end
28
+
29
+ it 'provides a err_name attribute' do
30
+ @err.must_respond_to :err_name
31
+ @err.err_name.must_equal @err_name
32
+ end
33
+ end
34
+
35
+ describe 'each XMPP SASL error type' do
36
+ %w[ aborted
37
+ incorrect-encoding
38
+ invalid-authzid
39
+ invalid-mechanism
40
+ mechanism-too-weak
41
+ not-authorized
42
+ temporary-auth-failure
43
+ ].each do |error_type|
44
+ it "provides a class for #{error_type}" do
45
+ e = SASLError.import sasl_error_node(error_type)
46
+ klass = error_type.gsub(/^\w/) { |v| v.upcase }.gsub(/\-(\w)/) { |v| v.delete('-').upcase }
47
+ e.must_be_instance_of eval("SASLError::#{klass}")
48
+ end
49
+
50
+ it "registers #{error_type} in the handler heirarchy" do
51
+ e = SASLError.import sasl_error_node(error_type)
52
+ e.handler_heirarchy.must_equal ["sasl_#{error_type.gsub('-','_').gsub('_error','')}_error".to_sym, :sasl_error, :error]
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,148 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ def stanza_error_node(type = 'cancel', error = 'internal-server-error', msg = nil)
4
+ node = Stanza::Message.new 'error@jabber.local', 'test message', :error
5
+ XML::Document.new.root = node
6
+
7
+ error_node = XMPPNode.new('error')
8
+ error_node['type'] = type.to_s
9
+
10
+ err = XMPPNode.new(error)
11
+ err.namespace = 'urn:ietf:params:xml:ns:xmpp-stanzas'
12
+ error_node << err
13
+
14
+ if msg
15
+ text = XMPPNode.new('text')
16
+ text.namespace = 'urn:ietf:params:xml:ns:xmpp-stanzas'
17
+ text << msg
18
+ error_node << text
19
+ end
20
+
21
+ extra = XMPPNode.new('extra-error')
22
+ extra.namespace = 'blather:stanza:error'
23
+ extra << 'Blather Error'
24
+ error_node << extra
25
+
26
+ node << error_node
27
+ node
28
+ end
29
+
30
+ describe 'Blather::StanzaError' do
31
+ it 'can import a node' do
32
+ StanzaError.must_respond_to :import
33
+ e = StanzaError.import stanza_error_node
34
+ e.must_be_kind_of StanzaError
35
+ end
36
+
37
+ it 'knows what class to instantiate' do
38
+ e = StanzaError.import stanza_error_node
39
+ e.must_be_instance_of StanzaError::InternalServerError
40
+ end
41
+
42
+ describe 'valid types' do
43
+ before { @original = Stanza::Message.new 'error@jabber.local', 'test message', :error }
44
+
45
+ it 'ensures type is one of Stanza::Message::VALID_TYPES' do
46
+ lambda { StanzaError.new @original, :invalid_type_name }.must_raise(Blather::ArgumentError)
47
+
48
+ StanzaError::VALID_TYPES.each do |valid_type|
49
+ msg = StanzaError.new @original, valid_type
50
+ msg.type.must_equal valid_type
51
+ end
52
+ end
53
+ end
54
+
55
+ describe 'when instantiated' do
56
+ before do
57
+ @type = 'cancel'
58
+ @err_name = 'internal-server-error'
59
+ @msg = 'the server has experienced a misconfiguration'
60
+ @err = StanzaError.import stanza_error_node(@type, @err_name, @msg)
61
+ end
62
+
63
+ it 'provides a type attribute' do
64
+ @err.must_respond_to :type
65
+ @err.type.must_equal @type.to_sym
66
+ end
67
+
68
+ it 'provides a err_name attribute' do
69
+ @err.must_respond_to :err_name
70
+ @err.err_name.must_equal @err_name
71
+ end
72
+
73
+ it 'provides a text attribute' do
74
+ @err.must_respond_to :text
75
+ @err.text.must_equal @msg
76
+ end
77
+
78
+ it 'provides a reader to the original node' do
79
+ @err.must_respond_to :original
80
+ @err.original.must_be_instance_of Stanza::Message
81
+ end
82
+
83
+ it 'provides an extras attribute' do
84
+ @err.must_respond_to :extras
85
+ @err.extras.must_be_instance_of Array
86
+ @err.extras.first.element_name.must_equal 'extra-error'
87
+ end
88
+
89
+ it 'describes itself' do
90
+ @err.to_s.must_match(/#{@err_name}/)
91
+ @err.to_s.must_match(/#{@msg}/)
92
+
93
+ @err.inspect.must_match(/#{@err_name}/)
94
+ @err.inspect.must_match(/#{@msg}/)
95
+ end
96
+
97
+ it 'can be turned into xml' do
98
+ @err.must_respond_to :to_xml
99
+ control = "<body>test message</body>\n<error>\n<internal-server-error xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\"/>\n<text xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\">the server has experienced a misconfiguration</text>\n<extra-error xmlns=\"blather:stanza:error\">Blather Error</extra-error>\n</error>\n</message>".split("\n")
100
+ test = @err.to_xml.split("\n")
101
+ test_msg = test.shift
102
+ test.must_equal control
103
+
104
+ test_msg.must_match(/<message[^>]*id="#{@err.original.id}"/)
105
+ test_msg.must_match(/<message[^>]*from="error@jabber\.local"/)
106
+ test_msg.must_match(/<message[^>]*type="error"/)
107
+ end
108
+ end
109
+
110
+ describe 'each XMPP stanza error type' do
111
+ %w[ bad-request
112
+ conflict
113
+ feature-not-implemented
114
+ forbidden
115
+ gone
116
+ internal-server-error
117
+ item-not-found
118
+ jid-malformed
119
+ not-acceptable
120
+ not-allowed
121
+ not-authorized
122
+ payment-required
123
+ recipient-unavailable
124
+ redirect
125
+ registration-required
126
+ remote-server-not-found
127
+ remote-server-timeout
128
+ resource-constraint
129
+ service-unavailable
130
+ subscription-required
131
+ undefined-condition
132
+ unexpected-request
133
+ ].each do |error_type|
134
+ it "provides a class for #{error_type}" do
135
+ e = StanzaError.import stanza_error_node(:cancel, error_type)
136
+ klass = error_type.gsub(/^\w/) { |v| v.upcase }.gsub(/\-(\w)/) { |v| v.delete('-').upcase }
137
+ e.must_be_instance_of eval("StanzaError::#{klass}")
138
+ end
139
+
140
+ it "registers #{error_type} in the handler heirarchy" do
141
+ e = StanzaError.import stanza_error_node(:cancel, error_type)
142
+ e.handler_heirarchy.must_equal ["stanza_#{error_type.gsub('-','_').gsub('_error','')}_error".to_sym, :stanza_error, :error]
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+
@@ -0,0 +1,114 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ def stream_error_node(error = 'internal-server-error', msg = nil)
4
+ node = XMPPNode.new('stream:error')
5
+ XML::Document.new.root = node
6
+
7
+ err = XMPPNode.new(error)
8
+ err.namespace = 'urn:ietf:params:xml:ns:xmpp-streams'
9
+ node << err
10
+
11
+ if msg
12
+ text = XMPPNode.new('text')
13
+ text.namespace = 'urn:ietf:params:xml:ns:xmpp-streams'
14
+ text << msg
15
+ node << text
16
+ end
17
+
18
+ extra = XMPPNode.new('extra-error')
19
+ extra.namespace = 'blather:stream:error'
20
+ extra << 'Blather Error'
21
+
22
+ node << extra
23
+ node
24
+ end
25
+
26
+ describe 'Blather::StreamError' do
27
+ it 'can import a node' do
28
+ StreamError.must_respond_to :import
29
+ e = StreamError.import stream_error_node
30
+ e.must_be_kind_of StreamError
31
+ end
32
+
33
+ it 'knows what class to instantiate' do
34
+ e = StreamError.import stream_error_node
35
+ e.must_be_instance_of StreamError::InternalServerError
36
+ end
37
+ end
38
+
39
+ describe 'Blather::StreamError when instantiated' do
40
+ before do
41
+ @err_name = 'internal-server-error'
42
+ @msg = 'the server has experienced a misconfiguration'
43
+ @err = StreamError.import stream_error_node(@err_name, @msg)
44
+ end
45
+
46
+ it 'provides a err_name attribute' do
47
+ @err.must_respond_to :err_name
48
+ @err.err_name.must_equal @err_name
49
+ end
50
+
51
+ it 'provides a text attribute' do
52
+ @err.must_respond_to :text
53
+ @err.text.must_equal @msg
54
+ end
55
+
56
+ it 'provides an extras attribute' do
57
+ @err.must_respond_to :extras
58
+ @err.extras.must_be_instance_of Array
59
+ @err.extras.size.must_equal 1
60
+ @err.extras.first.element_name.must_equal 'extra-error'
61
+ end
62
+
63
+ it 'describes itself' do
64
+ @err.to_s.must_match(/#{@type}/)
65
+ @err.to_s.must_match(/#{@msg}/)
66
+
67
+ @err.inspect.must_match(/#{@type}/)
68
+ @err.inspect.must_match(/#{@msg}/)
69
+ end
70
+
71
+ it 'can be turned into xml' do
72
+ @err.must_respond_to :to_xml
73
+ @err.to_xml.must_equal "<stream:error>\n<internal-server-error xmlns=\"urn:ietf:params:xml:ns:xmpp-streams\"/>\n<text xmlns=\"urn:ietf:params:xml:ns:xmpp-streams\">the server has experienced a misconfiguration</text>\n<extra-error xmlns=\"blather:stream:error\">Blather Error</extra-error>\n</stream:error>"
74
+ end
75
+ end
76
+
77
+ describe 'Each XMPP stream error type' do
78
+ %w[ bad-format
79
+ bad-namespace-prefix
80
+ conflict
81
+ connection-timeout
82
+ host-gone
83
+ host-unknown
84
+ improper-addressing
85
+ internal-server-error
86
+ invalid-from
87
+ invalid-id
88
+ invalid-namespace
89
+ invalid-xml
90
+ not-authorized
91
+ policy-violation
92
+ remote-connection-failed
93
+ resource-constraint
94
+ restricted-xml
95
+ see-other-host
96
+ system-shutdown
97
+ undefined-condition
98
+ unsupported-encoding
99
+ unsupported-stanza-type
100
+ unsupported-version
101
+ xml-not-well-formed
102
+ ].each do |error_type|
103
+ it "provides a class for #{error_type}" do
104
+ e = StreamError.import stream_error_node(error_type)
105
+ klass = error_type.gsub(/^\w/) { |v| v.upcase }.gsub(/\-(\w)/) { |v| v.delete('-').upcase }
106
+ e.must_be_instance_of eval("StreamError::#{klass}")
107
+ end
108
+
109
+ it "registers #{error_type} in the handler heirarchy" do
110
+ e = StreamError.import stream_error_node(error_type)
111
+ e.handler_heirarchy.must_equal ["stream_#{error_type.gsub('-','_').gsub('_error','')}_error".to_sym, :stream_error, :error]
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,40 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. spec_helper])
2
+
3
+ describe 'Blather::BlatherError' do
4
+ it 'is handled by :error' do
5
+ BlatherError.new.handler_heirarchy.must_equal [:error]
6
+ end
7
+ end
8
+
9
+ describe 'Blather::ParseError' do
10
+ before { @error = ParseError.new('</generate-parse-error>"') }
11
+
12
+ it 'is registers with the handler heirarchy' do
13
+ @error.handler_heirarchy.must_equal [:parse_error, :error]
14
+ end
15
+
16
+ it 'contains the error message' do
17
+ @error.must_respond_to :message
18
+ @error.message.must_equal '</generate-parse-error>"'
19
+ end
20
+ end
21
+
22
+ describe 'Blather::TLSFailure' do
23
+ it 'is registers with the handler heirarchy' do
24
+ TLSFailure.new.handler_heirarchy.must_equal [:tls_failure, :error]
25
+ end
26
+ end
27
+
28
+ describe 'Blather::UnknownResponse' do
29
+ before { @error = UnknownResponse.new(XMPPNode.new('foo-bar')) }
30
+
31
+ it 'is registers with the handler heirarchy' do
32
+ @error.handler_heirarchy.must_equal [:unknown_response_error, :error]
33
+ end
34
+
35
+ it 'holds on to a copy of the failure node' do
36
+ @error.must_respond_to :node
37
+ @error.node.element_name.must_equal 'foo-bar'
38
+ end
39
+ end
40
+
@@ -76,13 +76,6 @@ describe 'Blather::JID' do
76
76
  JID.new('n', 'd').to_s.must_equal 'n@d'
77
77
  end
78
78
 
79
- it 'falls back to basic ruby if idn gem is not present' do
80
- JID.const_set 'USE_STRINGPREP', false
81
- jid = JID.new 'AB#$cdE@&^FE'
82
- jid.node.must_equal 'ab#$cde'
83
- jid.domain.must_equal '&^fe'
84
- end
85
-
86
79
  it 'provides a #stripped? helper' do
87
80
  jid = JID.new 'a@b/c'
88
81
  jid.must_respond_to :stripped?
@@ -77,4 +77,9 @@ describe 'Blather::RosterItem' do
77
77
  @i.status = @p
78
78
  @i.status = @p2
79
79
  end
80
+
81
+ it 'initializes groups to [nil] if the item is not part of a group' do
82
+ i = RosterItem.new 'n@d'
83
+ i.groups.must_equal [nil]
84
+ end
80
85
  end
@@ -19,24 +19,24 @@ describe 'Blather::Roster' do
19
19
  it 'processes @stanzas with remove requests' do
20
20
  s = @roster['n@d/0r']
21
21
  s.subscription = :remove
22
- proc { @roster.process(s.to_stanza) }.must_change('@roster.items', :length, :by => -1)
22
+ proc { @roster.process(s.to_stanza) }.must_change('@roster.items.length', :by => -1)
23
23
  end
24
24
 
25
25
  it 'processes @stanzas with add requests' do
26
26
  s = Stanza::Iq::Roster::RosterItem.new('a@b/c').to_stanza
27
- proc { @roster.process(s) }.must_change('@roster.items', :length, :by => 1)
27
+ proc { @roster.process(s) }.must_change('@roster.items.length', :by => 1)
28
28
  end
29
29
 
30
30
  it 'allows a jid to be pushed' do
31
31
  jid = 'a@b/c'
32
- proc { @roster.push(jid) }.must_change('@roster.items', :length, :by => 1)
32
+ proc { @roster.push(jid) }.must_change('@roster.items.length', :by => 1)
33
33
  @roster[jid].wont_be_nil
34
34
  end
35
35
 
36
36
  it 'allows an item to be pushed' do
37
37
  jid = 'a@b/c'
38
38
  item = RosterItem.new(JID.new(jid))
39
- proc { @roster.push(item) }.must_change('@roster.items', :length, :by => 1)
39
+ proc { @roster.push(item) }.must_change('@roster.items.length', :by => 1)
40
40
  @roster[jid].wont_be_nil
41
41
  end
42
42
 
@@ -45,7 +45,7 @@ describe 'Blather::Roster' do
45
45
  item = RosterItem.new(JID.new(jid))
46
46
  jid2 = 'd@e/f'
47
47
  item2 = RosterItem.new(JID.new(jid2))
48
- proc { @roster << item << item2 }.must_change('@roster.items', :length, :by => 2)
48
+ proc { @roster << item << item2 }.must_change('@roster.items.length', :by => 2)
49
49
  @roster[jid].wont_be_nil
50
50
  @roster[jid2].wont_be_nil
51
51
  end
@@ -58,7 +58,7 @@ describe 'Blather::Roster' do
58
58
  end
59
59
 
60
60
  it 'removes a JID' do
61
- proc { @roster.delete 'n@d' }.must_change('@roster.items', :length, :by => -1)
61
+ proc { @roster.delete 'n@d' }.must_change('@roster.items.length', :by => -1)
62
62
  end
63
63
 
64
64
  it 'sends a @roster removal over the wire' do