cancer 0.1.0.a1

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.
Files changed (89) hide show
  1. data/.autotest +3 -0
  2. data/.gitignore +5 -0
  3. data/LICENSE.txt +22 -0
  4. data/Rakefile +46 -0
  5. data/VERSION +1 -0
  6. data/cancer.gemspec +156 -0
  7. data/documentation/STREAM_INITIATION.markdown +18 -0
  8. data/examples/example.rb +80 -0
  9. data/examples/example_2.rb +20 -0
  10. data/examples/example_em.rb +11 -0
  11. data/examples/example_em_helper.rb +23 -0
  12. data/examples/example_im_roster.rb +26 -0
  13. data/examples/example_xep_0004.rb +124 -0
  14. data/examples/example_xep_0047.rb +35 -0
  15. data/examples/example_xep_0050.rb +78 -0
  16. data/examples/example_xep_0065.rb +66 -0
  17. data/examples/example_xep_0096.dup.rb +40 -0
  18. data/examples/example_xep_0096_xep_0047.rb +42 -0
  19. data/examples/example_xep_0096_xep_0065.rb +40 -0
  20. data/lib/cancer.rb +122 -0
  21. data/lib/cancer/adapter.rb +33 -0
  22. data/lib/cancer/adapters/em.rb +88 -0
  23. data/lib/cancer/adapters/socket.rb +122 -0
  24. data/lib/cancer/builder.rb +28 -0
  25. data/lib/cancer/controller.rb +32 -0
  26. data/lib/cancer/dependencies.rb +187 -0
  27. data/lib/cancer/events/abstract_event.rb +10 -0
  28. data/lib/cancer/events/base_matchers.rb +63 -0
  29. data/lib/cancer/events/binary_matchers.rb +30 -0
  30. data/lib/cancer/events/eventable.rb +92 -0
  31. data/lib/cancer/events/exception_events.rb +28 -0
  32. data/lib/cancer/events/handler.rb +33 -0
  33. data/lib/cancer/events/manager.rb +130 -0
  34. data/lib/cancer/events/named_events.rb +25 -0
  35. data/lib/cancer/events/proc_matcher.rb +16 -0
  36. data/lib/cancer/events/xml_events.rb +44 -0
  37. data/lib/cancer/exceptions.rb +53 -0
  38. data/lib/cancer/jid.rb +215 -0
  39. data/lib/cancer/lock.rb +32 -0
  40. data/lib/cancer/spec.rb +35 -0
  41. data/lib/cancer/spec/error.rb +18 -0
  42. data/lib/cancer/spec/extentions.rb +79 -0
  43. data/lib/cancer/spec/matcher.rb +30 -0
  44. data/lib/cancer/spec/mock_adapter.rb +139 -0
  45. data/lib/cancer/spec/mock_stream.rb +15 -0
  46. data/lib/cancer/spec/transcript.rb +107 -0
  47. data/lib/cancer/stream.rb +182 -0
  48. data/lib/cancer/stream/builder.rb +20 -0
  49. data/lib/cancer/stream/controller.rb +36 -0
  50. data/lib/cancer/stream/event_helper.rb +12 -0
  51. data/lib/cancer/stream/xep.rb +52 -0
  52. data/lib/cancer/stream_parser.rb +144 -0
  53. data/lib/cancer/support.rb +27 -0
  54. data/lib/cancer/support/hash.rb +32 -0
  55. data/lib/cancer/support/string.rb +22 -0
  56. data/lib/cancer/synchronized_stanza.rb +79 -0
  57. data/lib/cancer/thread_pool.rb +118 -0
  58. data/lib/cancer/xep.rb +43 -0
  59. data/lib/cancer/xeps/core.rb +109 -0
  60. data/lib/cancer/xeps/core/bind.rb +33 -0
  61. data/lib/cancer/xeps/core/sasl.rb +113 -0
  62. data/lib/cancer/xeps/core/session.rb +22 -0
  63. data/lib/cancer/xeps/core/stream.rb +18 -0
  64. data/lib/cancer/xeps/core/terminator.rb +21 -0
  65. data/lib/cancer/xeps/core/tls.rb +34 -0
  66. data/lib/cancer/xeps/im.rb +323 -0
  67. data/lib/cancer/xeps/xep_0004_x_data.rb +692 -0
  68. data/lib/cancer/xeps/xep_0020_feature_neg.rb +35 -0
  69. data/lib/cancer/xeps/xep_0030_disco.rb +167 -0
  70. data/lib/cancer/xeps/xep_0047_ibb.rb +322 -0
  71. data/lib/cancer/xeps/xep_0050_commands.rb +256 -0
  72. data/lib/cancer/xeps/xep_0065_bytestreams.rb +306 -0
  73. data/lib/cancer/xeps/xep_0066_oob.rb +69 -0
  74. data/lib/cancer/xeps/xep_0095_si.rb +211 -0
  75. data/lib/cancer/xeps/xep_0096_si_filetransfer.rb +173 -0
  76. data/lib/cancer/xeps/xep_0114_component.rb +73 -0
  77. data/lib/cancer/xeps/xep_0115_caps.rb +180 -0
  78. data/lib/cancer/xeps/xep_0138_compress.rb +134 -0
  79. data/lib/cancer/xeps/xep_0144_rosterx.rb +250 -0
  80. data/lib/cancer/xeps/xep_0184_receipts.rb +40 -0
  81. data/lib/cancer/xeps/xep_0199_ping.rb +41 -0
  82. data/spec/spec.opts +2 -0
  83. data/spec/spec_helper.rb +14 -0
  84. data/spec/stream/stanza_errors_spec.rb +47 -0
  85. data/spec/stream/stream_errors_spec.rb +38 -0
  86. data/spec/stream/stream_initialization_spec.rb +160 -0
  87. data/spec/xep_0050/local_spec.rb +165 -0
  88. data/spec/xep_0050/remote_spec.rb +44 -0
  89. metadata +200 -0
@@ -0,0 +1,40 @@
1
+
2
+ module Cancer
3
+ # Message Receipts
4
+ module XEP_0184
5
+ include Cancer::XEP
6
+
7
+ NS = 'urn:xmpp:receipts'
8
+
9
+ dependency 'core'
10
+
11
+ def self.enhance_stream(stream)
12
+ stream.extend_builder do
13
+ include Cancer::XEP_0184::Builder
14
+ end
15
+ stream.install_controller(Cancer::XEP_0184::Controller)
16
+ end
17
+
18
+ module Builder
19
+
20
+ def request_receipt
21
+ request :xmlns => NS
22
+ end
23
+
24
+ end
25
+
26
+ class Controller < Cancer::Controller
27
+
28
+ priority 1000
29
+
30
+ on { |e| e.xpath('/c:message/n:request', 'n' => NS, 'c' => CLIENT_NS) }
31
+ def respond_with_received(e)
32
+ send_message(e.sender, nil, e.id) do |x|
33
+ x.received :xmlns => NS
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+
2
+ module Cancer
3
+ # XMPP Ping
4
+ # http://xmpp.org/extensions/xep-0199.html
5
+ module XEP_0199
6
+ include Cancer::XEP
7
+
8
+ NS = 'urn:xmpp:ping'
9
+
10
+ dependency 'core'
11
+ dependency 'xep-0030'
12
+
13
+ def self.enhance_stream(stream)
14
+ stream.extend_stream do
15
+ include Cancer::XEP_0199::StreamHelpers
16
+ end
17
+ stream.install_controller(Cancer::XEP_0199::Controller)
18
+ stream.disco.feature(NS)
19
+ end
20
+
21
+ module StreamHelpers
22
+
23
+ def ping(to)
24
+ send_iq(to) do |x|
25
+ x.ping(:xmlns => NS)
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ class Controller < Cancer::Controller
32
+
33
+ on { |e| e.xpath('/c:iq/p:ping', 'p' => NS, 'c' => CLIENT_NS) }
34
+ def pong(e)
35
+ send_iq(e.sender, :result, e.id)
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,2 @@
1
+ -f specdoc
2
+ --color
@@ -0,0 +1,14 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/cancer')
2
+
3
+ module Kernel
4
+ def hp(object)
5
+ puts "<pre>"+object.inspect.gsub('<', '&lt;')+"</pre>"
6
+ end
7
+ end
8
+
9
+ Cancer::Spec.debug = false
10
+ Cancer.logger.level = Logger::FATAL
11
+
12
+ Spec::Runner.configure do |config|
13
+ config.include(Cancer::Spec)
14
+ end
@@ -0,0 +1,47 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Cancer::StanzaError do
4
+
5
+ it "should be raised when a stream error is received" do
6
+ dialog_with do |stream|
7
+
8
+ lambda do
9
+ stream.send_iq('knave@wonderland.lit')
10
+ end.should raise_error(Cancer::StanzaModifyError)
11
+
12
+ end.should_produce_transcript do |t|
13
+ t.out %{<iq type="get" to="knave@wonderland.lit" id="cancer-3"/>}
14
+ t.in %{<iq type="error" to="alice@wonderland.lit" id="cancer-3"><error type="modify"><bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /></error></iq>}
15
+ end
16
+ end
17
+
18
+ it "should contain error conditions" do
19
+ dialog_with do |stream|
20
+
21
+ lambda do
22
+ stream.send_iq('knave@wonderland.lit')
23
+ end.should raise_error(Cancer::StanzaModifyError) { |error|
24
+ error.conditions.should_not be_empty
25
+ error.conditions.first.name.should == 'bad-request'
26
+ }
27
+
28
+ end.should_produce_transcript do |t|
29
+ t.out %{<iq type="get" to="knave@wonderland.lit" id="cancer-3"/>}
30
+ t.in %{<iq type="error" to="alice@wonderland.lit" id="cancer-3"><error type="modify"><bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /></error></iq>}
31
+ end
32
+ end
33
+
34
+ it "should have message equal to error/text if present" do
35
+ dialog_with do |stream|
36
+
37
+ lambda do
38
+ stream.send_iq('knave@wonderland.lit')
39
+ end.should raise_error(Cancer::StanzaModifyError, 'My error message')
40
+
41
+ end.should_produce_transcript do |t|
42
+ t.out %{<iq type="get" to="knave@wonderland.lit" id="cancer-3"/>}
43
+ t.in %{<iq type="error" to="alice@wonderland.lit" id="cancer-3"><error type="modify"><bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns='urn:ietf:params:xml:ns:xmpp-streams'>My error message</text></error></iq>}
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,38 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Cancer::StreamError do
4
+
5
+ it "should be raised when a stream error is received" do
6
+ lambda do
7
+ dialog_with do |stream|
8
+
9
+ end.should_produce_transcript do |t|
10
+ t.in %{<stream:error><bad-format xmlns='urn:ietf:params:xml:ns:xmpp-streams'/></stream:error>}
11
+ end
12
+ end.should raise_error(Cancer::StreamError)
13
+ end
14
+
15
+ it "should contain error conditions" do
16
+ lambda do
17
+ dialog_with do |stream|
18
+
19
+ end.should_produce_transcript do |t|
20
+ t.in %{<stream:error><bad-format xmlns='urn:ietf:params:xml:ns:xmpp-streams'/></stream:error>}
21
+ end
22
+ end.should raise_error(Cancer::StreamError) { |error|
23
+ error.conditions.should_not be_empty
24
+ error.conditions.first.name.should == 'bad-format'
25
+ }
26
+ end
27
+
28
+ it "should have message equal to stream:error/text if present" do
29
+ lambda do
30
+ dialog_with do |stream|
31
+
32
+ end.should_produce_transcript do |t|
33
+ t.in %{<stream:error><bad-format xmlns='urn:ietf:params:xml:ns:xmpp-streams'/><text xmlns='urn:ietf:params:xml:ns:xmpp-streams'>My error message</text></stream:error>}
34
+ end
35
+ end.should raise_error(Cancer::StreamError, 'My error message')
36
+ end
37
+
38
+ end
@@ -0,0 +1,160 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Cancer::Stream do
4
+
5
+ it "should initiate with tls sasl bind and session" do
6
+ dialog do |adapter|
7
+ Cancer::Stream.open(:adapter => adapter,
8
+ :jid => 'alice@wonderland.lit', :password => 'ihatethequeen') { }
9
+ end.should_produce_transcript do |t|
10
+ t.start_stream('alice@wonderland.lit')
11
+ t.in %{<stream:features>
12
+ <starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls">
13
+ <optional/>
14
+ </starttls>
15
+ <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
16
+ <mechanism>PLAIN</mechanism>
17
+ <required/>
18
+ </mechanisms>
19
+ </stream:features>}
20
+
21
+ t.out %{<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>}
22
+ t.in %{<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>}
23
+
24
+ t.start_stream('alice@wonderland.lit')
25
+ t.in %{<stream:features>
26
+ <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
27
+ <mechanism>PLAIN</mechanism>
28
+ <required/>
29
+ </mechanisms>
30
+ </stream:features>}
31
+
32
+ t.out %{<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">AGFsaWNlQHdvbmRlcmxhbmQubGl0AGloYXRldGhlcXVlZW4=</auth>}
33
+ t.in %{<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />}
34
+
35
+ t.start_stream('alice@wonderland.lit')
36
+ t.in %{<stream:features>
37
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
38
+ <required/>
39
+ </bind>
40
+ <session xmlns="urn:ietf:params:xml:ns:xmpp-session">
41
+ <optional/>
42
+ </session>
43
+ </stream:features>}
44
+
45
+ t.out %{<iq type="set" id="cancer-1"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>}
46
+ t.in %{<iq type="result" id="cancer-1">
47
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
48
+ <jid>alice@wonderland.lit/rabbithole</jid></bind></iq>}
49
+
50
+ t.out %{<iq type="set" id="cancer-2"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>}
51
+ t.in %{<iq type="result" id="cancer-2" />}
52
+
53
+ t.close_stream
54
+
55
+ end
56
+ end
57
+
58
+ it "should initiate with sasl bind and session" do
59
+ dialog do |adapter|
60
+ Cancer::Stream.open(:adapter => adapter,
61
+ :jid => 'alice@wonderland.lit', :password => 'ihatethequeen') { }
62
+ end.should_produce_transcript do |t|
63
+
64
+ t.start_stream('alice@wonderland.lit')
65
+ t.in %{<stream:features>
66
+ <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
67
+ <mechanism>PLAIN</mechanism>
68
+ <required/>
69
+ </mechanisms>
70
+ </stream:features>}
71
+
72
+ t.out %{<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">AGFsaWNlQHdvbmRlcmxhbmQubGl0AGloYXRldGhlcXVlZW4=</auth>}
73
+ t.in %{<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />}
74
+
75
+ t.start_stream('alice@wonderland.lit')
76
+ t.in %{<stream:features>
77
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
78
+ <required/>
79
+ </bind>
80
+ <session xmlns="urn:ietf:params:xml:ns:xmpp-session">
81
+ <optional/>
82
+ </session>
83
+ </stream:features>}
84
+
85
+ t.out %{<iq type="set" id="cancer-1"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>}
86
+ t.in %{<iq type="result" id="cancer-1">
87
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
88
+ <jid>alice@wonderland.lit/rabbithole</jid></bind></iq>}
89
+
90
+ t.out %{<iq type="set" id="cancer-2"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>}
91
+ t.in %{<iq type="result" id="cancer-2" />}
92
+
93
+ t.close_stream
94
+
95
+ end
96
+ end
97
+
98
+ it "should initiate with bind and session" do
99
+ dialog do |adapter|
100
+ Cancer::Stream.open(:adapter => adapter,
101
+ :jid => 'alice@wonderland.lit', :password => 'ihatethequeen') { }
102
+ end.should_produce_transcript do |t|
103
+
104
+ t.start_stream('alice@wonderland.lit')
105
+ t.in %{<stream:features>
106
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
107
+ <required/>
108
+ </bind>
109
+ <session xmlns="urn:ietf:params:xml:ns:xmpp-session">
110
+ <optional/>
111
+ </session>
112
+ </stream:features>}
113
+
114
+ t.out %{<iq type="set" id="cancer-1"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>}
115
+ t.in %{<iq type="result" id="cancer-1">
116
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
117
+ <jid>alice@wonderland.lit/rabbithole</jid></bind></iq>}
118
+
119
+ t.out %{<iq type="set" id="cancer-2"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>}
120
+ t.in %{<iq type="result" id="cancer-2" />}
121
+
122
+ t.close_stream
123
+
124
+ end
125
+ end
126
+
127
+ it "should initiate with bind" do
128
+ dialog do |adapter|
129
+ Cancer::Stream.open(:adapter => adapter,
130
+ :jid => 'alice@wonderland.lit', :password => 'ihatethequeen') { }
131
+ end.should_produce_transcript do |t|
132
+
133
+ t.start_stream('alice@wonderland.lit')
134
+ t.in %{<stream:features>
135
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
136
+ <required/>
137
+ </bind>
138
+ </stream:features>}
139
+
140
+ t.out %{<iq type="set" id="cancer-1"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>}
141
+ t.in %{<iq type="result" id="cancer-1">
142
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
143
+ <jid>alice@wonderland.lit/rabbithole</jid></bind></iq>}
144
+
145
+
146
+ t.close_stream
147
+
148
+ t.verify { |stream| stream.options[:jid].to_s.should == "alice@wonderland.lit/rabbithole" }
149
+ end
150
+ end
151
+
152
+ it "should initiate " do
153
+ dialog_with() do |stream|
154
+
155
+ end.should_produce_transcript do |t|
156
+ t.verify { |stream| stream.options[:jid].to_s.should == "alice@wonderland.lit/rabbithole" }
157
+ end
158
+ end
159
+
160
+ end
@@ -0,0 +1,165 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Cancer::XEP_0050, '#local' do
4
+
5
+ it "handles command events" do
6
+ dialog_with(:xeps => %w( xep-0050 )) do |stream|
7
+
8
+ stream.controller do
9
+
10
+ on { |e| e.command(:config) }
11
+ def config(e)
12
+ e.session.instance_variable_set :@id, 'config-1' # mock the session id
13
+
14
+ e.session.complete! do |x|
15
+ x.note(:type => 'info') do
16
+ x.text "Service 'httpd' has been configured."
17
+ end
18
+ end
19
+
20
+ ensure
21
+ $waiting_thread.wakeup
22
+ end
23
+
24
+ end
25
+
26
+ $waiting_thread = Thread.current and Thread.stop
27
+
28
+ end.should_produce_transcript do |t|
29
+ t.in %{<iq type="set" from="knave@wonderland.lit" id="cancer-3"><command xmlns='http://jabber.org/protocol/commands' node='config' action='execute'/></iq>}
30
+ t.out %{<iq type='result' to='knave@wonderland.lit' id='cancer-3'><command xmlns='http://jabber.org/protocol/commands' sessionid='config-1' node='config' status='completed'><note type='info'>Service 'httpd' has been configured.</note></command></iq>}
31
+ end
32
+ end
33
+
34
+ it "handles multistage commands" do
35
+ dialog_with(:xeps => %w( xep-0050 )) do |stream|
36
+
37
+ stream.controller do
38
+
39
+ define_form 'config.form' do |form|
40
+ form.title = "Setup your profile"
41
+ form.instructions << "Enter your profile in the fields below."
42
+
43
+ form.define :name do |f|
44
+ f.type = :'text-single'
45
+ f.required = true
46
+ f.label = "Name"
47
+ end
48
+
49
+ form.define :sex do |f|
50
+ f.type = :'list-single'
51
+ f.add_option 'm', 'Man'
52
+ f.add_option 'w', 'Woman'
53
+ f.add_option 'o', 'Other'
54
+ f.label = "Sex"
55
+ end
56
+
57
+ form.define :jid do |f|
58
+ f.type = :'jid-single'
59
+ f.required = true
60
+ f.add_option 'alice@wonderland.lit'
61
+ f.add_option 'knave@wonderland.lit'
62
+ f.label = "JID"
63
+ f.description = 'Choose your preferred jabber address.'
64
+ end
65
+ end
66
+
67
+ on { |e| e.command(:config) }
68
+ def config(e)
69
+
70
+ e.session.next! :run, 'config.form', :values => { :name => 'Simon Menke'}
71
+
72
+ throw :halt
73
+ rescue Exception => e
74
+ $action_exception = e
75
+ ensure
76
+ $waiting_thread.wakeup
77
+ end
78
+
79
+ on { |e| e.command(:config, :stage => :run) }
80
+ def config_run(e)
81
+
82
+ e.session.complete! do |x|
83
+ x.note(:type => 'info') do
84
+ x.text "Your profile has been updated."
85
+ end
86
+ end
87
+
88
+ rescue Exception => e
89
+ $action_exception = e
90
+ ensure
91
+ $waiting_thread.wakeup
92
+ end
93
+
94
+ end
95
+
96
+ $waiting_thread = Thread.current and Thread.stop
97
+ raise $action_exception if $action_exception
98
+
99
+ $waiting_thread = Thread.current and Thread.stop
100
+ raise $action_exception if $action_exception
101
+
102
+ end.should_produce_transcript do |t|
103
+ t.in %{
104
+ <iq type="set" from="knave@wonderland.lit" id="cancer-3">
105
+ <command xmlns="http://jabber.org/protocol/commands" node="config" action="execute" sessionid="config-1"/>
106
+ </iq>}
107
+
108
+ t.out %{
109
+ <iq type="result" to="knave@wonderland.lit" id="cancer-3">
110
+ <command xmlns="http://jabber.org/protocol/commands" status="executing" node="config" sessionid="config-1">
111
+ <actions/>
112
+ <x xmlns="jabber:x:data" type="form">
113
+ <title>Setup your profile</title>
114
+ <instructions>Enter your profile in the fields below.</instructions>
115
+ <field label="Name" var="name">
116
+ <required/>
117
+ <value>Simon Menke</value>
118
+ </field>
119
+ <field type="list-single" label="Sex" var="sex">
120
+ <option label="Woman">
121
+ <value>w</value>
122
+ </option>
123
+ <option label="Man">
124
+ <value>m</value>
125
+ </option>
126
+ <option label="Other">
127
+ <value>o</value>
128
+ </option>
129
+ </field>
130
+ <field type="jid-single" label="JID" var="jid">
131
+ <required/>
132
+ <desc>Choose your preferred jabber address.</desc>
133
+ <option>
134
+ <value>knave@wonderland.lit</value>
135
+ </option>
136
+ <option>
137
+ <value>alice@wonderland.lit</value>
138
+ </option>
139
+ </field>
140
+ </x>
141
+ </command>
142
+ </iq>}
143
+
144
+ t.in %{
145
+ <iq type="set" from="knave@wonderland.lit" id="cancer-4">
146
+ <command xmlns="http://jabber.org/protocol/commands" action="execute" node="config" sessionid="config-1">
147
+ <x xmlns="jabber:x:data" type="submit">
148
+ <field var="name">
149
+ <value>Hans Spooren</value>
150
+ </field>
151
+ <field type="list-single" var="sex">
152
+ <value>m</value>
153
+ </field>
154
+ <field type="jid-single" var="jid">
155
+ <value>knave@wonderland.lit</value>
156
+ </field>
157
+ </x>
158
+ </command>
159
+ </iq>}
160
+
161
+ t.out %{<iq type='result' to='knave@wonderland.lit' id='cancer-4'><command xmlns='http://jabber.org/protocol/commands' sessionid='config-1' node='config' status='completed'><note type='info'>Your profile has been updated.</note></command></iq>}
162
+ end
163
+ end
164
+
165
+ end