rack-test 1.1.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.md +120 -1
- data/MIT-LICENSE.txt +1 -0
- data/README.md +53 -64
- data/lib/rack/test/cookie_jar.rb +108 -50
- data/lib/rack/test/methods.rb +57 -46
- data/lib/rack/test/uploaded_file.rb +44 -16
- data/lib/rack/test/utils.rb +73 -61
- data/lib/rack/test/version.rb +1 -1
- data/lib/rack/test.rb +230 -182
- metadata +30 -101
- data/lib/rack/mock_session.rb +0 -63
- data/lib/rack/test/mock_digest_request.rb +0 -25
data/lib/rack/test.rb
CHANGED
@@ -1,23 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'uri'
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
require
|
6
|
-
|
7
|
-
require
|
8
|
-
|
9
|
-
|
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
|
-
#
|
20
|
-
#
|
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,98 @@ module Rack
|
|
25
54
|
extend Forwardable
|
26
55
|
include Rack::Test::Utils
|
27
56
|
|
28
|
-
|
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
|
29
65
|
|
30
|
-
#
|
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
|
71
|
+
|
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 = {}
|
37
|
-
@env = {}
|
38
|
-
@digest_username = nil
|
39
|
-
@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
77
|
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
|
58
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
67
115
|
end
|
68
116
|
|
69
|
-
#
|
70
|
-
|
71
|
-
|
72
|
-
# put "/"
|
73
|
-
def put(uri, params = {}, env = {}, &block)
|
74
|
-
custom_request('PUT', uri, params, env, &block)
|
117
|
+
# Run a block after the each request completes.
|
118
|
+
def after_request(&block)
|
119
|
+
@after_request << block
|
75
120
|
end
|
76
121
|
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
# patch "/"
|
81
|
-
def patch(uri, params = {}, env = {}, &block)
|
82
|
-
custom_request('PATCH', uri, params, env, &block)
|
122
|
+
# Replace the current cookie jar with an empty cookie jar.
|
123
|
+
def clear_cookies
|
124
|
+
@cookie_jar = CookieJar.new([], @default_host)
|
83
125
|
end
|
84
126
|
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
# delete "/"
|
89
|
-
def delete(uri, params = {}, env = {}, &block)
|
90
|
-
custom_request('DELETE', uri, params, env, &block)
|
127
|
+
# Set a cookie in the current cookie jar.
|
128
|
+
def set_cookie(cookie, uri = nil)
|
129
|
+
cookie_jar.merge(cookie, uri)
|
91
130
|
end
|
92
131
|
|
93
|
-
#
|
94
|
-
#
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
custom_request('OPTIONS', uri, params, 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
|
99
137
|
end
|
100
138
|
|
101
|
-
#
|
102
|
-
#
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
custom_request('HEAD', uri, params, 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
|
107
144
|
end
|
108
145
|
|
109
146
|
# Issue a request to the Rack app for the given URI and optional Rack
|
110
|
-
# environment.
|
111
|
-
# the app's response in #last_response. Yield #last_response to a block
|
112
|
-
# if given.
|
147
|
+
# environment. Example:
|
113
148
|
#
|
114
|
-
# Example:
|
115
149
|
# request "/"
|
116
150
|
def request(uri, env = {}, &block)
|
117
151
|
uri = parse_uri(uri, env)
|
@@ -119,9 +153,9 @@ module Rack
|
|
119
153
|
process_request(uri, env, &block)
|
120
154
|
end
|
121
155
|
|
122
|
-
# Issue a request using the given verb for the given URI
|
156
|
+
# Issue a request using the given HTTP verb for the given URI, with optional
|
157
|
+
# params and rack environment. Example:
|
123
158
|
#
|
124
|
-
# Example:
|
125
159
|
# custom_request "LINK", "/"
|
126
160
|
def custom_request(verb, uri, params = {}, env = {}, &block)
|
127
161
|
uri = parse_uri(uri, env)
|
@@ -133,22 +167,20 @@ module Rack
|
|
133
167
|
# session. Use a value of nil to remove a previously configured header.
|
134
168
|
#
|
135
169
|
# In accordance with the Rack spec, headers will be included in the Rack
|
136
|
-
# environment hash in HTTP_USER_AGENT form.
|
170
|
+
# environment hash in HTTP_USER_AGENT form. Example:
|
137
171
|
#
|
138
|
-
#
|
139
|
-
# header "User-Agent", "Firefox"
|
172
|
+
# header "user-agent", "Firefox"
|
140
173
|
def header(name, value)
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
end
|
174
|
+
name = name.upcase
|
175
|
+
name.tr!('-', '_')
|
176
|
+
name = "HTTP_#{name}" unless name == 'CONTENT_TYPE' || name == 'CONTENT_LENGTH'
|
177
|
+
env(name, value)
|
146
178
|
end
|
147
179
|
|
148
|
-
# Set an
|
149
|
-
# session. Use a value of nil to remove a previously
|
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:
|
150
183
|
#
|
151
|
-
# Example:
|
152
184
|
# env "rack.session", {:csrf => 'token'}
|
153
185
|
def env(name, value)
|
154
186
|
if value.nil?
|
@@ -170,16 +202,6 @@ module Rack
|
|
170
202
|
|
171
203
|
alias authorize basic_authorize
|
172
204
|
|
173
|
-
# Set the username and password for HTTP Digest authorization, to be
|
174
|
-
# included in subsequent requests in the HTTP_AUTHORIZATION header.
|
175
|
-
#
|
176
|
-
# Example:
|
177
|
-
# digest_authorize "bryan", "secret"
|
178
|
-
def digest_authorize(username, password)
|
179
|
-
@digest_username = username
|
180
|
-
@digest_password = password
|
181
|
-
end
|
182
|
-
|
183
205
|
# Rack::Test will not follow any redirects automatically. This method
|
184
206
|
# will follow the redirect returned (including setting the Referer header
|
185
207
|
# on the new request) in the last response. If the last response was not
|
@@ -188,147 +210,173 @@ module Rack
|
|
188
210
|
unless last_response.redirect?
|
189
211
|
raise Error, 'Last response was not a redirect. Cannot follow_redirect!'
|
190
212
|
end
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
213
|
+
|
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
|
197
221
|
|
198
222
|
# Compute the next location by appending the location header with the
|
199
223
|
# last request, as per https://tools.ietf.org/html/rfc7231#section-7.1.2
|
200
224
|
# Adding two absolute locations returns the right-hand location
|
201
225
|
next_location = URI.parse(last_request.url) + URI.parse(last_response['Location'])
|
202
226
|
|
203
|
-
|
204
|
-
request_method,
|
227
|
+
custom_request(
|
228
|
+
request_method,
|
229
|
+
next_location.to_s,
|
230
|
+
params,
|
205
231
|
'HTTP_REFERER' => last_request.url,
|
206
232
|
'rack.session' => last_request.session,
|
207
233
|
'rack.session.options' => last_request.session_options
|
208
234
|
)
|
209
235
|
end
|
210
236
|
|
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
|
255
|
+
|
211
256
|
private
|
212
257
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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)
|
218
267
|
end
|
219
268
|
end
|
220
269
|
|
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
|
278
|
+
|
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.
|
221
293
|
def env_for(uri, env)
|
222
|
-
env =
|
294
|
+
env = DEFAULT_ENV.merge(@env).merge!(env)
|
223
295
|
|
224
296
|
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
|
297
|
+
env['HTTPS'] = 'on' if URI::HTTPS === uri
|
227
298
|
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
299
|
env['REQUEST_METHOD'] ||= env[:method] ? env[:method].to_s.upcase : 'GET'
|
232
300
|
|
233
|
-
params = env.delete(:params)
|
301
|
+
params = env.delete(:params)
|
302
|
+
query_array = [uri.query]
|
234
303
|
|
235
304
|
if env['REQUEST_METHOD'] == 'GET'
|
236
|
-
#
|
305
|
+
# Treat params as query params
|
237
306
|
if params
|
238
|
-
|
239
|
-
|
240
|
-
uri.query = [uri.query, build_nested_query(params)].compact.reject { |v| v == '' }.join('&')
|
307
|
+
append_query_params(query_array, params)
|
241
308
|
end
|
242
309
|
elsif !env.key?(:input)
|
243
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/')
|
244
313
|
|
245
314
|
if params.is_a?(Hash)
|
246
|
-
if data = build_multipart(params)
|
315
|
+
if !params.empty? && data = build_multipart(params, false, multipart)
|
247
316
|
env[:input] = data
|
248
317
|
env['CONTENT_LENGTH'] ||= data.length.to_s
|
249
|
-
env['CONTENT_TYPE'] = "
|
318
|
+
env['CONTENT_TYPE'] = "#{multipart_content_type(env)}; boundary=#{MULTIPART_BOUNDARY}"
|
250
319
|
else
|
251
|
-
|
252
|
-
# Rack::ContentLength will determine it automatically.
|
253
|
-
env[:input] = params_to_string(params)
|
320
|
+
env[:input] = build_nested_query(params)
|
254
321
|
end
|
255
322
|
else
|
256
323
|
env[:input] = params
|
257
324
|
end
|
258
325
|
end
|
259
326
|
|
327
|
+
if query_params = env.delete(:query_params)
|
328
|
+
append_query_params(query_array, query_params)
|
329
|
+
end
|
330
|
+
query_array.compact!
|
331
|
+
query_array.reject!(&:empty?)
|
332
|
+
uri.query = query_array.join('&')
|
333
|
+
|
260
334
|
set_cookie(env.delete(:cookie), uri) if env.key?(:cookie)
|
261
335
|
|
262
336
|
Rack::MockRequest.env_for(uri.to_s, env)
|
263
337
|
end
|
264
338
|
|
265
|
-
|
266
|
-
|
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
|
267
344
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
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
|
273
350
|
else
|
274
|
-
|
275
|
-
|
276
|
-
last_response
|
351
|
+
'multipart/form-data'
|
277
352
|
end
|
278
353
|
end
|
279
354
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
'cnonce' => 'nonsensenonce',
|
287
|
-
'uri' => last_request.fullpath,
|
288
|
-
'method' => last_request.env['REQUEST_METHOD'])
|
289
|
-
|
290
|
-
params['response'] = MockDigestRequest.new(params).response(@digest_password)
|
291
|
-
|
292
|
-
"Digest #{params}"
|
293
|
-
end
|
294
|
-
|
295
|
-
def retry_with_digest_auth?(env)
|
296
|
-
last_response.status == 401 &&
|
297
|
-
digest_auth_configured? &&
|
298
|
-
!env['rack-test.digest_auth_retry']
|
299
|
-
end
|
300
|
-
|
301
|
-
def digest_auth_configured?
|
302
|
-
@digest_username
|
303
|
-
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 = {}
|
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
|
311
361
|
|
312
|
-
@
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
317
367
|
|
318
|
-
|
319
|
-
end
|
368
|
+
yield @last_response if block_given?
|
320
369
|
|
321
|
-
|
322
|
-
case params
|
323
|
-
when Hash then build_nested_query(params)
|
324
|
-
when nil then ''
|
325
|
-
else params
|
326
|
-
end
|
370
|
+
@last_response
|
327
371
|
end
|
328
372
|
end
|
329
373
|
|
374
|
+
# Whether the version of rack in use handles encodings.
|
330
375
|
def self.encoding_aware_strings?
|
331
|
-
|
376
|
+
Rack.release >= '1.6'
|
332
377
|
end
|
333
378
|
end
|
379
|
+
|
380
|
+
# For backwards compatibility with 1.1.0 and below
|
381
|
+
MockSession = Test::Session
|
334
382
|
end
|