sprsquish-blather 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +1 -0
- data/LICENSE +20 -0
- data/README.rdoc +3 -0
- data/blather.gemspec +73 -0
- data/lib/autotest/discover.rb +1 -0
- data/lib/autotest/spec.rb +60 -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 +105 -0
- data/lib/blather/core/roster.rb +62 -0
- data/lib/blather/core/roster_item.rb +64 -0
- data/lib/blather/core/stanza.rb +90 -0
- data/lib/blather/core/stream.rb +179 -0
- data/lib/blather/core/sugar.rb +148 -0
- data/lib/blather/core/xmpp_node.rb +95 -0
- data/lib/blather/extensions/last_activity.rb +57 -0
- data/lib/blather/extensions/version.rb +85 -0
- data/lib/blather.rb +54 -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/spec_helper.rb +49 -0
- metadata +116 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe 'Blather::RosterItem' do
|
4
|
+
it 'initializes with JID' do
|
5
|
+
jid = JID.new(jid)
|
6
|
+
i = RosterItem.new jid
|
7
|
+
i.jid.must_equal jid
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'initializes with an Iq::RosterItem' do
|
11
|
+
jid = 'n@d/r'
|
12
|
+
i = RosterItem.new Stanza::Iq::Roster::RosterItem.new(jid)
|
13
|
+
i.jid.must_equal JID.new(jid).stripped
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'has a JID setter that strips the JID' do
|
17
|
+
jid = JID.new('n@d/r')
|
18
|
+
i = RosterItem.new nil
|
19
|
+
i.jid = jid
|
20
|
+
i.jid.must_equal jid.stripped
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'has a subscription setter that forces a symbol' do
|
24
|
+
i = RosterItem.new nil
|
25
|
+
i.subscription = 'remove'
|
26
|
+
i.subscription.must_equal :remove
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'forces the type of subscription' do
|
30
|
+
proc { RosterItem.new(nil).subscription = 'foo' }.must_raise Blather::ArgumentError
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'returns :none if the subscription field is blank' do
|
34
|
+
RosterItem.new(nil).subscription.must_equal :none
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'ensure #ask is a symbol' do
|
38
|
+
i = RosterItem.new(nil)
|
39
|
+
i.ask = 'subscribe'
|
40
|
+
i.ask.must_equal :subscribe
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'forces #ask to be :subscribe or nothing at all' do
|
44
|
+
proc { RosterItem.new(nil).ask = 'foo' }.must_raise Blather::ArgumentError
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'generates a stanza with #to_stanza' do
|
48
|
+
jid = JID.new('n@d/r')
|
49
|
+
i = RosterItem.new jid
|
50
|
+
s = i.to_stanza
|
51
|
+
s.must_be_kind_of Stanza::Iq::Roster
|
52
|
+
s.items.first.jid.must_equal jid.stripped
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'returns status based on priority' do
|
56
|
+
setup_item_with_presences
|
57
|
+
@i.status.must_equal @p2
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'returns status based on resource' do
|
61
|
+
setup_item_with_presences
|
62
|
+
@i.status('a').must_equal @p
|
63
|
+
end
|
64
|
+
|
65
|
+
def setup_item_with_presences
|
66
|
+
@jid = JID.new('n@d/r')
|
67
|
+
@i = RosterItem.new @jid
|
68
|
+
|
69
|
+
@p = Stanza::Presence::Status.new(:away)
|
70
|
+
@p.from = 'n@d/a'
|
71
|
+
@p.priority = 0
|
72
|
+
|
73
|
+
@p2 = Stanza::Presence::Status.new(:dnd)
|
74
|
+
@p2.from = 'n@d/b'
|
75
|
+
@p2.priority = -1
|
76
|
+
|
77
|
+
@i.status = @p
|
78
|
+
@i.status = @p2
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe 'Blather::Roster' do
|
4
|
+
before do
|
5
|
+
@stream = mock()
|
6
|
+
@stream.stubs(:send_data)
|
7
|
+
|
8
|
+
@stanza = mock()
|
9
|
+
items = []; 4.times { |n| items << JID.new("n@d/#{n}r") }
|
10
|
+
@stanza.stubs(:items).returns(items)
|
11
|
+
|
12
|
+
@roster = Roster.new(@stream, @stanza)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'initializes with items' do
|
16
|
+
@roster.items.map { |_,i| i.jid.to_s }.must_equal(@stanza.items.map { |i| i.stripped.to_s }.uniq)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'processes @stanzas with remove requests' do
|
20
|
+
s = @roster['n@d/0r']
|
21
|
+
s.subscription = :remove
|
22
|
+
proc { @roster.process(s.to_stanza) }.must_change('@roster.items', :length, :by => -1)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'processes @stanzas with add requests' do
|
26
|
+
s = Stanza::Iq::Roster::RosterItem.new('a@b/c').to_stanza
|
27
|
+
proc { @roster.process(s) }.must_change('@roster.items', :length, :by => 1)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'allows a jid to be pushed' do
|
31
|
+
jid = 'a@b/c'
|
32
|
+
proc { @roster.push(jid) }.must_change('@roster.items', :length, :by => 1)
|
33
|
+
@roster[jid].wont_be_nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'allows an item to be pushed' do
|
37
|
+
jid = 'a@b/c'
|
38
|
+
item = RosterItem.new(JID.new(jid))
|
39
|
+
proc { @roster.push(item) }.must_change('@roster.items', :length, :by => 1)
|
40
|
+
@roster[jid].wont_be_nil
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'sends a @roster addition over the wire' do
|
44
|
+
stream = mock()
|
45
|
+
stream.expects(:send_data)
|
46
|
+
roster = Roster.new stream, @stanza
|
47
|
+
roster.push('a@b/c')
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'removes a JID' do
|
51
|
+
proc { @roster.delete 'n@d' }.must_change('@roster.items', :length, :by => -1)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'sends a @roster removal over the wire' do
|
55
|
+
stream = mock(:send_data => nil)
|
56
|
+
roster = Roster.new stream, @stanza
|
57
|
+
roster.delete('a@b/c')
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'returns an item through []' do
|
61
|
+
item = @roster['n@d']
|
62
|
+
item.must_be_kind_of RosterItem
|
63
|
+
item.jid.must_equal JID.new('n@d')
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'responds to #each' do
|
67
|
+
@roster.must_respond_to :each
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'cycles through the items using #each' do
|
71
|
+
@roster.map { |i| i }.sort.must_equal(@roster.items.sort)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'returns a duplicate of items through #items' do
|
75
|
+
items = @roster.items
|
76
|
+
items.delete 'n@d'
|
77
|
+
items.wont_equal @roster.items
|
78
|
+
end
|
79
|
+
end
|
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[.. lib blather])
|
2
|
+
require 'rubygems'
|
3
|
+
require 'minitest/spec'
|
4
|
+
require 'mocha'
|
5
|
+
|
6
|
+
module MiniTest
|
7
|
+
if MINI_DIR =~ %r{^./}
|
8
|
+
require 'pathname'
|
9
|
+
path = Pathname.new(MINI_DIR).realpath
|
10
|
+
# remove_const 'MINI_DIR'
|
11
|
+
# const_set 'MINI_DIR', path.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
module Assertions
|
15
|
+
def assert_change(obj, method, args = {}, msg = nil)
|
16
|
+
msg ||= proc {
|
17
|
+
m = "Expected #{obj}.#{method} to change"
|
18
|
+
m << " by #{mu_pp args[:by]}" if args[:by]
|
19
|
+
m << (args[:from] ? " from #{mu_pp args[:from]}" : '') + " to #{mu_pp args[:to]}" if args[:to]
|
20
|
+
m
|
21
|
+
}.call
|
22
|
+
|
23
|
+
init_val = eval(obj).__send__(method)
|
24
|
+
yield
|
25
|
+
new_val = eval(obj).__send__(method)
|
26
|
+
|
27
|
+
assert_equal(args[:by], (new_val - init_val), msg) if args[:by]
|
28
|
+
assert_equal([args[:from], args[:to]], [(init_val if args[:from]), new_val], msg) if args[:to]
|
29
|
+
refute_equal(init_val, new_val, msg) if args.empty?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Object
|
35
|
+
def must_change *args, &block
|
36
|
+
return MiniTest::Spec.current.assert_change(*args, &self) if Proc === self
|
37
|
+
return MiniTest::Spec.current.assert_change(args.first, self) if args.size == 1
|
38
|
+
return MiniTest::Spec.current.assert_change(self, *args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
require 'mocha/expectation_error'
|
43
|
+
|
44
|
+
include Blather
|
45
|
+
include MiniTest
|
46
|
+
|
47
|
+
LOG.level = Logger::INFO
|
48
|
+
|
49
|
+
Unit.autorun
|