test.rb 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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. module Rack
  10. module Test
  11. VERSION = "0.6.1"
  12. DEFAULT_HOST = "example.org"
  13. MULTIPART_BOUNDARY = "----------XnJLe9ZIbbGUYtzPQJ16u1"
  14. # The common base class for exceptions raised by Rack::Test
  15. class Error < StandardError; end
  16. # This class represents a series of requests issued to a Rack app, sharing
  17. # a single cookie jar
  18. #
  19. # Rack::Test::Session's methods are most often called through Rack::Test::Methods,
  20. # which will automatically build a session when it's first used.
  21. class Session
  22. extend Forwardable
  23. include Rack::Test::Utils
  24. def_delegators :@rack_mock_session, :clear_cookies, :set_cookie, :last_response, :last_request
  25. # Creates a Rack::Test::Session for a given Rack app or Rack::MockSession.
  26. #
  27. # Note: Generally, you won't need to initialize a Rack::Test::Session directly.
  28. # Instead, you should include Rack::Test::Methods into your testing context.
  29. # (See README.rdoc for an example)
  30. def initialize(mock_session)
  31. @headers = {}
  32. if mock_session.is_a?(MockSession)
  33. @rack_mock_session = mock_session
  34. else
  35. @rack_mock_session = MockSession.new(mock_session)
  36. end
  37. @default_host = @rack_mock_session.default_host
  38. end
  39. # Issue a GET request for the given URI with the given params and Rack
  40. # environment. Stores the issues request object in #last_request and
  41. # the app's response in #last_response. Yield #last_response to a block
  42. # if given.
  43. #
  44. # Example:
  45. # get "/"
  46. def get(uri, params = {}, env = {}, &block)
  47. env = env_for(uri, env.merge(:method => "GET", :params => params))
  48. process_request(uri, env, &block)
  49. end
  50. # Issue a POST request for the given URI. See #get
  51. #
  52. # Example:
  53. # post "/signup", "name" => "Bryan"
  54. def post(uri, params = {}, env = {}, &block)
  55. env = env_for(uri, env.merge(:method => "POST", :params => params))
  56. process_request(uri, env, &block)
  57. end
  58. # Issue a PUT request for the given URI. See #get
  59. #
  60. # Example:
  61. # put "/"
  62. def put(uri, params = {}, env = {}, &block)
  63. env = env_for(uri, env.merge(:method => "PUT", :params => params))
  64. process_request(uri, env, &block)
  65. end
  66. # Issue a DELETE request for the given URI. See #get
  67. #
  68. # Example:
  69. # delete "/"
  70. def delete(uri, params = {}, env = {}, &block)
  71. env = env_for(uri, env.merge(:method => "DELETE", :params => params))
  72. process_request(uri, env, &block)
  73. end
  74. # Issue an OPTIONS request for the given URI. See #get
  75. #
  76. # Example:
  77. # options "/"
  78. def options(uri, params = {}, env = {}, &block)
  79. env = env_for(uri, env.merge(:method => "OPTIONS", :params => params))
  80. process_request(uri, env, &block)
  81. end
  82. # Issue a HEAD request for the given URI. See #get
  83. #
  84. # Example:
  85. # head "/"
  86. def head(uri, params = {}, env = {}, &block)
  87. env = env_for(uri, env.merge(:method => "HEAD", :params => params))
  88. process_request(uri, env, &block)
  89. end
  90. # Issue a request to the Rack app for the given URI and optional Rack
  91. # environment. Stores the issues request object in #last_request and
  92. # the app's response in #last_response. Yield #last_response to a block
  93. # if given.
  94. #
  95. # Example:
  96. # request "/"
  97. def request(uri, env = {}, &block)
  98. env = env_for(uri, env)
  99. process_request(uri, env, &block)
  100. end
  101. # Set a header to be included on all subsequent requests through the
  102. # session. Use a value of nil to remove a previously configured header.
  103. #
  104. # In accordance with the Rack spec, headers will be included in the Rack
  105. # environment hash in HTTP_USER_AGENT form.
  106. #
  107. # Example:
  108. # header "User-Agent", "Firefox"
  109. def header(name, value)
  110. if value.nil?
  111. @headers.delete(name)
  112. else
  113. @headers[name] = value
  114. end
  115. end
  116. # Set the username and password for HTTP Basic authorization, to be
  117. # included in subsequent requests in the HTTP_AUTHORIZATION header.
  118. #
  119. # Example:
  120. # basic_authorize "bryan", "secret"
  121. def basic_authorize(username, password)
  122. encoded_login = ["#{username}:#{password}"].pack("m*")
  123. header('Authorization', "Basic #{encoded_login}")
  124. end
  125. alias_method :authorize, :basic_authorize
  126. # Set the username and password for HTTP Digest authorization, to be
  127. # included in subsequent requests in the HTTP_AUTHORIZATION header.
  128. #
  129. # Example:
  130. # digest_authorize "bryan", "secret"
  131. def digest_authorize(username, password)
  132. @digest_username = username
  133. @digest_password = password
  134. end
  135. # Rack::Test will not follow any redirects automatically. This method
  136. # will follow the redirect returned (including setting the Referer header
  137. # on the new request) in the last response. If the last response was not
  138. # a redirect, an error will be raised.
  139. def follow_redirect!
  140. unless last_response.redirect?
  141. raise Error.new("Last response was not a redirect. Cannot follow_redirect!")
  142. end
  143. get(last_response["Location"], {}, { "HTTP_REFERER" => last_request.url })
  144. end
  145. private
  146. def env_for(path, env)
  147. uri = URI.parse(path)
  148. uri.path = "/#{uri.path}" unless uri.path[0] == ?/
  149. uri.host ||= @default_host
  150. env = default_env.merge(env)
  151. env["HTTP_HOST"] ||= [uri.host, uri.port].compact.join(":")
  152. env.update("HTTPS" => "on") if URI::HTTPS === uri
  153. env["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" if env[:xhr]
  154. # TODO: Remove this after Rack 1.1 has been released.
  155. # Stringifying and upcasing methods has be commit upstream
  156. env["REQUEST_METHOD"] ||= env[:method] ? env[:method].to_s.upcase : "GET"
  157. if env["REQUEST_METHOD"] == "GET"
  158. params = env[:params] || {}
  159. params = parse_nested_query(params) if params.is_a?(String)
  160. params.update(parse_nested_query(uri.query))
  161. uri.query = build_nested_query(params)
  162. elsif !env.has_key?(:input)
  163. env["CONTENT_TYPE"] ||= "application/x-www-form-urlencoded"
  164. if env[:params].is_a?(Hash)
  165. if data = build_multipart(env[:params])
  166. env[:input] = data
  167. env["CONTENT_LENGTH"] ||= data.length.to_s
  168. env["CONTENT_TYPE"] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
  169. else
  170. env[:input] = params_to_string(env[:params])
  171. end
  172. else
  173. env[:input] = env[:params]
  174. end
  175. end
  176. env.delete(:params)
  177. if env.has_key?(:cookie)
  178. set_cookie(env.delete(:cookie), uri)
  179. end
  180. Rack::MockRequest.env_for(uri.to_s, env)
  181. end
  182. def process_request(uri, env)
  183. uri = URI.parse(uri)
  184. uri.host ||= @default_host
  185. @rack_mock_session.request(uri, env)
  186. if retry_with_digest_auth?(env)
  187. auth_env = env.merge({
  188. "HTTP_AUTHORIZATION" => digest_auth_header,
  189. "rack-test.digest_auth_retry" => true
  190. })
  191. auth_env.delete('rack.request')
  192. process_request(uri.path, auth_env)
  193. else
  194. yield last_response if block_given?
  195. last_response
  196. end
  197. end
  198. def digest_auth_header
  199. challenge = last_response["WWW-Authenticate"].split(" ", 2).last
  200. params = Rack::Auth::Digest::Params.parse(challenge)
  201. params.merge!({
  202. "username" => @digest_username,
  203. "nc" => "00000001",
  204. "cnonce" => "nonsensenonce",
  205. "uri" => last_request.path_info,
  206. "method" => last_request.env["REQUEST_METHOD"],
  207. })
  208. params["response"] = MockDigestRequest.new(params).response(@digest_password)
  209. "Digest #{params}"
  210. end
  211. def retry_with_digest_auth?(env)
  212. last_response.status == 401 &&
  213. digest_auth_configured? &&
  214. !env["rack-test.digest_auth_retry"]
  215. end
  216. def digest_auth_configured?
  217. @digest_username
  218. end
  219. def default_env
  220. { "rack.test" => true, "REMOTE_ADDR" => "127.0.0.1" }.merge(headers_for_env)
  221. end
  222. def headers_for_env
  223. converted_headers = {}
  224. @headers.each do |name, value|
  225. env_key = name.upcase.gsub("-", "_")
  226. env_key = "HTTP_" + env_key unless "CONTENT_TYPE" == env_key
  227. converted_headers[env_key] = value
  228. end
  229. converted_headers
  230. end
  231. def params_to_string(params)
  232. case params
  233. when Hash then build_nested_query(params)
  234. when nil then ""
  235. else params
  236. end
  237. end
  238. end
  239. def self.encoding_aware_strings?
  240. defined?(Encoding) && "".respond_to?(:encode)
  241. end
  242. end
  243. end