rack-test 1.1.0 → 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.
- 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
|