rack-test 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +18 -1
- data/Rakefile +6 -2
- data/lib/rack/test.rb +109 -41
- data/lib/rack/test/methods.rb +11 -22
- data/lib/rack/test/uploaded_file.rb +36 -0
- data/lib/rack/test/utils.rb +53 -5
- metadata +3 -2
data/History.txt
CHANGED
@@ -1,5 +1,22 @@
|
|
1
|
+
== 0.2.0 / 2009-04-26
|
2
|
+
|
3
|
+
Because #last_response is now a MockResponse instead of a Rack::Response,
|
4
|
+
#last_response.body now returns a string instead of an array.
|
5
|
+
|
6
|
+
* Major enhancements
|
7
|
+
|
8
|
+
* Support multipart requests via the UploadedFile class (thanks, Rails)
|
9
|
+
|
10
|
+
* Minor enhancements
|
11
|
+
|
12
|
+
* Updated for Rack 1.0
|
13
|
+
* Don't require rubygems (See http://gist.github.com/54177)
|
14
|
+
* Support HTTP Digest authentication with the #digest_authorize method
|
15
|
+
* #last_response returns a MockResponse instead of a Response
|
16
|
+
(Michael Fellinger)
|
17
|
+
|
1
18
|
== 0.1.0 / 2009-03-02
|
2
19
|
|
3
20
|
* 1 major enhancement
|
4
21
|
|
5
|
-
* Birthday!
|
22
|
+
* Birthday!
|
data/Rakefile
CHANGED
@@ -30,7 +30,7 @@ spec = Gem::Specification.new do |s|
|
|
30
30
|
s.summary = "Simple testing API built on Rack"
|
31
31
|
s.description = s.summary
|
32
32
|
s.files = %w[History.txt Rakefile README.rdoc] + Dir["lib/**/*"]
|
33
|
-
|
33
|
+
|
34
34
|
# rdoc
|
35
35
|
s.has_rdoc = true
|
36
36
|
s.extra_rdoc_files = %w(README.rdoc MIT-LICENSE.txt)
|
@@ -53,6 +53,10 @@ end
|
|
53
53
|
desc 'Install the package as a gem.'
|
54
54
|
task :install => [:clean, :package] do
|
55
55
|
gem = Dir['pkg/*.gem'].first
|
56
|
-
sh "sudo gem install --local #{gem}"
|
56
|
+
sh "sudo gem install --no-rdoc --no-ri --local #{gem}"
|
57
57
|
end
|
58
58
|
|
59
|
+
desc 'Removes trailing whitespace'
|
60
|
+
task :whitespace do
|
61
|
+
sh %{find . -name '*.rb' -exec sed -i '' 's/ *$//g' {} \\;}
|
62
|
+
end
|
data/lib/rack/test.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require "rubygems"
|
2
|
-
|
3
1
|
unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__) + "/.."))
|
4
2
|
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/.."))
|
5
3
|
end
|
@@ -9,16 +7,40 @@ require "rack"
|
|
9
7
|
require "rack/test/cookie_jar"
|
10
8
|
require "rack/test/utils"
|
11
9
|
require "rack/test/methods"
|
10
|
+
require "rack/test/uploaded_file"
|
12
11
|
|
13
12
|
module Rack
|
14
13
|
module Test
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
|
15
|
+
class MockDigestRequest
|
16
|
+
def initialize(params)
|
17
|
+
@params = params
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(sym)
|
21
|
+
if @params.has_key? k = sym.to_s
|
22
|
+
return @params[k]
|
23
|
+
end
|
24
|
+
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def method
|
29
|
+
@params['method']
|
30
|
+
end
|
31
|
+
|
32
|
+
def response(password)
|
33
|
+
Rack::Auth::Digest::MD5.new(nil).send :digest, self, password
|
34
|
+
end
|
20
35
|
end
|
21
|
-
|
36
|
+
|
37
|
+
VERSION = "0.2.0"
|
38
|
+
|
39
|
+
MULTIPART_BOUNDARY = "----------XnJLe9ZIbbGUYtzPQJ16u1"
|
40
|
+
|
41
|
+
# The common base class for exceptions raised by Rack::Test
|
42
|
+
class Error < StandardError; end
|
43
|
+
|
22
44
|
class Session
|
23
45
|
include Rack::Test::Utils
|
24
46
|
|
@@ -27,50 +49,50 @@ module Rack
|
|
27
49
|
raise ArgumentError.new("app must respond_to?(:call)") unless app.respond_to?(:call)
|
28
50
|
|
29
51
|
@headers = {}
|
30
|
-
@app
|
52
|
+
@app = app
|
31
53
|
end
|
32
|
-
|
54
|
+
|
33
55
|
# Issue a GET request for the given URI with the given params and Rack
|
34
56
|
# environment. Stores the issues request object in #last_request and
|
35
57
|
# the app's response in #last_response. Yield #last_response to a block
|
36
58
|
# if given.
|
37
|
-
#
|
59
|
+
#
|
38
60
|
# Example:
|
39
61
|
# get "/"
|
40
62
|
def get(uri, params = {}, env = {}, &block)
|
41
63
|
env = env_for(uri, env.merge(:method => "GET", :params => params))
|
42
64
|
process_request(uri, env, &block)
|
43
65
|
end
|
44
|
-
|
66
|
+
|
45
67
|
# Issue a POST request for the given URI. See #get
|
46
|
-
#
|
68
|
+
#
|
47
69
|
# Example:
|
48
70
|
# post "/signup", "name" => "Bryan"
|
49
71
|
def post(uri, params = {}, env = {}, &block)
|
50
72
|
env = env_for(uri, env.merge(:method => "POST", :params => params))
|
51
73
|
process_request(uri, env, &block)
|
52
74
|
end
|
53
|
-
|
75
|
+
|
54
76
|
# Issue a PUT request for the given URI. See #get
|
55
|
-
#
|
77
|
+
#
|
56
78
|
# Example:
|
57
79
|
# put "/"
|
58
80
|
def put(uri, params = {}, env = {}, &block)
|
59
81
|
env = env_for(uri, env.merge(:method => "PUT", :params => params))
|
60
82
|
process_request(uri, env, &block)
|
61
83
|
end
|
62
|
-
|
84
|
+
|
63
85
|
# Issue a DELETE request for the given URI. See #get
|
64
|
-
#
|
86
|
+
#
|
65
87
|
# Example:
|
66
88
|
# delete "/"
|
67
89
|
def delete(uri, params = {}, env = {}, &block)
|
68
90
|
env = env_for(uri, env.merge(:method => "DELETE", :params => params))
|
69
91
|
process_request(uri, env, &block)
|
70
92
|
end
|
71
|
-
|
93
|
+
|
72
94
|
# Issue a HEAD request for the given URI. See #get
|
73
|
-
#
|
95
|
+
#
|
74
96
|
# Example:
|
75
97
|
# head "/"
|
76
98
|
def head(uri, params = {}, env = {}, &block)
|
@@ -82,7 +104,7 @@ module Rack
|
|
82
104
|
# environment. Stores the issues request object in #last_request and
|
83
105
|
# the app's response in #last_response. Yield #last_response to a block
|
84
106
|
# if given.
|
85
|
-
#
|
107
|
+
#
|
86
108
|
# Example:
|
87
109
|
# request "/"
|
88
110
|
def request(uri, env = {}, &block)
|
@@ -92,7 +114,7 @@ module Rack
|
|
92
114
|
|
93
115
|
# Set a header to be included on all subsequent requests through the
|
94
116
|
# session. Use a value of nil to remove a previously configured header.
|
95
|
-
#
|
117
|
+
#
|
96
118
|
# Example:
|
97
119
|
# header "User-Agent", "Firefox"
|
98
120
|
def header(name, value)
|
@@ -102,17 +124,22 @@ module Rack
|
|
102
124
|
@headers[name] = value
|
103
125
|
end
|
104
126
|
end
|
105
|
-
|
127
|
+
|
106
128
|
# Set the username and password for HTTP Basic authorization, to be
|
107
129
|
# included in subsequent requests in the HTTP_AUTHORIZATION header.
|
108
|
-
#
|
130
|
+
#
|
109
131
|
# Example:
|
110
132
|
# authorize "bryan", "secret"
|
111
133
|
def authorize(username, password)
|
112
134
|
encoded_login = ["#{username}:#{password}"].pack("m*")
|
113
135
|
header('HTTP_AUTHORIZATION', "Basic #{encoded_login}")
|
114
136
|
end
|
115
|
-
|
137
|
+
|
138
|
+
def digest_authorize(username, password)
|
139
|
+
@digest_username = username
|
140
|
+
@digest_password = password
|
141
|
+
end
|
142
|
+
|
116
143
|
# Rack::Test will not follow any redirects automatically. This method
|
117
144
|
# will follow the redirect returned in the last response. If the last
|
118
145
|
# response was not a redirect, an error will be raised.
|
@@ -120,7 +147,7 @@ module Rack
|
|
120
147
|
unless last_response.redirect?
|
121
148
|
raise Error.new("Last response was not a redirect. Cannot follow_redirect!")
|
122
149
|
end
|
123
|
-
|
150
|
+
|
124
151
|
get(last_response["Location"])
|
125
152
|
end
|
126
153
|
|
@@ -142,28 +169,34 @@ module Rack
|
|
142
169
|
|
143
170
|
private
|
144
171
|
|
145
|
-
|
146
172
|
def env_for(path, env)
|
147
173
|
uri = URI.parse(path)
|
148
174
|
uri.host ||= "example.org"
|
149
175
|
|
150
176
|
env = default_env.merge(env)
|
151
177
|
|
152
|
-
if URI::HTTPS === uri
|
153
|
-
|
154
|
-
end
|
178
|
+
env.update("HTTPS" => "on") if URI::HTTPS === uri
|
179
|
+
env["X-Requested-With"] = "XMLHttpRequest" if env[:xhr]
|
155
180
|
|
156
|
-
if env[:xhr]
|
157
|
-
env["X-Requested-With"] = "XMLHttpRequest"
|
158
|
-
end
|
159
|
-
|
160
181
|
if (env[:method] == "POST" || env["REQUEST_METHOD"] == "POST") && !env.has_key?(:input)
|
161
182
|
env["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
|
162
|
-
|
183
|
+
|
184
|
+
multipart = (env[:params] || {}).any? do |k, v|
|
185
|
+
UploadedFile === v
|
186
|
+
end
|
187
|
+
|
188
|
+
if multipart
|
189
|
+
env[:input] = multipart_body(env.delete(:params))
|
190
|
+
env["CONTENT_LENGTH"] ||= env[:input].length.to_s
|
191
|
+
env["CONTENT_TYPE"] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
|
192
|
+
else
|
193
|
+
env[:input] = params_to_string(env.delete(:params))
|
194
|
+
end
|
163
195
|
end
|
164
196
|
|
165
197
|
params = env[:params] || {}
|
166
198
|
params.update(parse_query(uri.query))
|
199
|
+
|
167
200
|
uri.query = requestify(params)
|
168
201
|
|
169
202
|
if env.has_key?(:cookie)
|
@@ -175,7 +208,7 @@ module Rack
|
|
175
208
|
|
176
209
|
Rack::MockRequest.env_for(uri.to_s, env)
|
177
210
|
end
|
178
|
-
|
211
|
+
|
179
212
|
def cookie_jar
|
180
213
|
@cookie_jar || Rack::Test::CookieJar.new
|
181
214
|
end
|
@@ -187,12 +220,47 @@ module Rack
|
|
187
220
|
@last_request = Rack::Request.new(env)
|
188
221
|
|
189
222
|
status, headers, body = @app.call(@last_request.env)
|
190
|
-
@last_response =
|
223
|
+
@last_response = MockResponse.new(status, headers, body, env["rack.errors"])
|
224
|
+
|
191
225
|
@cookie_jar = cookie_jar.merge(uri, last_response.headers["Set-Cookie"])
|
192
226
|
|
193
|
-
|
194
|
-
|
195
|
-
|
227
|
+
if retry_with_digest_auth?(env)
|
228
|
+
auth_env = env.merge("HTTP_AUTHORIZATION" => digest_auth_header,
|
229
|
+
"rack-test.digest_auth_retry" => true)
|
230
|
+
auth_env.delete('rack.request')
|
231
|
+
process_request(uri.path, auth_env)
|
232
|
+
else
|
233
|
+
yield @last_response if block_given?
|
234
|
+
|
235
|
+
@last_response
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def digest_auth_header
|
240
|
+
challenge = @last_response["WWW-Authenticate"].split(" ", 2).last
|
241
|
+
params = Rack::Auth::Digest::Params.parse(challenge)
|
242
|
+
|
243
|
+
params.merge!({
|
244
|
+
"username" => @digest_username,
|
245
|
+
"nc" => "00000001",
|
246
|
+
"cnonce" => "nonsensenonce",
|
247
|
+
"uri" => @last_request.path_info,
|
248
|
+
"method" => @last_request.env["REQUEST_METHOD"],
|
249
|
+
})
|
250
|
+
|
251
|
+
params["response"] = MockDigestRequest.new(params).response(@digest_password)
|
252
|
+
|
253
|
+
return "Digest #{params}"
|
254
|
+
end
|
255
|
+
|
256
|
+
def retry_with_digest_auth?(env)
|
257
|
+
@last_response.status == 401 &&
|
258
|
+
digest_auth_configured? &&
|
259
|
+
!env["rack-test.digest_auth_retry"]
|
260
|
+
end
|
261
|
+
|
262
|
+
def digest_auth_configured?
|
263
|
+
@digest_username
|
196
264
|
end
|
197
265
|
|
198
266
|
def default_env
|
@@ -206,8 +274,8 @@ module Rack
|
|
206
274
|
else params
|
207
275
|
end
|
208
276
|
end
|
209
|
-
|
277
|
+
|
210
278
|
end
|
211
|
-
|
279
|
+
|
212
280
|
end
|
213
281
|
end
|
data/lib/rack/test/methods.rb
CHANGED
@@ -1,45 +1,34 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
module Test
|
3
|
-
|
4
5
|
module Methods
|
5
|
-
|
6
|
-
|
7
|
-
meths.each do |meth|
|
8
|
-
self.class_eval <<-RUBY
|
9
|
-
def #{meth}(*args, &blk)
|
10
|
-
rack_test_session.#{meth}(*args, &blk)
|
11
|
-
end
|
12
|
-
RUBY
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
6
|
+
extend Forwardable
|
7
|
+
|
16
8
|
def rack_test_session
|
17
9
|
@_rack_test_session ||= Rack::Test::Session.new(app)
|
18
10
|
end
|
19
|
-
|
20
|
-
|
11
|
+
|
12
|
+
METHODS = [
|
21
13
|
:request,
|
22
|
-
|
23
14
|
# HTTP verbs
|
24
15
|
:get,
|
25
16
|
:post,
|
26
17
|
:put,
|
27
18
|
:delete,
|
28
19
|
:head,
|
29
|
-
|
30
20
|
# Redirects
|
31
21
|
:follow_redirect!,
|
32
|
-
|
33
22
|
# Header-related features
|
34
23
|
:header,
|
35
24
|
:authorize,
|
36
|
-
|
25
|
+
:digest_authorize,
|
37
26
|
# Expose the last request and response
|
38
27
|
:last_response,
|
39
28
|
:last_request
|
40
|
-
|
41
|
-
|
29
|
+
]
|
30
|
+
|
31
|
+
def_delegators :rack_test_session, *METHODS
|
42
32
|
end
|
43
|
-
|
44
33
|
end
|
45
|
-
end
|
34
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Test
|
5
|
+
|
6
|
+
class UploadedFile
|
7
|
+
# The filename, *not* including the path, of the "uploaded" file
|
8
|
+
attr_reader :original_filename
|
9
|
+
|
10
|
+
# The content type of the "uploaded" file
|
11
|
+
attr_accessor :content_type
|
12
|
+
|
13
|
+
def initialize(path, content_type = "text/plain", binary = false)
|
14
|
+
raise "#{path} file does not exist" unless ::File.exist?(path)
|
15
|
+
@content_type = content_type
|
16
|
+
@original_filename = ::File.basename(path)
|
17
|
+
@tempfile = Tempfile.new(@original_filename)
|
18
|
+
@tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
|
19
|
+
@tempfile.binmode if binary
|
20
|
+
FileUtils.copy_file(path, @tempfile.path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def path
|
24
|
+
@tempfile.path
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method :local_path, :path
|
28
|
+
|
29
|
+
def method_missing(method_name, *args, &block) #:nodoc:
|
30
|
+
@tempfile.__send__(method_name, *args, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/lib/rack/test/utils.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Rack
|
2
2
|
module Test
|
3
|
-
|
3
|
+
|
4
4
|
module Utils
|
5
5
|
include Rack::Utils
|
6
|
-
|
6
|
+
|
7
7
|
def requestify(value, prefix = nil)
|
8
8
|
case value
|
9
9
|
when Array
|
@@ -18,10 +18,58 @@ module Rack
|
|
18
18
|
"#{prefix}=#{escape(value)}"
|
19
19
|
end
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
module_function :requestify
|
23
|
-
|
23
|
+
|
24
|
+
def multipart_requestify(params, first=true)
|
25
|
+
p = Hash.new
|
26
|
+
|
27
|
+
params.each do |key, value|
|
28
|
+
k = first ? key.to_s : "[#{key}]"
|
29
|
+
|
30
|
+
if Hash === value
|
31
|
+
multipart_requestify(value, false).each do |subkey, subvalue|
|
32
|
+
p[k + subkey] = subvalue
|
33
|
+
end
|
34
|
+
else
|
35
|
+
p[k] = value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
return p
|
40
|
+
end
|
41
|
+
|
42
|
+
module_function :multipart_requestify
|
43
|
+
|
44
|
+
def multipart_body(params)
|
45
|
+
multipart_requestify(params).map do |key, value|
|
46
|
+
if value.respond_to?(:original_filename)
|
47
|
+
::File.open(value.path, "rb") do |f|
|
48
|
+
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
|
49
|
+
|
50
|
+
<<-EOF
|
51
|
+
--#{MULTIPART_BOUNDARY}\r
|
52
|
+
Content-Disposition: form-data; name="#{key}"; filename="#{escape(value.original_filename)}"\r
|
53
|
+
Content-Type: #{value.content_type}\r
|
54
|
+
Content-Length: #{::File.stat(value.path).size}\r
|
55
|
+
\r
|
56
|
+
#{f.read}\r
|
57
|
+
EOF
|
58
|
+
end
|
59
|
+
else
|
60
|
+
<<-EOF
|
61
|
+
--#{MULTIPART_BOUNDARY}\r
|
62
|
+
Content-Disposition: form-data; name="#{key}"\r
|
63
|
+
\r
|
64
|
+
#{value}\r
|
65
|
+
EOF
|
66
|
+
end
|
67
|
+
end.join("")+"--#{MULTIPART_BOUNDARY}--\r"
|
68
|
+
end
|
69
|
+
|
70
|
+
module_function :multipart_body
|
71
|
+
|
24
72
|
end
|
25
|
-
|
73
|
+
|
26
74
|
end
|
27
75
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-test
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bryan Helmkamp
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-04-26 00:00:00 -04:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -30,6 +30,7 @@ files:
|
|
30
30
|
- lib/rack/test
|
31
31
|
- lib/rack/test/cookie_jar.rb
|
32
32
|
- lib/rack/test/methods.rb
|
33
|
+
- lib/rack/test/uploaded_file.rb
|
33
34
|
- lib/rack/test/utils.rb
|
34
35
|
- lib/rack/test.rb
|
35
36
|
- MIT-LICENSE.txt
|