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