joebadmo-rack-test 0.6.1

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.
@@ -0,0 +1,66 @@
1
+ module Rack
2
+
3
+ class MockSession # :nodoc:
4
+ attr_writer :cookie_jar
5
+ attr_reader :default_host
6
+
7
+ def initialize(app, default_host = Rack::Test::DEFAULT_HOST)
8
+ @app = app
9
+ @after_request = []
10
+ @default_host = default_host
11
+ @last_request = nil
12
+ @last_response = nil
13
+ end
14
+
15
+ def after_request(&block)
16
+ @after_request << block
17
+ end
18
+
19
+ def clear_cookies
20
+ @cookie_jar = Rack::Test::CookieJar.new([], @default_host)
21
+ end
22
+
23
+ def set_cookie(cookie, uri = nil)
24
+ cookie_jar.merge(cookie, uri)
25
+ end
26
+
27
+ def request(uri, env)
28
+ env["HTTP_COOKIE"] ||= cookie_jar.for(uri)
29
+ @last_request = Rack::Request.new(env)
30
+ status, headers, body = @app.call(@last_request.env)
31
+
32
+ @last_response = MockResponse.new(status, headers, body, env["rack.errors"].flush)
33
+ body.close if body.respond_to?(:close)
34
+
35
+ cookie_jar.merge(last_response.headers["Set-Cookie"], uri)
36
+
37
+ @after_request.each { |hook| hook.call }
38
+
39
+ if @last_response.respond_to?(:finish)
40
+ @last_response.finish
41
+ else
42
+ @last_response
43
+ end
44
+ end
45
+
46
+ # Return the last request issued in the session. Raises an error if no
47
+ # requests have been sent yet.
48
+ def last_request
49
+ raise Rack::Test::Error.new("No request yet. Request a page first.") unless @last_request
50
+ @last_request
51
+ end
52
+
53
+ # Return the last response received in the session. Raises an error if
54
+ # no requests have been sent yet.
55
+ def last_response
56
+ raise Rack::Test::Error.new("No response yet. Request a page first.") unless @last_response
57
+ @last_response
58
+ end
59
+
60
+ def cookie_jar
61
+ @cookie_jar ||= Rack::Test::CookieJar.new([], @default_host)
62
+ end
63
+
64
+ end
65
+
66
+ end
data/lib/rack/test.rb ADDED
@@ -0,0 +1,302 @@
1
+ require "uri"
2
+ require "rack"
3
+ require "rack/mock_session"
4
+ require "rack/test/cookie_jar"
5
+ require "rack/test/mock_digest_request"
6
+ require "rack/test/utils"
7
+ require "rack/test/methods"
8
+ require "rack/test/uploaded_file"
9
+
10
+ module Rack
11
+ module Test
12
+ VERSION = "0.6.1"
13
+
14
+ DEFAULT_HOST = "example.org"
15
+ MULTIPART_BOUNDARY = "----------XnJLe9ZIbbGUYtzPQJ16u1"
16
+
17
+ # The common base class for exceptions raised by Rack::Test
18
+ class Error < StandardError; end
19
+
20
+ # This class represents a series of requests issued to a Rack app, sharing
21
+ # a single cookie jar
22
+ #
23
+ # Rack::Test::Session's methods are most often called through Rack::Test::Methods,
24
+ # which will automatically build a session when it's first used.
25
+ class Session
26
+ extend Forwardable
27
+ include Rack::Test::Utils
28
+
29
+ def_delegators :@rack_mock_session, :clear_cookies, :set_cookie, :last_response, :last_request
30
+
31
+ # Creates a Rack::Test::Session for a given Rack app or Rack::MockSession.
32
+ #
33
+ # Note: Generally, you won't need to initialize a Rack::Test::Session directly.
34
+ # Instead, you should include Rack::Test::Methods into your testing context.
35
+ # (See README.rdoc for an example)
36
+ def initialize(mock_session)
37
+ @headers = {}
38
+
39
+ if mock_session.is_a?(MockSession)
40
+ @rack_mock_session = mock_session
41
+ else
42
+ @rack_mock_session = MockSession.new(mock_session)
43
+ end
44
+
45
+ @default_host = @rack_mock_session.default_host
46
+ end
47
+
48
+ # Issue a GET request for the given URI with the given params and Rack
49
+ # environment. Stores the issues request object in #last_request and
50
+ # the app's response in #last_response. Yield #last_response to a block
51
+ # if given.
52
+ #
53
+ # Example:
54
+ # get "/"
55
+ def get(uri, params = {}, env = {}, &block)
56
+ env = env_for(uri, env.merge(:method => "GET", :params => params))
57
+ process_request(uri, env, &block)
58
+ end
59
+
60
+ # Issue a POST request for the given URI. See #get
61
+ #
62
+ # Example:
63
+ # post "/signup", "name" => "Bryan"
64
+ def post(uri, params = {}, env = {}, &block)
65
+ env = env_for(uri, env.merge(:method => "POST", :params => params))
66
+ process_request(uri, env, &block)
67
+ end
68
+
69
+ # Issue a PUT request for the given URI. See #get
70
+ #
71
+ # Example:
72
+ # put "/"
73
+ def put(uri, params = {}, env = {}, &block)
74
+ env = env_for(uri, env.merge(:method => "PUT", :params => params))
75
+ process_request(uri, env, &block)
76
+ end
77
+
78
+ # Issue a PATCH request for the given URI. See #get
79
+ #
80
+ # Example:
81
+ # patch "/"
82
+ def patch(uri, params = {}, env = {}, &block)
83
+ env = env_for(uri, env.merge(:method => "PATCH", :params => params))
84
+ process_request(uri, env, &block)
85
+ end
86
+
87
+ # Issue a DELETE request for the given URI. See #get
88
+ #
89
+ # Example:
90
+ # delete "/"
91
+ def delete(uri, params = {}, env = {}, &block)
92
+ env = env_for(uri, env.merge(:method => "DELETE", :params => params))
93
+ process_request(uri, env, &block)
94
+ end
95
+
96
+ # Issue an OPTIONS request for the given URI. See #get
97
+ #
98
+ # Example:
99
+ # options "/"
100
+ def options(uri, params = {}, env = {}, &block)
101
+ env = env_for(uri, env.merge(:method => "OPTIONS", :params => params))
102
+ process_request(uri, env, &block)
103
+ end
104
+
105
+ # Issue a HEAD request for the given URI. See #get
106
+ #
107
+ # Example:
108
+ # head "/"
109
+ def head(uri, params = {}, env = {}, &block)
110
+ env = env_for(uri, env.merge(:method => "HEAD", :params => params))
111
+ process_request(uri, env, &block)
112
+ end
113
+
114
+ # Issue a request to the Rack app for the given URI and optional Rack
115
+ # environment. Stores the issues request object in #last_request and
116
+ # the app's response in #last_response. Yield #last_response to a block
117
+ # if given.
118
+ #
119
+ # Example:
120
+ # request "/"
121
+ def request(uri, env = {}, &block)
122
+ env = env_for(uri, env)
123
+ process_request(uri, env, &block)
124
+ end
125
+
126
+ # Set a header to be included on all subsequent requests through the
127
+ # session. Use a value of nil to remove a previously configured header.
128
+ #
129
+ # In accordance with the Rack spec, headers will be included in the Rack
130
+ # environment hash in HTTP_USER_AGENT form.
131
+ #
132
+ # Example:
133
+ # header "User-Agent", "Firefox"
134
+ def header(name, value)
135
+ if value.nil?
136
+ @headers.delete(name)
137
+ else
138
+ @headers[name] = value
139
+ end
140
+ end
141
+
142
+ # Set the username and password for HTTP Basic authorization, to be
143
+ # included in subsequent requests in the HTTP_AUTHORIZATION header.
144
+ #
145
+ # Example:
146
+ # basic_authorize "bryan", "secret"
147
+ def basic_authorize(username, password)
148
+ encoded_login = ["#{username}:#{password}"].pack("m*")
149
+ header('Authorization', "Basic #{encoded_login}")
150
+ end
151
+
152
+ alias_method :authorize, :basic_authorize
153
+
154
+ # Set the username and password for HTTP Digest authorization, to be
155
+ # included in subsequent requests in the HTTP_AUTHORIZATION header.
156
+ #
157
+ # Example:
158
+ # digest_authorize "bryan", "secret"
159
+ def digest_authorize(username, password)
160
+ @digest_username = username
161
+ @digest_password = password
162
+ end
163
+
164
+ # Rack::Test will not follow any redirects automatically. This method
165
+ # will follow the redirect returned (including setting the Referer header
166
+ # on the new request) in the last response. If the last response was not
167
+ # a redirect, an error will be raised.
168
+ def follow_redirect!
169
+ unless last_response.redirect?
170
+ raise Error.new("Last response was not a redirect. Cannot follow_redirect!")
171
+ end
172
+
173
+ get(last_response["Location"], {}, { "HTTP_REFERER" => last_request.url })
174
+ end
175
+
176
+ private
177
+
178
+ def env_for(path, env)
179
+ uri = URI.parse(path)
180
+ uri.path = "/#{uri.path}" unless uri.path[0] == ?/
181
+ uri.host ||= @default_host
182
+
183
+ env = default_env.merge(env)
184
+
185
+ env["HTTP_HOST"] ||= [uri.host, uri.port].compact.join(":")
186
+
187
+ env.update("HTTPS" => "on") if URI::HTTPS === uri
188
+ env["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" if env[:xhr]
189
+
190
+ # TODO: Remove this after Rack 1.1 has been released.
191
+ # Stringifying and upcasing methods has be commit upstream
192
+ env["REQUEST_METHOD"] ||= env[:method] ? env[:method].to_s.upcase : "GET"
193
+
194
+ if env["REQUEST_METHOD"] == "GET"
195
+ params = env[:params] || {}
196
+ params = parse_nested_query(params) if params.is_a?(String)
197
+ params.update(parse_nested_query(uri.query))
198
+ uri.query = build_nested_query(params)
199
+ elsif !env.has_key?(:input)
200
+ env["CONTENT_TYPE"] ||= "application/x-www-form-urlencoded"
201
+
202
+ if env[:params].is_a?(Hash)
203
+ if data = build_multipart(env[:params])
204
+ env[:input] = data
205
+ env["CONTENT_LENGTH"] ||= data.length.to_s
206
+ env["CONTENT_TYPE"] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
207
+ else
208
+ env[:input] = params_to_string(env[:params])
209
+ end
210
+ else
211
+ env[:input] = env[:params]
212
+ end
213
+ end
214
+
215
+ env.delete(:params)
216
+
217
+ if env.has_key?(:cookie)
218
+ set_cookie(env.delete(:cookie), uri)
219
+ end
220
+
221
+ Rack::MockRequest.env_for(uri.to_s, env)
222
+ end
223
+
224
+ def process_request(uri, env)
225
+ uri = URI.parse(uri)
226
+ uri.host ||= @default_host
227
+
228
+ @rack_mock_session.request(uri, env)
229
+
230
+ if retry_with_digest_auth?(env)
231
+ auth_env = env.merge({
232
+ "HTTP_AUTHORIZATION" => digest_auth_header,
233
+ "rack-test.digest_auth_retry" => true
234
+ })
235
+ auth_env.delete('rack.request')
236
+ process_request(uri.path, auth_env)
237
+ else
238
+ yield last_response if block_given?
239
+
240
+ last_response
241
+ end
242
+ end
243
+
244
+ def digest_auth_header
245
+ challenge = last_response["WWW-Authenticate"].split(" ", 2).last
246
+ params = Rack::Auth::Digest::Params.parse(challenge)
247
+
248
+ params.merge!({
249
+ "username" => @digest_username,
250
+ "nc" => "00000001",
251
+ "cnonce" => "nonsensenonce",
252
+ "uri" => last_request.fullpath,
253
+ "method" => last_request.env["REQUEST_METHOD"],
254
+ })
255
+
256
+ params["response"] = MockDigestRequest.new(params).response(@digest_password)
257
+
258
+ "Digest #{params}"
259
+ end
260
+
261
+ def retry_with_digest_auth?(env)
262
+ last_response.status == 401 &&
263
+ digest_auth_configured? &&
264
+ !env["rack-test.digest_auth_retry"]
265
+ end
266
+
267
+ def digest_auth_configured?
268
+ @digest_username
269
+ end
270
+
271
+ def default_env
272
+ { "rack.test" => true, "REMOTE_ADDR" => "127.0.0.1" }.merge(headers_for_env)
273
+ end
274
+
275
+ def headers_for_env
276
+ converted_headers = {}
277
+
278
+ @headers.each do |name, value|
279
+ env_key = name.upcase.gsub("-", "_")
280
+ env_key = "HTTP_" + env_key unless "CONTENT_TYPE" == env_key
281
+ converted_headers[env_key] = value
282
+ end
283
+
284
+ converted_headers
285
+ end
286
+
287
+ def params_to_string(params)
288
+ case params
289
+ when Hash then build_nested_query(params)
290
+ when nil then ""
291
+ else params
292
+ end
293
+ end
294
+
295
+ end
296
+
297
+ def self.encoding_aware_strings?
298
+ defined?(Encoding) && "".respond_to?(:encode)
299
+ end
300
+
301
+ end
302
+ end
@@ -0,0 +1,176 @@
1
+ require "uri"
2
+ require "time"
3
+
4
+ module Rack
5
+ module Test
6
+
7
+ class Cookie # :nodoc:
8
+ include Rack::Utils
9
+
10
+ # :api: private
11
+ attr_reader :name, :value
12
+
13
+ # :api: private
14
+ def initialize(raw, uri = nil, default_host = DEFAULT_HOST)
15
+ @default_host = default_host
16
+ uri ||= default_uri
17
+
18
+ # separate the name / value pair from the cookie options
19
+ @name_value_raw, options = raw.split(/[;,] */n, 2)
20
+
21
+ @name, @value = parse_query(@name_value_raw, ';').to_a.first
22
+ @options = parse_query(options, ';')
23
+
24
+ @options["domain"] ||= (uri.host || default_host)
25
+ @options["path"] ||= uri.path.sub(/\/[^\/]*\Z/, "")
26
+ end
27
+
28
+ def replaces?(other)
29
+ [name.downcase, domain, path] == [other.name.downcase, other.domain, other.path]
30
+ end
31
+
32
+ # :api: private
33
+ def raw
34
+ @name_value_raw
35
+ end
36
+
37
+ # :api: private
38
+ def empty?
39
+ @value.nil? || @value.empty?
40
+ end
41
+
42
+ # :api: private
43
+ def domain
44
+ @options["domain"]
45
+ end
46
+
47
+ def secure?
48
+ @options.has_key?("secure")
49
+ end
50
+
51
+ # :api: private
52
+ def path
53
+ @options["path"].strip || "/"
54
+ end
55
+
56
+ # :api: private
57
+ def expires
58
+ Time.parse(@options["expires"]) if @options["expires"]
59
+ end
60
+
61
+ # :api: private
62
+ def expired?
63
+ expires && expires < Time.now
64
+ end
65
+
66
+ # :api: private
67
+ def valid?(uri)
68
+ uri ||= default_uri
69
+
70
+ if uri.host.nil?
71
+ uri.host = @default_host
72
+ end
73
+
74
+ real_domain = domain =~ /^\./ ? domain[1..-1] : domain
75
+ (!secure? || (secure? && uri.scheme == "https")) &&
76
+ uri.host =~ Regexp.new("#{Regexp.escape(real_domain)}$", Regexp::IGNORECASE) &&
77
+ uri.path =~ Regexp.new("^#{Regexp.escape(path)}")
78
+ end
79
+
80
+ # :api: private
81
+ def matches?(uri)
82
+ ! expired? && valid?(uri)
83
+ end
84
+
85
+ # :api: private
86
+ def <=>(other)
87
+ # Orders the cookies from least specific to most
88
+ [name, path, domain.reverse] <=> [other.name, other.path, other.domain.reverse]
89
+ end
90
+
91
+ protected
92
+
93
+ def default_uri
94
+ URI.parse("//" + @default_host + "/")
95
+ end
96
+
97
+ end
98
+
99
+ class CookieJar # :nodoc:
100
+
101
+ # :api: private
102
+ def initialize(cookies = [], default_host = DEFAULT_HOST)
103
+ @default_host = default_host
104
+ @cookies = cookies
105
+ @cookies.sort!
106
+ end
107
+
108
+ def [](name)
109
+ cookies = hash_for(nil)
110
+ # TODO: Should be case insensitive
111
+ cookies[name] && cookies[name].value
112
+ end
113
+
114
+ def []=(name, value)
115
+ merge("#{name}=#{Rack::Utils.escape(value)}")
116
+ end
117
+
118
+ def merge(raw_cookies, uri = nil)
119
+ return unless raw_cookies
120
+
121
+ if raw_cookies.is_a? String
122
+ raw_cookies = raw_cookies.split("\n")
123
+ raw_cookies.reject!{|c| c.empty? }
124
+ end
125
+
126
+ raw_cookies.each do |raw_cookie|
127
+ cookie = Cookie.new(raw_cookie, uri, @default_host)
128
+ self << cookie if cookie.valid?(uri)
129
+ end
130
+ end
131
+
132
+ def <<(new_cookie)
133
+ @cookies.reject! do |existing_cookie|
134
+ new_cookie.replaces?(existing_cookie)
135
+ end
136
+
137
+ @cookies << new_cookie
138
+ @cookies.sort!
139
+ end
140
+
141
+ # :api: private
142
+ def for(uri)
143
+ hash_for(uri).values.map { |c| c.raw }.join(';')
144
+ end
145
+
146
+ def to_hash
147
+ cookies = {}
148
+
149
+ hash_for(nil).each do |name, cookie|
150
+ cookies[name] = cookie.value
151
+ end
152
+
153
+ return cookies
154
+ end
155
+
156
+ protected
157
+
158
+ def hash_for(uri = nil)
159
+ cookies = {}
160
+
161
+ # The cookies are sorted by most specific first. So, we loop through
162
+ # all the cookies in order and add it to a hash by cookie name if
163
+ # the cookie can be sent to the current URI. It's added to the hash
164
+ # so that when we are done, the cookies will be unique by name and
165
+ # we'll have grabbed the most specific to the URI.
166
+ @cookies.each do |cookie|
167
+ cookies[cookie.name] = cookie if !uri || cookie.matches?(uri)
168
+ end
169
+
170
+ return cookies
171
+ end
172
+
173
+ end
174
+
175
+ end
176
+ end