blather 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +1 -0
- data/LICENSE +20 -0
- data/Manifest +43 -0
- data/README.rdoc +78 -0
- data/Rakefile +16 -0
- data/blather.gemspec +41 -0
- data/examples/echo.rb +22 -0
- data/examples/shell_client.rb +28 -0
- data/lib/autotest/discover.rb +1 -0
- data/lib/autotest/spec.rb +60 -0
- data/lib/blather.rb +46 -0
- data/lib/blather/callback.rb +24 -0
- data/lib/blather/client.rb +81 -0
- data/lib/blather/core/errors.rb +24 -0
- data/lib/blather/core/jid.rb +101 -0
- data/lib/blather/core/roster.rb +84 -0
- data/lib/blather/core/roster_item.rb +92 -0
- data/lib/blather/core/stanza.rb +116 -0
- data/lib/blather/core/stanza/iq.rb +27 -0
- data/lib/blather/core/stanza/iq/query.rb +42 -0
- data/lib/blather/core/stanza/iq/roster.rb +96 -0
- data/lib/blather/core/stanza/message.rb +55 -0
- data/lib/blather/core/stanza/presence.rb +35 -0
- data/lib/blather/core/stanza/presence/status.rb +77 -0
- data/lib/blather/core/stanza/presence/subscription.rb +73 -0
- data/lib/blather/core/stream.rb +181 -0
- data/lib/blather/core/stream/parser.rb +74 -0
- data/lib/blather/core/stream/resource.rb +51 -0
- data/lib/blather/core/stream/sasl.rb +135 -0
- data/lib/blather/core/stream/session.rb +43 -0
- data/lib/blather/core/stream/tls.rb +29 -0
- data/lib/blather/core/sugar.rb +150 -0
- data/lib/blather/core/xmpp_node.rb +132 -0
- data/lib/blather/extensions.rb +4 -0
- data/lib/blather/extensions/last_activity.rb +55 -0
- data/lib/blather/extensions/version.rb +85 -0
- data/spec/blather/core/jid_spec.rb +78 -0
- data/spec/blather/core/roster_item_spec.rb +80 -0
- data/spec/blather/core/roster_spec.rb +79 -0
- data/spec/blather/core/stanza_spec.rb +95 -0
- data/spec/blather/core/stream_spec.rb +263 -0
- data/spec/blather/core/xmpp_node_spec.rb +130 -0
- data/spec/build_safe.rb +20 -0
- data/spec/spec_helper.rb +49 -0
- metadata +172 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe 'Blather::Stanza' do
|
4
|
+
it 'provides .next_id helper for generating new IDs' do
|
5
|
+
proc { Stanza.next_id }.must_change 'Stanza', :next_id
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'can import a node' do
|
9
|
+
s = Stanza.import XMPPNode.new('foo')
|
10
|
+
s.element_name.must_equal 'foo'
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'sets the ID when created' do
|
14
|
+
Stanza.new('message').id.wont_be_nil
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'sets the document when created' do
|
18
|
+
Stanza.new('message').doc.wont_be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'provides an #error? helper' do
|
22
|
+
s = Stanza.new('message')
|
23
|
+
s.error?.must_equal false
|
24
|
+
s.type = :error
|
25
|
+
s.error?.must_equal true
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'will generate a reply' do
|
29
|
+
s = Stanza.new('message')
|
30
|
+
s.from = f = JID.new('n@d/r')
|
31
|
+
s.to = t = JID.new('d@n/r')
|
32
|
+
|
33
|
+
r = s.reply
|
34
|
+
r.object_id.wont_equal s.object_id
|
35
|
+
r.from.must_equal t
|
36
|
+
r.to.must_equal f
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'convert to a reply' do
|
40
|
+
s = Stanza.new('message')
|
41
|
+
s.from = f = JID.new('n@d/r')
|
42
|
+
s.to = t = JID.new('d@n/r')
|
43
|
+
|
44
|
+
r = s.reply!
|
45
|
+
r.object_id.must_equal s.object_id
|
46
|
+
r.from.must_equal t
|
47
|
+
r.to.must_equal f
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'provides "attr_accessor" for id' do
|
51
|
+
s = Stanza.new('message')
|
52
|
+
s.id.wont_be_nil
|
53
|
+
s['id'].wont_be_nil
|
54
|
+
|
55
|
+
s.id = nil
|
56
|
+
s.id.must_be_nil
|
57
|
+
s['id'].must_be_nil
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'provides "attr_accessor" for to' do
|
61
|
+
s = Stanza.new('message')
|
62
|
+
s.to.must_be_nil
|
63
|
+
s['to'].must_be_nil
|
64
|
+
|
65
|
+
s.to = JID.new('n@d/r')
|
66
|
+
s.to.wont_be_nil
|
67
|
+
s.to.must_be_kind_of JID
|
68
|
+
|
69
|
+
s['to'].wont_be_nil
|
70
|
+
s['to'].must_equal 'n@d/r'
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'provides "attr_accessor" for from' do
|
74
|
+
s = Stanza.new('message')
|
75
|
+
s.from.must_be_nil
|
76
|
+
s['from'].must_be_nil
|
77
|
+
|
78
|
+
s.from = JID.new('n@d/r')
|
79
|
+
s.from.wont_be_nil
|
80
|
+
s.from.must_be_kind_of JID
|
81
|
+
|
82
|
+
s['from'].wont_be_nil
|
83
|
+
s['from'].must_equal 'n@d/r'
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'provides "attr_accessor" for type' do
|
87
|
+
s = Stanza.new('message')
|
88
|
+
s.type.must_be_nil
|
89
|
+
s['type'].must_be_nil
|
90
|
+
|
91
|
+
s.type = 'testing'
|
92
|
+
s.type.wont_be_nil
|
93
|
+
s['type'].wont_be_nil
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe 'Blather::Stream' do
|
4
|
+
class MockStream; include Stream; end
|
5
|
+
def mock_stream(&block)
|
6
|
+
@client = mock()
|
7
|
+
@client.stubs(:jid=)
|
8
|
+
stream = MockStream.new @client, JID.new('n@d/r'), 'pass'
|
9
|
+
|
10
|
+
stream.expects(:send_data).at_least(1).with &block
|
11
|
+
stream
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'can be started' do
|
15
|
+
client = mock()
|
16
|
+
params = [client, 'n@d/r', 'pass', 'host', 1234]
|
17
|
+
EM.expects(:connect).with do |*parms|
|
18
|
+
parms[0] == 'host' &&
|
19
|
+
parms[1] == 1234 &&
|
20
|
+
parms[3] == client &&
|
21
|
+
parms[5] == 'pass' &&
|
22
|
+
parms[4] == JID.new('n@d/r')
|
23
|
+
end
|
24
|
+
|
25
|
+
Stream.start *(params)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'can figure out the host to use based on the jid' do
|
29
|
+
client = mock()
|
30
|
+
params = [client, 'n@d/r', 'pass', 'd', 5222]
|
31
|
+
EM.expects(:connect).with do |*parms|
|
32
|
+
parms[0] == 'd' &&
|
33
|
+
parms[1] == 5222 &&
|
34
|
+
parms[3] == client &&
|
35
|
+
parms[5] == 'pass' &&
|
36
|
+
parms[4] == JID.new('n@d/r')
|
37
|
+
end
|
38
|
+
|
39
|
+
Stream.start client, 'n@d/r', 'pass'
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'starts the stream once the connection is complete' do
|
43
|
+
s = mock_stream { |d| d =~ /stream:stream/ }
|
44
|
+
s.connection_completed
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'starts TLS when asked' do
|
48
|
+
state = nil
|
49
|
+
@stream = mock_stream do |val|
|
50
|
+
case
|
51
|
+
when state.nil? && val =~ /stream:stream/ then state = :started
|
52
|
+
when state == :started && val =~ /starttls/ then true
|
53
|
+
else false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@stream.connection_completed
|
57
|
+
@stream.receive_data "<stream:stream><stream:features><starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' /></stream:features>"
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'connects via SASL MD5 when asked' do
|
61
|
+
Time.any_instance.stubs(:to_f).returns(1.1)
|
62
|
+
|
63
|
+
state = nil
|
64
|
+
client = mock()
|
65
|
+
client.stubs(:jid=)
|
66
|
+
stream = MockStream.new client, JID.new('n@d/r'), 'pass'
|
67
|
+
|
68
|
+
stream.expects(:send_data).times(5).with do |val|
|
69
|
+
case state
|
70
|
+
when nil
|
71
|
+
val.must_match(/stream:stream/)
|
72
|
+
state = :started
|
73
|
+
stream.receive_data "<stream:stream><stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>DIGEST-MD5</mechanism></mechanisms></stream:features>"
|
74
|
+
true
|
75
|
+
|
76
|
+
when :started
|
77
|
+
val.must_match(/auth.*DIGEST\-MD5/)
|
78
|
+
state = :auth_sent
|
79
|
+
stream.receive_data "<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cmVhbG09InNvbWVyZWFsbSIsbm9uY2U9Ik9BNk1HOXRFUUdtMmhoIixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNzCg==</challenge>"
|
80
|
+
true
|
81
|
+
|
82
|
+
when :auth_sent
|
83
|
+
val.must_equal('<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">bm9uY2U9Ik9BNk1HOXRFUUdtMmhoIixjaGFyc2V0PXV0Zi04LHVzZXJuYW1lPSJuIixyZWFsbT0ic29tZXJlYWxtIixjbm9uY2U9Ijc3N2Q0NWJiYmNkZjUwZDQ5YzQyYzcwYWQ3YWNmNWZlIixuYz0wMDAwMDAwMSxxb3A9YXV0aCxkaWdlc3QtdXJpPSJ4bXBwL2Qi</response>')
|
84
|
+
state = :response1_sent
|
85
|
+
stream.receive_data "<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZAo=</challenge>"
|
86
|
+
true
|
87
|
+
|
88
|
+
when :response1_sent
|
89
|
+
val.must_equal('<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>')
|
90
|
+
state = :response2_sent
|
91
|
+
stream.receive_data "<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"
|
92
|
+
true
|
93
|
+
|
94
|
+
when :response2_sent
|
95
|
+
val.must_match(/stream:stream/)
|
96
|
+
state = :complete
|
97
|
+
true
|
98
|
+
|
99
|
+
else
|
100
|
+
false
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
stream.connection_completed
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'will connect via SSL PLAIN when asked' do
|
108
|
+
state = nil
|
109
|
+
client = mock()
|
110
|
+
client.stubs(:jid=)
|
111
|
+
stream = MockStream.new client, JID.new('n@d/r'), 'pass'
|
112
|
+
|
113
|
+
stream.expects(:send_data).times(3).with do |val|
|
114
|
+
case state
|
115
|
+
when nil
|
116
|
+
val.must_match(/stream:stream/)
|
117
|
+
state = :started
|
118
|
+
stream.receive_data "<stream:stream><stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>PLAIN</mechanism></mechanisms></stream:features>"
|
119
|
+
true
|
120
|
+
|
121
|
+
when :started
|
122
|
+
val.must_equal('<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">bkBkAG4AcGFzcw==</auth>')
|
123
|
+
state = :auth_sent
|
124
|
+
stream.receive_data "<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"
|
125
|
+
true
|
126
|
+
|
127
|
+
when :auth_sent
|
128
|
+
val.must_match(/stream:stream/)
|
129
|
+
state = :complete
|
130
|
+
true
|
131
|
+
|
132
|
+
else
|
133
|
+
false
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
137
|
+
stream.connection_completed
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'will connect via SSL ANONYMOUS when asked' do
|
141
|
+
state = nil
|
142
|
+
client = mock()
|
143
|
+
client.stubs(:jid=)
|
144
|
+
stream = MockStream.new client, JID.new('n@d/r'), 'pass'
|
145
|
+
|
146
|
+
stream.expects(:send_data).times(3).with do |val|
|
147
|
+
case state
|
148
|
+
when nil
|
149
|
+
val.must_match(/stream:stream/)
|
150
|
+
state = :started
|
151
|
+
stream.receive_data "<stream:stream><stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>ANONYMOUS</mechanism></mechanisms></stream:features>"
|
152
|
+
true
|
153
|
+
|
154
|
+
when :started
|
155
|
+
val.must_equal('<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="ANONYMOUS">bg==</auth>')
|
156
|
+
state = :auth_sent
|
157
|
+
stream.receive_data "<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"
|
158
|
+
true
|
159
|
+
|
160
|
+
when :auth_sent
|
161
|
+
val.must_match(/stream:stream/)
|
162
|
+
state = :complete
|
163
|
+
true
|
164
|
+
|
165
|
+
else
|
166
|
+
false
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
170
|
+
stream.connection_completed
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'will bind to a resource set by the server' do
|
174
|
+
state = nil
|
175
|
+
class Client; attr_accessor :jid; end
|
176
|
+
client = Client.new
|
177
|
+
|
178
|
+
jid = JID.new('n@d')
|
179
|
+
stream = MockStream.new client, jid, 'pass'
|
180
|
+
|
181
|
+
stream.expects(:send_data).times(2).with do |val|
|
182
|
+
case state
|
183
|
+
when nil
|
184
|
+
val.must_match(/stream:stream/)
|
185
|
+
state = :started
|
186
|
+
stream.receive_data "<stream:stream><stream:features><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></stream:features>"
|
187
|
+
true
|
188
|
+
|
189
|
+
when :started
|
190
|
+
val.must_match(%r{<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"\s*/>})
|
191
|
+
val =~ %r{<iq[^>]+id="([^"]+)"}
|
192
|
+
state = :complete
|
193
|
+
stream.receive_data "<iq type='result' id='#{$1}'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>#{jid}/server_resource</jid></bind></iq>"
|
194
|
+
client.jid.must_equal JID.new('n@d/server_resource')
|
195
|
+
true
|
196
|
+
|
197
|
+
else
|
198
|
+
false
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|
202
|
+
stream.connection_completed
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'will bind to a resource set by the client' do
|
206
|
+
state = nil
|
207
|
+
class Client; attr_accessor :jid; end
|
208
|
+
client = Client.new
|
209
|
+
|
210
|
+
jid = JID.new('n@d/r')
|
211
|
+
stream = MockStream.new client, jid, 'pass'
|
212
|
+
|
213
|
+
stream.expects(:send_data).times(2).with do |val|
|
214
|
+
case state
|
215
|
+
when nil
|
216
|
+
val.must_match(/stream:stream/)
|
217
|
+
state = :started
|
218
|
+
stream.receive_data "<stream:stream><stream:features><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></stream:features>"
|
219
|
+
true
|
220
|
+
|
221
|
+
when :started
|
222
|
+
val.must_match(%r{<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>r</resource></bind>})
|
223
|
+
val =~ %r{<iq[^>]+id="([^"]+)"}
|
224
|
+
state = :complete
|
225
|
+
stream.receive_data "<iq type='result' id='#{$1}'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>#{jid}</jid></bind></iq>"
|
226
|
+
client.jid.must_equal JID.new('n@d/r')
|
227
|
+
true
|
228
|
+
|
229
|
+
else
|
230
|
+
false
|
231
|
+
|
232
|
+
end
|
233
|
+
end
|
234
|
+
stream.connection_completed
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'will establish a session if requested' do
|
238
|
+
state = nil
|
239
|
+
client = mock()
|
240
|
+
client.stubs(:jid=)
|
241
|
+
stream = MockStream.new client, JID.new('n@d/r'), 'pass'
|
242
|
+
|
243
|
+
stream.expects(:send_data).times(2).with do |val|
|
244
|
+
case state
|
245
|
+
when nil
|
246
|
+
val.must_match(/stream:stream/)
|
247
|
+
state = :started
|
248
|
+
stream.receive_data "<stream:stream><stream:features><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></stream:features>"
|
249
|
+
true
|
250
|
+
|
251
|
+
when :started
|
252
|
+
val.must_match('<iq id="[^"]+" type="set" to="d"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>')
|
253
|
+
state = :completed
|
254
|
+
true
|
255
|
+
|
256
|
+
else
|
257
|
+
false
|
258
|
+
|
259
|
+
end
|
260
|
+
end
|
261
|
+
stream.connection_completed
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe 'Blather::XMPPNode' do
|
4
|
+
it 'generates a new node' do
|
5
|
+
n = XMPPNode.new 'foo'
|
6
|
+
n.element_name.must_equal 'foo'
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'generates a node based on the current name' do
|
10
|
+
class Foo < XMPPNode; end
|
11
|
+
Foo.name = 'foo'
|
12
|
+
Foo.new.element_name.must_equal 'foo'
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'sets the namespace on creation' do
|
16
|
+
class Foo < XMPPNode; end
|
17
|
+
Foo.xmlns = 'foo'
|
18
|
+
Foo.new('foo').xmlns.must_equal 'foo'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'registers sub classes' do
|
22
|
+
class Foo < XMPPNode; register 'foo', 'foo:bar'; end
|
23
|
+
Foo.name.must_equal 'foo'
|
24
|
+
Foo.xmlns.must_equal 'foo:bar'
|
25
|
+
XMPPNode.class_from_registration('foo', 'foo:bar').must_equal Foo
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'imports another node' do
|
29
|
+
class Foo < XMPPNode; register 'foo', 'foo:bar'; end
|
30
|
+
n = XMPPNode.new('foo')
|
31
|
+
n.xmlns = 'foo:bar'
|
32
|
+
XMPPNode.import(n).must_be_kind_of Foo
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'can convert itself into a stanza' do
|
36
|
+
class Foo < XMPPNode; register 'foo'; end
|
37
|
+
n = XMPPNode.new('foo')
|
38
|
+
n.to_stanza.must_be_kind_of Foo
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'provides "attr_accessor" for xmlns' do
|
42
|
+
n = XMPPNode.new('foo')
|
43
|
+
n.xmlns.must_be_nil
|
44
|
+
n['xmlns'].must_be_nil
|
45
|
+
|
46
|
+
n.xmlns = 'foo:bar'
|
47
|
+
n.xmlns.must_equal 'foo:bar'
|
48
|
+
n['xmlns'].must_equal 'foo:bar'
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'will remove a child element' do
|
52
|
+
n = XMPPNode.new 'foo'
|
53
|
+
n << XMPPNode.new('bar')
|
54
|
+
n << XMPPNode.new('bar')
|
55
|
+
|
56
|
+
n.find('bar').size.must_equal 2
|
57
|
+
n.remove_child 'bar'
|
58
|
+
n.find('bar').size.must_equal 1
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'will remove a child with a specific xmlns' do
|
62
|
+
n = XMPPNode.new 'foo'
|
63
|
+
n << XMPPNode.new('bar')
|
64
|
+
c = XMPPNode.new('bar')
|
65
|
+
c.xmlns = 'foo:bar'
|
66
|
+
n << c
|
67
|
+
|
68
|
+
n.find('bar').size.must_equal 2
|
69
|
+
n.remove_child 'bar', 'foo:bar'
|
70
|
+
n.find('bar').size.must_equal 1
|
71
|
+
n.find('bar').first.xmlns.must_be_nil
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'will remove all child elements' do
|
75
|
+
n = XMPPNode.new 'foo'
|
76
|
+
n << XMPPNode.new('bar')
|
77
|
+
n << XMPPNode.new('bar')
|
78
|
+
|
79
|
+
n.find('bar').size.must_equal 2
|
80
|
+
n.remove_children 'bar'
|
81
|
+
n.find('bar').size.must_equal 0
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'provides a helper to grab content from a child' do
|
85
|
+
n = XMPPNode.new 'foo'
|
86
|
+
n << XMPPNode.new('bar', 'baz')
|
87
|
+
n.content_from(:bar).must_equal 'baz'
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'provides a copy mechanism' do
|
91
|
+
n = XMPPNode.new 'foo'
|
92
|
+
n2 = n.copy
|
93
|
+
n2.object_id.wont_equal n.object_id
|
94
|
+
n2.element_name.must_equal n.element_name
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'provides an inhert mechanism' do
|
98
|
+
n = XMPPNode.new 'foo'
|
99
|
+
n2 = XMPPNode.new 'foo', 'bar'
|
100
|
+
n2['foo'] = 'bar'
|
101
|
+
|
102
|
+
n.inherit(n2)
|
103
|
+
n['foo'].must_equal 'bar'
|
104
|
+
n.content.must_equal 'bar'
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'provides a mechanism to inherit attrs' do
|
108
|
+
n = XMPPNode.new 'foo'
|
109
|
+
n2 = XMPPNode.new 'foo'
|
110
|
+
n2['foo'] = 'bar'
|
111
|
+
|
112
|
+
n.inherit_attrs(n2.attributes)
|
113
|
+
n['foo'].must_equal 'bar'
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'cuts line breaks out of #to_s' do
|
117
|
+
n = XMPPNode.new 'foo'
|
118
|
+
n << XMPPNode.new('bar', 'baz')
|
119
|
+
n.to_s.scan(">\n<").size.must_equal 0
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'overrides #find to find without xpath' do
|
123
|
+
n = XMPPNode.new 'foo'
|
124
|
+
n << XMPPNode.new('bar', 'baz')
|
125
|
+
n.find('bar').must_be_kind_of Array
|
126
|
+
|
127
|
+
XML::Document.new.root = n
|
128
|
+
n.find('bar').must_be_kind_of XML::XPath::Object
|
129
|
+
end
|
130
|
+
end
|