Use lower case normalisation for cookie attributes. (#1849)
[rack.git] / lib / rack / mock.rb
blob04e69561c0740d5cd255c86285f65f4d44942011
1 # frozen_string_literal: true
3 require 'uri'
4 require 'stringio'
5 require 'cgi/cookie'
6 require 'time'
8 require_relative 'response'
9 require_relative 'version'
10 require_relative 'constants'
11 require_relative 'headers'
13 module Rack
14   # Rack::MockRequest helps testing your Rack application without
15   # actually using HTTP.
16   #
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
19   # testing.
20   #
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.
27   class MockRequest
28     class FatalWarning < RuntimeError
29     end
31     class FatalWarner
32       def puts(warning)
33         raise FatalWarning, warning
34       end
36       def write(warning)
37         raise FatalWarning, warning
38       end
40       def flush
41       end
43       def string
44         ""
45       end
46     end
48     DEFAULT_ENV = {
49       RACK_VERSION      => Rack::VERSION,
50       RACK_INPUT        => StringIO.new,
51       RACK_ERRORS       => StringIO.new,
52     }.freeze
54     def initialize(app)
55       @app = app
56     end
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))
79       if opts[:lint]
80         app = Rack::Lint.new(@app)
81       else
82         app = @app
83       end
85       errors = env[RACK_ERRORS]
86       status, headers, body = app.call(env)
87       MockResponse.new(status, headers, body, errors)
88     ensure
89       body.close if body.respond_to?(:close)
90     end
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
96       @parser.parse(uri)
97     end
99     # Return the Rack environment used for a request to +uri+.
100     # All options that are strings are added to the returned environment.
101     # Options:
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] || ""
123       if opts[:fatal]
124         env[RACK_ERRORS] = FatalWarner.new
125       else
126         env[RACK_ERRORS] = StringIO.new
127       end
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)
138               opts[:input] = data
139               opts["CONTENT_LENGTH"] ||= data.length.to_s
140               opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
141             else
142               opts[:input] = Utils.build_nested_query(params)
143             end
144           else
145             opts[:input] = params
146           end
147         end
148       end
150       empty_str = String.new
151       opts[:input] ||= empty_str
152       if String === opts[:input]
153         rack_input = StringIO.new(opts[:input])
154       else
155         rack_input = opts[:input]
156       end
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
165       }
167       env
168     end
169   end
171   # Rack::MockResponse provides useful helpers for testing your apps.
172   # Usually, you don't create the MockResponse on your own, but use
173   # MockRequest.
175   class MockResponse < Rack::Response
176     class << self
177       alias [] new
178     end
180     # Headers
181     attr_reader :original_headers, :cookies
183     # Errors
184     attr_accessor :errors
186     def initialize(status, headers, body, errors = nil)
187       @original_headers = headers
189       if errors
190         @errors = errors.string if errors.respond_to?(:string)
191       else
192         @errors = ""
193       end
195       super(body, status, headers)
197       @cookies = parse_cookies_from_header
198       buffered_body!
199     end
201     def =~(other)
202       body =~ other
203     end
205     def match(other)
206       body.match other
207     end
209     def body
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.
213       #
214       # See spec_showstatus.rb:
215       #
216       #   should "not replace existing messages" do
217       #     ...
218       #     res.body.should == "foo!"
219       #   end
220       buffer = String.new
222       super.each do |chunk|
223         buffer << chunk
224       end
226       return buffer
227     end
229     def empty?
230       [201, 204, 304].include? status
231     end
233     def cookie(name)
234       cookies.fetch(name, nil)
235     end
237     private
239     def parse_cookies_from_header
240       cookies = Hash.new
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)
254             )
255             cookies.store(cookie_name, parsed_cookie)
256           end
257         end
258       end
259       cookies
260     end
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|
267         if bit.include? '='
268           cookie_attribute, attribute_value = bit.split('=', 2)
269           cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
270         end
271         if bit.include? 'secure'
272           cookie_attributes.store('secure', true)
273         end
274       end
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']))
280       end
282       cookie_attributes
283     end
285   end