mock.rb 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. require 'uri'
  2. require 'stringio'
  3. require 'rack'
  4. require 'rack/lint'
  5. require 'rack/utils'
  6. require 'rack/response'
  7. module Rack
  8. # Rack::MockRequest helps testing your Rack application without
  9. # actually using HTTP.
  10. #
  11. # After performing a request on a URL with get/post/put/delete, it
  12. # returns a MockResponse with useful helper methods for effective
  13. # testing.
  14. #
  15. # You can pass a hash with additional configuration to the
  16. # get/post/put/delete.
  17. # <tt>:input</tt>:: A String or IO-like to be used as rack.input.
  18. # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
  19. # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
  20. class MockRequest
  21. class FatalWarning < RuntimeError
  22. end
  23. class FatalWarner
  24. def puts(warning)
  25. raise FatalWarning, warning
  26. end
  27. def write(warning)
  28. raise FatalWarning, warning
  29. end
  30. def flush
  31. end
  32. def string
  33. ""
  34. end
  35. end
  36. DEFAULT_ENV = {
  37. "rack.version" => Rack::VERSION,
  38. "rack.input" => StringIO.new,
  39. "rack.errors" => StringIO.new,
  40. "rack.multithread" => true,
  41. "rack.multiprocess" => true,
  42. "rack.run_once" => false,
  43. }
  44. def initialize(app)
  45. @app = app
  46. end
  47. def get(uri, opts={}) request("GET", uri, opts) end
  48. def post(uri, opts={}) request("POST", uri, opts) end
  49. def put(uri, opts={}) request("PUT", uri, opts) end
  50. def delete(uri, opts={}) request("DELETE", uri, opts) end
  51. def head(uri, opts={}) request("HEAD", uri, opts) end
  52. def request(method="GET", uri="", opts={})
  53. env = self.class.env_for(uri, opts.merge(:method => method))
  54. if opts[:lint]
  55. app = Rack::Lint.new(@app)
  56. else
  57. app = @app
  58. end
  59. errors = env["rack.errors"]
  60. status, headers, body = app.call(env)
  61. MockResponse.new(status, headers, body, errors)
  62. ensure
  63. body.close if body.respond_to?(:close)
  64. end
  65. # Return the Rack environment used for a request to +uri+.
  66. def self.env_for(uri="", opts={})
  67. uri = URI(uri)
  68. uri.path = "/#{uri.path}" unless uri.path[0] == ?/
  69. env = DEFAULT_ENV.dup
  70. env["REQUEST_METHOD"] = opts[:method] ? opts[:method].to_s.upcase : "GET"
  71. env["SERVER_NAME"] = uri.host || "example.org"
  72. env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
  73. env["QUERY_STRING"] = uri.query.to_s
  74. env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
  75. env["rack.url_scheme"] = uri.scheme || "http"
  76. env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
  77. env["SCRIPT_NAME"] = opts[:script_name] || ""
  78. if opts[:fatal]
  79. env["rack.errors"] = FatalWarner.new
  80. else
  81. env["rack.errors"] = StringIO.new
  82. end
  83. if params = opts[:params]
  84. if env["REQUEST_METHOD"] == "GET"
  85. params = Utils.parse_nested_query(params) if params.is_a?(String)
  86. params.update(Utils.parse_nested_query(env["QUERY_STRING"]))
  87. env["QUERY_STRING"] = Utils.build_nested_query(params)
  88. elsif !opts.has_key?(:input)
  89. opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
  90. if params.is_a?(Hash)
  91. if data = Utils::Multipart.build_multipart(params)
  92. opts[:input] = data
  93. opts["CONTENT_LENGTH"] ||= data.length.to_s
  94. opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}"
  95. else
  96. opts[:input] = Utils.build_nested_query(params)
  97. end
  98. else
  99. opts[:input] = params
  100. end
  101. end
  102. end
  103. empty_str = ""
  104. empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding
  105. opts[:input] ||= empty_str
  106. if String === opts[:input]
  107. rack_input = StringIO.new(opts[:input])
  108. else
  109. rack_input = opts[:input]
  110. end
  111. rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
  112. env['rack.input'] = rack_input
  113. env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s
  114. opts.each { |field, value|
  115. env[field] = value if String === field
  116. }
  117. env
  118. end
  119. end
  120. # Rack::MockResponse provides useful helpers for testing your apps.
  121. # Usually, you don't create the MockResponse on your own, but use
  122. # MockRequest.
  123. class MockResponse < Rack::Response
  124. # Headers
  125. attr_reader :original_headers
  126. # Errors
  127. attr_accessor :errors
  128. def initialize(status, headers, body, errors=StringIO.new(""))
  129. @original_headers = headers
  130. @errors = errors.string if errors.respond_to?(:string)
  131. @body_string = nil
  132. super(body, status, headers)
  133. end
  134. def =~(other)
  135. body =~ other
  136. end
  137. def match(other)
  138. body.match other
  139. end
  140. def body
  141. # FIXME: apparently users of MockResponse expect the return value of
  142. # MockResponse#body to be a string. However, the real response object
  143. # returns the body as a list.
  144. #
  145. # See spec_showstatus.rb:
  146. #
  147. # should "not replace existing messages" do
  148. # ...
  149. # res.body.should == "foo!"
  150. # end
  151. super.join
  152. end
  153. def empty?
  154. [201, 204, 205, 304].include? status
  155. end
  156. end
  157. end