require 'webrick' require 'singleton' require 'tap/test/subset_test' module Tap module Test # HttpTest facilitates testing of HTTP requests by initializing a # Webrick server that echos requests, and providing methods to # validate echoed requests. # module HttpTest # The test-specific web server. All request to the server # are echoed back. class Server include Singleton include WEBrick attr_accessor :server_thread, :server def initialize # set the default log level to warn to prevent general access logs, unless otherwise specified log_level = ENV["WEB_LOG_LEVEL"] ? ENV["WEB_LOG_LEVEL"].upcase : "WARN" logger = Log.new($stderr, Log.const_get( log_level ) ) self.server = HTTPServer.new(:Port => 2000, :Logger => logger, :AccessLog => [ [ logger, AccessLog::COMMON_LOG_FORMAT ], [ logger, AccessLog::REFERER_LOG_FORMAT ]]) server.mount_proc("/") do |req, res| res.body << req.request_line res.body << req.raw_header.join('') # an extra line must be added to delimit the headers from the body. if req.body res.body << "\r\n" res.body << req.body end res['Content-Type'] = "text/html" end end # Starts the server on a new thread. def start_web_server self.server_thread ||= Thread.new { server.start } end end def self.included(base) base.send(:include, Tap::Test::SubsetTest) end # WEB subset of tests. Starts HTTPTest::Server if necessary # and yields to the block. def web_test subset_test("WEB", "w") do Server.instance.start_web_server yield end end REQUEST_ATTRIBUTES = %w{ request_method http_version host port path script_name path_info header cookies query accept accept_charset accept_encoding accept_language user addr peeraddr attributes keep_alive} UNCHECKED_REQUEST_ATTRIBUTES = %w{ request_line unparsed_uri request_uri request_time raw_header query_string} # Parses expected and actual as an http request (using Tap::Net.parse_http_request) # and asserts that all of the REQUEST_ATTRIBUTES are equal. See the parse_http_request # documentation for some important notes, particularly involving "\r\n" vs "\n" and # post requests. def assert_request_equal(expected, actual) e = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) e.parse( StringIO.new(expected) ) a = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) a.parse( StringIO.new(actual) ) errors = [] REQUEST_ATTRIBUTES.each do |attribute| exp = e.send(attribute) act = a.send(attribute) next if exp == act errors << "<#{PP.singleline_pp(exp, '')}> expected for #{attribute} but was:\n<#{PP.singleline_pp(act, '')}>." end if errors.empty? # this rather unecessary assertion is used simply to # make assert_request_equal cause an assertion. assert errors.empty? else flunk errors.join("\n") end end # Convenience method that strips str, strips each line of str, and # then rejoins them using "\r\n" as is typical of HTTP messages. # # strip_align %Q{ # GET /echo HTTP/1.1 # Accept: */* # Host: localhost:2000} # # # => "GET /echo HTTP/1.1\r\nAccept: */*\r\nHost: localhost:2000\r\n" def strip_align(str) str.strip.split(/\r?\n/).collect do |line| "#{line.strip}\r\n" end.compact.join('') end # Turns a hash of parameters into an encoded HTTP query string. # Multiple values for a given key can be specified by an array. # # to_query('key' => 'value', 'array' => ['one', 'two']) # => "array=one&array=two&key=value" # # Note: the order of the parameters in the result is determined # by hash.each_pair and is thus fairly unpredicatable. def to_query(hash) query = [] hash.each_pair do |key,values| values = values.kind_of?(Array) ? values : [values] values.each { |value| query << "#{key}=#{value}" } end URI.encode(query.join('&')) end module RequestLibrary def get_request msg = <<-_end_of_message_ GET /path?str=value&int=123&yaml=---+%0A-+a%0A-+b%0A-+c%0A&float=1.23 HTTP/1.1 Host: www.example.com Keep-Alive: 300 Connection: keep-alive _end_of_message_ end def _get_request { :url => "http://www.example.com/path", :http_version => '1.1', :request_method => 'GET', :headers => { "Host" => "www.example.com", "Keep-Alive" => 300, "Connection" => 'keep-alive'}, :params => { 'str' => 'value', 'int' => 123, 'float' => 1.23, 'yaml' => ['a', 'b', 'c']} } end def get_request_with_multiple_values msg = <<-_end_of_message_ GET /path?one=value&one=123&one=---+%0A-+a%0A-+b%0A-+c%0A&two=1.23 HTTP/1.1 Host: www.example.com Keep-Alive: 300 Connection: keep-alive _end_of_message_ end def _get_request_with_multiple_values { :url => "http://www.example.com/path", :http_version => '1.1', :request_method => 'GET', :headers => { "Host" => "www.example.com", "Keep-Alive" => 300, "Connection" => 'keep-alive'}, :params => { 'one' => ['value', 123, ['a', 'b', 'c']], 'two' => 1.23} } end def multipart_request msg = <<-_end_of_message_ POST /path HTTP/1.1 Host: www.example.com Content-Type: multipart/form-data; boundary=1234567890 Content-Length: 305 --1234567890 Content-Disposition: form-data; name="str" string value --1234567890 Content-Disposition: form-data; name="int" 123 --1234567890 Content-Disposition: form-data; name="float" 1.23 --1234567890 Content-Disposition: form-data; name="yaml" --- - a - b - c --1234567890-- _end_of_message_ msg.gsub(/\n/, "\r\n") end def _multipart_request { :url => "http://www.example.com/path", :http_version => '1.1', :request_method => 'POST', :headers => { "Host" => 'www.example.com', "Content-Type" => "multipart/form-data; boundary=1234567890", "Content-Length" => 305}, :params => { 'str' => 'string value', 'int' => 123, 'float' => 1.23, 'yaml' => ['a', 'b', 'c']} } end def multipart_request_with_multiple_values msg = <<-_end_of_message_ POST /path HTTP/1.1 Host: www.example.com Content-Type: multipart/form-data; boundary=1234567890 Content-Length: 302 --1234567890 Content-Disposition: form-data; name="one" string value --1234567890 Content-Disposition: form-data; name="one" 123 --1234567890 Content-Disposition: form-data; name="two" 1.23 --1234567890 Content-Disposition: form-data; name="one" --- - a - b - c --1234567890-- _end_of_message_ msg.gsub(/\n/, "\r\n") end def _multipart_request_with_multiple_values { :url => "http://www.example.com/path", :http_version => '1.1', :request_method => 'POST', :headers => { "Host" => 'www.example.com', "Content-Type" => "multipart/form-data; boundary=1234567890", "Content-Length" => 302}, :params => { 'one' => ['string value', 123, ['a', 'b', 'c']], 'two' => 1.23} } end end end end end