1 # frozen_string_literal: true
8 require_relative 'response'
9 require_relative 'version'
10 require_relative 'constants'
11 require_relative 'headers'
14 # Rack::MockRequest helps testing your Rack application without
15 # actually using HTTP.
17 # After performing a request on a URL with get/post/put/patch/delete, it
18 # returns a MockResponse with useful helper methods for effective
21 # You can pass a hash with additional configuration to the
22 # get/post/put/patch/delete.
23 # <tt>:input</tt>:: A String or IO-like to be used as rack.input.
24 # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
25 # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
28 class FatalWarning < RuntimeError
33 raise FatalWarning, warning
37 raise FatalWarning, warning
49 RACK_VERSION => Rack::VERSION,
50 RACK_INPUT => StringIO.new,
51 RACK_ERRORS => StringIO.new,
58 # Make a GET request and return a MockResponse. See #request.
59 def get(uri, opts = {}) request(GET, uri, opts) end
60 # Make a POST request and return a MockResponse. See #request.
61 def post(uri, opts = {}) request(POST, uri, opts) end
62 # Make a PUT request and return a MockResponse. See #request.
63 def put(uri, opts = {}) request(PUT, uri, opts) end
64 # Make a PATCH request and return a MockResponse. See #request.
65 def patch(uri, opts = {}) request(PATCH, uri, opts) end
66 # Make a DELETE request and return a MockResponse. See #request.
67 def delete(uri, opts = {}) request(DELETE, uri, opts) end
68 # Make a HEAD request and return a MockResponse. See #request.
69 def head(uri, opts = {}) request(HEAD, uri, opts) end
70 # Make an OPTIONS request and return a MockResponse. See #request.
71 def options(uri, opts = {}) request(OPTIONS, uri, opts) end
73 # Make a request using the given request method for the given
74 # uri to the rack application and return a MockResponse.
75 # Options given are passed to MockRequest.env_for.
76 def request(method = GET, uri = "", opts = {})
77 env = self.class.env_for(uri, opts.merge(method: method))
80 app = Rack::Lint.new(@app)
85 errors = env[RACK_ERRORS]
86 status, headers, body = app.call(env)
87 MockResponse.new(status, headers, body, errors)
89 body.close if body.respond_to?(:close)
92 # For historical reasons, we're pinning to RFC 2396.
93 # URI::Parser = URI::RFC2396_Parser
94 def self.parse_uri_rfc2396(uri)
95 @parser ||= URI::Parser.new
99 # Return the Rack environment used for a request to +uri+.
100 # All options that are strings are added to the returned environment.
102 # :fatal :: Whether to raise an exception if request outputs to rack.errors
103 # :input :: The rack.input to set
104 # :method :: The HTTP request method to use
105 # :params :: The params to use
106 # :script_name :: The SCRIPT_NAME to set
107 def self.env_for(uri = "", opts = {})
108 uri = parse_uri_rfc2396(uri)
109 uri.path = "/#{uri.path}" unless uri.path[0] == ?/
111 env = DEFAULT_ENV.dup
113 env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
114 env[SERVER_NAME] = (uri.host || "example.org").b
115 env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b
116 env[QUERY_STRING] = (uri.query.to_s).b
117 env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
118 env[RACK_URL_SCHEME] = (uri.scheme || "http").b
119 env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
121 env[SCRIPT_NAME] = opts[:script_name] || ""
124 env[RACK_ERRORS] = FatalWarner.new
126 env[RACK_ERRORS] = StringIO.new
129 if params = opts[:params]
130 if env[REQUEST_METHOD] == GET
131 params = Utils.parse_nested_query(params) if params.is_a?(String)
132 params.update(Utils.parse_nested_query(env[QUERY_STRING]))
133 env[QUERY_STRING] = Utils.build_nested_query(params)
134 elsif !opts.has_key?(:input)
135 opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
136 if params.is_a?(Hash)
137 if data = Rack::Multipart.build_multipart(params)
139 opts["CONTENT_LENGTH"] ||= data.length.to_s
140 opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
142 opts[:input] = Utils.build_nested_query(params)
145 opts[:input] = params
150 empty_str = String.new
151 opts[:input] ||= empty_str
152 if String === opts[:input]
153 rack_input = StringIO.new(opts[:input])
155 rack_input = opts[:input]
158 rack_input.set_encoding(Encoding::BINARY)
159 env[RACK_INPUT] = rack_input
161 env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
163 opts.each { |field, value|
164 env[field] = value if String === field
171 # Rack::MockResponse provides useful helpers for testing your apps.
172 # Usually, you don't create the MockResponse on your own, but use
175 class MockResponse < Rack::Response
181 attr_reader :original_headers, :cookies
184 attr_accessor :errors
186 def initialize(status, headers, body, errors = nil)
187 @original_headers = headers
190 @errors = errors.string if errors.respond_to?(:string)
195 super(body, status, headers)
197 @cookies = parse_cookies_from_header
210 # FIXME: apparently users of MockResponse expect the return value of
211 # MockResponse#body to be a string. However, the real response object
212 # returns the body as a list.
214 # See spec_showstatus.rb:
216 # should "not replace existing messages" do
218 # res.body.should == "foo!"
222 super.each do |chunk|
230 [201, 204, 304].include? status
234 cookies.fetch(name, nil)
239 def parse_cookies_from_header
241 if headers.has_key? 'set-cookie'
242 set_cookie_header = headers.fetch('set-cookie')
243 Array(set_cookie_header).each do |header_value|
244 header_value.split("\n").each do |cookie|
245 cookie_name, cookie_filling = cookie.split('=', 2)
246 cookie_attributes = identify_cookie_attributes cookie_filling
247 parsed_cookie = CGI::Cookie.new(
248 'name' => cookie_name.strip,
249 'value' => cookie_attributes.fetch('value'),
250 'path' => cookie_attributes.fetch('path', nil),
251 'domain' => cookie_attributes.fetch('domain', nil),
252 'expires' => cookie_attributes.fetch('expires', nil),
253 'secure' => cookie_attributes.fetch('secure', false)
255 cookies.store(cookie_name, parsed_cookie)
262 def identify_cookie_attributes(cookie_filling)
263 cookie_bits = cookie_filling.split(';')
264 cookie_attributes = Hash.new
265 cookie_attributes.store('value', cookie_bits[0].strip)
266 cookie_bits.drop(1).each do |bit|
268 cookie_attribute, attribute_value = bit.split('=', 2)
269 cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
271 if bit.include? 'secure'
272 cookie_attributes.store('secure', true)
276 if cookie_attributes.key? 'max-age'
277 cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
278 elsif cookie_attributes.key? 'expires'
279 cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))