roda 3.12.0 → 3.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0839d9d46fb688ab858d224dff6293be6572d9fc2938ea0696cb34bc6cb14949'
4
- data.tar.gz: c3cbc4adc620b1f4776d8258e44751f7206837ba12a6a8e4917e5bf6d5a7efec
3
+ metadata.gz: f5b811d1ec0ddceb3a222c490b2af8e1488c6d454b637c7b6522962c149e6fd6
4
+ data.tar.gz: 0f3fad8d3d14f799fcc0e92a29bbc9210aadec3bade4bf68379409ce8780b29a
5
5
  SHA512:
6
- metadata.gz: 7e595d7c11c1486131b3e46b596c6d38a65dac41bb72e686fdbc260f629ce4a639543501e30754412d6252942204d8fa906579bb54a6ff224233dfb333610bfd
7
- data.tar.gz: 97f598575e2a13907021cb9024cfa4c6c7c6e1ce68c69639794f89db068a7ee2c27b35bace18b568ae841ae51b6ebf6404c611f3f3f1692b48b95bf1207073a4
6
+ metadata.gz: 8de657deae4c712c6d18c329c1137e103b979d7c1c4e1f7a36fa9a72f3bd0c202610bc73e78b7ce30c80806adfcd19a67d42da0ce5ab4932e04a8df6aba7a54d
7
+ data.tar.gz: 66cc1c59eca2bc0fb7a5186fc353332ca630b255de48406429fbb1ecfb7a42253102d2616b98196c8e4bd608a335ea2ce7af973b5b15c6150fddfc124b7b7591
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ = 3.13.0 (2018-10-12)
2
+
3
+ * Make Stream#write in streaming plugin return number of bytes written instead of self, so it works with IO.copy_stream (jeremyevans)
4
+
5
+ * Add exception_page plugin for showing a page with debugging information for a given exception (jeremyevans)
6
+
7
+ * Make common_logger plugin handle raised errors (jeremyevans)
8
+
1
9
  = 3.12.0 (2018-09-14)
2
10
 
3
11
  * Add common_logger plugin for common log support (jeremyevans)
@@ -0,0 +1,38 @@
1
+ = New Features
2
+
3
+ * An exception_page plugin has been added for displaying debugging
4
+ information for a given exception. It is based on
5
+ Rack::ShowExceptions, with the following differences:
6
+
7
+ * Not a middleware, so it doesn't handle exceptions itself, and
8
+ has no effect on the callstack unless the exception_page
9
+ method is called.
10
+ * Supports external javascript and stylesheets, allowing context
11
+ toggling to work in applications that use a content security
12
+ policy to restrict inline javascript and stylesheets (:assets,
13
+ :css_file, and :js_file options).
14
+ * Has fewer dependencies (does not require ostruct and erb).
15
+ * Sets the Content-Type for the response, and returns the body
16
+ string, but does not modify other headers or the response status.
17
+ * Supports a configurable amount of context lines in backtraces
18
+ (:context option).
19
+ * Supports optional JSON formatted output, if used with the json
20
+ plugin (:json option).
21
+
22
+ Because this plugin just adds a method you can call, you can
23
+ selectively choose when to display a debugging page and when not
24
+ to, as well as customize the debugging parameters on a per-call
25
+ basis (such as returning JSON formatted debugging information
26
+ for JSON requests, and HTML formatted debugging information for
27
+ normal requests).
28
+
29
+ = Other Improvements
30
+
31
+ * The common_logger plugin now correctly handles cases where an
32
+ exception is being raised and there is no rack response to
33
+ introspect.
34
+
35
+ = Backwards Compatibility
36
+
37
+ * Stream#write in the streaming plugin now returns the number of
38
+ bytes written instead of self, so it works with IO.copy_stream.
@@ -47,6 +47,8 @@ class Roda
47
47
 
48
48
  # Log request/response information in common log format to logger.
49
49
  def _roda_after_90__common_logger(result)
50
+ return unless result && result[0] && result[1]
51
+
50
52
  elapsed_time = if timer = @_request_timer
51
53
  '%0.4f' % (CommonLogger.start_timer - timer)
52
54
  else
@@ -0,0 +1,405 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The exception_page plugin provides an exception_page method that is designed
7
+ # to be called inside the error handler to provide a page to the developer
8
+ # with debugging information. It should only be used in developer environments
9
+ # with trusted clients, as it can leak source code and other information that
10
+ # may be useful for attackers if used in other environments.
11
+ #
12
+ # Example:
13
+ #
14
+ # plugin :exception_page
15
+ # plugin :error_handler do |e|
16
+ # next exception_page(e) if ENV['RACK_ENV'] == 'development'
17
+ # # ...
18
+ # end
19
+ #
20
+ # The exception_page plugin is based on Rack::ShowExceptions, with the following
21
+ # differences:
22
+ #
23
+ # * Not a middleware, so it doesn't handle exceptions itself, and has no effect
24
+ # on the callstack unless the exception_page method is called.
25
+ # * Supports external javascript and stylesheets, allowing context toggling to
26
+ # work in applications that use a content security policy to restrict inline
27
+ # javascript and stylesheets (:assets, :css_file, and :js_file options).
28
+ # * Has fewer dependencies (does not require ostruct and erb).
29
+ # * Sets the Content-Type for the response, and returns the body string, but does
30
+ # not modify other headers or the response status.
31
+ # * Supports a configurable amount of context lines in backtraces (:context option).
32
+ # * Supports optional JSON formatted output, if used with the json plugin (:json option).
33
+ #
34
+ # To use the external javascript and stylesheets, you can call +r.exception_page_assets+
35
+ # in your routing tree:
36
+ #
37
+ # route do |r|
38
+ # # ...
39
+ #
40
+ # # serve GET /exception_page.{css,js} requests
41
+ # # Use with assets: true +exception_page+ option
42
+ # r.exception_page_assets
43
+ #
44
+ # r.on "static" do
45
+ # # serve GET /static/exception_page.{css,js} requests
46
+ # # Use with assets: '/static' +exception_page+ option
47
+ # r.exception_page_assets
48
+ # end
49
+ # end
50
+ #
51
+ # It's also possible to store the asset information in static files and serve those,
52
+ # you can get the current assets by calling:
53
+ #
54
+ # Roda::RodaPlugins::ExceptionPage.css
55
+ # Roda::RodaPlugins::ExceptionPage.js
56
+ #
57
+ # As the exception_page plugin is based on Rack::ShowExceptions, it is also under
58
+ # rack's license:
59
+ #
60
+ # Copyright (C) 2007-2018 Christian Neukirchen <http://chneukirchen.org/infopage.html>
61
+ #
62
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
63
+ # of this software and associated documentation files (the "Software"), to
64
+ # deal in the Software without restriction, including without limitation the
65
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
66
+ # sell copies of the Software, and to permit persons to whom the Software is
67
+ # furnished to do so, subject to the following conditions:
68
+ #
69
+ # The above copyright notice and this permission notice shall be included in
70
+ # all copies or substantial portions of the Software.
71
+ #
72
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
73
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
74
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
75
+ # THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
76
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
77
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
78
+ #
79
+ # The HTML template used in Rack::ShowExceptions was based on Django's
80
+ # template and is under the following license:
81
+ #
82
+ # adapted from Django <www.djangoproject.com>
83
+ # Copyright (c) Django Software Foundation and individual contributors.
84
+ # Used under the modified BSD license:
85
+ # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
86
+ module ExceptionPage
87
+ def self.load_dependencies(app)
88
+ app.plugin :h
89
+ end
90
+
91
+ # Stylesheet used by the HTML exception page
92
+ def self.css
93
+ <<END
94
+ html * { padding:0; margin:0; }
95
+ body * { padding:10px 20px; }
96
+ body * * { padding:0; }
97
+ body { font:small sans-serif; }
98
+ body>div { border-bottom:1px solid #ddd; }
99
+ h1 { font-weight:normal; }
100
+ h2 { margin-bottom:.8em; }
101
+ h2 span { font-size:80%; color:#666; font-weight:normal; }
102
+ h3 { margin:1em 0 .5em 0; }
103
+ h4 { margin:0 0 .5em 0; font-weight: normal; }
104
+ table {
105
+ border:1px solid #ccc; border-collapse: collapse; background:white; }
106
+ tbody td, tbody th { vertical-align:top; padding:2px 3px; }
107
+ thead th {
108
+ padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
109
+ font-weight:normal; font-size:11px; border:1px solid #ddd; }
110
+ tbody th { text-align:right; color:#666; padding-right:.5em; }
111
+ table.vars { margin:5px 0 2px 40px; }
112
+ table.vars td, table.req td { font-family:monospace; }
113
+ table td.code { width:100%;}
114
+ table td.code div { overflow:hidden; }
115
+ table.source th { color:#666; }
116
+ table.source td {
117
+ font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
118
+ ul.traceback { list-style-type:none; }
119
+ ul.traceback li.frame { margin-bottom:1em; }
120
+ div.context { margin: 10px 0; }
121
+ div.context ol {
122
+ padding-left:30px; margin:0 10px; list-style-position: inside; }
123
+ div.context ol li {
124
+ font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
125
+ div.context ol.context-line li { color:black; background-color:#ccc; }
126
+ div.context ol.context-line li span { float: right; }
127
+ div.commands { margin-left: 40px; }
128
+ div.commands a { color:black; text-decoration:none; }
129
+ #summary { background: #ffc; }
130
+ #summary h2 { font-weight: normal; color: #666; }
131
+ #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
132
+ #summary ul#quicklinks li { float: left; padding: 0 1em; }
133
+ #summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
134
+ #explanation { background:#eee; }
135
+ #traceback { background:#eee; }
136
+ #requestinfo { background:#f6f6f6; padding-left:120px; }
137
+ #summary table { border:none; background:transparent; }
138
+ #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
139
+ #requestinfo h3 { margin-bottom:-1em; }
140
+ .error { background: #ffc; }
141
+ .specific { color:#cc3300; font-weight:bold; }
142
+ END
143
+ end
144
+
145
+ # Javascript used by the HTML exception page for context toggling
146
+ def self.js
147
+ <<END
148
+ var contexts = document.getElementsByClassName('context');
149
+ var num_contexts = contexts.length;
150
+ function toggle() {
151
+ for (var i = 0; i < arguments.length; i++) {
152
+ var e = document.getElementById(arguments[i]);
153
+ if (e) {
154
+ e.style.display = e.style.display == 'none' ? 'block' : 'none';
155
+ }
156
+ }
157
+ return false;
158
+ }
159
+ for (var j = 0; j < num_contexts; j++) {
160
+ contexts[j].onclick = function(){toggle('b'+this.id, 'a'+this.id);}
161
+ contexts[j].onclick();
162
+ }
163
+ END
164
+ end
165
+
166
+ module InstanceMethods
167
+ # Return a HTML page showing the exception, allowing a developer
168
+ # more information for debugging. Designed to be called inside
169
+ # an exception handler, passing in the received exception.
170
+ # Sets the Content-Type header in the response, and returns the
171
+ # string used for the body. If the Accept request header is present
172
+ # and text/html is accepted, return an HTML page with the backtrace
173
+ # with the ability to see the context for each backtrace line, as
174
+ # well as the GET, POST, cookie, and rack environment data. If text/html
175
+ # is not accepted, then just show a plain text page with the exception
176
+ # class, message, and backtrace.
177
+ #
178
+ # Options:
179
+ #
180
+ # :assets :: If +true+, sets :css_file to +/exception_page.css+ and :js_file to
181
+ # +/exception_page.js+, assuming that +r.exception_page_assets+ is called
182
+ # in the route block to serve the exception page assets. If a String,
183
+ # uses the string as a prefix, assuming that +r.exception_page_assets+
184
+ # is called in a nested block inside the route block. If false, doesn't
185
+ # use any CSS or JS.
186
+ # :context :: The number of context lines before and after each line in
187
+ # the backtrace (default: 7).
188
+ # :css_file :: A path to the external CSS file for the HTML exception page. If false,
189
+ # doesn't use any CSS.
190
+ # :js_file :: A path to the external javascript file for the HTML exception page. If
191
+ # false, doesn't use any JS.
192
+ # :json :: Return a hash of exception information. The hash will have
193
+ # a single key, "exception", with a value being a hash with
194
+ # three keys, "class", "message", and "backtrace", which
195
+ # contain information derived from the given exception.
196
+ # Designed to be used with the +json+ exception, which will
197
+ # automatically convert the hash to JSON format.
198
+ def exception_page(exception, opts=OPTS)
199
+ if opts[:json]
200
+ @_response['Content-Type'] = "application/json"
201
+ {
202
+ "exception"=>{
203
+ "class"=>exception.class.to_s,
204
+ "message"=>exception.message.to_s,
205
+ "backtrace"=>exception.backtrace.map(&:to_s)
206
+ }
207
+ }
208
+ elsif env['HTTP_ACCEPT'] =~ /text\/html/
209
+ @_response['Content-Type'] = "text/html"
210
+
211
+ context = opts[:context] || 7
212
+ css_file = opts[:css_file]
213
+ js_file = opts[:js_file]
214
+
215
+ case prefix = opts[:assets]
216
+ when false
217
+ css_file = false if css_file.nil?
218
+ js_file = false if js_file.nil?
219
+ when nil
220
+ # nothing
221
+ else
222
+ prefix = '' if prefix == true
223
+ css_file ||= "#{prefix}/exception_page.css"
224
+ js_file ||= "#{prefix}/exception_page.js"
225
+ end
226
+
227
+ css = case css_file
228
+ when nil
229
+ "<style type=\"text/css\">#{ExceptionPage.css}</style>"
230
+ when false
231
+ # :nothing
232
+ else
233
+ "<link rel=\"stylesheet\" href=\"#{h css_file}\" />"
234
+ end
235
+
236
+ js = case js_file
237
+ when nil
238
+ "<script type=\"text/javascript\">\n//<!--\n#{ExceptionPage.js}\n//-->\n</script>"
239
+ when false
240
+ # :nothing
241
+ else
242
+ "<script type=\"text/javascript\" src=\"#{h js_file}\"></script>"
243
+ end
244
+
245
+ frames = exception.backtrace.map.with_index do |line, i|
246
+ frame = {:id=>i}
247
+ if line =~ /(.*?):(\d+)(:in `(.*)')?/
248
+ filename = frame[:filename] = $1
249
+ lineno = frame[:lineno] = $2.to_i
250
+ frame[:function] = $4
251
+
252
+ begin
253
+ lineno -= 1
254
+ lines = ::File.readlines(filename)
255
+ pre_lineno = frame[:pre_context_lineno] = [lineno-context, 0].max
256
+ frame[:pre_context] = lines[pre_lineno...lineno]
257
+ frame[:context_line] = lines[lineno].chomp
258
+ post_lineno = frame[:post_context_lineno] = [lineno+context, lines.size].min
259
+ frame[:post_context] = lines[lineno+1..post_lineno]
260
+ rescue
261
+ end
262
+
263
+ frame
264
+ else
265
+ nil
266
+ end
267
+ end.compact
268
+
269
+ r = @_request
270
+ info = lambda do |title, id, var, none|
271
+ <<END
272
+ <h3 id="#{id}">#{title}</h3>
273
+ #{(var && !var.empty?) ? (<<END1) : "<p>#{none}</p>"
274
+ <table class="req">
275
+ <thead>
276
+ <tr>
277
+ <th>Variable</th>
278
+ <th>Value</th>
279
+ </tr>
280
+ </thead>
281
+ <tbody>
282
+ #{var.sort_by{|k, _| k.to_s}.map{|key, val| (<<END2)}.join
283
+ <tr>
284
+ <td>#{h key}</td>
285
+ <td class="code"><div>#{h val.inspect}</div></td>
286
+ </tr>
287
+ END2
288
+ }
289
+ </tbody>
290
+ </table>
291
+ END1
292
+ }
293
+ END
294
+ end
295
+
296
+ <<END
297
+ <!DOCTYPE html>
298
+ <html lang="en">
299
+ <head>
300
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
301
+ <title>#{h exception.class} at #{h r.path}</title>
302
+ #{css}
303
+ </head>
304
+ <body>
305
+
306
+ <div id="summary">
307
+ <h1>#{h exception.class} at #{h r.path}</h1>
308
+ <h2>#{h exception.message}</h2>
309
+ <table><tr>
310
+ <th>Ruby</th>
311
+ <td>
312
+ #{(first = frames.first) ? "<code>#{h first[:filename]}</code>: in <code>#{h first[:function]}</code>, line #{first[:lineno]}" : "unknown location"}
313
+ </td>
314
+ </tr><tr>
315
+ <th>Web</th>
316
+ <td><code>#{r.request_method} #{h r.host}#{h r.path}</code></td>
317
+ </tr></table>
318
+
319
+ <h3>Jump to:</h3>
320
+ <ul id="quicklinks">
321
+ <li><a href="#get-info">GET</a></li>
322
+ <li><a href="#post-info">POST</a></li>
323
+ <li><a href="#cookie-info">Cookies</a></li>
324
+ <li><a href="#env-info">ENV</a></li>
325
+ </ul>
326
+ </div>
327
+
328
+ <div id="traceback">
329
+ <h2>Traceback <span>(innermost first)</span></h2>
330
+ <ul class="traceback">
331
+ #{frames.map{|frame| id = frame[:id]; (<<END1)}.join
332
+ <li class="frame">
333
+ <code>#{h frame[:filename]}</code>: in <code>#{h frame[:function]}</code>
334
+
335
+ #{frame[:context_line] ? (<<END2) : '</li>'
336
+ <div class="context" id="c#{id}">
337
+ #{frame[:pre_context] ? (<<END3) : ''
338
+ <ol start="#{frame[:pre_context_lineno]+1}" id="bc#{id}">
339
+ #{frame[:pre_context].map{|line| "<li>#{h line}</li>"}.join}
340
+ </ol>
341
+ END3
342
+ }
343
+
344
+ <ol start="#{frame[:lineno]}" class="context-line">
345
+ <li>#{h frame[:context_line]}<span>...</span></li>
346
+ </ol>
347
+
348
+ #{frame[:post_context] ? (<<END4) : ''
349
+ <ol start='#{frame[:lineno]+1}' id="ac#{id}">
350
+ #{frame[:post_context].map{|line| "<li>#{h line}</li>"}.join}
351
+ </ol>
352
+ END4
353
+ }
354
+ </div>
355
+ END2
356
+ }
357
+ END1
358
+ }
359
+ </ul>
360
+ </div>
361
+
362
+ <div id="requestinfo">
363
+ <h2>Request information</h2>
364
+
365
+ #{info.call('GET', 'get-info', r.GET, 'No GET data')}
366
+ #{info.call('POST', 'post-info', r.POST, 'No POST data')}
367
+ #{info.call('Cookies', 'cookie-info', r.cookies, 'No cookie data')}
368
+ #{info.call('Rack ENV', 'env-info', r.env, 'No Rack env?')}
369
+ </div>
370
+
371
+ <div id="explanation">
372
+ <p>
373
+ You're seeing this error because you use the Roda exception_page plugin.
374
+ </p>
375
+ </div>
376
+
377
+ #{js}
378
+ </body>
379
+ </html>
380
+ END
381
+ else
382
+ @_response['Content-Type'] = "text/plain"
383
+ "#{exception.class}: #{exception.message}\n#{exception.backtrace.map{|l| "\t#{l}"}.join("\n")}"
384
+ end
385
+ end
386
+ end
387
+
388
+ module RequestMethods
389
+ # Serve exception page assets
390
+ def exception_page_assets
391
+ get 'exception_page.css' do
392
+ response['Content-Type'] = "text/css"
393
+ ExceptionPage.css
394
+ end
395
+ get 'exception_page.js' do
396
+ response['Content-Type'] = "application/javascript"
397
+ ExceptionPage.js
398
+ end
399
+ end
400
+ end
401
+ end
402
+
403
+ register_plugin(:exception_page, ExceptionPage)
404
+ end
405
+ end
@@ -53,12 +53,18 @@ class Roda
53
53
  @closed = false
54
54
  end
55
55
 
56
- # Add output to the streaming response body.
56
+ # Add output to the streaming response body. Returns number of bytes written.
57
57
  def write(data)
58
- @out.call(data.to_s)
58
+ data = data.to_s
59
+ @out.call(data)
60
+ data.bytesize
61
+ end
62
+
63
+ # Add output to the streaming response body. Returns self.
64
+ def <<(data)
65
+ write(data)
59
66
  self
60
67
  end
61
- alias << write
62
68
 
63
69
  # If not already closed, close the connection, and call
64
70
  # any callbacks.
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 12
7
+ RodaMinorVersion = 13
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
@@ -56,4 +56,30 @@ describe "common_logger plugin" do
56
56
  @logger.rewind
57
57
  @logger.read.must_match /\A- - - \[\d\d\/[A-Z][a-z]{2}\/\d\d\d\d:\d\d:\d\d:\d\d [-+]\d\d\d\d\] "GET \/ " 500 - 0.\d\d\d\d\n\z/
58
58
  end
59
+
60
+ it 'does not log if an error is raised' do
61
+ cl_app do |r|
62
+ raise "foo"
63
+ end
64
+
65
+ begin
66
+ body
67
+ rescue => e
68
+ end
69
+ e.must_be_instance_of(RuntimeError)
70
+ e.message.must_equal 'foo'
71
+ end
72
+
73
+ it 'logs errors if used with error_handler' do
74
+ cl_app do |r|
75
+ raise "foo"
76
+ end
77
+ @app.plugin :error_handler do |_|
78
+ "bad"
79
+ end
80
+
81
+ body.must_equal 'bad'
82
+ @logger.rewind
83
+ @logger.read.must_match /\A- - - \[\d\d\/[A-Z][a-z]{2}\/\d\d\d\d:\d\d:\d\d:\d\d [-+]\d\d\d\d\] "GET \/ " 500 3 0.\d\d\d\d\n\z/
84
+ end
59
85
  end
@@ -0,0 +1,157 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "exception_page plugin" do
4
+ def ep_app(&block)
5
+ app(:exception_page) do |r|
6
+ raise "foo" rescue block ? instance_exec($!, &block) : exception_page($!)
7
+ end
8
+ end
9
+
10
+ def req(path = '/', headers={})
11
+ if path.is_a?(Hash)
12
+ super(path.merge('rack.input'=>StringIO.new))
13
+ else
14
+ super(path, headers.merge('rack.input'=>StringIO.new))
15
+ end
16
+ end
17
+
18
+ it "returns HTML page with exception information if text/html is accepted" do
19
+ ep_app
20
+ s, h, body = req('HTTP_ACCEPT'=>'text/html')
21
+
22
+ s.must_equal 200
23
+ h['Content-Type'].must_equal 'text/html'
24
+ body = body.join
25
+ body.must_include "<title>RuntimeError at /"
26
+ body.must_include "<h1>RuntimeError at /</h1>"
27
+ body.must_include "<h2>foo</h2>"
28
+ body.must_include __FILE__
29
+ body.must_include "No GET data"
30
+ body.must_include "No POST data"
31
+ body.must_include "No cookie data"
32
+ body.must_include "Rack ENV"
33
+ body.must_include "HTTP_ACCEPT"
34
+ body.must_include "text/html"
35
+ body.must_include "table td.code"
36
+ body.must_include "function toggle()"
37
+ body.wont_include "\"/exception_page.css\""
38
+ body.wont_include "\"/exception_page.js\""
39
+
40
+ size = body.size
41
+ ep_app{|e| exception_page(e, :context=>10)}
42
+ body('HTTP_ACCEPT'=>'text/html').size.must_be :>, size
43
+
44
+ ep_app{|e| exception_page(e, :assets=>true, :context=>0)}
45
+ body = body('HTTP_ACCEPT'=>'text/html')
46
+ body.wont_include "table td.code"
47
+ body.wont_include "function toggle()"
48
+ body.must_include "\"/exception_page.css\""
49
+ body.must_include "\"/exception_page.js\""
50
+
51
+ ep_app{|e| exception_page(e, :assets=>"/static", :context=>0)}
52
+ body = body('HTTP_ACCEPT'=>'text/html')
53
+ body.wont_include "table td.code"
54
+ body.wont_include "function toggle()"
55
+ body.must_include "\"/static/exception_page.css\""
56
+ body.must_include "\"/static/exception_page.js\""
57
+
58
+ ep_app{|e| exception_page(e, :css_file=>"/foo.css", :context=>0)}
59
+ body = body('HTTP_ACCEPT'=>'text/html')
60
+ body.wont_include "table td.code"
61
+ body.must_include "function toggle()"
62
+ body.must_include "\"/foo.css\""
63
+
64
+ ep_app{|e| exception_page(e, :js_file=>"/foo.js", :context=>0)}
65
+ body = body('HTTP_ACCEPT'=>'text/html')
66
+ body.must_include "table td.code"
67
+ body.wont_include "function toggle()"
68
+ body.must_include "\"/foo.js\""
69
+
70
+ ep_app{|e| exception_page(e, :assets=>false, :context=>0)}
71
+ body = body('HTTP_ACCEPT'=>'text/html')
72
+ body.wont_include "table td.code"
73
+ body.wont_include "function toggle()"
74
+ body.wont_include "\"/exception_page.css\""
75
+ body.wont_include "\"/exception_page.js\""
76
+
77
+ ep_app{|e| exception_page(e, :assets=>false, :css_file=>"/foo.css", :context=>0)}
78
+ body = body('HTTP_ACCEPT'=>'text/html')
79
+ body.wont_include "table td.code"
80
+ body.wont_include "function toggle()"
81
+ body.must_include "\"/foo.css\""
82
+
83
+ ep_app{|e| exception_page(e, :assets=>false, :js_file=>"/foo.js", :context=>0)}
84
+ body = body('HTTP_ACCEPT'=>'text/html')
85
+ body.wont_include "table td.code"
86
+ body.wont_include "function toggle()"
87
+ body.must_include "\"/foo.js\""
88
+
89
+ ep_app{|e| exception_page(e, :css_file=>false, :context=>0)}
90
+ body = body('HTTP_ACCEPT'=>'text/html')
91
+ body.wont_include "table td.code"
92
+ body.must_include "function toggle()"
93
+ body.wont_include "\"/exception_page.css\""
94
+ body.wont_include "\"/exception_page.js\""
95
+
96
+ ep_app{|e| exception_page(e, :js_file=>false, :context=>0)}
97
+ body = body('HTTP_ACCEPT'=>'text/html')
98
+ body.must_include "table td.code"
99
+ body.wont_include "function toggle()"
100
+ body.wont_include "\"/exception_page.css\""
101
+ body.wont_include "\"/exception_page.js\""
102
+ end
103
+
104
+ it "returns plain text page with exception information if text/html is not accepted" do
105
+ ep_app
106
+ s, h, body = req
107
+
108
+ s.must_equal 200
109
+ h['Content-Type'].must_equal 'text/plain'
110
+ body = body.join
111
+ first, *bt = body.split("\n")
112
+ first.must_equal "RuntimeError: foo"
113
+ bt.first.must_include __FILE__
114
+ end
115
+
116
+ it "returns JSON with exception information if :json information is used" do
117
+ ep_app{|e| exception_page(e, :json=>true)}
118
+ @app.plugin :json
119
+ s, h, body = req
120
+
121
+ s.must_equal 200
122
+ h['Content-Type'].must_equal 'application/json'
123
+ hash = JSON.parse(body.join)
124
+ bt = hash["exception"].delete("backtrace")
125
+ hash.must_equal("exception"=>{"class"=>"RuntimeError", "message"=>"foo"})
126
+ bt.must_be_kind_of Array
127
+ bt.each{|line| line.must_be_kind_of String}
128
+ end
129
+
130
+ it "should handle backtrace lines in unexpected forms" do
131
+ ep_app do |e|
132
+ e.backtrace.first.upcase!
133
+ e.backtrace[-1] = ''
134
+ exception_page(e)
135
+ end
136
+ body = body('HTTP_ACCEPT'=>'text/html')
137
+ body.must_include "RuntimeError: foo"
138
+ body.must_include __FILE__
139
+ body.wont_include 'id="c0"'
140
+ end
141
+
142
+ it "should serve exception page assets" do
143
+ app(:exception_page) do |r|
144
+ r.exception_page_assets
145
+ end
146
+
147
+ s, h, b = req('/exception_page.css')
148
+ s.must_equal 200
149
+ h['Content-Type'].must_equal 'text/css'
150
+ b.join.must_equal Roda::RodaPlugins::ExceptionPage.css
151
+
152
+ s, h, b = req('/exception_page.js')
153
+ s.must_equal 200
154
+ h['Content-Type'].must_equal 'application/javascript'
155
+ b.join.must_equal Roda::RodaPlugins::ExceptionPage.js
156
+ end
157
+ end
@@ -4,7 +4,10 @@ describe "streaming plugin" do
4
4
  it "adds stream method for streaming responses" do
5
5
  app(:streaming) do |r|
6
6
  stream do |out|
7
- %w'a b c'.each{|v| out << v; out.write(v) }
7
+ %w'a b c'.each do |v|
8
+ (out << v).must_equal out
9
+ out.write(v).must_equal 1
10
+ end
8
11
  end
9
12
  end
10
13
 
@@ -14,6 +17,20 @@ describe "streaming plugin" do
14
17
  b.to_a.must_equal %w'a a b b c c'
15
18
  end
16
19
 
20
+ it "works with IO.copy_stream" do
21
+ app(:streaming) do |r|
22
+ stream do |out|
23
+ %w'a b c'.each{|v| IO.copy_stream(StringIO.new(v), out) }
24
+ end
25
+ end
26
+
27
+ s, h, b = req
28
+ s.must_equal 200
29
+ h.must_equal('Content-Type'=>'text/html')
30
+ # dup as copy_stream reuses the buffer
31
+ b.map(&:dup).must_equal %w'a b c'
32
+ end
33
+
17
34
  it "should handle errors when streaming, and run callbacks" do
18
35
  a = []
19
36
  app(:streaming) do |r|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.12.0
4
+ version: 3.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-14 00:00:00.000000000 Z
11
+ date: 2018-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -208,6 +208,7 @@ extra_rdoc_files:
208
208
  - doc/release_notes/3.10.0.txt
209
209
  - doc/release_notes/3.11.0.txt
210
210
  - doc/release_notes/3.12.0.txt
211
+ - doc/release_notes/3.13.0.txt
211
212
  files:
212
213
  - CHANGELOG
213
214
  - MIT-LICENSE
@@ -254,6 +255,7 @@ files:
254
255
  - doc/release_notes/3.10.0.txt
255
256
  - doc/release_notes/3.11.0.txt
256
257
  - doc/release_notes/3.12.0.txt
258
+ - doc/release_notes/3.13.0.txt
257
259
  - doc/release_notes/3.2.0.txt
258
260
  - doc/release_notes/3.3.0.txt
259
261
  - doc/release_notes/3.4.0.txt
@@ -293,6 +295,7 @@ files:
293
295
  - lib/roda/plugins/error_email.rb
294
296
  - lib/roda/plugins/error_handler.rb
295
297
  - lib/roda/plugins/error_mail.rb
298
+ - lib/roda/plugins/exception_page.rb
296
299
  - lib/roda/plugins/flash.rb
297
300
  - lib/roda/plugins/h.rb
298
301
  - lib/roda/plugins/halt.rb
@@ -394,6 +397,7 @@ files:
394
397
  - spec/plugin/error_email_spec.rb
395
398
  - spec/plugin/error_handler_spec.rb
396
399
  - spec/plugin/error_mail_spec.rb
400
+ - spec/plugin/exception_page_spec.rb
397
401
  - spec/plugin/flash_spec.rb
398
402
  - spec/plugin/h_spec.rb
399
403
  - spec/plugin/halt_spec.rb