rack 2.2.10 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -100
  3. data/CONTRIBUTING.md +53 -47
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +293 -0
  6. data/SPEC.rdoc +174 -126
  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 +2 -1
  10. data/lib/rack/auth/digest/md5.rb +1 -131
  11. data/lib/rack/auth/digest/nonce.rb +1 -53
  12. data/lib/rack/auth/digest/params.rb +1 -54
  13. data/lib/rack/auth/digest/request.rb +1 -43
  14. data/lib/rack/auth/digest.rb +256 -0
  15. data/lib/rack/body_proxy.rb +3 -1
  16. data/lib/rack/builder.rb +60 -42
  17. data/lib/rack/cascade.rb +2 -0
  18. data/lib/rack/chunked.rb +16 -13
  19. data/lib/rack/common_logger.rb +23 -18
  20. data/lib/rack/conditional_get.rb +18 -15
  21. data/lib/rack/constants.rb +63 -0
  22. data/lib/rack/content_length.rb +12 -16
  23. data/lib/rack/content_type.rb +8 -5
  24. data/lib/rack/deflater.rb +40 -26
  25. data/lib/rack/directory.rb +9 -3
  26. data/lib/rack/etag.rb +14 -23
  27. data/lib/rack/events.rb +4 -0
  28. data/lib/rack/file.rb +2 -0
  29. data/lib/rack/files.rb +15 -17
  30. data/lib/rack/head.rb +9 -8
  31. data/lib/rack/headers.rb +154 -0
  32. data/lib/rack/lint.rb +754 -648
  33. data/lib/rack/lock.rb +2 -5
  34. data/lib/rack/logger.rb +2 -0
  35. data/lib/rack/media_type.rb +4 -9
  36. data/lib/rack/method_override.rb +5 -1
  37. data/lib/rack/mime.rb +8 -0
  38. data/lib/rack/mock.rb +1 -271
  39. data/lib/rack/mock_request.rb +166 -0
  40. data/lib/rack/mock_response.rb +126 -0
  41. data/lib/rack/multipart/generator.rb +7 -5
  42. data/lib/rack/multipart/parser.rb +123 -85
  43. data/lib/rack/multipart/uploaded_file.rb +4 -0
  44. data/lib/rack/multipart.rb +20 -40
  45. data/lib/rack/null_logger.rb +9 -0
  46. data/lib/rack/query_parser.rb +76 -44
  47. data/lib/rack/recursive.rb +2 -0
  48. data/lib/rack/reloader.rb +0 -2
  49. data/lib/rack/request.rb +189 -91
  50. data/lib/rack/response.rb +131 -61
  51. data/lib/rack/rewindable_input.rb +24 -5
  52. data/lib/rack/runtime.rb +7 -6
  53. data/lib/rack/sendfile.rb +30 -25
  54. data/lib/rack/show_exceptions.rb +15 -2
  55. data/lib/rack/show_status.rb +17 -7
  56. data/lib/rack/static.rb +8 -8
  57. data/lib/rack/tempfile_reaper.rb +15 -4
  58. data/lib/rack/urlmap.rb +4 -2
  59. data/lib/rack/utils.rb +212 -202
  60. data/lib/rack/version.rb +9 -4
  61. data/lib/rack.rb +5 -76
  62. metadata +15 -35
  63. data/README.rdoc +0 -320
  64. data/Rakefile +0 -130
  65. data/bin/rackup +0 -5
  66. data/contrib/rack.png +0 -0
  67. data/contrib/rack.svg +0 -150
  68. data/contrib/rack_logo.svg +0 -164
  69. data/contrib/rdoc.css +0 -412
  70. data/example/lobster.ru +0 -6
  71. data/example/protectedlobster.rb +0 -16
  72. data/example/protectedlobster.ru +0 -10
  73. data/lib/rack/core_ext/regexp.rb +0 -14
  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 -85
  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
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'logger'
4
4
 
5
+ require_relative 'constants'
6
+
5
7
  module Rack
6
8
  # Sets up rack.logger to write to rack.errors stream
7
9
  class Logger
@@ -4,7 +4,7 @@ module Rack
4
4
  # Rack::MediaType parse media type and parameters out of content_type string
5
5
 
6
6
  class MediaType
7
- SPLIT_PATTERN = /[;,]/
7
+ SPLIT_PATTERN = %r{\s*[;,]\s*}
8
8
 
9
9
  class << self
10
10
  # The media type (type/subtype) portion of the CONTENT_TYPE header
@@ -15,11 +15,7 @@ module Rack
15
15
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
16
16
  def type(content_type)
17
17
  return nil unless content_type
18
- if type = content_type.split(SPLIT_PATTERN, 2).first
19
- type.rstrip!
20
- type.downcase!
21
- type
22
- end
18
+ content_type.split(SPLIT_PATTERN, 2).first.tap(&:downcase!)
23
19
  end
24
20
 
25
21
  # The media type parameters provided in CONTENT_TYPE as a Hash, or
@@ -31,10 +27,9 @@ module Rack
31
27
  return {} if content_type.nil?
32
28
 
33
29
  content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
34
- s.strip!
35
30
  k, v = s.split('=', 2)
36
- k.downcase!
37
- hsh[k] = strip_doublequotes(v)
31
+
32
+ hsh[k.tap(&:downcase!)] = strip_doublequotes(v)
38
33
  end
39
34
  end
40
35
 
@@ -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]
@@ -43,7 +47,7 @@ module Rack
43
47
 
44
48
  def method_override_param(req)
45
49
  req.POST[METHOD_OVERRIDE_PARAM_KEY]
46
- rescue Utils::InvalidParameterError, Utils::ParameterTypeError, QueryParser::ParamsTooDeepError
50
+ rescue Utils::InvalidParameterError, Utils::ParameterTypeError
47
51
  req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params"
48
52
  rescue EOFError
49
53
  req.get_header(RACK_ERRORS).puts "Bad request content body"
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",
@@ -617,6 +624,7 @@ module Rack
617
624
  ".wbs" => "application/vnd.criticaltools.wbs+xml",
618
625
  ".wbxml" => "application/vnd.wap.wbxml",
619
626
  ".webm" => "video/webm",
627
+ ".webp" => "image/webp",
620
628
  ".wm" => "video/x-ms-wm",
621
629
  ".wma" => "audio/x-ms-wma",
622
630
  ".wmd" => "application/x-ms-wmd",
data/lib/rack/mock.rb CHANGED
@@ -1,273 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'uri'
4
- require 'stringio'
5
- require_relative '../rack'
6
- require 'cgi/cookie'
7
-
8
- module Rack
9
- # Rack::MockRequest helps testing your Rack application without
10
- # actually using HTTP.
11
- #
12
- # After performing a request on a URL with get/post/put/patch/delete, it
13
- # returns a MockResponse with useful helper methods for effective
14
- # testing.
15
- #
16
- # You can pass a hash with additional configuration to the
17
- # get/post/put/patch/delete.
18
- # <tt>:input</tt>:: A String or IO-like to be used as rack.input.
19
- # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
20
- # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
21
-
22
- class MockRequest
23
- class FatalWarning < RuntimeError
24
- end
25
-
26
- class FatalWarner
27
- def puts(warning)
28
- raise FatalWarning, warning
29
- end
30
-
31
- def write(warning)
32
- raise FatalWarning, warning
33
- end
34
-
35
- def flush
36
- end
37
-
38
- def string
39
- ""
40
- end
41
- end
42
-
43
- DEFAULT_ENV = {
44
- RACK_VERSION => Rack::VERSION,
45
- RACK_INPUT => StringIO.new,
46
- RACK_ERRORS => StringIO.new,
47
- RACK_MULTITHREAD => true,
48
- RACK_MULTIPROCESS => true,
49
- RACK_RUNONCE => false,
50
- }.freeze
51
-
52
- def initialize(app)
53
- @app = app
54
- end
55
-
56
- # Make a GET request and return a MockResponse. See #request.
57
- def get(uri, opts = {}) request(GET, uri, opts) end
58
- # Make a POST request and return a MockResponse. See #request.
59
- def post(uri, opts = {}) request(POST, uri, opts) end
60
- # Make a PUT request and return a MockResponse. See #request.
61
- def put(uri, opts = {}) request(PUT, uri, opts) end
62
- # Make a PATCH request and return a MockResponse. See #request.
63
- def patch(uri, opts = {}) request(PATCH, uri, opts) end
64
- # Make a DELETE request and return a MockResponse. See #request.
65
- def delete(uri, opts = {}) request(DELETE, uri, opts) end
66
- # Make a HEAD request and return a MockResponse. See #request.
67
- def head(uri, opts = {}) request(HEAD, uri, opts) end
68
- # Make an OPTIONS request and return a MockResponse. See #request.
69
- def options(uri, opts = {}) request(OPTIONS, uri, opts) end
70
-
71
- # Make a request using the given request method for the given
72
- # uri to the rack application and return a MockResponse.
73
- # Options given are passed to MockRequest.env_for.
74
- def request(method = GET, uri = "", opts = {})
75
- env = self.class.env_for(uri, opts.merge(method: method))
76
-
77
- if opts[:lint]
78
- app = Rack::Lint.new(@app)
79
- else
80
- app = @app
81
- end
82
-
83
- errors = env[RACK_ERRORS]
84
- status, headers, body = app.call(env)
85
- MockResponse.new(status, headers, body, errors)
86
- ensure
87
- body.close if body.respond_to?(:close)
88
- end
89
-
90
- # For historical reasons, we're pinning to RFC 2396.
91
- # URI::Parser = URI::RFC2396_Parser
92
- def self.parse_uri_rfc2396(uri)
93
- @parser ||= URI::Parser.new
94
- @parser.parse(uri)
95
- end
96
-
97
- # Return the Rack environment used for a request to +uri+.
98
- # All options that are strings are added to the returned environment.
99
- # Options:
100
- # :fatal :: Whether to raise an exception if request outputs to rack.errors
101
- # :input :: The rack.input to set
102
- # :method :: The HTTP request method to use
103
- # :params :: The params to use
104
- # :script_name :: The SCRIPT_NAME to set
105
- def self.env_for(uri = "", opts = {})
106
- uri = parse_uri_rfc2396(uri)
107
- uri.path = "/#{uri.path}" unless uri.path[0] == ?/
108
-
109
- env = DEFAULT_ENV.dup
110
-
111
- env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
112
- env[SERVER_NAME] = (uri.host || "example.org").b
113
- env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b
114
- env[QUERY_STRING] = (uri.query.to_s).b
115
- env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
116
- env[RACK_URL_SCHEME] = (uri.scheme || "http").b
117
- env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
118
-
119
- env[SCRIPT_NAME] = opts[:script_name] || ""
120
-
121
- if opts[:fatal]
122
- env[RACK_ERRORS] = FatalWarner.new
123
- else
124
- env[RACK_ERRORS] = StringIO.new
125
- end
126
-
127
- if params = opts[:params]
128
- if env[REQUEST_METHOD] == GET
129
- params = Utils.parse_nested_query(params) if params.is_a?(String)
130
- params.update(Utils.parse_nested_query(env[QUERY_STRING]))
131
- env[QUERY_STRING] = Utils.build_nested_query(params)
132
- elsif !opts.has_key?(:input)
133
- opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
134
- if params.is_a?(Hash)
135
- if data = Rack::Multipart.build_multipart(params)
136
- opts[:input] = data
137
- opts["CONTENT_LENGTH"] ||= data.length.to_s
138
- opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
139
- else
140
- opts[:input] = Utils.build_nested_query(params)
141
- end
142
- else
143
- opts[:input] = params
144
- end
145
- end
146
- end
147
-
148
- empty_str = String.new
149
- opts[:input] ||= empty_str
150
- if String === opts[:input]
151
- rack_input = StringIO.new(opts[:input])
152
- else
153
- rack_input = opts[:input]
154
- end
155
-
156
- rack_input.set_encoding(Encoding::BINARY)
157
- env[RACK_INPUT] = rack_input
158
-
159
- env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
160
-
161
- opts.each { |field, value|
162
- env[field] = value if String === field
163
- }
164
-
165
- env
166
- end
167
- end
168
-
169
- # Rack::MockResponse provides useful helpers for testing your apps.
170
- # Usually, you don't create the MockResponse on your own, but use
171
- # MockRequest.
172
-
173
- class MockResponse < Rack::Response
174
- class << self
175
- alias [] new
176
- end
177
-
178
- # Headers
179
- attr_reader :original_headers, :cookies
180
-
181
- # Errors
182
- attr_accessor :errors
183
-
184
- def initialize(status, headers, body, errors = StringIO.new(""))
185
- @original_headers = headers
186
- @errors = errors.string if errors.respond_to?(:string)
187
- @cookies = parse_cookies_from_header
188
-
189
- super(body, status, headers)
190
-
191
- buffered_body!
192
- end
193
-
194
- def =~(other)
195
- body =~ other
196
- end
197
-
198
- def match(other)
199
- body.match other
200
- end
201
-
202
- def body
203
- # FIXME: apparently users of MockResponse expect the return value of
204
- # MockResponse#body to be a string. However, the real response object
205
- # returns the body as a list.
206
- #
207
- # See spec_showstatus.rb:
208
- #
209
- # should "not replace existing messages" do
210
- # ...
211
- # res.body.should == "foo!"
212
- # end
213
- buffer = String.new
214
-
215
- super.each do |chunk|
216
- buffer << chunk
217
- end
218
-
219
- return buffer
220
- end
221
-
222
- def empty?
223
- [201, 204, 304].include? status
224
- end
225
-
226
- def cookie(name)
227
- cookies.fetch(name, nil)
228
- end
229
-
230
- private
231
-
232
- def parse_cookies_from_header
233
- cookies = Hash.new
234
- if original_headers.has_key? 'Set-Cookie'
235
- set_cookie_header = original_headers.fetch('Set-Cookie')
236
- set_cookie_header.split("\n").each do |cookie|
237
- cookie_name, cookie_filling = cookie.split('=', 2)
238
- cookie_attributes = identify_cookie_attributes cookie_filling
239
- parsed_cookie = CGI::Cookie.new(
240
- 'name' => cookie_name.strip,
241
- 'value' => cookie_attributes.fetch('value'),
242
- 'path' => cookie_attributes.fetch('path', nil),
243
- 'domain' => cookie_attributes.fetch('domain', nil),
244
- 'expires' => cookie_attributes.fetch('expires', nil),
245
- 'secure' => cookie_attributes.fetch('secure', false)
246
- )
247
- cookies.store(cookie_name, parsed_cookie)
248
- end
249
- end
250
- cookies
251
- end
252
-
253
- def identify_cookie_attributes(cookie_filling)
254
- cookie_bits = cookie_filling.split(';')
255
- cookie_attributes = Hash.new
256
- cookie_attributes.store('value', cookie_bits[0].strip)
257
- cookie_bits.each do |bit|
258
- if bit.include? '='
259
- cookie_attribute, attribute_value = bit.split('=')
260
- cookie_attributes.store(cookie_attribute.strip, attribute_value.strip)
261
- if cookie_attribute.include? 'max-age'
262
- cookie_attributes.store('expires', Time.now + attribute_value.strip.to_i)
263
- end
264
- end
265
- if bit.include? 'secure'
266
- cookie_attributes.store('secure', true)
267
- end
268
- end
269
- cookie_attributes
270
- end
271
-
272
- end
273
- end
3
+ require_relative 'mock_request'
@@ -0,0 +1,166 @@
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
+ DEFAULT_ENV = {
45
+ RACK_INPUT => StringIO.new,
46
+ RACK_ERRORS => StringIO.new,
47
+ }.freeze
48
+
49
+ def initialize(app)
50
+ @app = app
51
+ end
52
+
53
+ # Make a GET request and return a MockResponse. See #request.
54
+ def get(uri, opts = {}) request(GET, uri, opts) end
55
+ # Make a POST request and return a MockResponse. See #request.
56
+ def post(uri, opts = {}) request(POST, uri, opts) end
57
+ # Make a PUT request and return a MockResponse. See #request.
58
+ def put(uri, opts = {}) request(PUT, uri, opts) end
59
+ # Make a PATCH request and return a MockResponse. See #request.
60
+ def patch(uri, opts = {}) request(PATCH, uri, opts) end
61
+ # Make a DELETE request and return a MockResponse. See #request.
62
+ def delete(uri, opts = {}) request(DELETE, uri, opts) end
63
+ # Make a HEAD request and return a MockResponse. See #request.
64
+ def head(uri, opts = {}) request(HEAD, uri, opts) end
65
+ # Make an OPTIONS request and return a MockResponse. See #request.
66
+ def options(uri, opts = {}) request(OPTIONS, uri, opts) end
67
+
68
+ # Make a request using the given request method for the given
69
+ # uri to the rack application and return a MockResponse.
70
+ # Options given are passed to MockRequest.env_for.
71
+ def request(method = GET, uri = "", opts = {})
72
+ env = self.class.env_for(uri, opts.merge(method: method))
73
+
74
+ if opts[:lint]
75
+ app = Rack::Lint.new(@app)
76
+ else
77
+ app = @app
78
+ end
79
+
80
+ errors = env[RACK_ERRORS]
81
+ status, headers, body = app.call(env)
82
+ MockResponse.new(status, headers, body, errors)
83
+ ensure
84
+ body.close if body.respond_to?(:close)
85
+ end
86
+
87
+ # For historical reasons, we're pinning to RFC 2396.
88
+ # URI::Parser = URI::RFC2396_Parser
89
+ def self.parse_uri_rfc2396(uri)
90
+ @parser ||= URI::Parser.new
91
+ @parser.parse(uri)
92
+ end
93
+
94
+ # Return the Rack environment used for a request to +uri+.
95
+ # All options that are strings are added to the returned environment.
96
+ # Options:
97
+ # :fatal :: Whether to raise an exception if request outputs to rack.errors
98
+ # :input :: The rack.input to set
99
+ # :http_version :: The SERVER_PROTOCOL to set
100
+ # :method :: The HTTP request method to use
101
+ # :params :: The params to use
102
+ # :script_name :: The SCRIPT_NAME to set
103
+ def self.env_for(uri = "", opts = {})
104
+ uri = parse_uri_rfc2396(uri)
105
+ uri.path = "/#{uri.path}" unless uri.path[0] == ?/
106
+
107
+ env = DEFAULT_ENV.dup
108
+
109
+ env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
110
+ env[SERVER_NAME] = (uri.host || "example.org").b
111
+ env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b
112
+ env[SERVER_PROTOCOL] = opts[:http_version] || 'HTTP/1.1'
113
+ env[QUERY_STRING] = (uri.query.to_s).b
114
+ env[PATH_INFO] = (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
+ opts[:input] ||= String.new
148
+ if String === opts[:input]
149
+ rack_input = StringIO.new(opts[:input])
150
+ else
151
+ rack_input = opts[:input]
152
+ end
153
+
154
+ rack_input.set_encoding(Encoding::BINARY)
155
+ env[RACK_INPUT] = rack_input
156
+
157
+ env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
158
+
159
+ opts.each { |field, value|
160
+ env[field] = value if String === field
161
+ }
162
+
163
+ env
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi/cookie'
4
+ require 'time'
5
+
6
+ require_relative 'response'
7
+
8
+ module Rack
9
+ # Rack::MockResponse provides useful helpers for testing your apps.
10
+ # Usually, you don't create the MockResponse on your own, but use
11
+ # MockRequest.
12
+
13
+ class MockResponse < Rack::Response
14
+ class << self
15
+ alias [] new
16
+ end
17
+
18
+ # Headers
19
+ attr_reader :original_headers, :cookies
20
+
21
+ # Errors
22
+ attr_accessor :errors
23
+
24
+ def initialize(status, headers, body, errors = nil)
25
+ @original_headers = headers
26
+
27
+ if errors
28
+ @errors = errors.string if errors.respond_to?(:string)
29
+ else
30
+ @errors = ""
31
+ end
32
+
33
+ super(body, status, headers)
34
+
35
+ @cookies = parse_cookies_from_header
36
+ buffered_body!
37
+ end
38
+
39
+ def =~(other)
40
+ body =~ other
41
+ end
42
+
43
+ def match(other)
44
+ body.match other
45
+ end
46
+
47
+ def body
48
+ return @buffered_body if defined?(@buffered_body)
49
+
50
+ # FIXME: apparently users of MockResponse expect the return value of
51
+ # MockResponse#body to be a string. However, the real response object
52
+ # returns the body as a list.
53
+ #
54
+ # See spec_showstatus.rb:
55
+ #
56
+ # should "not replace existing messages" do
57
+ # ...
58
+ # res.body.should == "foo!"
59
+ # end
60
+ buffer = @buffered_body = String.new
61
+
62
+ @body.each do |chunk|
63
+ buffer << chunk
64
+ end
65
+
66
+ return buffer
67
+ end
68
+
69
+ def empty?
70
+ [201, 204, 304].include? status
71
+ end
72
+
73
+ def cookie(name)
74
+ cookies.fetch(name, nil)
75
+ end
76
+
77
+ private
78
+
79
+ def parse_cookies_from_header
80
+ cookies = Hash.new
81
+ if headers.has_key? 'set-cookie'
82
+ set_cookie_header = headers.fetch('set-cookie')
83
+ Array(set_cookie_header).each do |header_value|
84
+ header_value.split("\n").each do |cookie|
85
+ cookie_name, cookie_filling = cookie.split('=', 2)
86
+ cookie_attributes = identify_cookie_attributes cookie_filling
87
+ parsed_cookie = CGI::Cookie.new(
88
+ 'name' => cookie_name.strip,
89
+ 'value' => cookie_attributes.fetch('value'),
90
+ 'path' => cookie_attributes.fetch('path', nil),
91
+ 'domain' => cookie_attributes.fetch('domain', nil),
92
+ 'expires' => cookie_attributes.fetch('expires', nil),
93
+ 'secure' => cookie_attributes.fetch('secure', false)
94
+ )
95
+ cookies.store(cookie_name, parsed_cookie)
96
+ end
97
+ end
98
+ end
99
+ cookies
100
+ end
101
+
102
+ def identify_cookie_attributes(cookie_filling)
103
+ cookie_bits = cookie_filling.split(';')
104
+ cookie_attributes = Hash.new
105
+ cookie_attributes.store('value', cookie_bits[0].strip)
106
+ cookie_bits.drop(1).each do |bit|
107
+ if bit.include? '='
108
+ cookie_attribute, attribute_value = bit.split('=', 2)
109
+ cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
110
+ end
111
+ if bit.include? 'secure'
112
+ cookie_attributes.store('secure', true)
113
+ end
114
+ end
115
+
116
+ if cookie_attributes.key? 'max-age'
117
+ cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
118
+ elsif cookie_attributes.key? 'expires'
119
+ cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
120
+ end
121
+
122
+ cookie_attributes
123
+ end
124
+
125
+ end
126
+ end