rack-test 0.6.3 → 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,24 +1,52 @@
1
- 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"
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
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'
9
28
 
10
29
  module Rack
11
30
  module Test
12
- VERSION = "0.6.3"
31
+ # The default host to use for requests, when a full URI is not
32
+ # provided.
33
+ DEFAULT_HOST = 'example.org'.freeze
13
34
 
14
- DEFAULT_HOST = "example.org"
15
- MULTIPART_BOUNDARY = "----------XnJLe9ZIbbGUYtzPQJ16u1"
35
+ # The default multipart boundary to use for multipart request bodies
36
+ MULTIPART_BOUNDARY = '----------XnJLe9ZIbbGUYtzPQJ16u1'.freeze
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
16
43
 
17
44
  # The common base class for exceptions raised by Rack::Test
18
45
  class Error < StandardError; end
19
46
 
20
- # This class represents a series of requests issued to a Rack app, sharing
21
- # 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.
22
50
  #
23
51
  # Rack::Test::Session's methods are most often called through Rack::Test::Methods,
24
52
  # which will automatically build a session when it's first used.
@@ -26,124 +54,133 @@ module Rack
26
54
  extend Forwardable
27
55
  include Rack::Test::Utils
28
56
 
29
- 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
30
68
 
31
- # Creates a Rack::Test::Session for a given Rack app or Rack::MockSession.
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.
32
73
  #
33
74
  # Note: Generally, you won't need to initialize a Rack::Test::Session directly.
34
75
  # Instead, you should include Rack::Test::Methods into your testing context.
35
76
  # (See README.rdoc for an example)
36
- def initialize(mock_session)
37
- @headers = {}
38
- @env = {}
39
-
40
- if mock_session.is_a?(MockSession)
41
- @rack_mock_session = mock_session
42
- else
43
- @rack_mock_session = MockSession.new(mock_session)
44
- end
45
-
46
- @default_host = @rack_mock_session.default_host
47
- end
48
-
49
- # Issue a GET request for the given URI with the given params and Rack
50
- # environment. Stores the issues request object in #last_request and
51
- # the app's response in #last_response. Yield #last_response to a block
52
- # if given.
53
77
  #
54
- # Example:
55
- # get "/"
56
- def get(uri, params = {}, env = {}, &block)
57
- env = env_for(uri, env.merge(:method => "GET", :params => params))
58
- process_request(uri, 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
- env = env_for(uri, env.merge(:method => "POST", :params => params))
67
- process_request(uri, 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
68
115
  end
69
116
 
70
- # Issue a PUT request for the given URI. See #get
71
- #
72
- # Example:
73
- # put "/"
74
- def put(uri, params = {}, env = {}, &block)
75
- env = env_for(uri, env.merge(:method => "PUT", :params => params))
76
- process_request(uri, env, &block)
117
+ # Run a block after the each request completes.
118
+ def after_request(&block)
119
+ @after_request << block
77
120
  end
78
121
 
79
- # Issue a PATCH request for the given URI. See #get
80
- #
81
- # Example:
82
- # patch "/"
83
- def patch(uri, params = {}, env = {}, &block)
84
- env = env_for(uri, env.merge(:method => "PATCH", :params => params))
85
- process_request(uri, env, &block)
122
+ # Replace the current cookie jar with an empty cookie jar.
123
+ def clear_cookies
124
+ @cookie_jar = CookieJar.new([], @default_host)
86
125
  end
87
126
 
88
- # Issue a DELETE request for the given URI. See #get
89
- #
90
- # Example:
91
- # delete "/"
92
- def delete(uri, params = {}, env = {}, &block)
93
- env = env_for(uri, env.merge(:method => "DELETE", :params => params))
94
- process_request(uri, env, &block)
127
+ # Set a cookie in the current cookie jar.
128
+ def set_cookie(cookie, uri = nil)
129
+ cookie_jar.merge(cookie, uri)
95
130
  end
96
131
 
97
- # Issue an OPTIONS request for the given URI. See #get
98
- #
99
- # Example:
100
- # options "/"
101
- def options(uri, params = {}, env = {}, &block)
102
- env = env_for(uri, env.merge(:method => "OPTIONS", :params => params))
103
- process_request(uri, 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
104
137
  end
105
138
 
106
- # Issue a HEAD request for the given URI. See #get
107
- #
108
- # Example:
109
- # head "/"
110
- def head(uri, params = {}, env = {}, &block)
111
- env = env_for(uri, env.merge(:method => "HEAD", :params => params))
112
- process_request(uri, 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
113
144
  end
114
145
 
115
146
  # Issue a request to the Rack app for the given URI and optional Rack
116
- # environment. Stores the issues request object in #last_request and
117
- # the app's response in #last_response. Yield #last_response to a block
118
- # if given.
147
+ # environment. Example:
119
148
  #
120
- # Example:
121
149
  # request "/"
122
150
  def request(uri, env = {}, &block)
151
+ uri = parse_uri(uri, env)
123
152
  env = env_for(uri, env)
124
153
  process_request(uri, env, &block)
125
154
  end
126
155
 
156
+ # Issue a request using the given HTTP verb for the given URI, with optional
157
+ # params and rack environment. Example:
158
+ #
159
+ # custom_request "LINK", "/"
160
+ def custom_request(verb, uri, params = {}, env = {}, &block)
161
+ uri = parse_uri(uri, env)
162
+ env = env_for(uri, env.merge(method: verb.to_s.upcase, params: params))
163
+ process_request(uri, env, &block)
164
+ end
165
+
127
166
  # Set a header to be included on all subsequent requests through the
128
167
  # session. Use a value of nil to remove a previously configured header.
129
168
  #
130
169
  # In accordance with the Rack spec, headers will be included in the Rack
131
- # environment hash in HTTP_USER_AGENT form.
170
+ # environment hash in HTTP_USER_AGENT form. Example:
132
171
  #
133
- # Example:
134
- # header "User-Agent", "Firefox"
172
+ # header "user-agent", "Firefox"
135
173
  def header(name, value)
136
- if value.nil?
137
- @headers.delete(name)
138
- else
139
- @headers[name] = value
140
- end
174
+ name = name.upcase
175
+ name.tr!('-', '_')
176
+ name = "HTTP_#{name}" unless name == 'CONTENT_TYPE' || name == 'CONTENT_LENGTH'
177
+ env(name, value)
141
178
  end
142
179
 
143
- # Set an env var to be included on all subsequent requests through the
144
- # 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:
145
183
  #
146
- # Example:
147
184
  # env "rack.session", {:csrf => 'token'}
148
185
  def env(name, value)
149
186
  if value.nil?
@@ -159,21 +196,11 @@ module Rack
159
196
  # Example:
160
197
  # basic_authorize "bryan", "secret"
161
198
  def basic_authorize(username, password)
162
- encoded_login = ["#{username}:#{password}"].pack("m*")
199
+ encoded_login = ["#{username}:#{password}"].pack('m0')
163
200
  header('Authorization', "Basic #{encoded_login}")
164
201
  end
165
202
 
166
- alias_method :authorize, :basic_authorize
167
-
168
- # Set the username and password for HTTP Digest authorization, to be
169
- # included in subsequent requests in the HTTP_AUTHORIZATION header.
170
- #
171
- # Example:
172
- # digest_authorize "bryan", "secret"
173
- def digest_authorize(username, password)
174
- @digest_username = username
175
- @digest_password = password
176
- end
203
+ alias authorize basic_authorize
177
204
 
178
205
  # Rack::Test will not follow any redirects automatically. This method
179
206
  # will follow the redirect returned (including setting the Referer header
@@ -181,138 +208,175 @@ module Rack
181
208
  # a redirect, an error will be raised.
182
209
  def follow_redirect!
183
210
  unless last_response.redirect?
184
- raise Error.new("Last response was not a redirect. Cannot follow_redirect!")
211
+ raise Error, 'Last response was not a redirect. Cannot follow_redirect!'
185
212
  end
186
213
 
187
- get(last_response["Location"], {}, { "HTTP_REFERER" => last_request.url })
188
- end
189
-
190
- private
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
191
221
 
192
- def env_for(path, env)
193
- uri = URI.parse(path)
194
- uri.path = "/#{uri.path}" unless uri.path[0] == ?/
195
- uri.host ||= @default_host
222
+ # Compute the next location by appending the location header with the
223
+ # last request, as per https://tools.ietf.org/html/rfc7231#section-7.1.2
224
+ # Adding two absolute locations returns the right-hand location
225
+ next_location = URI.parse(last_request.url) + URI.parse(last_response['Location'])
226
+
227
+ custom_request(
228
+ request_method,
229
+ next_location.to_s,
230
+ params,
231
+ 'HTTP_REFERER' => last_request.url,
232
+ 'rack.session' => last_request.session,
233
+ 'rack.session.options' => last_request.session_options
234
+ )
235
+ end
196
236
 
197
- env = default_env.merge(env)
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
198
255
 
199
- env["HTTP_HOST"] ||= [uri.host, (uri.port if uri.port != uri.default_port)].compact.join(":")
256
+ private
200
257
 
201
- env.update("HTTPS" => "on") if URI::HTTPS === uri
202
- env["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" if env[:xhr]
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)
267
+ end
268
+ end
203
269
 
204
- # TODO: Remove this after Rack 1.1 has been released.
205
- # Stringifying and upcasing methods has be commit upstream
206
- env["REQUEST_METHOD"] ||= env[:method] ? env[:method].to_s.upcase : "GET"
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
207
278
 
208
- if env["REQUEST_METHOD"] == "GET"
209
- # merge :params with the query string
210
- if params = env[:params]
211
- params = parse_nested_query(params) if params.is_a?(String)
212
- params.update(parse_nested_query(uri.query))
213
- uri.query = build_nested_query(params)
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.
293
+ def env_for(uri, env)
294
+ env = DEFAULT_ENV.merge(@env).merge!(env)
295
+
296
+ env['HTTP_HOST'] ||= [uri.host, (uri.port if uri.port != uri.default_port)].compact.join(':')
297
+ env['HTTPS'] = 'on' if URI::HTTPS === uri
298
+ env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' if env[:xhr]
299
+ env['REQUEST_METHOD'] ||= env[:method] ? env[:method].to_s.upcase : 'GET'
300
+
301
+ params = env.delete(:params)
302
+ query_array = [uri.query]
303
+
304
+ if env['REQUEST_METHOD'] == 'GET'
305
+ # Treat params as query params
306
+ if params
307
+ append_query_params(query_array, params)
214
308
  end
215
- elsif !env.has_key?(:input)
216
- env["CONTENT_TYPE"] ||= "application/x-www-form-urlencoded"
309
+ elsif !env.key?(:input)
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/')
217
313
 
218
- if env[:params].is_a?(Hash)
219
- if data = build_multipart(env[:params])
314
+ if params.is_a?(Hash)
315
+ if !params.empty? && data = build_multipart(params, false, multipart)
220
316
  env[:input] = data
221
- env["CONTENT_LENGTH"] ||= data.length.to_s
222
- env["CONTENT_TYPE"] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
317
+ env['CONTENT_LENGTH'] ||= data.length.to_s
318
+ env['CONTENT_TYPE'] = "#{multipart_content_type(env)}; boundary=#{MULTIPART_BOUNDARY}"
223
319
  else
224
- env[:input] = params_to_string(env[:params])
320
+ env[:input] = build_nested_query(params)
225
321
  end
226
322
  else
227
- env[:input] = env[:params]
323
+ env[:input] = params
228
324
  end
229
325
  end
230
326
 
231
- env.delete(:params)
232
-
233
- if env.has_key?(:cookie)
234
- set_cookie(env.delete(:cookie), uri)
327
+ if query_params = env.delete(:query_params)
328
+ append_query_params(query_array, query_params)
235
329
  end
330
+ query_array.compact!
331
+ query_array.reject!(&:empty?)
332
+ uri.query = query_array.join('&')
333
+
334
+ set_cookie(env.delete(:cookie), uri) if env.key?(:cookie)
236
335
 
237
336
  Rack::MockRequest.env_for(uri.to_s, env)
238
337
  end
239
338
 
240
- def process_request(uri, env)
241
- uri = URI.parse(uri)
242
- uri.host ||= @default_host
243
-
244
- @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
245
344
 
246
- if retry_with_digest_auth?(env)
247
- auth_env = env.merge({
248
- "HTTP_AUTHORIZATION" => digest_auth_header,
249
- "rack-test.digest_auth_retry" => true
250
- })
251
- auth_env.delete('rack.request')
252
- 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
253
350
  else
254
- yield last_response if block_given?
255
-
256
- last_response
351
+ 'multipart/form-data'
257
352
  end
258
353
  end
259
354
 
260
- def digest_auth_header
261
- challenge = last_response["WWW-Authenticate"].split(" ", 2).last
262
- params = Rack::Auth::Digest::Params.parse(challenge)
263
-
264
- params.merge!({
265
- "username" => @digest_username,
266
- "nc" => "00000001",
267
- "cnonce" => "nonsensenonce",
268
- "uri" => last_request.fullpath,
269
- "method" => last_request.env["REQUEST_METHOD"],
270
- })
271
-
272
- params["response"] = MockDigestRequest.new(params).response(@digest_password)
273
-
274
- "Digest #{params}"
275
- end
276
-
277
- def retry_with_digest_auth?(env)
278
- last_response.status == 401 &&
279
- digest_auth_configured? &&
280
- !env["rack-test.digest_auth_retry"]
281
- end
282
-
283
- def digest_auth_configured?
284
- @digest_username
285
- end
286
-
287
- def default_env
288
- { "rack.test" => true, "REMOTE_ADDR" => "127.0.0.1" }.merge(@env).merge(headers_for_env)
289
- end
290
-
291
- def headers_for_env
292
- 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
293
361
 
294
- @headers.each do |name, value|
295
- env_key = name.upcase.gsub("-", "_")
296
- env_key = "HTTP_" + env_key unless "CONTENT_TYPE" == env_key
297
- converted_headers[env_key] = value
298
- 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
299
367
 
300
- converted_headers
301
- end
368
+ yield @last_response if block_given?
302
369
 
303
- def params_to_string(params)
304
- case params
305
- when Hash then build_nested_query(params)
306
- when nil then ""
307
- else params
308
- end
370
+ @last_response
309
371
  end
310
-
311
372
  end
312
373
 
374
+ # Whether the version of rack in use handles encodings.
313
375
  def self.encoding_aware_strings?
314
- defined?(Encoding) && "".respond_to?(:encode)
376
+ Rack.release >= '1.6'
315
377
  end
316
-
317
378
  end
379
+
380
+ # For backwards compatibility with 1.1.0 and below
381
+ MockSession = Test::Session
318
382
  end