vines 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. data/README +2 -2
  2. data/Rakefile +63 -8
  3. data/bin/vines +0 -1
  4. data/conf/config.rb +16 -7
  5. data/lib/vines.rb +21 -16
  6. data/lib/vines/command/init.rb +5 -3
  7. data/lib/vines/config.rb +34 -0
  8. data/lib/vines/contact.rb +14 -0
  9. data/lib/vines/stanza.rb +26 -0
  10. data/lib/vines/stanza/iq.rb +1 -1
  11. data/lib/vines/stanza/iq/disco_info.rb +3 -0
  12. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  13. data/lib/vines/stanza/iq/roster.rb +26 -30
  14. data/lib/vines/stanza/presence.rb +0 -12
  15. data/lib/vines/stanza/presence/subscribe.rb +3 -20
  16. data/lib/vines/stanza/presence/subscribed.rb +9 -10
  17. data/lib/vines/stanza/presence/unsubscribe.rb +8 -15
  18. data/lib/vines/stanza/presence/unsubscribed.rb +8 -8
  19. data/lib/vines/storage.rb +28 -0
  20. data/lib/vines/storage/couchdb.rb +29 -0
  21. data/lib/vines/storage/local.rb +22 -0
  22. data/lib/vines/storage/redis.rb +26 -0
  23. data/lib/vines/storage/sql.rb +48 -5
  24. data/lib/vines/stream/client.rb +6 -8
  25. data/lib/vines/stream/http.rb +23 -21
  26. data/lib/vines/stream/http/auth.rb +1 -1
  27. data/lib/vines/stream/http/bind.rb +1 -1
  28. data/lib/vines/stream/http/bind_restart.rb +4 -3
  29. data/lib/vines/stream/http/ready.rb +1 -1
  30. data/lib/vines/stream/http/request.rb +94 -5
  31. data/lib/vines/stream/http/session.rb +8 -6
  32. data/lib/vines/version.rb +1 -1
  33. data/test/config_test.rb +12 -0
  34. data/test/contact_test.rb +40 -0
  35. data/test/rake_test_loader.rb +11 -3
  36. data/test/stanza/iq/private_storage_test.rb +177 -0
  37. data/test/stanza/iq/roster_test.rb +1 -1
  38. data/test/stanza/iq_test.rb +63 -0
  39. data/test/storage/couchdb_test.rb +7 -1
  40. data/test/storage/local_test.rb +8 -2
  41. data/test/storage/redis_test.rb +16 -7
  42. data/test/storage/sql_test.rb +8 -1
  43. data/test/storage/storage_tests.rb +50 -0
  44. data/test/stream/http/auth_test.rb +3 -0
  45. data/test/stream/http/ready_test.rb +3 -0
  46. data/test/stream/http/request_test.rb +86 -0
  47. data/test/stream/parser_test.rb +2 -0
  48. data/web/404.html +43 -0
  49. data/web/apple-touch-icon.png +0 -0
  50. data/web/chat/coffeescripts/chat.coffee +385 -0
  51. data/web/chat/coffeescripts/init.coffee +15 -0
  52. data/web/chat/coffeescripts/logout.coffee +5 -0
  53. data/web/chat/index.html +17 -0
  54. data/web/chat/javascripts/app.js +1 -0
  55. data/web/chat/javascripts/chat.js +436 -0
  56. data/web/chat/javascripts/init.js +21 -0
  57. data/web/chat/javascripts/logout.js +11 -0
  58. data/web/chat/stylesheets/chat.css +290 -0
  59. data/web/favicon.png +0 -0
  60. data/web/lib/coffeescripts/contact.coffee +32 -0
  61. data/web/lib/coffeescripts/layout.coffee +30 -0
  62. data/web/lib/coffeescripts/login.coffee +52 -0
  63. data/web/lib/coffeescripts/navbar.coffee +84 -0
  64. data/web/lib/coffeescripts/router.coffee +40 -0
  65. data/web/lib/coffeescripts/session.coffee +211 -0
  66. data/web/lib/images/default-user.png +0 -0
  67. data/web/lib/images/logo-large.png +0 -0
  68. data/web/lib/images/logo-small.png +0 -0
  69. data/web/lib/javascripts/base.js +9 -0
  70. data/web/lib/javascripts/contact.js +94 -0
  71. data/web/lib/javascripts/icons.js +101 -0
  72. data/web/lib/javascripts/jquery.cookie.js +91 -0
  73. data/web/lib/javascripts/jquery.js +18 -0
  74. data/web/lib/javascripts/layout.js +48 -0
  75. data/web/lib/javascripts/login.js +61 -0
  76. data/web/lib/javascripts/navbar.js +69 -0
  77. data/web/lib/javascripts/raphael.js +8 -0
  78. data/web/lib/javascripts/router.js +105 -0
  79. data/web/lib/javascripts/session.js +322 -0
  80. data/web/lib/javascripts/strophe.js +1 -0
  81. data/web/lib/stylesheets/base.css +223 -0
  82. data/web/lib/stylesheets/login.css +63 -0
  83. metadata +51 -9
@@ -24,20 +24,18 @@ module Vines
24
24
  end
25
25
  end
26
26
 
27
+ %w[max_stanza_size max_resources_per_account private_storage?].each do |name|
28
+ define_method name do |*args|
29
+ @config[:client].send(name, *args)
30
+ end
31
+ end
32
+
27
33
  def ssl_handshake_completed
28
34
  if get_peer_cert
29
35
  close_connection unless cert_domain_matches?(@session.domain)
30
36
  end
31
37
  end
32
38
 
33
- def max_stanza_size
34
- @config[:client].max_stanza_size
35
- end
36
-
37
- def max_resources_per_account
38
- @config[:client].max_resources_per_account
39
- end
40
-
41
39
  def unbind
42
40
  @session.unbind!(self)
43
41
  super
@@ -17,38 +17,40 @@ module Vines
17
17
  body = ''
18
18
  p.on_body = proc {|data| body << data }
19
19
  p.on_message_complete = proc {
20
- process_request(body)
20
+ process_request(Request.new(self, @parser, body))
21
21
  body = ''
22
22
  }
23
23
  end
24
24
  end
25
25
 
26
- # Return true if this session ID matches the stream's session ID. Clients
27
- # are only allowed one session per stream so they must send the same
28
- # session ID on each request.
26
+ # If the session ID is valid, switch this stream's session to the new
27
+ # ID and return true. Some clients, like Google Chrome, reuse one stream
28
+ # for multiple sessions.
29
29
  def valid_session?(sid)
30
- @session.id == sid
31
- end
32
-
33
- def max_stanza_size
34
- @config[:http].max_stanza_size
30
+ if session = Sessions[sid]
31
+ @session = session
32
+ end
33
+ !!session
35
34
  end
36
35
 
37
- def max_resources_per_account
38
- @config[:http].max_resources_per_account
36
+ %w[max_stanza_size max_resources_per_account private_storage? bind root].each do |name|
37
+ define_method name do |*args|
38
+ @config[:http].send(name, *args)
39
+ end
39
40
  end
40
41
 
41
- def process_request(body)
42
- # proxy server ping
43
- if body.empty?
44
- req = Request.new(self, nil, 'text/plain')
45
- req.reply('online')
46
- close_connection_after_writing
47
- else
48
- body = Nokogiri::XML(body).root
49
- req = Request.new(self, body['rid'], @session.content_type)
50
- @session.request(req)
42
+ def process_request(request)
43
+ if request.path == self.bind
44
+ body = Nokogiri::XML(request.body).root
45
+ if session = Sessions[body['sid']]
46
+ session.request(request)
47
+ else
48
+ @session = Http::Session.new(self)
49
+ @session.request(request)
50
+ end
51
51
  @nodes.push(body)
52
+ else
53
+ request.reply_with_file(self.root)
52
54
  end
53
55
  end
54
56
 
@@ -9,7 +9,7 @@ module Vines
9
9
  end
10
10
 
11
11
  def node(node)
12
- unless body?(node) && node['rid'] && stream.valid_session?(node['sid'])
12
+ unless stream.valid_session?(node['sid']) && body?(node) && node['rid']
13
13
  raise StreamErrors::NotAuthorized
14
14
  end
15
15
  nodes = stream.parse_body(node)
@@ -11,7 +11,7 @@ module Vines
11
11
  end
12
12
 
13
13
  def node(node)
14
- unless body?(node) && node['rid'] && stream.valid_session?(node['sid'])
14
+ unless stream.valid_session?(node['sid']) && body?(node) && node['rid']
15
15
  raise StreamErrors::NotAuthorized
16
16
  end
17
17
  nodes = stream.parse_body(node)
@@ -9,7 +9,7 @@ module Vines
9
9
  end
10
10
 
11
11
  def node(node)
12
- raise StreamErrors::NotAuthorized unless body?(node) && restart?(node)
12
+ raise StreamErrors::NotAuthorized unless restart?(node)
13
13
 
14
14
  doc = Document.new
15
15
  body = doc.create_element('body') do |el|
@@ -26,9 +26,10 @@ module Vines
26
26
  private
27
27
 
28
28
  def restart?(node)
29
+ session = stream.valid_session?(node['sid'])
29
30
  restart = node.attribute_with_ns('restart', NAMESPACES[:bosh]).value rescue nil
30
- domain = node['to'] == stream.domain
31
- domain && restart == 'true' && node['rid'] && stream.valid_session?(node['sid'])
31
+ domain = node['to'] == stream.domain
32
+ session && body?(node) && domain && restart == 'true' && node['rid']
32
33
  end
33
34
  end
34
35
  end
@@ -7,7 +7,7 @@ module Vines
7
7
  RID, SID, TYPE, TERMINATE = %w[rid sid type terminate].map {|s| s.freeze }
8
8
 
9
9
  def node(node)
10
- unless body?(node) && node[RID] && stream.valid_session?(node[SID])
10
+ unless stream.valid_session?(node[SID]) && body?(node) && node[RID]
11
11
  raise StreamErrors::NotAuthorized
12
12
  end
13
13
  stream.parse_body(node).each do |child|
@@ -4,10 +4,32 @@ module Vines
4
4
  class Stream
5
5
  class Http
6
6
  class Request
7
- attr_reader :rid, :stream
7
+ BUF_SIZE = 1024
8
+ MODIFIED = '%a, %d %b %Y %H:%M:%S GMT'.freeze
9
+ NOT_FOUND = 'Not Found'.freeze
10
+ NOT_MODIFIED = 'Not Modified'.freeze
11
+ IF_MODIFIED = 'If-Modified-Since'.freeze
12
+ TEXT_PLAIN = 'text/plain'.freeze
13
+ CONTENT_TYPES = {
14
+ 'html' => 'text/html; charset="utf-8"',
15
+ 'js' => 'application/javascript; charset="utf-8"',
16
+ 'css' => 'text/css',
17
+ 'png' => 'image/png',
18
+ 'jpg' => 'image/jpeg',
19
+ 'jpeg' => 'image/jpeg',
20
+ 'gif' => 'image/gif',
21
+ 'manifest' => 'text/cache-manifest'
22
+ }.freeze
8
23
 
9
- def initialize(stream, rid, content_type)
10
- @stream, @rid, @content = stream, rid, content_type
24
+ attr_reader :stream, :body, :headers, :method, :path, :url, :query
25
+
26
+ def initialize(stream, parser, body)
27
+ @stream, @body = stream, body
28
+ @headers = parser.headers
29
+ @method = parser.http_method
30
+ @path = parser.request_path
31
+ @url = parser.request_url
32
+ @query = parser.query_string
11
33
  @received = Time.now
12
34
  end
13
35
 
@@ -16,17 +38,84 @@ module Vines
16
38
  Time.now - @received
17
39
  end
18
40
 
41
+ # Write the requested file to the client out of the given document root
42
+ # directory. Take care to prevent directory traversal attacks with paths
43
+ # like ../../../etc/passwd. Use the If-Modified-Since request header
44
+ # to implement caching.
45
+ def reply_with_file(dir)
46
+ path = File.expand_path(File.join(dir, @path))
47
+ path = File.join(path, 'index.html') if File.directory?(path)
48
+
49
+ if path.start_with?(dir) && File.exist?(path)
50
+ modified?(path) ? send_file(path) : send_status(304, NOT_MODIFIED)
51
+ else
52
+ missing = File.join(dir, '404.html')
53
+ if File.exist?(missing)
54
+ send_file(missing, 404, NOT_FOUND)
55
+ else
56
+ send_status(404, NOT_FOUND)
57
+ end
58
+ end
59
+ end
60
+
19
61
  # Send an HTTP 200 OK response wrapping the XMPP node content back
20
62
  # to the client.
21
- def reply(node)
63
+ def reply(node, content_type)
22
64
  body = node.to_s
23
65
  header = [
24
66
  "HTTP/1.1 200 OK",
25
- "Content-Type: #{@content}",
67
+ "Content-Type: #{content_type}",
26
68
  "Content-Length: #{body.bytesize}"
27
69
  ].join("\r\n")
28
70
  @stream.stream_write([header, body].join("\r\n\r\n"))
29
71
  end
72
+
73
+ private
74
+
75
+ # Return true if the file has been modified since the client last
76
+ # requested it with the If-Modified-Since header.
77
+ def modified?(path)
78
+ @headers[IF_MODIFIED] != mtime(path)
79
+ end
80
+
81
+ def mtime(path)
82
+ File.mtime(path).utc.strftime(MODIFIED)
83
+ end
84
+
85
+ def send_status(status, message)
86
+ header = [
87
+ "HTTP/1.1 #{status} #{message}",
88
+ "Connection: close"
89
+ ].join("\r\n")
90
+ @stream.stream_write("#{header}\r\n\r\n")
91
+ @stream.close_connection_after_writing
92
+ end
93
+
94
+ # Stream the contents of the file to the client in a 200 OK response.
95
+ # Send a Last-Modified response header so clients can send us an
96
+ # If-Modified-Since request header for caching.
97
+ def send_file(path, status=200, message='OK')
98
+ header = [
99
+ "HTTP/1.1 #{status} #{message}",
100
+ "Connection: close",
101
+ "Content-Type: #{content_type(path)}",
102
+ "Content-Length: #{File.size(path)}",
103
+ "Last-Modified: #{mtime(path)}"
104
+ ].join("\r\n")
105
+ @stream.stream_write("#{header}\r\n\r\n")
106
+
107
+ File.open(path) do |file|
108
+ while (buf = file.read(BUF_SIZE)) != nil
109
+ @stream.stream_write(buf)
110
+ end
111
+ end
112
+ @stream.close_connection_after_writing
113
+ end
114
+
115
+ def content_type(path)
116
+ ext = File.extname(path).sub('.', '')
117
+ CONTENT_TYPES[ext] || TEXT_PLAIN
118
+ end
30
119
  end
31
120
  end
32
121
  end
@@ -57,12 +57,12 @@ module Vines
57
57
 
58
58
  def request(request)
59
59
  if @responses.any?
60
- request.reply(wrap_body(@responses.join))
60
+ request.reply(wrap_body(@responses.join), @content_type)
61
61
  @replied = Time.now
62
62
  @responses.clear
63
63
  else
64
64
  while @requests.size >= @hold
65
- @requests.shift.reply(wrap_body(''))
65
+ @requests.shift.reply(wrap_body(''), @content_type)
66
66
  @replied = Time.now
67
67
  end
68
68
  @requests << request
@@ -72,8 +72,10 @@ module Vines
72
72
  # Send an HTTP 200 OK response wrapping the XMPP node content back
73
73
  # to the client.
74
74
  def reply(node)
75
- @requests.shift.reply(node)
76
- @replied = Time.now
75
+ if request = @requests.shift
76
+ request.reply(node, @content_type)
77
+ @replied = Time.now
78
+ end
77
79
  end
78
80
 
79
81
  # Write the XMPP node to the client stream after wrapping it in a BOSH
@@ -81,7 +83,7 @@ module Vines
81
83
  # immediately. If not, it's queued until the next request arrives.
82
84
  def write(node)
83
85
  if request = @requests.shift
84
- request.reply(wrap_body(node))
86
+ request.reply(wrap_body(node), @content_type)
85
87
  @replied = Time.now
86
88
  else
87
89
  @responses << node.to_s
@@ -97,7 +99,7 @@ module Vines
97
99
  def respond_to_expired_requests
98
100
  expired = @requests.select {|req| req.age > @wait }
99
101
  expired.each do |request|
100
- request.reply(wrap_body(''))
102
+ request.reply(wrap_body(''), @content_type)
101
103
  @requests.delete(request)
102
104
  @replied = Time.now
103
105
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Vines
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -205,6 +205,7 @@ class ConfigTest < MiniTest::Unit::TestCase
205
205
  assert_equal 5222, port.port
206
206
  assert_equal 131_072, port.max_stanza_size
207
207
  assert_equal 5, port.max_resources_per_account
208
+ refute port.private_storage?
208
209
  assert_equal Vines::Stream::Client, port.stream
209
210
  assert_same config, port.config
210
211
  assert_equal 1, config.ports.size
@@ -216,6 +217,7 @@ class ConfigTest < MiniTest::Unit::TestCase
216
217
  storage(:fs) { dir '.' }
217
218
  end
218
219
  client '0.0.0.1', 42 do
220
+ private_storage true
219
221
  max_stanza_size 60_000
220
222
  max_resources_per_account 1
221
223
  end
@@ -227,6 +229,7 @@ class ConfigTest < MiniTest::Unit::TestCase
227
229
  assert_equal 42, port.port
228
230
  assert_equal 60_000, port.max_stanza_size
229
231
  assert_equal 1, port.max_resources_per_account
232
+ assert port.private_storage?
230
233
  assert_equal Vines::Stream::Client, port.stream
231
234
  assert_same config, port.config
232
235
  assert_equal 1, config.ports.size
@@ -301,6 +304,9 @@ class ConfigTest < MiniTest::Unit::TestCase
301
304
  assert_equal 5280, port.port
302
305
  assert_equal 131_072, port.max_stanza_size
303
306
  assert_equal 5, port.max_resources_per_account
307
+ assert_equal File.join(Dir.pwd, 'web'), port.root
308
+ assert_equal '/xmpp', port.bind
309
+ refute port.private_storage?
304
310
  assert_equal Vines::Stream::Http, port.stream
305
311
  assert_same config, port.config
306
312
  assert_equal 1, config.ports.size
@@ -312,8 +318,11 @@ class ConfigTest < MiniTest::Unit::TestCase
312
318
  storage(:fs) { dir '.' }
313
319
  end
314
320
  http '0.0.0.1', 42 do
321
+ bind '/custom'
322
+ private_storage true
315
323
  max_stanza_size 60_000
316
324
  max_resources_per_account 1
325
+ root '/var/www/html'
317
326
  end
318
327
  end
319
328
  port = config.ports.first
@@ -323,6 +332,9 @@ class ConfigTest < MiniTest::Unit::TestCase
323
332
  assert_equal 42, port.port
324
333
  assert_equal 60_000, port.max_stanza_size
325
334
  assert_equal 1, port.max_resources_per_account
335
+ assert_equal '/var/www/html', port.root
336
+ assert_equal '/custom', port.bind
337
+ assert port.private_storage?
326
338
  assert_equal Vines::Stream::Http, port.stream
327
339
  assert_same config, port.config
328
340
  assert_equal 1, config.ports.size
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  require 'vines'
4
+ require 'ext/nokogiri'
4
5
  require 'minitest/autorun'
5
6
 
6
7
  class ContactTest < MiniTest::Unit::TestCase
@@ -39,4 +40,43 @@ class ContactTest < MiniTest::Unit::TestCase
39
40
 
40
41
  assert_equal expected, contact.to_roster_xml.to_xml(:indent => 0).gsub(/\n/, '')
41
42
  end
43
+
44
+ def test_send_roster_push
45
+ contact = Vines::Contact.new(
46
+ :jid => 'alice@wonderland.lit',
47
+ :name => "Alice",
48
+ :groups => %w[Friends Buddies],
49
+ :subscription => 'from')
50
+
51
+ recipient = MiniTest::Mock.new
52
+ recipient.expect(:user, Vines::User.new(:jid => 'hatter@wonderland.lit'))
53
+ def recipient.nodes; @nodes; end
54
+ def recipient.write(node)
55
+ @nodes ||= []
56
+ @nodes << node
57
+ end
58
+
59
+ contact.send_roster_push(recipient)
60
+ assert recipient.verify
61
+ assert_equal 1, recipient.nodes.size
62
+
63
+ expected = node(%q{
64
+ <iq to="hatter@wonderland.lit" type="set">
65
+ <query xmlns="jabber:iq:roster">
66
+ <item jid="alice@wonderland.lit" name="Alice" subscription="from">
67
+ <group>Buddies</group>
68
+ <group>Friends</group>
69
+ </item>
70
+ </query>
71
+ </iq>
72
+ }.strip.gsub(/\n/, '').gsub(/\s{2,}/, ''))
73
+ recipient.nodes[0].remove_attribute('id') # id is random
74
+ assert_equal expected, recipient.nodes[0]
75
+ end
76
+
77
+ private
78
+
79
+ def node(xml)
80
+ Nokogiri::XML(xml).root
81
+ end
42
82
  end
@@ -1,9 +1,17 @@
1
- #!/usr/bin/env ruby
1
+ require 'rake'
2
2
 
3
3
  # Use the latest MiniTest gem instead of the buggy
4
4
  # version included with Ruby 1.9.2.
5
- gem 'minitest'
5
+ gem 'minitest', '2.2.2'
6
6
 
7
7
  # Load the test files from the command line.
8
8
 
9
- ARGV.each { |f| load f unless f =~ /^-/ }
9
+ ARGV.each do |f|
10
+ next if f =~ /^-/
11
+
12
+ if f =~ /\*/
13
+ FileList[f].to_a.each { |fn| require File.expand_path(fn) }
14
+ else
15
+ require File.expand_path(f)
16
+ end
17
+ end
@@ -0,0 +1,177 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'vines'
4
+ require 'ext/nokogiri'
5
+ require 'minitest/autorun'
6
+
7
+ class PrivateStorageTest < MiniTest::Unit::TestCase
8
+ def setup
9
+ @stream = MiniTest::Mock.new
10
+ end
11
+
12
+ def test_feature_disabled_raises_error
13
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/></query>}
14
+ node = node(%Q{<iq id="42" type="get">#{query}</iq>})
15
+
16
+ @stream.expect(:private_storage?, false)
17
+
18
+ stanza = Vines::Stanza::Iq::PrivateStorage.new(node, @stream)
19
+ assert_raises(Vines::StanzaErrors::ServiceUnavailable) { stanza.process }
20
+ assert @stream.verify
21
+ end
22
+
23
+ def test_get_another_user_fragment_raises_error
24
+ alice = Vines::User.new(:jid => 'alice@wonderland.lit/tea')
25
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/></query>}
26
+ node = node(%Q{<iq id="42" to="hatter@wonderland.lit" type="get">#{query}</iq>})
27
+
28
+ @stream.expect(:private_storage?, true)
29
+ @stream.expect(:user, alice)
30
+
31
+ stanza = Vines::Stanza::Iq::PrivateStorage.new(node, @stream)
32
+ assert_raises(Vines::StanzaErrors::Forbidden) { stanza.process }
33
+ assert @stream.verify
34
+ end
35
+
36
+ def test_get_with_zero_children_raises_error
37
+ alice = Vines::User.new(:jid => 'alice@wonderland.lit/tea')
38
+ query = %q{<query xmlns="jabber:iq:private"></query>}
39
+ node = node(%Q{<iq id="42" type="get">#{query}</iq>})
40
+
41
+ @stream.expect(:private_storage?, true)
42
+
43
+ stanza = Vines::Stanza::Iq::PrivateStorage.new(node, @stream)
44
+ assert_raises(Vines::StanzaErrors::NotAcceptable) { stanza.process }
45
+ assert @stream.verify
46
+ end
47
+
48
+ def test_get_with_two_children_raises_error
49
+ alice = Vines::User.new(:jid => 'alice@wonderland.lit/tea')
50
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/><two xmlns="b"/></query>}
51
+ node = node(%Q{<iq id="42" type="get">#{query}</iq>})
52
+
53
+ @stream.expect(:private_storage?, true)
54
+
55
+ stanza = Vines::Stanza::Iq::PrivateStorage.new(node, @stream)
56
+ assert_raises(Vines::StanzaErrors::NotAcceptable) { stanza.process }
57
+ assert @stream.verify
58
+ end
59
+
60
+ def test_set_with_zero_children_raises_error
61
+ alice = Vines::User.new(:jid => 'alice@wonderland.lit/tea')
62
+ query = %q{<query xmlns="jabber:iq:private"></query>}
63
+ node = node(%Q{<iq id="42" type="set">#{query}</iq>})
64
+
65
+ @stream.expect(:private_storage?, true)
66
+
67
+ stanza = Vines::Stanza::Iq::PrivateStorage.new(node, @stream)
68
+ assert_raises(Vines::StanzaErrors::NotAcceptable) { stanza.process }
69
+ assert @stream.verify
70
+ end
71
+
72
+ def test_get_without_namespace_raises_error
73
+ alice = Vines::User.new(:jid => 'alice@wonderland.lit/tea')
74
+ query = %q{<query xmlns="jabber:iq:private"><one/></query>}
75
+ node = node(%Q{<iq id="42" type="get">#{query}</iq>})
76
+
77
+ @stream.expect(:private_storage?, true)
78
+
79
+ stanza = Vines::Stanza::Iq::PrivateStorage.new(node, @stream)
80
+ assert_raises(Vines::StanzaErrors::NotAcceptable) { stanza.process }
81
+ assert @stream.verify
82
+ end
83
+
84
+ def test_get_missing_fragment_raises_error
85
+ alice = Vines::User.new(:jid => 'alice@wonderland.lit/tea')
86
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/></query>}
87
+ node = node(%Q{<iq id="42" type="get">#{query}</iq>})
88
+
89
+ storage = MiniTest::Mock.new
90
+ storage.expect(:find_fragment, nil, [alice.jid, node.elements[0].elements[0]])
91
+
92
+ @stream.expect(:private_storage?, true)
93
+ @stream.expect(:domain, 'wonderland.lit')
94
+ @stream.expect(:storage, storage, ['wonderland.lit'])
95
+ @stream.expect(:user, alice)
96
+
97
+ stanza = Vines::Stanza::Iq::PrivateStorage.new(node, @stream)
98
+ assert_raises(Vines::StanzaErrors::ItemNotFound) { stanza.process }
99
+ assert @stream.verify
100
+ assert storage.verify
101
+ end
102
+
103
+ def test_get_finds_fragment_writes_to_stream
104
+ alice = Vines::User.new(:jid => 'alice@wonderland.lit/tea')
105
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/></query>}
106
+ node = node(%Q{<iq id="42" type="get">#{query}</iq>})
107
+
108
+ data = %q{<one xmlns="a"><child>data</child></one>}
109
+ query = %Q{<query xmlns="jabber:iq:private">#{data}</query>}
110
+ expected = node(%Q{<iq from="#{alice.jid}" id="42" to="#{alice.jid}" type="result">#{query}</iq>})
111
+
112
+ storage = MiniTest::Mock.new
113
+ storage.expect(:find_fragment, node(data), [alice.jid, node.elements[0].elements[0]])
114
+
115
+ @stream.expect(:private_storage?, true)
116
+ @stream.expect(:domain, 'wonderland.lit')
117
+ @stream.expect(:storage, storage, ['wonderland.lit'])
118
+ @stream.expect(:user, alice)
119
+ @stream.expect(:write, nil, [expected])
120
+
121
+ stanza = Vines::Stanza::Iq::PrivateStorage.new(node, @stream)
122
+ stanza.process
123
+ assert @stream.verify
124
+ assert storage.verify
125
+ end
126
+
127
+ def test_set_one_fragment_writes_result_to_stream
128
+ alice = Vines::User.new(:jid => 'alice@wonderland.lit/tea')
129
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/></query>}
130
+ node = node(%Q{<iq id="42" type="set">#{query}</iq>})
131
+
132
+ storage = MiniTest::Mock.new
133
+ storage.expect(:save_fragment, nil, [alice.jid, node.elements[0].elements[0]])
134
+
135
+ expected = node(%Q{<iq from="#{alice.jid}" id="42" to="#{alice.jid}" type="result"/>})
136
+
137
+ @stream.expect(:private_storage?, true)
138
+ @stream.expect(:domain, 'wonderland.lit')
139
+ @stream.expect(:storage, storage, ['wonderland.lit'])
140
+ @stream.expect(:user, alice)
141
+ @stream.expect(:write, nil, [expected])
142
+
143
+ stanza = Vines::Stanza::Iq::PrivateStorage.new(node, @stream)
144
+ stanza.process
145
+ assert @stream.verify
146
+ assert storage.verify
147
+ end
148
+
149
+ def test_set_two_fragments_writes_result_to_stream
150
+ alice = Vines::User.new(:jid => 'alice@wonderland.lit/tea')
151
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/><two xmlns="a"/></query>}
152
+ node = node(%Q{<iq id="42" type="set">#{query}</iq>})
153
+
154
+ storage = MiniTest::Mock.new
155
+ storage.expect(:save_fragment, nil, [alice.jid, node.elements[0].elements[0]])
156
+ storage.expect(:save_fragment, nil, [alice.jid, node.elements[0].elements[1]])
157
+
158
+ expected = node(%Q{<iq from="#{alice.jid}" id="42" to="#{alice.jid}" type="result"/>})
159
+
160
+ @stream.expect(:private_storage?, true)
161
+ @stream.expect(:domain, 'wonderland.lit')
162
+ @stream.expect(:storage, storage, ['wonderland.lit'])
163
+ @stream.expect(:user, alice)
164
+ @stream.expect(:write, nil, [expected])
165
+
166
+ stanza = Vines::Stanza::Iq::PrivateStorage.new(node, @stream)
167
+ stanza.process
168
+ assert @stream.verify
169
+ assert storage.verify
170
+ end
171
+
172
+ private
173
+
174
+ def node(xml)
175
+ Nokogiri::XML(xml).root
176
+ end
177
+ end