rack-test 1.0.0 → 2.0.1

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,81 +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
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
197
- send(
198
- request_method, last_response['Location'], params,
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
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,
199
248
  'HTTP_REFERER' => last_request.url,
200
- 'rack.session' => last_request.session
249
+ 'rack.session' => last_request.session,
250
+ 'rack.session.options' => last_request.session_options
201
251
  )
202
252
  end
203
253
 
204
254
  private
205
255
 
206
- def parse_uri(path, env)
207
- URI.parse(path).tap do |uri|
208
- uri.path = "/#{uri.path}" unless uri.path[0] == '/'
209
- uri.host ||= @default_host
210
- 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)
211
265
  end
212
266
  end
213
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.
214
291
  def env_for(uri, env)
215
- env = default_env.merge(env)
292
+ env = DEFAULT_ENV.merge(@env).merge!(env)
216
293
 
217
294
  env['HTTP_HOST'] ||= [uri.host, (uri.port if uri.port != uri.default_port)].compact.join(':')
218
-
219
- env.update('HTTPS' => 'on') if URI::HTTPS === uri
295
+ env['HTTPS'] = 'on' if URI::HTTPS === uri
220
296
  env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' if env[:xhr]
221
-
222
- # TODO: Remove this after Rack 1.1 has been released.
223
- # Stringifying and upcasing methods has be commit upstream
224
297
  env['REQUEST_METHOD'] ||= env[:method] ? env[:method].to_s.upcase : 'GET'
225
298
 
226
- params = env.delete(:params) do {} end
299
+ params = env.delete(:params)
300
+ query_array = [uri.query]
227
301
 
228
302
  if env['REQUEST_METHOD'] == 'GET'
229
- # merge :params with the query string
303
+ # Treat params as query params
230
304
  if params
231
- params = parse_nested_query(params) if params.is_a?(String)
232
-
233
- uri.query = [uri.query, build_nested_query(params)].compact.reject { |v| v == '' }.join('&')
305
+ append_query_params(query_array, params)
234
306
  end
235
307
  elsif !env.key?(:input)
236
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/')
237
311
 
238
312
  if params.is_a?(Hash)
239
- if data = build_multipart(params)
313
+ if data = build_multipart(params, false, multipart)
240
314
  env[:input] = data
241
315
  env['CONTENT_LENGTH'] ||= data.length.to_s
242
- env['CONTENT_TYPE'] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
316
+ env['CONTENT_TYPE'] = "#{multipart_content_type(env)}; boundary=#{MULTIPART_BOUNDARY}"
243
317
  else
244
- # NB: We do not need to set CONTENT_LENGTH here;
245
- # Rack::ContentLength will determine it automatically.
246
- env[:input] = params_to_string(params)
318
+ env[:input] = build_nested_query(params)
247
319
  end
248
320
  else
249
321
  env[:input] = params
250
322
  end
251
323
  end
252
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
+
253
332
  set_cookie(env.delete(:cookie), uri) if env.key?(:cookie)
254
333
 
255
334
  Rack::MockRequest.env_for(uri.to_s, env)
256
335
  end
257
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.
258
355
  def process_request(uri, env)
259
- @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
260
365
 
261
366
  if retry_with_digest_auth?(env)
262
367
  auth_env = env.merge('HTTP_AUTHORIZATION' => digest_auth_header,
263
368
  'rack-test.digest_auth_retry' => true)
264
369
  auth_env.delete('rack.request')
265
- process_request(uri.path, auth_env)
370
+ process_request(uri, auth_env)
266
371
  else
267
372
  yield last_response if block_given?
268
373
 
@@ -271,6 +376,8 @@ module Rack
271
376
  end
272
377
 
273
378
  def digest_auth_header
379
+ require_relative 'test/mock_digest_request'
380
+
274
381
  challenge = last_response['WWW-Authenticate'].split(' ', 2).last
275
382
  params = Rack::Auth::Digest::Params.parse(challenge)
276
383
 
@@ -280,7 +387,7 @@ module Rack
280
387
  'uri' => last_request.fullpath,
281
388
  'method' => last_request.env['REQUEST_METHOD'])
282
389
 
283
- params['response'] = MockDigestRequest.new(params).response(@digest_password)
390
+ params['response'] = MockDigestRequest_.new(params).response(@digest_password)
284
391
 
285
392
  "Digest #{params}"
286
393
  end
@@ -294,34 +401,14 @@ module Rack
294
401
  def digest_auth_configured?
295
402
  @digest_username
296
403
  end
297
-
298
- def default_env
299
- { 'rack.test' => true, 'REMOTE_ADDR' => '127.0.0.1' }.merge(@env).merge(headers_for_env)
300
- end
301
-
302
- def headers_for_env
303
- converted_headers = {}
304
-
305
- @headers.each do |name, value|
306
- env_key = name.upcase.tr('-', '_')
307
- env_key = 'HTTP_' + env_key unless env_key == 'CONTENT_TYPE'
308
- converted_headers[env_key] = value
309
- end
310
-
311
- converted_headers
312
- end
313
-
314
- def params_to_string(params)
315
- case params
316
- when Hash then build_nested_query(params)
317
- when nil then ''
318
- else params
319
- end
320
- end
321
404
  end
322
405
 
406
+ # Whether the version of rack in use handles encodings.
323
407
  def self.encoding_aware_strings?
324
- defined?(Encoding) && ''.respond_to?(:encode)
408
+ Rack.release >= '1.6'
325
409
  end
326
410
  end
411
+
412
+ # For backwards compatibility with 1.1.0 and below
413
+ MockSession = Test::Session
327
414
  end