rack-test 0.8.3 → 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
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
- #
55
- # Example:
56
- # get "/"
57
- def get(uri, params = {}, env = {}, &block)
58
- custom_request('GET', uri, params, env, &block)
103
+ @app = app
104
+ @after_request = []
105
+ @default_host = default_host
106
+ @last_request = nil
107
+ @last_response = nil
108
+ clear_cookies
59
109
  end
60
110
 
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)
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
67
117
  end
68
118
 
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)
119
+ # Run a block after the each request completes.
120
+ def after_request(&block)
121
+ @after_request << block
75
122
  end
76
123
 
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)
124
+ # Replace the current cookie jar with an empty cookie jar.
125
+ def clear_cookies
126
+ @cookie_jar = CookieJar.new([], @default_host)
83
127
  end
84
128
 
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)
129
+ # Set a cookie in the current cookie jar.
130
+ def set_cookie(cookie, uri = nil)
131
+ cookie_jar.merge(cookie, uri)
91
132
  end
92
133
 
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,73 +227,147 @@ module Rack
188
227
  unless last_response.redirect?
189
228
  raise Error, 'Last response was not a redirect. Cannot follow_redirect!'
190
229
  end
230
+
191
231
  if last_response.status == 307
192
- send(last_request.request_method.downcase.to_sym, last_response['Location'], last_request.params, 'HTTP_REFERER' => last_request.url)
232
+ request_method = last_request.request_method
233
+ params = last_request.params
193
234
  else
194
- get(last_response['Location'], {}, 'HTTP_REFERER' => last_request.url)
235
+ request_method = 'GET'
236
+ params = {}
195
237
  end
238
+
239
+ # Compute the next location by appending the location header with the
240
+ # last request, as per https://tools.ietf.org/html/rfc7231#section-7.1.2
241
+ # Adding two absolute locations returns the right-hand location
242
+ next_location = URI.parse(last_request.url) + URI.parse(last_response['Location'])
243
+
244
+ custom_request(
245
+ request_method,
246
+ next_location.to_s,
247
+ params,
248
+ 'HTTP_REFERER' => last_request.url,
249
+ 'rack.session' => last_request.session,
250
+ 'rack.session.options' => last_request.session_options
251
+ )
196
252
  end
197
253
 
198
254
  private
199
255
 
200
- def parse_uri(path, env)
201
- URI.parse(path).tap do |uri|
202
- uri.path = "/#{uri.path}" unless uri.path[0] == '/'
203
- uri.host ||= @default_host
204
- 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)
205
260
  end
261
+ # :nocov:
262
+ else
263
+ # close() gets called automatically in newer Rack versions.
264
+ def close_body(body)
265
+ end
266
+ end
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']
206
285
  end
286
+ # :nocov:
287
+ DEFAULT_ENV.freeze
288
+ private_constant :DEFAULT_ENV
207
289
 
290
+ # Update environment to use based on given URI.
208
291
  def env_for(uri, env)
209
- env = default_env.merge(env)
292
+ env = DEFAULT_ENV.merge(@env).merge!(env)
210
293
 
211
294
  env['HTTP_HOST'] ||= [uri.host, (uri.port if uri.port != uri.default_port)].compact.join(':')
212
-
213
- env.update('HTTPS' => 'on') if URI::HTTPS === uri
295
+ env['HTTPS'] = 'on' if URI::HTTPS === uri
214
296
  env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' if env[:xhr]
215
-
216
- # TODO: Remove this after Rack 1.1 has been released.
217
- # Stringifying and upcasing methods has be commit upstream
218
297
  env['REQUEST_METHOD'] ||= env[:method] ? env[:method].to_s.upcase : 'GET'
219
298
 
220
- params = env.delete(:params) do {} end
299
+ params = env.delete(:params)
300
+ query_array = [uri.query]
221
301
 
222
302
  if env['REQUEST_METHOD'] == 'GET'
223
- # merge :params with the query string
303
+ # Treat params as query params
224
304
  if params
225
- params = parse_nested_query(params) if params.is_a?(String)
226
-
227
- uri.query = [uri.query, build_nested_query(params)].compact.reject { |v| v == '' }.join('&')
305
+ append_query_params(query_array, params)
228
306
  end
229
307
  elsif !env.key?(:input)
230
- env['CONTENT_TYPE'] ||= 'application/x-www-form-urlencoded' unless params.nil?
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/')
231
311
 
232
312
  if params.is_a?(Hash)
233
- if data = build_multipart(params)
313
+ if data = build_multipart(params, false, multipart)
234
314
  env[:input] = data
235
315
  env['CONTENT_LENGTH'] ||= data.length.to_s
236
- env['CONTENT_TYPE'] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
316
+ env['CONTENT_TYPE'] = "#{multipart_content_type(env)}; boundary=#{MULTIPART_BOUNDARY}"
237
317
  else
238
- env[:input] = params_to_string(params)
318
+ env[:input] = build_nested_query(params)
239
319
  end
240
320
  else
241
321
  env[:input] = params
242
322
  end
243
323
  end
244
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
+
245
332
  set_cookie(env.delete(:cookie), uri) if env.key?(:cookie)
246
333
 
247
334
  Rack::MockRequest.env_for(uri.to_s, env)
248
335
  end
249
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.
250
355
  def process_request(uri, env)
251
- @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
252
365
 
253
366
  if retry_with_digest_auth?(env)
254
367
  auth_env = env.merge('HTTP_AUTHORIZATION' => digest_auth_header,
255
368
  'rack-test.digest_auth_retry' => true)
256
369
  auth_env.delete('rack.request')
257
- process_request(uri.path, auth_env)
370
+ process_request(uri, auth_env)
258
371
  else
259
372
  yield last_response if block_given?
260
373
 
@@ -263,6 +376,8 @@ module Rack
263
376
  end
264
377
 
265
378
  def digest_auth_header
379
+ require_relative 'test/mock_digest_request'
380
+
266
381
  challenge = last_response['WWW-Authenticate'].split(' ', 2).last
267
382
  params = Rack::Auth::Digest::Params.parse(challenge)
268
383
 
@@ -272,7 +387,7 @@ module Rack
272
387
  'uri' => last_request.fullpath,
273
388
  'method' => last_request.env['REQUEST_METHOD'])
274
389
 
275
- params['response'] = MockDigestRequest.new(params).response(@digest_password)
390
+ params['response'] = MockDigestRequest_.new(params).response(@digest_password)
276
391
 
277
392
  "Digest #{params}"
278
393
  end
@@ -286,34 +401,14 @@ module Rack
286
401
  def digest_auth_configured?
287
402
  @digest_username
288
403
  end
289
-
290
- def default_env
291
- { 'rack.test' => true, 'REMOTE_ADDR' => '127.0.0.1' }.merge(@env).merge(headers_for_env)
292
- end
293
-
294
- def headers_for_env
295
- converted_headers = {}
296
-
297
- @headers.each do |name, value|
298
- env_key = name.upcase.tr('-', '_')
299
- env_key = 'HTTP_' + env_key unless env_key == 'CONTENT_TYPE'
300
- converted_headers[env_key] = value
301
- end
302
-
303
- converted_headers
304
- end
305
-
306
- def params_to_string(params)
307
- case params
308
- when Hash then build_nested_query(params)
309
- when nil then ''
310
- else params
311
- end
312
- end
313
404
  end
314
405
 
406
+ # Whether the version of rack in use handles encodings.
315
407
  def self.encoding_aware_strings?
316
- defined?(Encoding) && ''.respond_to?(:encode)
408
+ Rack.release >= '1.6'
317
409
  end
318
410
  end
411
+
412
+ # For backwards compatibility with 1.1.0 and below
413
+ MockSession = Test::Session
319
414
  end