rack-test 1.1.0 → 2.0.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 +82 -1
- data/MIT-LICENSE.txt +1 -0
- data/README.md +52 -63
- data/lib/rack/mock_session.rb +2 -63
- data/lib/rack/test/cookie_jar.rb +101 -49
- data/lib/rack/test/methods.rb +57 -45
- data/lib/rack/test/mock_digest_request.rb +11 -1
- data/lib/rack/test/uploaded_file.rb +35 -11
- data/lib/rack/test/utils.rb +75 -61
- data/lib/rack/test/version.rb +1 -1
- data/lib/rack/test.rb +223 -143
- metadata +27 -96
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,100 @@ 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
|
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::
|
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
|
-
|
36
|
-
|
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
|
-
@
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
#
|
62
|
-
|
63
|
-
|
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
|
-
#
|
70
|
-
|
71
|
-
|
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
|
-
#
|
78
|
-
|
79
|
-
|
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
|
-
#
|
86
|
-
#
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
#
|
102
|
-
#
|
103
|
-
|
104
|
-
|
105
|
-
|
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.
|
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
|
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
|
-
#
|
139
|
-
# header "User-Agent", "Firefox"
|
174
|
+
# header "user-agent", "Firefox"
|
140
175
|
def header(name, value)
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
149
|
-
# session. Use a value of nil to remove a previously
|
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,20 +227,24 @@ 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
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
197
238
|
|
198
239
|
# Compute the next location by appending the location header with the
|
199
240
|
# last request, as per https://tools.ietf.org/html/rfc7231#section-7.1.2
|
200
241
|
# Adding two absolute locations returns the right-hand location
|
201
242
|
next_location = URI.parse(last_request.url) + URI.parse(last_response['Location'])
|
202
243
|
|
203
|
-
|
204
|
-
request_method,
|
244
|
+
custom_request(
|
245
|
+
request_method,
|
246
|
+
next_location.to_s,
|
247
|
+
params,
|
205
248
|
'HTTP_REFERER' => last_request.url,
|
206
249
|
'rack.session' => last_request.session,
|
207
250
|
'rack.session.options' => last_request.session_options
|
@@ -210,66 +253,121 @@ module Rack
|
|
210
253
|
|
211
254
|
private
|
212
255
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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)
|
218
265
|
end
|
219
266
|
end
|
220
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.
|
221
291
|
def env_for(uri, env)
|
222
|
-
env =
|
292
|
+
env = DEFAULT_ENV.merge(@env).merge!(env)
|
223
293
|
|
224
294
|
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
|
295
|
+
env['HTTPS'] = 'on' if URI::HTTPS === uri
|
227
296
|
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
297
|
env['REQUEST_METHOD'] ||= env[:method] ? env[:method].to_s.upcase : 'GET'
|
232
298
|
|
233
|
-
params = env.delete(:params)
|
299
|
+
params = env.delete(:params)
|
300
|
+
query_array = [uri.query]
|
234
301
|
|
235
302
|
if env['REQUEST_METHOD'] == 'GET'
|
236
|
-
#
|
303
|
+
# Treat params as query params
|
237
304
|
if params
|
238
|
-
|
239
|
-
|
240
|
-
uri.query = [uri.query, build_nested_query(params)].compact.reject { |v| v == '' }.join('&')
|
305
|
+
append_query_params(query_array, params)
|
241
306
|
end
|
242
307
|
elsif !env.key?(:input)
|
243
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/')
|
244
311
|
|
245
312
|
if params.is_a?(Hash)
|
246
|
-
if data = build_multipart(params)
|
313
|
+
if data = build_multipart(params, false, multipart)
|
247
314
|
env[:input] = data
|
248
315
|
env['CONTENT_LENGTH'] ||= data.length.to_s
|
249
|
-
env['CONTENT_TYPE'] = "
|
316
|
+
env['CONTENT_TYPE'] = "#{multipart_content_type(env)}; boundary=#{MULTIPART_BOUNDARY}"
|
250
317
|
else
|
251
|
-
|
252
|
-
# Rack::ContentLength will determine it automatically.
|
253
|
-
env[:input] = params_to_string(params)
|
318
|
+
env[:input] = build_nested_query(params)
|
254
319
|
end
|
255
320
|
else
|
256
321
|
env[:input] = params
|
257
322
|
end
|
258
323
|
end
|
259
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
|
+
|
260
332
|
set_cookie(env.delete(:cookie), uri) if env.key?(:cookie)
|
261
333
|
|
262
334
|
Rack::MockRequest.env_for(uri.to_s, env)
|
263
335
|
end
|
264
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.
|
265
355
|
def process_request(uri, env)
|
266
|
-
|
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
|
267
365
|
|
268
366
|
if retry_with_digest_auth?(env)
|
269
367
|
auth_env = env.merge('HTTP_AUTHORIZATION' => digest_auth_header,
|
270
368
|
'rack-test.digest_auth_retry' => true)
|
271
369
|
auth_env.delete('rack.request')
|
272
|
-
process_request(uri
|
370
|
+
process_request(uri, auth_env)
|
273
371
|
else
|
274
372
|
yield last_response if block_given?
|
275
373
|
|
@@ -278,6 +376,8 @@ module Rack
|
|
278
376
|
end
|
279
377
|
|
280
378
|
def digest_auth_header
|
379
|
+
require_relative 'test/mock_digest_request'
|
380
|
+
|
281
381
|
challenge = last_response['WWW-Authenticate'].split(' ', 2).last
|
282
382
|
params = Rack::Auth::Digest::Params.parse(challenge)
|
283
383
|
|
@@ -287,7 +387,7 @@ module Rack
|
|
287
387
|
'uri' => last_request.fullpath,
|
288
388
|
'method' => last_request.env['REQUEST_METHOD'])
|
289
389
|
|
290
|
-
params['response'] =
|
390
|
+
params['response'] = MockDigestRequest_.new(params).response(@digest_password)
|
291
391
|
|
292
392
|
"Digest #{params}"
|
293
393
|
end
|
@@ -301,34 +401,14 @@ module Rack
|
|
301
401
|
def digest_auth_configured?
|
302
402
|
@digest_username
|
303
403
|
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 = {}
|
311
|
-
|
312
|
-
@headers.each do |name, value|
|
313
|
-
env_key = name.upcase.tr('-', '_')
|
314
|
-
env_key = 'HTTP_' + env_key unless env_key == 'CONTENT_TYPE'
|
315
|
-
converted_headers[env_key] = value
|
316
|
-
end
|
317
|
-
|
318
|
-
converted_headers
|
319
|
-
end
|
320
|
-
|
321
|
-
def params_to_string(params)
|
322
|
-
case params
|
323
|
-
when Hash then build_nested_query(params)
|
324
|
-
when nil then ''
|
325
|
-
else params
|
326
|
-
end
|
327
|
-
end
|
328
404
|
end
|
329
405
|
|
406
|
+
# Whether the version of rack in use handles encodings.
|
330
407
|
def self.encoding_aware_strings?
|
331
|
-
|
408
|
+
Rack.release >= '1.6'
|
332
409
|
end
|
333
410
|
end
|
411
|
+
|
412
|
+
# For backwards compatibility with 1.1.0 and below
|
413
|
+
MockSession = Test::Session
|
334
414
|
end
|