rack-test 1.1.0 → 2.0.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,100 @@ 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
65
+
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
29
71
 
30
- # Creates a Rack::Test::Session for a given Rack app or Rack::MockSession.
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 = {}
77
+ #
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)
37
100
  @env = {}
38
101
  @digest_username = nil
39
102
  @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
103
+ @app = app
104
+ @after_request = []
105
+ @default_host = default_host
106
+ @last_request = nil
107
+ @last_response = nil
108
+ clear_cookies
48
109
  end
49
110
 
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
- #
55
- # Example:
56
- # get "/"
57
- def get(uri, params = {}, env = {}, &block)
58
- custom_request('GET', uri, params, env, &block)
111
+ %w[get post put patch delete options head].each do |method_name|
112
+ class_eval(<<-END, __FILE__, __LINE__+1)
113
+ def #{method_name}(uri, params = {}, env = {}, &block)
114
+ custom_request('#{method_name.upcase}', uri, params, env, &block)
115
+ end
116
+ END
59
117
  end
60
118
 
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)
119
+ # Run a block after the each request completes.
120
+ def after_request(&block)
121
+ @after_request << block
67
122
  end
68
123
 
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)
124
+ # Replace the current cookie jar with an empty cookie jar.
125
+ def clear_cookies
126
+ @cookie_jar = CookieJar.new([], @default_host)
75
127
  end
76
128
 
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)
129
+ # Set a cookie in the current cookie jar.
130
+ def set_cookie(cookie, uri = nil)
131
+ cookie_jar.merge(cookie, uri)
83
132
  end
84
133
 
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)
91
- end
92
-
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)
134
+ # Return the last request issued in the session. Raises an error if no
135
+ # requests have been sent yet.
136
+ def last_request
137
+ raise Error, 'No request yet. Request a page first.' unless @last_request
138
+ @last_request
99
139
  end
100
140
 
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)
141
+ # Return the last response received in the session. Raises an error if
142
+ # no requests have been sent yet.
143
+ def last_response
144
+ raise Error, 'No response yet. Request a page first.' unless @last_response
145
+ @last_response
107
146
  end
108
147
 
109
148
  # 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.
149
+ # environment. Example:
113
150
  #
114
- # Example:
115
151
  # request "/"
116
152
  def request(uri, env = {}, &block)
117
153
  uri = parse_uri(uri, env)
@@ -119,9 +155,9 @@ module Rack
119
155
  process_request(uri, env, &block)
120
156
  end
121
157
 
122
- # Issue a request using the given verb for the given URI. See #get
158
+ # Issue a request using the given HTTP verb for the given URI, with optional
159
+ # params and rack environment. Example:
123
160
  #
124
- # Example:
125
161
  # custom_request "LINK", "/"
126
162
  def custom_request(verb, uri, params = {}, env = {}, &block)
127
163
  uri = parse_uri(uri, env)
@@ -133,22 +169,20 @@ module Rack
133
169
  # session. Use a value of nil to remove a previously configured header.
134
170
  #
135
171
  # In accordance with the Rack spec, headers will be included in the Rack
136
- # environment hash in HTTP_USER_AGENT form.
172
+ # environment hash in HTTP_USER_AGENT form. Example:
137
173
  #
138
- # Example:
139
- # header "User-Agent", "Firefox"
174
+ # header "user-agent", "Firefox"
140
175
  def header(name, value)
141
- if value.nil?
142
- @headers.delete(name)
143
- else
144
- @headers[name] = value
145
- end
176
+ name = name.upcase
177
+ name.tr!('-', '_')
178
+ name = "HTTP_#{name}" unless name == 'CONTENT_TYPE' || name == 'CONTENT_LENGTH'
179
+ env(name, value)
146
180
  end
147
181
 
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.
182
+ # Set an entry in the rack environment to be included on all subsequent
183
+ # requests through the session. Use a value of nil to remove a previously
184
+ # value. Example:
150
185
  #
151
- # Example:
152
186
  # env "rack.session", {:csrf => 'token'}
153
187
  def env(name, value)
154
188
  if value.nil?
@@ -172,10 +206,15 @@ module Rack
172
206
 
173
207
  # Set the username and password for HTTP Digest authorization, to be
174
208
  # included in subsequent requests in the HTTP_AUTHORIZATION header.
209
+ # This method is deprecated and will be removed in rack-test 2.1
175
210
  #
176
211
  # Example:
177
212
  # digest_authorize "bryan", "secret"
178
213
  def digest_authorize(username, password)
214
+ warn 'digest authentication support will be removed in rack-test 2.1', uplevel: 1
215
+ _digest_authorize(username, password)
216
+ end
217
+ def _digest_authorize(username, password) # :nodoc:
179
218
  @digest_username = username
180
219
  @digest_password = password
181
220
  end
@@ -188,20 +227,24 @@ module Rack
188
227
  unless last_response.redirect?
189
228
  raise Error, 'Last response was not a redirect. Cannot follow_redirect!'
190
229
  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
230
+
231
+ if last_response.status == 307
232
+ request_method = last_request.request_method
233
+ params = last_request.params
234
+ else
235
+ request_method = 'GET'
236
+ params = {}
237
+ end
197
238
 
198
239
  # Compute the next location by appending the location header with the
199
240
  # last request, as per https://tools.ietf.org/html/rfc7231#section-7.1.2
200
241
  # Adding two absolute locations returns the right-hand location
201
242
  next_location = URI.parse(last_request.url) + URI.parse(last_response['Location'])
202
243
 
203
- send(
204
- request_method, next_location.to_s, params,
244
+ custom_request(
245
+ request_method,
246
+ next_location.to_s,
247
+ params,
205
248
  'HTTP_REFERER' => last_request.url,
206
249
  'rack.session' => last_request.session,
207
250
  'rack.session.options' => last_request.session_options
@@ -210,66 +253,121 @@ module Rack
210
253
 
211
254
  private
212
255
 
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'
256
+ # :nocov:
257
+ if !defined?(Rack::RELEASE) || Gem::Version.new(Rack::RELEASE) < Gem::Version.new('2.2.2')
258
+ def close_body(body)
259
+ body.close if body.respond_to?(:close)
260
+ end
261
+ # :nocov:
262
+ else
263
+ # close() gets called automatically in newer Rack versions.
264
+ def close_body(body)
218
265
  end
219
266
  end
220
267
 
268
+ # Normalize URI based on given URI/path and environment.
269
+ def parse_uri(path, env)
270
+ uri = URI.parse(path)
271
+ uri.path = "/#{uri.path}" unless uri.path.start_with?('/')
272
+ uri.host ||= @default_host
273
+ uri.scheme ||= 'https' if env['HTTPS'] == 'on'
274
+ uri
275
+ end
276
+
277
+ DEFAULT_ENV = {
278
+ 'rack.test' => true,
279
+ 'REMOTE_ADDR' => '127.0.0.1',
280
+ 'SERVER_PROTOCOL' => 'HTTP/1.0',
281
+ }
282
+ # :nocov:
283
+ unless Rack.release >= '2.3'
284
+ DEFAULT_ENV['HTTP_VERSION'] = DEFAULT_ENV['SERVER_PROTOCOL']
285
+ end
286
+ # :nocov:
287
+ DEFAULT_ENV.freeze
288
+ private_constant :DEFAULT_ENV
289
+
290
+ # Update environment to use based on given URI.
221
291
  def env_for(uri, env)
222
- env = default_env.merge(env)
292
+ env = DEFAULT_ENV.merge(@env).merge!(env)
223
293
 
224
294
  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
295
+ env['HTTPS'] = 'on' if URI::HTTPS === uri
227
296
  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
297
  env['REQUEST_METHOD'] ||= env[:method] ? env[:method].to_s.upcase : 'GET'
232
298
 
233
- params = env.delete(:params) do {} end
299
+ params = env.delete(:params)
300
+ query_array = [uri.query]
234
301
 
235
302
  if env['REQUEST_METHOD'] == 'GET'
236
- # merge :params with the query string
303
+ # Treat params as query params
237
304
  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('&')
305
+ append_query_params(query_array, params)
241
306
  end
242
307
  elsif !env.key?(:input)
243
308
  env['CONTENT_TYPE'] ||= 'application/x-www-form-urlencoded'
309
+ params ||= {}
310
+ multipart = env.has_key?(:multipart) ? env.delete(:multipart) : env['CONTENT_TYPE'].start_with?('multipart/')
244
311
 
245
312
  if params.is_a?(Hash)
246
- if data = build_multipart(params)
313
+ if data = build_multipart(params, false, multipart)
247
314
  env[:input] = data
248
315
  env['CONTENT_LENGTH'] ||= data.length.to_s
249
- env['CONTENT_TYPE'] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
316
+ env['CONTENT_TYPE'] = "#{multipart_content_type(env)}; boundary=#{MULTIPART_BOUNDARY}"
250
317
  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)
318
+ env[:input] = build_nested_query(params)
254
319
  end
255
320
  else
256
321
  env[:input] = params
257
322
  end
258
323
  end
259
324
 
325
+ if query_params = env.delete(:query_params)
326
+ append_query_params(query_array, query_params)
327
+ end
328
+ query_array.compact!
329
+ query_array.reject!(&:empty?)
330
+ uri.query = query_array.join('&')
331
+
260
332
  set_cookie(env.delete(:cookie), uri) if env.key?(:cookie)
261
333
 
262
334
  Rack::MockRequest.env_for(uri.to_s, env)
263
335
  end
264
336
 
337
+ # Append a string version of the query params to the array of query params.
338
+ def append_query_params(query_array, query_params)
339
+ query_params = parse_nested_query(query_params) if query_params.is_a?(String)
340
+ query_array << build_nested_query(query_params)
341
+ end
342
+
343
+ # Return the multipart content type to use based on the environment.
344
+ def multipart_content_type(env)
345
+ requested_content_type = env['CONTENT_TYPE']
346
+ if requested_content_type.start_with?('multipart/')
347
+ requested_content_type
348
+ else
349
+ 'multipart/form-data'
350
+ end
351
+ end
352
+
353
+ # Submit the request with the given URI and rack environment to
354
+ # the mock session. Returns and potentially yields the last response.
265
355
  def process_request(uri, env)
266
- @rack_mock_session.request(uri, env)
356
+ env['HTTP_COOKIE'] ||= cookie_jar.for(uri)
357
+ @last_request = Rack::Request.new(env)
358
+ status, headers, body = @app.call(env).to_a
359
+
360
+ @last_response = MockResponse.new(status, headers, body, env['rack.errors'].flush)
361
+ close_body(body)
362
+ cookie_jar.merge(last_response.headers['set-cookie'], uri)
363
+ @after_request.each(&:call)
364
+ @last_response.finish
267
365
 
268
366
  if retry_with_digest_auth?(env)
269
367
  auth_env = env.merge('HTTP_AUTHORIZATION' => digest_auth_header,
270
368
  'rack-test.digest_auth_retry' => true)
271
369
  auth_env.delete('rack.request')
272
- process_request(uri.path, auth_env)
370
+ process_request(uri, auth_env)
273
371
  else
274
372
  yield last_response if block_given?
275
373
 
@@ -278,6 +376,8 @@ module Rack
278
376
  end
279
377
 
280
378
  def digest_auth_header
379
+ require_relative 'test/mock_digest_request'
380
+
281
381
  challenge = last_response['WWW-Authenticate'].split(' ', 2).last
282
382
  params = Rack::Auth::Digest::Params.parse(challenge)
283
383
 
@@ -287,7 +387,7 @@ module Rack
287
387
  'uri' => last_request.fullpath,
288
388
  'method' => last_request.env['REQUEST_METHOD'])
289
389
 
290
- params['response'] = MockDigestRequest.new(params).response(@digest_password)
390
+ params['response'] = MockDigestRequest_.new(params).response(@digest_password)
291
391
 
292
392
  "Digest #{params}"
293
393
  end
@@ -301,34 +401,14 @@ module Rack
301
401
  def digest_auth_configured?
302
402
  @digest_username
303
403
  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 = {}
311
-
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
317
-
318
- converted_headers
319
- end
320
-
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
327
- end
328
404
  end
329
405
 
406
+ # Whether the version of rack in use handles encodings.
330
407
  def self.encoding_aware_strings?
331
- defined?(Encoding) && ''.respond_to?(:encode)
408
+ Rack.release >= '1.6'
332
409
  end
333
410
  end
411
+
412
+ # For backwards compatibility with 1.1.0 and below
413
+ MockSession = Test::Session
334
414
  end