vines 0.4.9 → 0.4.10

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.
@@ -25,13 +25,22 @@ module Vines
25
25
 
26
26
  attr_reader :stream, :body, :headers, :method, :path, :url, :query
27
27
 
28
+ # Create a new request parsed from an HTTP client connection. We'll try
29
+ # to keep this request open until there are stanzas available to send
30
+ # as a response.
31
+ #
32
+ # stream - The Stream::Http client connection that received the request.
33
+ # parser - The Http::Parser that parsed the HTTP request.
34
+ # body - The String request body.
28
35
  def initialize(stream, parser, body)
29
- @stream, @body = stream, body
36
+ uri = URI(parser.request_url)
37
+ @stream = stream
38
+ @body = body
30
39
  @headers = parser.headers
31
40
  @method = parser.http_method
32
- @path = parser.request_path
33
41
  @url = parser.request_url
34
- @query = parser.query_string
42
+ @path = uri.path
43
+ @query = uri.query
35
44
  @received = Time.now
36
45
  end
37
46
 
@@ -44,10 +53,12 @@ module Vines
44
53
  # directory. Take care to prevent directory traversal attacks with paths
45
54
  # like ../../../etc/passwd. Use the If-Modified-Since request header
46
55
  # to implement caching.
56
+ #
57
+ # Returns nothing.
47
58
  def reply_with_file(dir)
48
59
  path = File.expand_path(File.join(dir, @path))
49
60
 
50
- # redirect requests missing a slash so relative links work
61
+ # Redirect requests missing a slash so relative links work.
51
62
  if File.directory?(path) && !@path.end_with?('/')
52
63
  send_status(301, MOVED, "Location: #{redirect_uri}")
53
64
  return
@@ -69,6 +80,8 @@ module Vines
69
80
 
70
81
  # Send an HTTP 200 OK response wrapping the XMPP node content back
71
82
  # to the client.
83
+ #
84
+ # Returns nothing.
72
85
  def reply(node, content_type)
73
86
  body = node.to_s
74
87
  header = [
@@ -90,6 +103,8 @@ module Vines
90
103
  # Send a 200 OK response, allowing any origin domain to connect to the
91
104
  # server, in response to CORS preflight OPTIONS requests. This allows
92
105
  # any web application using strophe.js to connect to our BOSH port.
106
+ #
107
+ # Returns nothing.
93
108
  def reply_to_options
94
109
  allow = @headers['Access-Control-Request-Headers']
95
110
  headers = [
@@ -107,6 +122,8 @@ module Vines
107
122
  # wasn't sent by the client, just return the relative path that
108
123
  # was requested. The Location response header must contain the fully
109
124
  # qualified URI, but most browsers will accept relative paths as well.
125
+ #
126
+ # Returns the String URL.
110
127
  def redirect_uri
111
128
  host = headers['Host']
112
129
  uri = "#{path}/"
@@ -137,6 +154,8 @@ module Vines
137
154
  # Stream the contents of the file to the client in a 200 OK response.
138
155
  # Send a Last-Modified response header so clients can send us an
139
156
  # If-Modified-Since request header for caching.
157
+ #
158
+ # Returns nothing.
140
159
  def send_file(path, status=200, message='OK')
141
160
  header = [
142
161
  "HTTP/1.1 #{status} #{message}",
@@ -162,6 +181,8 @@ module Vines
162
181
  # HTTP server. Reverse proxy servers (nginx/apache) can use this cookie
163
182
  # to implement sticky sessions. Return nil if vroute was not set in
164
183
  # config.rb and no cookie should be sent.
184
+ #
185
+ # Returns a String cookie value or nil if disabled.
165
186
  def vroute_cookie
166
187
  route = @stream.config[:http].vroute
167
188
  route ? "Set-Cookie: vroute=#{route}; path=/; HttpOnly" : nil
@@ -169,4 +190,4 @@ module Vines
169
190
  end
170
191
  end
171
192
  end
172
- end
193
+ end
@@ -73,6 +73,10 @@ module Vines
73
73
 
74
74
  # Send an HTTP 200 OK response wrapping the XMPP node content back
75
75
  # to the client.
76
+ #
77
+ # node - The XML::Node to send to the client.
78
+ #
79
+ # Returns nothing.
76
80
  def reply(node)
77
81
  if request = @requests.shift
78
82
  request.reply(node, @content_type)
@@ -83,6 +87,10 @@ module Vines
83
87
  # Write the XMPP node to the client stream after wrapping it in a BOSH
84
88
  # body tag. If there's a waiting request, the node is written
85
89
  # immediately. If not, it's queued until the next request arrives.
90
+ #
91
+ # data - The XML String or XML::Node to send in the next HTTP response.
92
+ #
93
+ # Returns nothing.
86
94
  def write(node)
87
95
  if request = @requests.shift
88
96
  request.reply(wrap_body(node), @content_type)
@@ -125,23 +125,29 @@ module Vines
125
125
 
126
126
  private
127
127
 
128
- # The +to+ and +from+ domain addresses set on the initial stream header
128
+ # The `to` and `from` domain addresses set on the initial stream header
129
129
  # must not change during stream restarts. This prevents a server from
130
130
  # authenticating as one domain, then sending stanzas from users in a
131
131
  # different domain.
132
+ #
133
+ # to - The String domain the other server thinks we are.
134
+ # from - The String domain the other server is asserting as its identity.
135
+ #
136
+ # Returns true if the other server is misbehaving and its connection
137
+ # should be closed.
132
138
  def domain_change?(to, from)
133
139
  to != @domain || from != @remote_domain
134
140
  end
135
141
 
136
142
  def send_stream_header
137
143
  attrs = {
138
- 'xmlns' => NAMESPACES[:server],
144
+ 'xmlns' => NAMESPACES[:server],
139
145
  'xmlns:stream' => NAMESPACES[:stream],
140
- 'xml:lang' => 'en',
141
- 'id' => Kit.uuid,
142
- 'from' => @domain,
143
- 'to' => @remote_domain,
144
- 'version' => '1.0'
146
+ 'xml:lang' => 'en',
147
+ 'id' => Kit.uuid,
148
+ 'from' => @domain,
149
+ 'to' => @remote_domain,
150
+ 'version' => '1.0'
145
151
  }
146
152
  write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
147
153
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Vines
4
- VERSION = '0.4.9'
4
+ VERSION = '0.4.10'
5
5
  end
@@ -3,96 +3,113 @@
3
3
  require 'test_helper'
4
4
 
5
5
  describe Vines::Store do
6
- before do
7
- dir = 'conf/certs'
8
-
9
- domain, key = certificate('wonderland.lit')
10
- File.open("#{dir}/wonderland.lit.crt", 'w') {|f| f.write(domain) }
11
- File.open("#{dir}/wonderland.lit.key", 'w') {|f| f.write(key) }
12
-
13
- wildcard, key = certificate('*.wonderland.lit')
14
- File.open("#{dir}/wildcard.lit.crt", 'w') {|f| f.write(wildcard) }
15
- File.open("#{dir}/wildcard.lit.key", 'w') {|f| f.write(key) }
6
+ let(:dir) { 'conf/certs' }
7
+ let(:domain_pair) { certificate('wonderland.lit') }
8
+ let(:wildcard_pair) { certificate('*.wonderland.lit') }
9
+ subject { Vines::Store.new(dir) }
16
10
 
17
- @store = Vines::Store.new('conf/certs')
11
+ before do
12
+ @files =
13
+ save('wonderland.lit', domain_pair) +
14
+ save('wildcard.lit', wildcard_pair) +
15
+ save('duplicate.lit', domain_pair)
18
16
  end
19
17
 
20
18
  after do
21
- %w[wonderland.lit.crt wonderland.lit.key wildcard.lit.crt wildcard.lit.key].each do |f|
22
- name = "conf/certs/#{f}"
19
+ @files.each do |name|
23
20
  File.delete(name) if File.exists?(name)
24
21
  end
25
22
  end
26
23
 
27
- it 'parses certificate files' do
28
- refute @store.certs.empty?
29
- assert_equal OpenSSL::X509::Certificate, @store.certs.first.class
30
- end
24
+ describe 'creating a store' do
25
+ it 'parses certificate files' do
26
+ refute subject.certs.empty?
27
+ assert_equal OpenSSL::X509::Certificate, subject.certs.first.class
28
+ end
29
+
30
+ it 'ignores expired certificates' do
31
+ assert subject.certs.all? {|c| c.not_after > Time.new }
32
+ end
31
33
 
32
- it 'ignores expired certificates' do
33
- assert @store.certs.all? {|c| c.not_after > Time.new }
34
+ it 'does not raise an error for duplicate certificates' do
35
+ assert Vines::Store.new(dir)
36
+ end
34
37
  end
35
38
 
36
39
  describe 'files_for_domain' do
37
40
  it 'handles invalid input' do
38
- assert_nil @store.files_for_domain(nil)
39
- assert_nil @store.files_for_domain('')
41
+ assert_nil subject.files_for_domain(nil)
42
+ assert_nil subject.files_for_domain('')
40
43
  end
41
44
 
42
45
  it 'finds files by name' do
43
- refute_nil @store.files_for_domain('wonderland.lit')
44
- cert, key = @store.files_for_domain('wonderland.lit')
46
+ refute_nil subject.files_for_domain('wonderland.lit')
47
+ cert, key = subject.files_for_domain('wonderland.lit')
45
48
  assert_certificate_matches_key cert, key
46
49
  assert_equal 'wonderland.lit.crt', File.basename(cert)
47
50
  assert_equal 'wonderland.lit.key', File.basename(key)
48
51
  end
49
52
 
50
53
  it 'finds files for wildcard' do
51
- refute_nil @store.files_for_domain('foo.wonderland.lit')
52
- cert, key = @store.files_for_domain('foo.wonderland.lit')
54
+ refute_nil subject.files_for_domain('foo.wonderland.lit')
55
+ cert, key = subject.files_for_domain('foo.wonderland.lit')
53
56
  assert_certificate_matches_key cert, key
54
57
  assert_equal 'wildcard.lit.crt', File.basename(cert)
55
58
  assert_equal 'wildcard.lit.key', File.basename(key)
56
59
  end
57
60
  end
58
61
 
62
+ describe 'trusted?' do
63
+ it 'does not trust malformed certificates' do
64
+ refute subject.trusted?('bogus')
65
+ end
66
+
67
+ it 'does not trust unsigned certificates' do
68
+ pair = certificate('something.lit')
69
+ refute subject.trusted?(pair.cert)
70
+ end
71
+ end
72
+
59
73
  describe 'domain?' do
60
74
  it 'handles invalid input' do
61
- cert, key = certificate('wonderland.lit')
62
- refute @store.domain?(nil, nil)
63
- refute @store.domain?(cert, nil)
64
- refute @store.domain?(cert, '')
65
- refute @store.domain?(nil, '')
66
- assert @store.domain?(cert, 'wonderland.lit')
75
+ pair = certificate('wonderland.lit')
76
+ refute subject.domain?(nil, nil)
77
+ refute subject.domain?(pair.cert, nil)
78
+ refute subject.domain?(pair.cert, '')
79
+ refute subject.domain?(nil, '')
80
+ assert subject.domain?(pair.cert, 'wonderland.lit')
67
81
  end
68
82
 
69
83
  it 'verifies certificate subject domains' do
70
- cert, key = certificate('wonderland.lit')
71
- refute @store.domain?(cert, 'bogus')
72
- refute @store.domain?(cert, 'www.wonderland.lit')
73
- assert @store.domain?(cert, 'wonderland.lit')
84
+ pair = certificate('wonderland.lit')
85
+ refute subject.domain?(pair.cert, 'bogus')
86
+ refute subject.domain?(pair.cert, 'www.wonderland.lit')
87
+ assert subject.domain?(pair.cert, 'wonderland.lit')
74
88
  end
75
89
 
76
90
  it 'verifies certificate subject alt domains' do
77
- cert, key = certificate('wonderland.lit', 'www.wonderland.lit')
78
- refute @store.domain?(cert, 'bogus')
79
- refute @store.domain?(cert, 'tea.wonderland.lit')
80
- assert @store.domain?(cert, 'www.wonderland.lit')
81
- assert @store.domain?(cert, 'wonderland.lit')
91
+ pair = certificate('wonderland.lit', 'www.wonderland.lit')
92
+ refute subject.domain?(pair.cert, 'bogus')
93
+ refute subject.domain?(pair.cert, 'tea.wonderland.lit')
94
+ assert subject.domain?(pair.cert, 'www.wonderland.lit')
95
+ assert subject.domain?(pair.cert, 'wonderland.lit')
82
96
  end
83
97
 
84
98
  it 'verifies certificate wildcard domains' do
85
- cert, key = certificate('wonderland.lit', '*.wonderland.lit')
86
- refute @store.domain?(cert, 'bogus')
87
- refute @store.domain?(cert, 'one.two.wonderland.lit')
88
- assert @store.domain?(cert, 'tea.wonderland.lit')
89
- assert @store.domain?(cert, 'www.wonderland.lit')
90
- assert @store.domain?(cert, 'wonderland.lit')
99
+ pair = certificate('wonderland.lit', '*.wonderland.lit')
100
+ refute subject.domain?(pair.cert, 'bogus')
101
+ refute subject.domain?(pair.cert, 'one.two.wonderland.lit')
102
+ assert subject.domain?(pair.cert, 'tea.wonderland.lit')
103
+ assert subject.domain?(pair.cert, 'www.wonderland.lit')
104
+ assert subject.domain?(pair.cert, 'wonderland.lit')
91
105
  end
92
106
  end
93
107
 
94
108
  private
95
109
 
110
+ # A public certificate + private key pair.
111
+ Pair = Struct.new(:cert, :key)
112
+
96
113
  def assert_certificate_matches_key(cert, key)
97
114
  refute_nil cert
98
115
  refute_nil key
@@ -102,7 +119,7 @@ describe Vines::Store do
102
119
  end
103
120
 
104
121
  def certificate(domain, altname=nil)
105
- # use small key so tests are fast
122
+ # Use small key so tests are fast.
106
123
  key = OpenSSL::PKey::RSA.generate(256)
107
124
 
108
125
  name = OpenSSL::X509::Name.parse("/C=US/ST=Colorado/L=Denver/O=Test/CN=#{domain}")
@@ -125,6 +142,21 @@ describe Vines::Store do
125
142
  ].map {|k, v| factory.create_ext(k, v) }
126
143
  end
127
144
 
128
- [cert.to_pem, key.to_pem]
145
+ Pair.new(cert.to_pem, key.to_pem)
146
+ end
147
+
148
+ # Write the domain's certificate and private key files to the filesystem for
149
+ # the store to use.
150
+ #
151
+ # domain - The domain name String to use in the file name (e.g. wonderland.lit).
152
+ # pair - The Pair containing the public certificate and private key data.
153
+ #
154
+ # Returns a String Array of file names that were written.
155
+ def save(domain, pair)
156
+ crt = File.expand_path("#{domain}.crt", dir)
157
+ key = File.expand_path("#{domain}.key", dir)
158
+ File.open(crt, 'w') {|f| f.write(pair.cert) }
159
+ File.open(key, 'w') {|f| f.write(pair.key) }
160
+ [crt, key]
129
161
  end
130
162
  end
@@ -3,20 +3,13 @@
3
3
  require 'test_helper'
4
4
 
5
5
  describe Vines::Stream::Http::Request do
6
- PASSWORD = File.expand_path('../passwords')
7
- INDEX = File.expand_path('index.html')
6
+ PASSWORD = File.expand_path('../passwords').freeze
7
+ INDEX = File.expand_path('index.html').freeze
8
8
 
9
9
  before do
10
10
  File.open(PASSWORD, 'w') {|f| f.puts '/etc/passwd contents' }
11
11
  File.open(INDEX, 'w') {|f| f.puts 'index.html contents' }
12
-
13
12
  @stream = MiniTest::Mock.new
14
- @parser = MiniTest::Mock.new
15
- @parser.expect(:headers, {'Content-Type' => 'text/html', 'Host' => 'wonderland.lit'})
16
- @parser.expect(:http_method, 'GET')
17
- @parser.expect(:request_path, '/blogs/12')
18
- @parser.expect(:request_url, '/blogs/12?ok=true')
19
- @parser.expect(:query_string, 'ok=true')
20
13
  end
21
14
 
22
15
  after do
@@ -26,21 +19,26 @@ describe Vines::Stream::Http::Request do
26
19
 
27
20
  describe 'initialize' do
28
21
  it 'copies request info from parser' do
29
- request = Vines::Stream::Http::Request.new(@stream, @parser, '<html></html>')
30
- assert_equal request.headers, {'Content-Type' => 'text/html', 'Host' => 'wonderland.lit'}
22
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
23
+ parser = parser('GET', '/blogs/12?ok=true', headers)
24
+ request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
25
+
26
+ assert_equal request.headers, {'Host' => 'wonderland.lit', 'Content-Type' => 'text/html'}
31
27
  assert_equal request.method, 'GET'
32
28
  assert_equal request.path, '/blogs/12'
33
29
  assert_equal request.url, '/blogs/12?ok=true'
34
30
  assert_equal request.query, 'ok=true'
35
31
  assert_equal request.body, '<html></html>'
36
32
  assert @stream.verify
37
- assert @parser.verify
38
33
  end
39
34
  end
40
35
 
41
36
  describe 'reply_with_file' do
42
37
  it 'returns 404 file not found' do
43
- request = Vines::Stream::Http::Request.new(@stream, @parser, '<html></html>')
38
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
39
+ parser = parser('GET', '/blogs/12?ok=true', headers)
40
+ request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
41
+
44
42
  headers = [
45
43
  "HTTP/1.1 404 Not Found",
46
44
  "Content-Length: 0"
@@ -50,17 +48,11 @@ describe Vines::Stream::Http::Request do
50
48
 
51
49
  request.reply_with_file(Dir.pwd)
52
50
  assert @stream.verify
53
- assert @parser.verify
54
51
  end
55
52
 
56
53
  it 'prevents directory traversal with 404 response' do
57
- parser = MiniTest::Mock.new
58
- parser.expect(:headers, {'Content-Type' => 'text/html', 'Host' => 'wonderland.lit'})
59
- parser.expect(:http_method, 'GET')
60
- parser.expect(:request_path, '/../passwords')
61
- parser.expect(:request_url, '/../passwords')
62
- parser.expect(:query_string, '')
63
-
54
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
55
+ parser = parser('GET', '/../passwords', headers)
64
56
  request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
65
57
 
66
58
  headers = [
@@ -72,17 +64,11 @@ describe Vines::Stream::Http::Request do
72
64
 
73
65
  request.reply_with_file(Dir.pwd)
74
66
  assert @stream.verify
75
- assert parser.verify
76
67
  end
77
68
 
78
69
  it 'serves index.html for directory request' do
79
- parser = MiniTest::Mock.new
80
- parser.expect(:headers, {'Content-Type' => 'text/html', 'Host' => 'wonderland.lit'})
81
- parser.expect(:http_method, 'GET')
82
- parser.expect(:request_path, '/')
83
- parser.expect(:request_url, '/?ok=true')
84
- parser.expect(:query_string, 'ok=true')
85
-
70
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
71
+ parser = parser('GET', '/?ok=true', headers)
86
72
  request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
87
73
 
88
74
  mtime = File.mtime(INDEX).utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
@@ -98,17 +84,11 @@ describe Vines::Stream::Http::Request do
98
84
 
99
85
  request.reply_with_file(Dir.pwd)
100
86
  assert @stream.verify
101
- assert parser.verify
102
87
  end
103
88
 
104
89
  it 'redirects for missing trailing slash' do
105
- parser = MiniTest::Mock.new
106
- parser.expect(:headers, {'Content-Type' => 'text/html', 'Host' => 'wonderland.lit'})
107
- parser.expect(:http_method, 'GET')
108
- parser.expect(:request_path, '/http')
109
- parser.expect(:request_url, '/http?ok=true')
110
- parser.expect(:query_string, 'ok=true')
111
-
90
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
91
+ parser = parser('GET', '/http?ok=true', headers)
112
92
  request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
113
93
 
114
94
  headers = [
@@ -121,23 +101,18 @@ describe Vines::Stream::Http::Request do
121
101
  # so the /http url above will work
122
102
  request.reply_with_file(File.expand_path('../../', __FILE__))
123
103
  assert @stream.verify
124
- assert parser.verify
125
104
  end
126
105
  end
127
106
 
128
107
  describe 'reply_to_options' do
129
108
  it 'returns cors headers' do
130
- parser = MiniTest::Mock.new
131
- parser.expect(:headers, {
132
- 'Content-Type' => 'text/xml',
133
- 'Host' => 'wonderland.lit',
134
- 'Origin' => 'remote.wonderland.lit',
135
- 'Access-Control-Request-Headers' => 'Content-Type, Origin'})
136
- parser.expect(:http_method, 'OPTIONS')
137
- parser.expect(:request_path, '/xmpp')
138
- parser.expect(:request_url, '/xmpp')
139
- parser.expect(:query_string, '')
140
-
109
+ headers = [
110
+ 'Content-Type: text/xml',
111
+ 'Host: wonderland.lit',
112
+ 'Origin: remote.wonderland.lit',
113
+ 'Access-Control-Request-Headers: Content-Type, Origin'
114
+ ]
115
+ parser = parser('OPTIONS', '/xmpp', headers)
141
116
  request = Vines::Stream::Http::Request.new(@stream, parser, '')
142
117
 
143
118
  headers = [
@@ -152,7 +127,6 @@ describe Vines::Stream::Http::Request do
152
127
  @stream.expect(:stream_write, nil, ["#{headers}\r\n\r\n"])
153
128
  request.reply_to_options
154
129
  assert @stream.verify
155
- assert parser.verify
156
130
  end
157
131
  end
158
132
 
@@ -168,6 +142,21 @@ describe Vines::Stream::Http::Request do
168
142
 
169
143
  private
170
144
 
145
+ # Create a parser that has completed one valid HTTP request.
146
+ #
147
+ # method - The HTTP method String (e.g. 'GET', 'POST').
148
+ # url - The request URL String (e.g. '/blogs/12?ok=true').
149
+ # headers - The optional Array of request headers.
150
+ #
151
+ # Returns an Http::Parser.
152
+ def parser(method, url, headers = [])
153
+ head = ["#{method} #{url} HTTP/1.1"].concat(headers).join("\r\n")
154
+ body = '<html></html>'
155
+ Http::Parser.new.tap do |parser|
156
+ parser << "%s\r\n\r\n%s" % [head, body]
157
+ end
158
+ end
159
+
171
160
  def reply_with_cookie(cookie)
172
161
  config = Vines::Config.new do
173
162
  host 'wonderland.lit' do
@@ -178,15 +167,12 @@ describe Vines::Stream::Http::Request do
178
167
  end
179
168
  end
180
169
 
181
- parser = MiniTest::Mock.new
182
- parser.expect(:headers, {
183
- 'Content-Type' => 'text/xml',
184
- 'Host' => 'wonderland.lit',
185
- 'Origin' => 'remote.wonderland.lit'})
186
- parser.expect(:http_method, 'POST')
187
- parser.expect(:request_path, '/xmpp')
188
- parser.expect(:request_url, '/xmpp')
189
- parser.expect(:query_string, '')
170
+ headers = [
171
+ 'Content-Type: text/xml',
172
+ 'Host: wonderland.lit',
173
+ 'Origin: remote.wonderland.lit'
174
+ ]
175
+ parser = parser('POST', '/xmpp', headers)
190
176
 
191
177
  request = Vines::Stream::Http::Request.new(@stream, parser, '')
192
178
  message = '<message>hello</message>'
@@ -204,6 +190,5 @@ describe Vines::Stream::Http::Request do
204
190
  @stream.expect(:config, config)
205
191
  request.reply(message, 'application/xml')
206
192
  assert @stream.verify
207
- assert parser.verify
208
193
  end
209
194
  end