rack-test 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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