roda 3.12.0 → 3.13.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.
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