rack 2.2.15 → 3.1.15

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +429 -70
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +355 -0
  6. data/SPEC.rdoc +204 -131
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +3 -1
  9. data/lib/rack/auth/basic.rb +1 -3
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +21 -3
  12. data/lib/rack/builder.rb +102 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +22 -17
  15. data/lib/rack/conditional_get.rb +18 -15
  16. data/lib/rack/constants.rb +67 -0
  17. data/lib/rack/content_length.rb +12 -16
  18. data/lib/rack/content_type.rb +8 -5
  19. data/lib/rack/deflater.rb +40 -26
  20. data/lib/rack/directory.rb +9 -3
  21. data/lib/rack/etag.rb +17 -23
  22. data/lib/rack/events.rb +4 -0
  23. data/lib/rack/files.rb +15 -17
  24. data/lib/rack/head.rb +9 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +840 -644
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/media_type.rb +8 -3
  30. data/lib/rack/method_override.rb +5 -1
  31. data/lib/rack/mime.rb +14 -5
  32. data/lib/rack/mock.rb +1 -300
  33. data/lib/rack/mock_request.rb +161 -0
  34. data/lib/rack/mock_response.rb +153 -0
  35. data/lib/rack/multipart/generator.rb +7 -5
  36. data/lib/rack/multipart/parser.rb +213 -95
  37. data/lib/rack/multipart/uploaded_file.rb +4 -0
  38. data/lib/rack/multipart.rb +53 -40
  39. data/lib/rack/null_logger.rb +9 -0
  40. data/lib/rack/query_parser.rb +79 -101
  41. data/lib/rack/recursive.rb +2 -0
  42. data/lib/rack/reloader.rb +0 -2
  43. data/lib/rack/request.rb +260 -123
  44. data/lib/rack/response.rb +151 -66
  45. data/lib/rack/rewindable_input.rb +24 -5
  46. data/lib/rack/runtime.rb +7 -6
  47. data/lib/rack/sendfile.rb +30 -25
  48. data/lib/rack/show_exceptions.rb +21 -4
  49. data/lib/rack/show_status.rb +17 -7
  50. data/lib/rack/static.rb +8 -8
  51. data/lib/rack/tempfile_reaper.rb +15 -4
  52. data/lib/rack/urlmap.rb +3 -1
  53. data/lib/rack/utils.rb +236 -237
  54. data/lib/rack/version.rb +1 -9
  55. data/lib/rack.rb +13 -89
  56. metadata +13 -39
  57. data/README.rdoc +0 -347
  58. data/Rakefile +0 -130
  59. data/bin/rackup +0 -5
  60. data/contrib/rack.png +0 -0
  61. data/contrib/rack.svg +0 -150
  62. data/contrib/rack_logo.svg +0 -164
  63. data/contrib/rdoc.css +0 -412
  64. data/example/lobster.ru +0 -6
  65. data/example/protectedlobster.rb +0 -16
  66. data/example/protectedlobster.ru +0 -10
  67. data/lib/rack/auth/digest/md5.rb +0 -131
  68. data/lib/rack/auth/digest/nonce.rb +0 -53
  69. data/lib/rack/auth/digest/params.rb +0 -54
  70. data/lib/rack/auth/digest/request.rb +0 -43
  71. data/lib/rack/chunked.rb +0 -117
  72. data/lib/rack/core_ext/regexp.rb +0 -14
  73. data/lib/rack/file.rb +0 -7
  74. data/lib/rack/handler/cgi.rb +0 -59
  75. data/lib/rack/handler/fastcgi.rb +0 -100
  76. data/lib/rack/handler/lsws.rb +0 -61
  77. data/lib/rack/handler/scgi.rb +0 -71
  78. data/lib/rack/handler/thin.rb +0 -36
  79. data/lib/rack/handler/webrick.rb +0 -129
  80. data/lib/rack/handler.rb +0 -104
  81. data/lib/rack/lobster.rb +0 -70
  82. data/lib/rack/server.rb +0 -466
  83. data/lib/rack/session/abstract/id.rb +0 -523
  84. data/lib/rack/session/cookie.rb +0 -203
  85. data/lib/rack/session/memcache.rb +0 -10
  86. data/lib/rack/session/pool.rb +0 -90
  87. data/rack.gemspec +0 -46
data/lib/rack/lock.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'thread'
3
+ require_relative 'body_proxy'
4
4
 
5
5
  module Rack
6
6
  # Rack::Lock locks every request inside a mutex, so that every request
@@ -12,10 +12,8 @@ module Rack
12
12
 
13
13
  def call(env)
14
14
  @mutex.lock
15
- @env = env
16
- @old_rack_multithread = env[RACK_MULTITHREAD]
17
15
  begin
18
- response = @app.call(env.merge!(RACK_MULTITHREAD => false))
16
+ response = @app.call(env)
19
17
  returned = response << BodyProxy.new(response.pop) { unlock }
20
18
  ensure
21
19
  unlock unless returned
@@ -26,7 +24,6 @@ module Rack
26
24
 
27
25
  def unlock
28
26
  @mutex.unlock
29
- @env[RACK_MULTITHREAD] = @old_rack_multithread
30
27
  end
31
28
  end
32
29
  end
data/lib/rack/logger.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'logger'
4
+ require_relative 'constants'
5
+
6
+ warn "Rack::Logger is deprecated and will be removed in Rack 3.2.", uplevel: 1
4
7
 
5
8
  module Rack
6
9
  # Sets up rack.logger to write to rack.errors stream
@@ -27,6 +27,11 @@ module Rack
27
27
  # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
28
28
  # this method responds with the following Hash:
29
29
  # { 'charset' => 'utf-8' }
30
+ #
31
+ # This will pass back parameters with empty strings in the hash if they
32
+ # lack a value (e.g., "text/plain;charset=" will return { 'charset' => '' },
33
+ # and "text/plain;charset" will return { 'charset' => '' }, similarly to
34
+ # the query params parser (barring the latter case, which returns nil instead)).
30
35
  def params(content_type)
31
36
  return {} if content_type.nil?
32
37
 
@@ -40,9 +45,9 @@ module Rack
40
45
 
41
46
  private
42
47
 
43
- def strip_doublequotes(str)
44
- (str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str
45
- end
48
+ def strip_doublequotes(str)
49
+ (str && str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str || ''
50
+ end
46
51
  end
47
52
  end
48
53
  end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'constants'
4
+ require_relative 'request'
5
+ require_relative 'utils'
6
+
3
7
  module Rack
4
8
  class MethodOverride
5
9
  HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK]
@@ -42,7 +46,7 @@ module Rack
42
46
  end
43
47
 
44
48
  def method_override_param(req)
45
- req.POST[METHOD_OVERRIDE_PARAM_KEY]
49
+ req.POST[METHOD_OVERRIDE_PARAM_KEY] if req.form_data? || req.parseable_data?
46
50
  rescue Utils::InvalidParameterError, Utils::ParameterTypeError, QueryParser::ParamsTooDeepError
47
51
  req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params"
48
52
  rescue EOFError
data/lib/rack/mime.rb CHANGED
@@ -63,6 +63,7 @@ module Rack
63
63
  ".aif" => "audio/x-aiff",
64
64
  ".aiff" => "audio/x-aiff",
65
65
  ".ami" => "application/vnd.amiga.ami",
66
+ ".apng" => "image/apng",
66
67
  ".appcache" => "text/cache-manifest",
67
68
  ".apr" => "application/vnd.lotus-approach",
68
69
  ".asc" => "application/pgp-signature",
@@ -77,6 +78,7 @@ module Rack
77
78
  ".atx" => "application/vnd.antix.game-component",
78
79
  ".au" => "audio/basic",
79
80
  ".avi" => "video/x-msvideo",
81
+ ".avif" => "image/avif",
80
82
  ".bat" => "application/x-msdownload",
81
83
  ".bcpio" => "application/x-bcpio",
82
84
  ".bdm" => "application/vnd.syncml.dm+wbxml",
@@ -197,6 +199,7 @@ module Rack
197
199
  ".fe_launch" => "application/vnd.denovo.fcselayout-link",
198
200
  ".fg5" => "application/vnd.fujitsu.oasysgp",
199
201
  ".fli" => "video/x-fli",
202
+ ".flif" => "image/flif",
200
203
  ".flo" => "application/vnd.micrografx.flo",
201
204
  ".flv" => "video/x-flv",
202
205
  ".flw" => "application/vnd.kde.kivio",
@@ -237,6 +240,10 @@ module Rack
237
240
  ".h264" => "video/h264",
238
241
  ".hbci" => "application/vnd.hbci",
239
242
  ".hdf" => "application/x-hdf",
243
+ ".heic" => "image/heic",
244
+ ".heics" => "image/heic-sequence",
245
+ ".heif" => "image/heif",
246
+ ".heifs" => "image/heif-sequence",
240
247
  ".hh" => "text/x-c",
241
248
  ".hlp" => "application/winhlp",
242
249
  ".hpgl" => "application/vnd.hp-hpgl",
@@ -283,7 +290,7 @@ module Rack
283
290
  ".jpg" => "image/jpeg",
284
291
  ".jpgv" => "video/jpeg",
285
292
  ".jpm" => "video/jpm",
286
- ".js" => "application/javascript",
293
+ ".js" => "text/javascript",
287
294
  ".json" => "application/json",
288
295
  ".karbon" => "application/vnd.kde.karbon",
289
296
  ".kfo" => "application/vnd.kde.kformula",
@@ -331,6 +338,7 @@ module Rack
331
338
  ".mif" => "application/vnd.mif",
332
339
  ".mime" => "message/rfc822",
333
340
  ".mj2" => "video/mj2",
341
+ ".mjs" => "text/javascript",
334
342
  ".mlp" => "application/vnd.dolby.mlp",
335
343
  ".mmd" => "application/vnd.chipnuts.karaoke-mmd",
336
344
  ".mmf" => "application/vnd.smaf",
@@ -402,7 +410,7 @@ module Rack
402
410
  ".ogx" => "application/ogg",
403
411
  ".org" => "application/vnd.lotus-organizer",
404
412
  ".otc" => "application/vnd.oasis.opendocument.chart-template",
405
- ".otf" => "application/vnd.oasis.opendocument.formula-template",
413
+ ".otf" => "font/otf",
406
414
  ".otg" => "application/vnd.oasis.opendocument.graphics-template",
407
415
  ".oth" => "application/vnd.oasis.opendocument.text-web",
408
416
  ".oti" => "application/vnd.oasis.opendocument.image-template",
@@ -583,7 +591,7 @@ module Rack
583
591
  ".trm" => "application/x-msterminal",
584
592
  ".ts" => "video/mp2t",
585
593
  ".tsv" => "text/tab-separated-values",
586
- ".ttf" => "application/octet-stream",
594
+ ".ttf" => "font/ttf",
587
595
  ".twd" => "application/vnd.simtech-mindmapper",
588
596
  ".txd" => "application/vnd.genomatix.tuxedo",
589
597
  ".txf" => "application/vnd.mobius.txf",
@@ -617,6 +625,7 @@ module Rack
617
625
  ".wbs" => "application/vnd.criticaltools.wbs+xml",
618
626
  ".wbxml" => "application/vnd.wap.wbxml",
619
627
  ".webm" => "video/webm",
628
+ ".webp" => "image/webp",
620
629
  ".wm" => "video/x-ms-wm",
621
630
  ".wma" => "audio/x-ms-wma",
622
631
  ".wmd" => "application/x-ms-wmd",
@@ -628,8 +637,8 @@ module Rack
628
637
  ".wmv" => "video/x-ms-wmv",
629
638
  ".wmx" => "video/x-ms-wmx",
630
639
  ".wmz" => "application/x-ms-wmz",
631
- ".woff" => "application/font-woff",
632
- ".woff2" => "application/font-woff2",
640
+ ".woff" => "font/woff",
641
+ ".woff2" => "font/woff2",
633
642
  ".wpd" => "application/vnd.wordperfect",
634
643
  ".wpl" => "application/vnd.ms-wpl",
635
644
  ".wps" => "application/vnd.ms-works",
data/lib/rack/mock.rb CHANGED
@@ -1,302 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'uri'
4
- require 'stringio'
5
- require_relative '../rack'
6
-
7
- module Rack
8
- # Rack::MockRequest helps testing your Rack application without
9
- # actually using HTTP.
10
- #
11
- # After performing a request on a URL with get/post/put/patch/delete, it
12
- # returns a MockResponse with useful helper methods for effective
13
- # testing.
14
- #
15
- # You can pass a hash with additional configuration to the
16
- # get/post/put/patch/delete.
17
- # <tt>:input</tt>:: A String or IO-like to be used as rack.input.
18
- # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
19
- # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
20
-
21
- class MockRequest
22
- class FatalWarning < RuntimeError
23
- end
24
-
25
- class FatalWarner
26
- def puts(warning)
27
- raise FatalWarning, warning
28
- end
29
-
30
- def write(warning)
31
- raise FatalWarning, warning
32
- end
33
-
34
- def flush
35
- end
36
-
37
- def string
38
- ""
39
- end
40
- end
41
-
42
- DEFAULT_ENV = {
43
- RACK_VERSION => Rack::VERSION,
44
- RACK_INPUT => StringIO.new,
45
- RACK_ERRORS => StringIO.new,
46
- RACK_MULTITHREAD => true,
47
- RACK_MULTIPROCESS => true,
48
- RACK_RUNONCE => false,
49
- }.freeze
50
-
51
- def initialize(app)
52
- @app = app
53
- end
54
-
55
- # Make a GET request and return a MockResponse. See #request.
56
- def get(uri, opts = {}) request(GET, uri, opts) end
57
- # Make a POST request and return a MockResponse. See #request.
58
- def post(uri, opts = {}) request(POST, uri, opts) end
59
- # Make a PUT request and return a MockResponse. See #request.
60
- def put(uri, opts = {}) request(PUT, uri, opts) end
61
- # Make a PATCH request and return a MockResponse. See #request.
62
- def patch(uri, opts = {}) request(PATCH, uri, opts) end
63
- # Make a DELETE request and return a MockResponse. See #request.
64
- def delete(uri, opts = {}) request(DELETE, uri, opts) end
65
- # Make a HEAD request and return a MockResponse. See #request.
66
- def head(uri, opts = {}) request(HEAD, uri, opts) end
67
- # Make an OPTIONS request and return a MockResponse. See #request.
68
- def options(uri, opts = {}) request(OPTIONS, uri, opts) end
69
-
70
- # Make a request using the given request method for the given
71
- # uri to the rack application and return a MockResponse.
72
- # Options given are passed to MockRequest.env_for.
73
- def request(method = GET, uri = "", opts = {})
74
- env = self.class.env_for(uri, opts.merge(method: method))
75
-
76
- if opts[:lint]
77
- app = Rack::Lint.new(@app)
78
- else
79
- app = @app
80
- end
81
-
82
- errors = env[RACK_ERRORS]
83
- status, headers, body = app.call(env)
84
- MockResponse.new(status, headers, body, errors)
85
- ensure
86
- body.close if body.respond_to?(:close)
87
- end
88
-
89
- # For historical reasons, we're pinning to RFC 2396.
90
- # URI::Parser = URI::RFC2396_Parser
91
- def self.parse_uri_rfc2396(uri)
92
- @parser ||= URI::Parser.new
93
- @parser.parse(uri)
94
- end
95
-
96
- # Return the Rack environment used for a request to +uri+.
97
- # All options that are strings are added to the returned environment.
98
- # Options:
99
- # :fatal :: Whether to raise an exception if request outputs to rack.errors
100
- # :input :: The rack.input to set
101
- # :method :: The HTTP request method to use
102
- # :params :: The params to use
103
- # :script_name :: The SCRIPT_NAME to set
104
- def self.env_for(uri = "", opts = {})
105
- uri = parse_uri_rfc2396(uri)
106
- uri.path = "/#{uri.path}" unless uri.path[0] == ?/
107
-
108
- env = DEFAULT_ENV.dup
109
-
110
- env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
111
- env[SERVER_NAME] = (uri.host || "example.org").b
112
- env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b
113
- env[QUERY_STRING] = (uri.query.to_s).b
114
- env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
115
- env[RACK_URL_SCHEME] = (uri.scheme || "http").b
116
- env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
117
-
118
- env[SCRIPT_NAME] = opts[:script_name] || ""
119
-
120
- if opts[:fatal]
121
- env[RACK_ERRORS] = FatalWarner.new
122
- else
123
- env[RACK_ERRORS] = StringIO.new
124
- end
125
-
126
- if params = opts[:params]
127
- if env[REQUEST_METHOD] == GET
128
- params = Utils.parse_nested_query(params) if params.is_a?(String)
129
- params.update(Utils.parse_nested_query(env[QUERY_STRING]))
130
- env[QUERY_STRING] = Utils.build_nested_query(params)
131
- elsif !opts.has_key?(:input)
132
- opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
133
- if params.is_a?(Hash)
134
- if data = Rack::Multipart.build_multipart(params)
135
- opts[:input] = data
136
- opts["CONTENT_LENGTH"] ||= data.length.to_s
137
- opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
138
- else
139
- opts[:input] = Utils.build_nested_query(params)
140
- end
141
- else
142
- opts[:input] = params
143
- end
144
- end
145
- end
146
-
147
- empty_str = String.new
148
- opts[:input] ||= empty_str
149
- if String === opts[:input]
150
- rack_input = StringIO.new(opts[:input])
151
- else
152
- rack_input = opts[:input]
153
- end
154
-
155
- rack_input.set_encoding(Encoding::BINARY)
156
- env[RACK_INPUT] = rack_input
157
-
158
- env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
159
-
160
- opts.each { |field, value|
161
- env[field] = value if String === field
162
- }
163
-
164
- env
165
- end
166
- end
167
-
168
- # Rack::MockResponse provides useful helpers for testing your apps.
169
- # Usually, you don't create the MockResponse on your own, but use
170
- # MockRequest.
171
-
172
- class MockResponse < Rack::Response
173
- begin
174
- # Recent versions of the CGI gem may not provide `CGI::Cookie`.
175
- require 'cgi/cookie'
176
- Cookie = CGI::Cookie
177
- rescue LoadError
178
- class Cookie
179
- attr_reader :name, :value, :path, :domain, :expires, :secure
180
-
181
- def initialize(args)
182
- @name = args["name"]
183
- @value = args["value"]
184
- @path = args["path"]
185
- @domain = args["domain"]
186
- @expires = args["expires"]
187
- @secure = args["secure"]
188
- end
189
-
190
- def method_missing(method_name, *args, &block)
191
- @value.send(method_name, *args, &block)
192
- end
193
- # :nocov:
194
- ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
195
- # :nocov:
196
-
197
- def respond_to_missing?(method_name, include_all = false)
198
- @value.respond_to?(method_name, include_all) || super
199
- end
200
- end
201
- end
202
-
203
- class << self
204
- alias [] new
205
- end
206
-
207
- # Headers
208
- attr_reader :original_headers, :cookies
209
-
210
- # Errors
211
- attr_accessor :errors
212
-
213
- def initialize(status, headers, body, errors = StringIO.new(""))
214
- @original_headers = headers
215
- @errors = errors.string if errors.respond_to?(:string)
216
- @cookies = parse_cookies_from_header
217
-
218
- super(body, status, headers)
219
-
220
- buffered_body!
221
- end
222
-
223
- def =~(other)
224
- body =~ other
225
- end
226
-
227
- def match(other)
228
- body.match other
229
- end
230
-
231
- def body
232
- # FIXME: apparently users of MockResponse expect the return value of
233
- # MockResponse#body to be a string. However, the real response object
234
- # returns the body as a list.
235
- #
236
- # See spec_showstatus.rb:
237
- #
238
- # should "not replace existing messages" do
239
- # ...
240
- # res.body.should == "foo!"
241
- # end
242
- buffer = String.new
243
-
244
- super.each do |chunk|
245
- buffer << chunk
246
- end
247
-
248
- return buffer
249
- end
250
-
251
- def empty?
252
- [201, 204, 304].include? status
253
- end
254
-
255
- def cookie(name)
256
- cookies.fetch(name, nil)
257
- end
258
-
259
- private
260
-
261
- def parse_cookies_from_header
262
- cookies = Hash.new
263
- if original_headers.has_key? 'Set-Cookie'
264
- set_cookie_header = original_headers.fetch('Set-Cookie')
265
- set_cookie_header.split("\n").each do |cookie|
266
- cookie_name, cookie_filling = cookie.split('=', 2)
267
- cookie_attributes = identify_cookie_attributes cookie_filling
268
- parsed_cookie = CGI::Cookie.new(
269
- 'name' => cookie_name.strip,
270
- 'value' => cookie_attributes.fetch('value'),
271
- 'path' => cookie_attributes.fetch('path', nil),
272
- 'domain' => cookie_attributes.fetch('domain', nil),
273
- 'expires' => cookie_attributes.fetch('expires', nil),
274
- 'secure' => cookie_attributes.fetch('secure', false)
275
- )
276
- cookies.store(cookie_name, parsed_cookie)
277
- end
278
- end
279
- cookies
280
- end
281
-
282
- def identify_cookie_attributes(cookie_filling)
283
- cookie_bits = cookie_filling.split(';')
284
- cookie_attributes = Hash.new
285
- cookie_attributes.store('value', cookie_bits[0].strip)
286
- cookie_bits.each do |bit|
287
- if bit.include? '='
288
- cookie_attribute, attribute_value = bit.split('=')
289
- cookie_attributes.store(cookie_attribute.strip, attribute_value.strip)
290
- if cookie_attribute.include? 'max-age'
291
- cookie_attributes.store('expires', Time.now + attribute_value.strip.to_i)
292
- end
293
- end
294
- if bit.include? 'secure'
295
- cookie_attributes.store('secure', true)
296
- end
297
- end
298
- cookie_attributes
299
- end
300
-
301
- end
302
- end
3
+ require_relative 'mock_request'
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'stringio'
5
+
6
+ require_relative 'constants'
7
+ require_relative 'mock_response'
8
+
9
+ module Rack
10
+ # Rack::MockRequest helps testing your Rack application without
11
+ # actually using HTTP.
12
+ #
13
+ # After performing a request on a URL with get/post/put/patch/delete, it
14
+ # returns a MockResponse with useful helper methods for effective
15
+ # testing.
16
+ #
17
+ # You can pass a hash with additional configuration to the
18
+ # get/post/put/patch/delete.
19
+ # <tt>:input</tt>:: A String or IO-like to be used as rack.input.
20
+ # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
21
+ # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
22
+
23
+ class MockRequest
24
+ class FatalWarning < RuntimeError
25
+ end
26
+
27
+ class FatalWarner
28
+ def puts(warning)
29
+ raise FatalWarning, warning
30
+ end
31
+
32
+ def write(warning)
33
+ raise FatalWarning, warning
34
+ end
35
+
36
+ def flush
37
+ end
38
+
39
+ def string
40
+ ""
41
+ end
42
+ end
43
+
44
+ def initialize(app)
45
+ @app = app
46
+ end
47
+
48
+ # Make a GET request and return a MockResponse. See #request.
49
+ def get(uri, opts = {}) request(GET, uri, opts) end
50
+ # Make a POST request and return a MockResponse. See #request.
51
+ def post(uri, opts = {}) request(POST, uri, opts) end
52
+ # Make a PUT request and return a MockResponse. See #request.
53
+ def put(uri, opts = {}) request(PUT, uri, opts) end
54
+ # Make a PATCH request and return a MockResponse. See #request.
55
+ def patch(uri, opts = {}) request(PATCH, uri, opts) end
56
+ # Make a DELETE request and return a MockResponse. See #request.
57
+ def delete(uri, opts = {}) request(DELETE, uri, opts) end
58
+ # Make a HEAD request and return a MockResponse. See #request.
59
+ def head(uri, opts = {}) request(HEAD, uri, opts) end
60
+ # Make an OPTIONS request and return a MockResponse. See #request.
61
+ def options(uri, opts = {}) request(OPTIONS, uri, opts) end
62
+
63
+ # Make a request using the given request method for the given
64
+ # uri to the rack application and return a MockResponse.
65
+ # Options given are passed to MockRequest.env_for.
66
+ def request(method = GET, uri = "", opts = {})
67
+ env = self.class.env_for(uri, opts.merge(method: method))
68
+
69
+ if opts[:lint]
70
+ app = Rack::Lint.new(@app)
71
+ else
72
+ app = @app
73
+ end
74
+
75
+ errors = env[RACK_ERRORS]
76
+ status, headers, body = app.call(env)
77
+ MockResponse.new(status, headers, body, errors)
78
+ ensure
79
+ body.close if body.respond_to?(:close)
80
+ end
81
+
82
+ # For historical reasons, we're pinning to RFC 2396.
83
+ # URI::Parser = URI::RFC2396_Parser
84
+ def self.parse_uri_rfc2396(uri)
85
+ @parser ||= URI::Parser.new
86
+ @parser.parse(uri)
87
+ end
88
+
89
+ # Return the Rack environment used for a request to +uri+.
90
+ # All options that are strings are added to the returned environment.
91
+ # Options:
92
+ # :fatal :: Whether to raise an exception if request outputs to rack.errors
93
+ # :input :: The rack.input to set
94
+ # :http_version :: The SERVER_PROTOCOL to set
95
+ # :method :: The HTTP request method to use
96
+ # :params :: The params to use
97
+ # :script_name :: The SCRIPT_NAME to set
98
+ def self.env_for(uri = "", opts = {})
99
+ uri = parse_uri_rfc2396(uri)
100
+ uri.path = "/#{uri.path}" unless uri.path[0] == ?/
101
+
102
+ env = {}
103
+
104
+ env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
105
+ env[SERVER_NAME] = (uri.host || "example.org").b
106
+ env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b
107
+ env[SERVER_PROTOCOL] = opts[:http_version] || 'HTTP/1.1'
108
+ env[QUERY_STRING] = (uri.query.to_s).b
109
+ env[PATH_INFO] = (uri.path).b
110
+ env[RACK_URL_SCHEME] = (uri.scheme || "http").b
111
+ env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
112
+
113
+ env[SCRIPT_NAME] = opts[:script_name] || ""
114
+
115
+ if opts[:fatal]
116
+ env[RACK_ERRORS] = FatalWarner.new
117
+ else
118
+ env[RACK_ERRORS] = StringIO.new
119
+ end
120
+
121
+ if params = opts[:params]
122
+ if env[REQUEST_METHOD] == GET
123
+ params = Utils.parse_nested_query(params) if params.is_a?(String)
124
+ params.update(Utils.parse_nested_query(env[QUERY_STRING]))
125
+ env[QUERY_STRING] = Utils.build_nested_query(params)
126
+ elsif !opts.has_key?(:input)
127
+ opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
128
+ if params.is_a?(Hash)
129
+ if data = Rack::Multipart.build_multipart(params)
130
+ opts[:input] = data
131
+ opts["CONTENT_LENGTH"] ||= data.length.to_s
132
+ opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
133
+ else
134
+ opts[:input] = Utils.build_nested_query(params)
135
+ end
136
+ else
137
+ opts[:input] = params
138
+ end
139
+ end
140
+ end
141
+
142
+ rack_input = opts[:input]
143
+ if String === rack_input
144
+ rack_input = StringIO.new(rack_input)
145
+ end
146
+
147
+ if rack_input
148
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
149
+ env[RACK_INPUT] = rack_input
150
+
151
+ env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
152
+ end
153
+
154
+ opts.each { |field, value|
155
+ env[field] = value if String === field
156
+ }
157
+
158
+ env
159
+ end
160
+ end
161
+ end