vines 0.1.1 → 0.2.0

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 (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