module Typhoeus USER_AGENT = "Typhoeus - http://github.com/dbalatero/typhoeus/tree/master" def self.included(base) base.extend ClassMethods end class MockExpectedError < StandardError; end module ClassMethods def allow_net_connect @allow_net_connect = true if @allow_net_connect.nil? @allow_net_connect end def allow_net_connect=(value) @allow_net_connect = value end def mock(method, args = {}) @remote_mocks ||= {} @remote_mocks[method] ||= {} args[:code] ||= 200 args[:body] ||= "" args[:headers] ||= "" args[:time] ||= 0 url = args.delete(:url) url ||= :catch_all params = args.delete(:params) key = mock_key_for(url, params) @remote_mocks[method][key] = args end # Returns a key for a given URL and passed in # set of Typhoeus options to be used to store/retrieve # a corresponding mock. def mock_key_for(url, params = nil) if url == :catch_all url else key = url if params and !params.empty? key += flatten_and_sort_hash(params).to_s end key end end def flatten_and_sort_hash(params) params = params.dup # Flatten any sub-hashes to a single string. params.keys.each do |key| if params[key].is_a?(Hash) params[key] = params[key].sort_by { |k, v| k.to_s.downcase }.to_s end end params.sort_by { |k, v| k.to_s.downcase } end def get_mock(method, url, options) return nil unless @remote_mocks if @remote_mocks.has_key? method extra_response_args = { :requested_http_method => method, :requested_url => url, :start_time => Time.now } mock_key = mock_key_for(url, options[:params]) if @remote_mocks[method].has_key? mock_key get_mock_and_run_handlers(method, @remote_mocks[method][mock_key].merge( extra_response_args), options) elsif @remote_mocks[method].has_key? :catch_all get_mock_and_run_handlers(method, @remote_mocks[method][:catch_all].merge( extra_response_args), options) else nil end else nil end end def enforce_allow_net_connect!(http_verb, url, params = nil) if !allow_net_connect message = "Real HTTP connections are disabled. Unregistered request: " << "#{http_verb.to_s.upcase} #{url}\n" << " Try: mock(:#{http_verb}, :url => \"#{url}\"" if params message << ",\n :params => #{params.inspect}" end message << ")" raise MockExpectedError, message end end def check_expected_headers!(response_args, options) missing_headers = {} response_args[:expected_headers].each do |key, value| if options[:headers].nil? missing_headers[key] = [value, nil] elsif ((options[:headers][key] && value != :anything) && options[:headers][key] != value) missing_headers[key] = [value, options[:headers][key]] end end unless missing_headers.empty? raise headers_error_summary(response_args, options, missing_headers, 'expected') end end def check_unexpected_headers!(response_args, options) bad_headers = {} response_args[:unexpected_headers].each do |key, value| if (options[:headers][key] && value == :anything) || (options[:headers][key] == value) bad_headers[key] = [value, options[:headers][key]] end end unless bad_headers.empty? raise headers_error_summary(response_args, options, bad_headers, 'did not expect') end end def headers_error_summary(response_args, options, missing_headers, lead_in) error = "#{lead_in} the following headers: #{response_args[:expected_headers].inspect}, but received: #{options[:headers].inspect}\n\n" error << "Differences:\n" error << "------------\n" missing_headers.each do |key, values| error << " - #{key}: #{lead_in} #{values[0].inspect}, got #{values[1].inspect}\n" end error end private :headers_error_summary def get_mock_and_run_handlers(method, response_args, options) response = Response.new(response_args) if response_args.has_key? :expected_body raise "#{method} expected body of \"#{response_args[:expected_body]}\" but received #{options[:body]}" if response_args[:expected_body] != options[:body] end if response_args.has_key? :expected_headers check_expected_headers!(response_args, options) end if response_args.has_key? :unexpected_headers check_unexpected_headers!(response_args, options) end if response.code >= 200 && response.code < 300 && options.has_key?(:on_success) response = options[:on_success].call(response) elsif options.has_key?(:on_failure) response = options[:on_failure].call(response) end encode_nil_response(response) end [:get, :post, :put, :delete].each do |method| line = __LINE__ + 2 # get any errors on the correct line num code = <<-SRC def #{method.to_s}(url, options = {}) mock_object = get_mock(:#{method.to_s}, url, options) unless mock_object.nil? decode_nil_response(mock_object) else enforce_allow_net_connect!(:#{method.to_s}, url, options[:params]) remote_proxy_object(url, :#{method.to_s}, options) end end SRC module_eval(code, "./lib/typhoeus/remote.rb", line) end def remote_proxy_object(url, method, options) easy = Typhoeus.get_easy_object easy.url = url easy.method = method easy.headers = options[:headers] if options.has_key?(:headers) easy.headers["User-Agent"] = (options[:user_agent] || Typhoeus::USER_AGENT) easy.params = options[:params] if options[:params] easy.request_body = options[:body] if options[:body] easy.timeout = options[:timeout] if options[:timeout] easy.set_headers proxy = Typhoeus::RemoteProxyObject.new(clear_memoized_proxy_objects, easy, options) set_memoized_proxy_object(method, url, options, proxy) end def remote_defaults(options) @remote_defaults ||= {} @remote_defaults.merge!(options) if options @remote_defaults end # If we get subclassed, make sure that child inherits the remote defaults # of the parent class. def inherited(child) child.__send__(:remote_defaults, @remote_defaults) end def call_remote_method(method_name, args) m = @remote_methods[method_name] base_uri = args.delete(:base_uri) || m.base_uri || "" if args.has_key? :path path = args.delete(:path) else path = m.interpolate_path_with_arguments(args) end path ||= "" http_method = m.http_method url = base_uri + path options = m.merge_options(args) # proxy_object = memoized_proxy_object(http_method, url, options) # return proxy_object unless proxy_object.nil? # # if m.cache_responses? # object = @cache.get(get_memcache_response_key(method_name, args)) # if object # set_memoized_proxy_object(http_method, url, options, object) # return object # end # end proxy = memoized_proxy_object(http_method, url, options) unless proxy if m.cache_responses? options[:cache] = @cache options[:cache_key] = get_memcache_response_key(method_name, args) options[:cache_timeout] = m.cache_ttl end proxy = send(http_method, url, options) end proxy end def set_memoized_proxy_object(http_method, url, options, object) @memoized_proxy_objects ||= {} @memoized_proxy_objects["#{http_method}_#{url}_#{options.to_s}"] = object end def memoized_proxy_object(http_method, url, options) @memoized_proxy_objects ||= {} @memoized_proxy_objects["#{http_method}_#{url}_#{options.to_s}"] end def clear_memoized_proxy_objects lambda { @memoized_proxy_objects = {} } end def get_memcache_response_key(remote_method_name, args) result = "#{remote_method_name.to_s}-#{args.to_s}" (Digest::SHA2.new << result).to_s end def cache=(cache) @cache = cache end def define_remote_method(name, args = {}) @remote_defaults ||= {} args[:method] ||= @remote_defaults[:method] args[:on_success] ||= @remote_defaults[:on_success] args[:on_failure] ||= @remote_defaults[:on_failure] args[:base_uri] ||= @remote_defaults[:base_uri] args[:path] ||= @remote_defaults[:path] m = RemoteMethod.new(args) @remote_methods ||= {} @remote_methods[name] = m class_eval <<-SRC def self.#{name.to_s}(args = {}) call_remote_method(:#{name.to_s}, args) end SRC end private def encode_nil_response(response) response == nil ? :__nil__ : response end def decode_nil_response(response) response == :__nil__ ? nil : response end end # ClassMethods end