rack-test 1.1.0 → 2.1.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.
data/lib/rack/test.rb CHANGED
@@ -1,23 +1,52 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
- require 'rack'
3
- require 'rack/mock_session'
4
- require 'rack/test/cookie_jar'
5
- require 'rack/test/mock_digest_request'
6
- require 'rack/test/utils'
7
- require 'rack/test/methods'
8
- require 'rack/test/uploaded_file'
9
- require 'rack/test/version'
4
+
5
+ # :nocov:
6
+ begin
7
+ require "rack/version"
8
+ rescue LoadError
9
+ require "rack"
10
+ else
11
+ if Rack.release >= '2.3'
12
+ require "rack/request"
13
+ require "rack/mock"
14
+ require "rack/utils"
15
+ else
16
+ require "rack"
17
+ end
18
+ end
19
+ # :nocov:
20
+
21
+ require 'forwardable'
22
+
23
+ require_relative 'test/cookie_jar'
24
+ require_relative 'test/utils'
25
+ require_relative 'test/methods'
26
+ require_relative 'test/uploaded_file'
27
+ require_relative 'test/version'
10
28
 
11
29
  module Rack
12
30
  module Test
31
+ # The default host to use for requests, when a full URI is not
32
+ # provided.
13
33
  DEFAULT_HOST = 'example.org'.freeze
34
+
35
+ # The default multipart boundary to use for multipart request bodies
14
36
  MULTIPART_BOUNDARY = '----------XnJLe9ZIbbGUYtzPQJ16u1'.freeze
15
37
 
38
+ # The starting boundary in multipart requests
39
+ START_BOUNDARY = "--#{MULTIPART_BOUNDARY}\r\n".freeze
40
+
41
+ # The ending boundary in multipart requests
42
+ END_BOUNDARY = "--#{MULTIPART_BOUNDARY}--\r\n".freeze
43
+
16
44
  # The common base class for exceptions raised by Rack::Test
17
45
  class Error < StandardError; end
18
46
 
19
- # This class represents a series of requests issued to a Rack app, sharing
20
- # a single cookie jar
47
+ # Rack::Test::Session handles a series of requests issued to a Rack app.
48
+ # It keeps track of the cookies for the session, and allows for setting headers
49
+ # and a default rack environment that is used for future requests.
21
50
  #
22
51
  # Rack::Test::Session's methods are most often called through Rack::Test::Methods,
23
52
  # which will automatically build a session when it's first used.
@@ -25,93 +54,98 @@ module Rack
25
54
  extend Forwardable
26
55
  include Rack::Test::Utils
27
56
 
28
- def_delegators :@rack_mock_session, :clear_cookies, :set_cookie, :last_response, :last_request
57
+ def self.new(app, default_host = DEFAULT_HOST) # :nodoc:
58
+ if app.is_a?(self)
59
+ # Backwards compatibility for initializing with Rack::MockSession
60
+ app
61
+ else
62
+ super
63
+ end
64
+ end
29
65
 
30
- # Creates a Rack::Test::Session for a given Rack app or Rack::MockSession.
66
+ # The Rack::Test::CookieJar for the cookies for the current session.
67
+ attr_accessor :cookie_jar
68
+
69
+ # The default host used for the session for when using paths for URIs.
70
+ attr_reader :default_host
71
+
72
+ # Creates a Rack::Test::Session for a given Rack app or Rack::Test::BasicSession.
31
73
  #
32
74
  # Note: Generally, you won't need to initialize a Rack::Test::Session directly.
33
75
  # Instead, you should include Rack::Test::Methods into your testing context.
34
76
  # (See README.rdoc for an example)
35
- def initialize(mock_session)
36
- @headers = {}
37
- @env = {}
38
- @digest_username = nil
39
- @digest_password = nil
40
-
41
- @rack_mock_session = if mock_session.is_a?(MockSession)
42
- mock_session
43
- else
44
- MockSession.new(mock_session)
45
- end
46
-
47
- @default_host = @rack_mock_session.default_host
48
- end
49
-
50
- # Issue a GET request for the given URI with the given params and Rack
51
- # environment. Stores the issues request object in #last_request and
52
- # the app's response in #last_response. Yield #last_response to a block
53
- # if given.
54
77
  #
55
- # Example:
56
- # get "/"
57
- def get(uri, params = {}, env = {}, &block)
58
- custom_request('GET', uri, params, env, &block)
78
+ # The following methods are defined via metaprogramming: get, post, put, patch,
79
+ # delete, options, and head. Each method submits a request with the given request
80
+ # method, with the given URI and optional parameters and rack environment.
81
+ # Examples:
82
+ #
83
+ # # URI only:
84
+ # get("/") # GET /
85
+ # get("/?foo=bar") # GET /?foo=bar
86
+ #
87
+ # # URI and parameters
88
+ # get("/foo", 'bar'=>'baz') # GET /foo?bar=baz
89
+ # post("/foo", 'bar'=>'baz') # POST /foo (bar=baz in request body)
90
+ #
91
+ # # URI, parameters, and rack environment
92
+ # get("/bar", {}, 'CONTENT_TYPE'=>'foo')
93
+ # get("/bar", {'foo'=>'baz'}, 'HTTP_ACCEPT'=>'*')
94
+ #
95
+ # The above methods as well as #request and #custom_request store the Rack::Request
96
+ # submitted in #last_request. The methods store a Rack::MockResponse based on the
97
+ # response in #last_response. #last_response is also returned by the methods.
98
+ # If a block is given, #last_response is also yielded to the block.
99
+ def initialize(app, default_host = DEFAULT_HOST)
100
+ @env = {}
101
+ @app = app
102
+ @after_request = []
103
+ @default_host = default_host
104
+ @last_request = nil
105
+ @last_response = nil
106
+ clear_cookies
59
107
  end
60
108
 
61
- # Issue a POST request for the given URI. See #get
62
- #
63
- # Example:
64
- # post "/signup", "name" => "Bryan"
65
- def post(uri, params = {}, env = {}, &block)
66
- custom_request('POST', uri, params, env, &block)
109
+ %w[get post put patch delete options head].each do |method_name|
110
+ class_eval(<<-END, __FILE__, __LINE__+1)
111
+ def #{method_name}(uri, params = {}, env = {}, &block)
112
+ custom_request('#{method_name.upcase}', uri, params, env, &block)
113
+ end
114
+ END
67
115
  end
68
116
 
69
- # Issue a PUT request for the given URI. See #get
70
- #
71
- # Example:
72
- # put "/"
73
- def put(uri, params = {}, env = {}, &block)
74
- custom_request('PUT', uri, params, env, &block)
117
+ # Run a block after the each request completes.
118
+ def after_request(&block)
119
+ @after_request << block
75
120
  end
76
121
 
77
- # Issue a PATCH request for the given URI. See #get
78
- #
79
- # Example:
80
- # patch "/"
81
- def patch(uri, params = {}, env = {}, &block)
82
- custom_request('PATCH', uri, params, env, &block)
122
+ # Replace the current cookie jar with an empty cookie jar.
123
+ def clear_cookies
124
+ @cookie_jar = CookieJar.new([], @default_host)
83
125
  end
84
126
 
85
- # Issue a DELETE request for the given URI. See #get
86
- #
87
- # Example:
88
- # delete "/"
89
- def delete(uri, params = {}, env = {}, &block)
90
- custom_request('DELETE', uri, params, env, &block)
127
+ # Set a cookie in the current cookie jar.
128
+ def set_cookie(cookie, uri = nil)
129
+ cookie_jar.merge(cookie, uri)
91
130
  end
92
131
 
93
- # Issue an OPTIONS request for the given URI. See #get
94
- #
95
- # Example:
96
- # options "/"
97
- def options(uri, params = {}, env = {}, &block)
98
- custom_request('OPTIONS', uri, params, env, &block)
132
+ # Return the last request issued in the session. Raises an error if no
133
+ # requests have been sent yet.
134
+ def last_request
135
+ raise Error, 'No request yet. Request a page first.' unless @last_request
136
+ @last_request
99
137
  end
100
138
 
101
- # Issue a HEAD request for the given URI. See #get
102
- #
103
- # Example:
104
- # head "/"
105
- def head(uri, params = {}, env = {}, &block)
106
- custom_request('HEAD', uri, params, env, &block)
139
+ # Return the last response received in the session. Raises an error if
140
+ # no requests have been sent yet.
141
+ def last_response
142
+ raise Error, 'No response yet. Request a page first.' unless @last_response
143
+ @last_response
107
144
  end
108
145
 
109
146
  # Issue a request to the Rack app for the given URI and optional Rack
110
- # environment. Stores the issues request object in #last_request and
111
- # the app's response in #last_response. Yield #last_response to a block
112
- # if given.
147
+ # environment. Example:
113
148
  #
114
- # Example:
115
149
  # request "/"
116
150
  def request(uri, env = {}, &block)
117
151
  uri = parse_uri(uri, env)
@@ -119,9 +153,9 @@ module Rack
119
153
  process_request(uri, env, &block)
120
154
  end
121
155
 
122
- # Issue a request using the given verb for the given URI. See #get
156
+ # Issue a request using the given HTTP verb for the given URI, with optional
157
+ # params and rack environment. Example:
123
158
  #
124
- # Example:
125
159
  # custom_request "LINK", "/"
126
160
  def custom_request(verb, uri, params = {}, env = {}, &block)
127
161
  uri = parse_uri(uri, env)
@@ -133,22 +167,20 @@ module Rack
133
167
  # session. Use a value of nil to remove a previously configured header.
134
168
  #
135
169
  # In accordance with the Rack spec, headers will be included in the Rack
136
- # environment hash in HTTP_USER_AGENT form.
170
+ # environment hash in HTTP_USER_AGENT form. Example:
137
171
  #
138
- # Example:
139
- # header "User-Agent", "Firefox"
172
+ # header "user-agent", "Firefox"
140
173
  def header(name, value)
141
- if value.nil?
142
- @headers.delete(name)
143
- else
144
- @headers[name] = value
145
- end
174
+ name = name.upcase
175
+ name.tr!('-', '_')
176
+ name = "HTTP_#{name}" unless name == 'CONTENT_TYPE' || name == 'CONTENT_LENGTH'
177
+ env(name, value)
146
178
  end
147
179
 
148
- # Set an env var to be included on all subsequent requests through the
149
- # session. Use a value of nil to remove a previously configured env.
180
+ # Set an entry in the rack environment to be included on all subsequent
181
+ # requests through the session. Use a value of nil to remove a previously
182
+ # value. Example:
150
183
  #
151
- # Example:
152
184
  # env "rack.session", {:csrf => 'token'}
153
185
  def env(name, value)
154
186
  if value.nil?
@@ -170,16 +202,6 @@ module Rack
170
202
 
171
203
  alias authorize basic_authorize
172
204
 
173
- # Set the username and password for HTTP Digest authorization, to be
174
- # included in subsequent requests in the HTTP_AUTHORIZATION header.
175
- #
176
- # Example:
177
- # digest_authorize "bryan", "secret"
178
- def digest_authorize(username, password)
179
- @digest_username = username
180
- @digest_password = password
181
- end
182
-
183
205
  # Rack::Test will not follow any redirects automatically. This method
184
206
  # will follow the redirect returned (including setting the Referer header
185
207
  # on the new request) in the last response. If the last response was not
@@ -188,147 +210,173 @@ module Rack
188
210
  unless last_response.redirect?
189
211
  raise Error, 'Last response was not a redirect. Cannot follow_redirect!'
190
212
  end
191
- request_method, params =
192
- if last_response.status == 307
193
- [last_request.request_method.downcase.to_sym, last_request.params]
194
- else
195
- [:get, {}]
196
- end
213
+
214
+ if last_response.status == 307
215
+ request_method = last_request.request_method
216
+ params = last_request.params
217
+ else
218
+ request_method = 'GET'
219
+ params = {}
220
+ end
197
221
 
198
222
  # Compute the next location by appending the location header with the
199
223
  # last request, as per https://tools.ietf.org/html/rfc7231#section-7.1.2
200
224
  # Adding two absolute locations returns the right-hand location
201
225
  next_location = URI.parse(last_request.url) + URI.parse(last_response['Location'])
202
226
 
203
- send(
204
- request_method, next_location.to_s, params,
227
+ custom_request(
228
+ request_method,
229
+ next_location.to_s,
230
+ params,
205
231
  'HTTP_REFERER' => last_request.url,
206
232
  'rack.session' => last_request.session,
207
233
  'rack.session.options' => last_request.session_options
208
234
  )
209
235
  end
210
236
 
237
+ # Yield to the block, and restore the last request, last response, and
238
+ # cookie jar to the state they were prior to block execution upon
239
+ # exiting the block.
240
+ def restore_state
241
+ request = @last_request
242
+ response = @last_response
243
+ cookie_jar = @cookie_jar.dup
244
+ after_request = @after_request.dup
245
+
246
+ begin
247
+ yield
248
+ ensure
249
+ @last_request = request
250
+ @last_response = response
251
+ @cookie_jar = cookie_jar
252
+ @after_request = after_request
253
+ end
254
+ end
255
+
211
256
  private
212
257
 
213
- def parse_uri(path, env)
214
- URI.parse(path).tap do |uri|
215
- uri.path = "/#{uri.path}" unless uri.path[0] == '/'
216
- uri.host ||= @default_host
217
- uri.scheme ||= 'https' if env['HTTPS'] == 'on'
258
+ # :nocov:
259
+ if !defined?(Rack::RELEASE) || Gem::Version.new(Rack::RELEASE) < Gem::Version.new('2.2.2')
260
+ def close_body(body)
261
+ body.close if body.respond_to?(:close)
262
+ end
263
+ # :nocov:
264
+ else
265
+ # close() gets called automatically in newer Rack versions.
266
+ def close_body(body)
218
267
  end
219
268
  end
220
269
 
270
+ # Normalize URI based on given URI/path and environment.
271
+ def parse_uri(path, env)
272
+ uri = URI.parse(path)
273
+ uri.path = "/#{uri.path}" unless uri.path.start_with?('/')
274
+ uri.host ||= @default_host
275
+ uri.scheme ||= 'https' if env['HTTPS'] == 'on'
276
+ uri
277
+ end
278
+
279
+ DEFAULT_ENV = {
280
+ 'rack.test' => true,
281
+ 'REMOTE_ADDR' => '127.0.0.1',
282
+ 'SERVER_PROTOCOL' => 'HTTP/1.0',
283
+ }
284
+ # :nocov:
285
+ unless Rack.release >= '2.3'
286
+ DEFAULT_ENV['HTTP_VERSION'] = DEFAULT_ENV['SERVER_PROTOCOL']
287
+ end
288
+ # :nocov:
289
+ DEFAULT_ENV.freeze
290
+ private_constant :DEFAULT_ENV
291
+
292
+ # Update environment to use based on given URI.
221
293
  def env_for(uri, env)
222
- env = default_env.merge(env)
294
+ env = DEFAULT_ENV.merge(@env).merge!(env)
223
295
 
224
296
  env['HTTP_HOST'] ||= [uri.host, (uri.port if uri.port != uri.default_port)].compact.join(':')
225
-
226
- env.update('HTTPS' => 'on') if URI::HTTPS === uri
297
+ env['HTTPS'] = 'on' if URI::HTTPS === uri
227
298
  env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' if env[:xhr]
228
-
229
- # TODO: Remove this after Rack 1.1 has been released.
230
- # Stringifying and upcasing methods has be commit upstream
231
299
  env['REQUEST_METHOD'] ||= env[:method] ? env[:method].to_s.upcase : 'GET'
232
300
 
233
- params = env.delete(:params) do {} end
301
+ params = env.delete(:params)
302
+ query_array = [uri.query]
234
303
 
235
304
  if env['REQUEST_METHOD'] == 'GET'
236
- # merge :params with the query string
305
+ # Treat params as query params
237
306
  if params
238
- params = parse_nested_query(params) if params.is_a?(String)
239
-
240
- uri.query = [uri.query, build_nested_query(params)].compact.reject { |v| v == '' }.join('&')
307
+ append_query_params(query_array, params)
241
308
  end
242
309
  elsif !env.key?(:input)
243
310
  env['CONTENT_TYPE'] ||= 'application/x-www-form-urlencoded'
311
+ params ||= {}
312
+ multipart = env.has_key?(:multipart) ? env.delete(:multipart) : env['CONTENT_TYPE'].start_with?('multipart/')
244
313
 
245
314
  if params.is_a?(Hash)
246
- if data = build_multipart(params)
315
+ if !params.empty? && data = build_multipart(params, false, multipart)
247
316
  env[:input] = data
248
317
  env['CONTENT_LENGTH'] ||= data.length.to_s
249
- env['CONTENT_TYPE'] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
318
+ env['CONTENT_TYPE'] = "#{multipart_content_type(env)}; boundary=#{MULTIPART_BOUNDARY}"
250
319
  else
251
- # NB: We do not need to set CONTENT_LENGTH here;
252
- # Rack::ContentLength will determine it automatically.
253
- env[:input] = params_to_string(params)
320
+ env[:input] = build_nested_query(params)
254
321
  end
255
322
  else
256
323
  env[:input] = params
257
324
  end
258
325
  end
259
326
 
327
+ if query_params = env.delete(:query_params)
328
+ append_query_params(query_array, query_params)
329
+ end
330
+ query_array.compact!
331
+ query_array.reject!(&:empty?)
332
+ uri.query = query_array.join('&')
333
+
260
334
  set_cookie(env.delete(:cookie), uri) if env.key?(:cookie)
261
335
 
262
336
  Rack::MockRequest.env_for(uri.to_s, env)
263
337
  end
264
338
 
265
- def process_request(uri, env)
266
- @rack_mock_session.request(uri, env)
339
+ # Append a string version of the query params to the array of query params.
340
+ def append_query_params(query_array, query_params)
341
+ query_params = parse_nested_query(query_params) if query_params.is_a?(String)
342
+ query_array << build_nested_query(query_params)
343
+ end
267
344
 
268
- if retry_with_digest_auth?(env)
269
- auth_env = env.merge('HTTP_AUTHORIZATION' => digest_auth_header,
270
- 'rack-test.digest_auth_retry' => true)
271
- auth_env.delete('rack.request')
272
- process_request(uri.path, auth_env)
345
+ # Return the multipart content type to use based on the environment.
346
+ def multipart_content_type(env)
347
+ requested_content_type = env['CONTENT_TYPE']
348
+ if requested_content_type.start_with?('multipart/')
349
+ requested_content_type
273
350
  else
274
- yield last_response if block_given?
275
-
276
- last_response
351
+ 'multipart/form-data'
277
352
  end
278
353
  end
279
354
 
280
- def digest_auth_header
281
- challenge = last_response['WWW-Authenticate'].split(' ', 2).last
282
- params = Rack::Auth::Digest::Params.parse(challenge)
283
-
284
- params.merge!('username' => @digest_username,
285
- 'nc' => '00000001',
286
- 'cnonce' => 'nonsensenonce',
287
- 'uri' => last_request.fullpath,
288
- 'method' => last_request.env['REQUEST_METHOD'])
289
-
290
- params['response'] = MockDigestRequest.new(params).response(@digest_password)
291
-
292
- "Digest #{params}"
293
- end
294
-
295
- def retry_with_digest_auth?(env)
296
- last_response.status == 401 &&
297
- digest_auth_configured? &&
298
- !env['rack-test.digest_auth_retry']
299
- end
300
-
301
- def digest_auth_configured?
302
- @digest_username
303
- end
304
-
305
- def default_env
306
- { 'rack.test' => true, 'REMOTE_ADDR' => '127.0.0.1' }.merge(@env).merge(headers_for_env)
307
- end
308
-
309
- def headers_for_env
310
- converted_headers = {}
355
+ # Submit the request with the given URI and rack environment to
356
+ # the mock session. Returns and potentially yields the last response.
357
+ def process_request(uri, env)
358
+ env['HTTP_COOKIE'] ||= cookie_jar.for(uri)
359
+ @last_request = Rack::Request.new(env)
360
+ status, headers, body = @app.call(env).to_a
311
361
 
312
- @headers.each do |name, value|
313
- env_key = name.upcase.tr('-', '_')
314
- env_key = 'HTTP_' + env_key unless env_key == 'CONTENT_TYPE'
315
- converted_headers[env_key] = value
316
- end
362
+ @last_response = MockResponse.new(status, headers, body, env['rack.errors'].flush)
363
+ close_body(body)
364
+ cookie_jar.merge(last_response.headers['set-cookie'], uri)
365
+ @after_request.each(&:call)
366
+ @last_response.finish
317
367
 
318
- converted_headers
319
- end
368
+ yield @last_response if block_given?
320
369
 
321
- def params_to_string(params)
322
- case params
323
- when Hash then build_nested_query(params)
324
- when nil then ''
325
- else params
326
- end
370
+ @last_response
327
371
  end
328
372
  end
329
373
 
374
+ # Whether the version of rack in use handles encodings.
330
375
  def self.encoding_aware_strings?
331
- defined?(Encoding) && ''.respond_to?(:encode)
376
+ Rack.release >= '1.6'
332
377
  end
333
378
  end
379
+
380
+ # For backwards compatibility with 1.1.0 and below
381
+ MockSession = Test::Session
334
382
  end