lib/goliath/test_helper.rb



require 'em-synchrony'
require 'em-synchrony/em-http'

require 'goliath/api'
require 'goliath/server'
require 'goliath/rack'
require 'rack'

module Goliath
  # Methods to help with testing Goliath APIs
  #
  # @example
  #   describe Echo do
  #     include Goliath::TestHelper
  #
  #     let(:err) { Proc.new { fail "API request failed" } }
  #     it 'returns the echo param' do
  #       with_api(Echo) do
  #         get_request({:query => {:echo => 'test'}}, err) do |c|
  #           b = MultiJson.load(c.response)
  #           b['response'].should == 'test'
  #         end
  #       end
  #     end
  #   end
  #
  module TestHelper
    def self.included(mod)
      Goliath.env = :test
    end

    # Launches an instance of a given API server. The server
    # will launch on the specified port.
    #
    # @param api [Class] The API class to launch
    # @param port [Integer] The port to run the server on
    # @param options [Hash] The options hash to provide to the server
    # @return [Goliath::Server] The executed server
    def server(api, port, options = {}, &blk)
      op = OptionParser.new

      s = Goliath::Server.new
      s.logger = setup_logger(options)
      s.api = api.new
      s.app = Goliath::Rack::Builder.build(api, s.api)
      s.api.options_parser(op, options)
      s.options = options
      s.port = port
      s.plugins = api.plugins
      @test_server_port = s.port if blk
      s.start(&blk)
      s
    end

    def setup_logger(opts)
      return fake_logger if opts[:log_file].nil? && opts[:log_stdout].nil?

      log = Log4r::Logger.new('goliath')
      log_format = Log4r::PatternFormatter.new(:pattern => "[#{Process.pid}:%l] %d :: %m")
      log.level = opts[:verbose].nil? ? Log4r::INFO : Log4r::DEBUG

      if opts[:log_stdout]
        log.add(Log4r::StdoutOutputter.new('console', :formatter => log_format))
      elsif opts[:log_file]
        file = opts[:log_file]
        FileUtils.mkdir_p(File.dirname(file))

       log.add(Log4r::FileOutputter.new('fileOutput', {:filename => file,
                                                       :trunc => false,
                                                       :formatter => log_format}))
      end
      log
    end

    # Stops the launched API
    #
    # @return [Nil]
    def stop
      EM.stop_event_loop
    end

    # Wrapper for launching API and executing given code block. This
    # will start the EventMachine reactor running.
    #
    # @param api [Class] The API class to launch
    # @param options [Hash] The options to pass to the server
    # @param blk [Proc] The code to execute after the server is launched.
    # @note This will not return until stop is called.
    def with_api(api, options = {}, &blk)
      server(api, options.delete(:port) || 9900, options, &blk)
    end

    # Helper method to setup common callbacks for various request methods.
    # The given err and callback handlers will be attached and a callback
    # to stop the reactor will be added.
    #
    # @param req [EM::HttpRequest] The HTTP request to augment
    # @param errback [Proc] An error handler to attach
    # @param blk [Proc] The callback handler to attach
    # @return [Nil]
    # @api private
    def hookup_request_callbacks(req, errback, &blk)
      req.callback &blk
      req.callback { stop }

      req.errback &errback if errback
      req.errback { stop }
    end

    # Make a HEAD request against the currently launched API.
    #
    # @param request_data [Hash] Any data to pass to the HEAD request.
    # @param errback [Proc] An error handler to attach
    # @param blk [Proc] The callback block to execute
    def head_request(request_data = {}, errback = nil, &blk)
      req = create_test_request(request_data).head(request_data)
      hookup_request_callbacks(req, errback, &blk)
    end

    # Make a GET request against the currently launched API.
    #
    # @param request_data [Hash] Any data to pass to the GET request.
    # @param errback [Proc] An error handler to attach
    # @param blk [Proc] The callback block to execute
    def get_request(request_data = {}, errback = nil, &blk)
      req = create_test_request(request_data).get(request_data)
      hookup_request_callbacks(req, errback, &blk)
    end

    # Make a POST request against the currently launched API.
    #
    # @param request_data [Hash] Any data to pass to the POST request.
    # @param errback [Proc] An error handler to attach
    # @param blk [Proc] The callback block to execute
    def post_request(request_data = {}, errback = nil, &blk)
      req = create_test_request(request_data).post(request_data)
      hookup_request_callbacks(req, errback, &blk)
    end

    # Make a PUT request the currently launched API.
    #
    # @param request_data [Hash] Any data to pass to the PUT request.
    # @param errback [Proc] An error handler to attach
    # @param blk [Proc] The callback block to execute
    def put_request(request_data = {}, errback = nil, &blk)
      req = create_test_request(request_data).put(request_data)
      hookup_request_callbacks(req, errback, &blk)
    end

    # Make a PATCH request against the currently launched API.
    #
    # @param request_data [Hash] Any data to pass to the PUT request.
    # @param errback [Proc] An error handler to attach
    # @param blk [Proc] The callback block to execute
    def patch_request(request_data = {}, errback = nil, &blk)
      req = create_test_request(request_data).patch(request_data)
      hookup_request_callbacks(req, errback, &blk)
    end

    # Make a DELETE request against the currently launched API.
    #
    # @param request_data [Hash] Any data to pass to the DELETE request.
    # @param errback [Proc] An error handler to attach
    # @param blk [Proc] The callback block to execute
    def delete_request(request_data = {}, errback = nil, &blk)
      req = create_test_request(request_data).delete(request_data)
      hookup_request_callbacks(req, errback, &blk)
    end

    # Make an OPTIONS request against the currently launched API.
    #
    # @param request_data [Hash] Any data to pass to the OPTIONS request.
    # @param errback [Proc] An error handler to attach
    # @param blk [Proc] The callback block to execute
    def options_request(request_data = {}, errback = nil, &blk)
      req = create_test_request(request_data).options(request_data)
      hookup_request_callbacks(req, errback, &blk)
    end

    def create_test_request(request_data)
      path = request_data.delete(:path) || ''
      opts = request_data.delete(:connection_options) || {}
      EM::HttpRequest.new("http://localhost:#{@test_server_port}#{path}", opts)
    end

    private

    def fake_logger
      Class.new do
        def method_missing(name, *args, &blk)
          nil
        end
      end.new
    end
  end
end