require 'rubygems'
require 'minitest/autorun'
require 'net/http/persistent'
require 'stringio'

HAVE_OPENSSL = defined?(OpenSSL::SSL)

module Net::HTTP::Persistent::TestConnect
  def self.included mod
    mod.send :alias_method, :orig_connect, :connect

    def mod.use_connect which
      self.send :remove_method, :connect
      self.send :alias_method, :connect, which
    end
  end

  def host_down_connect
    raise Errno::EHOSTDOWN
  end

  def test_connect
    unless use_ssl? then
      io = Object.new
      def io.setsockopt(*a) @setsockopts ||= []; @setsockopts << a end

      @socket = Net::BufferedIO.new io

      return
    end

    io = open '/dev/null'
    def io.setsockopt(*a) @setsockopts ||= []; @setsockopts << a end

    @ssl_context ||= OpenSSL::SSL::SSLContext.new

    @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless
      @ssl_context.verify_mode

    s = OpenSSL::SSL::SSLSocket.new io, @ssl_context

    @socket = Net::BufferedIO.new s
  end

  def refused_connect
    raise Errno::ECONNREFUSED
  end
end

class Net::HTTP
  include Net::HTTP::Persistent::TestConnect
end

class TestNetHttpPersistent < Minitest::Test

  def setup
    @http = Net::HTTP::Persistent.new

    @uri  = URI.parse 'http://example.com/path'

    ENV.delete 'http_proxy'
    ENV.delete 'HTTP_PROXY'
    ENV.delete 'http_proxy_user'
    ENV.delete 'HTTP_PROXY_USER'
    ENV.delete 'http_proxy_pass'
    ENV.delete 'HTTP_PROXY_PASS'
    ENV.delete 'no_proxy'
    ENV.delete 'NO_PROXY'

    Net::HTTP.use_connect :test_connect
  end

  def teardown
    Net::HTTP.use_connect :orig_connect
  end

  class BasicConnection
    attr_accessor :started, :finished, :address, :port, :use_ssl,
                  :read_timeout, :open_timeout, :keep_alive_timeout
    attr_accessor :ciphers, :ssl_timeout, :ssl_version, :min_version,
                  :max_version, :verify_depth, :verify_mode, :cert_store,
                  :ca_file, :ca_path, :cert, :key
    attr_reader :req, :debug_output
    def initialize
      @started, @finished = 0, 0
      @address, @port = 'example.com', 80
      @use_ssl = false
    end
    def finish
      @finished += 1
      @socket = nil
    end
    def finished?
      @finished >= 1
    end
    def pipeline requests, &block
      requests.map { |r| r.path }
    end
    def reset?
      @started == @finished + 1
    end
    def set_debug_output io
      @debug_output = io
    end
    def start
      @started += 1
      io = Object.new
      def io.setsockopt(*a) @setsockopts ||= []; @setsockopts << a end
      @socket = Net::BufferedIO.new io
    end
    def started?
      @started >= 1
    end
    def proxy_address
    end
    def proxy_port
    end
  end

  def basic_connection
    raise "#{@uri} is not HTTP" unless @uri.scheme.downcase == 'http'

    net_http_args = [@uri.host, @uri.port, nil, nil, nil, nil]

    connection = Net::HTTP::Persistent::Connection.allocate
    connection.ssl_generation = @http.ssl_generation
    connection.http = BasicConnection.new
    connection.reset

    @http.pool.available.push connection, connection_args: net_http_args

    connection
  end

  def connection
    connection = basic_connection
    connection.last_use = Time.now

    def (connection.http).request(req)
      @req = req
      r = Net::HTTPResponse.allocate
      r.instance_variable_set :@header, {}
      def r.http_version() '1.1' end
      def r.read_body() :read_body end
      yield r if block_given?
      r
    end

    connection
  end

  def ssl_connection
    raise "#{@uri} is not HTTPS" unless @uri.scheme.downcase == 'https'

    net_http_args = [@uri.host, @uri.port, nil, nil, nil, nil]

    connection = Net::HTTP::Persistent::Connection.allocate
    connection.ssl_generation = @http.ssl_generation
    connection.http = BasicConnection.new
    connection.reset

    @http.pool.available.push connection, connection_args: net_http_args

    connection
  end

  def test_initialize
    assert_nil @http.proxy_uri

    assert_empty @http.no_proxy

    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    ssl_session_exists = OpenSSL::SSL.const_defined? :Session

    assert_equal ssl_session_exists, @http.reuse_ssl_sessions
  end

  def test_initialize_name
    http = Net::HTTP::Persistent.new name: 'name'
    assert_equal 'name', http.name
  end

  def test_initialize_no_ssl_session
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    skip "OpenSSL::SSL::Session does not exist on #{RUBY_PLATFORM}" unless
      OpenSSL::SSL.const_defined? :Session

    ssl_session = OpenSSL::SSL::Session

    OpenSSL::SSL.send :remove_const, :Session

    http = Net::HTTP::Persistent.new

    refute http.reuse_ssl_sessions
  ensure
    OpenSSL::SSL.const_set :Session, ssl_session if ssl_session
  end

  def test_initialize_proxy
    proxy_uri = URI.parse 'http://proxy.example'

    http = Net::HTTP::Persistent.new proxy: proxy_uri

    assert_equal proxy_uri, http.proxy_uri
  end

  def test_ca_file_equals
    @http.ca_file = :ca_file

    assert_equal :ca_file, @http.ca_file
    assert_equal 1, @http.ssl_generation
  end

  def test_ca_path_equals
    @http.ca_path = :ca_path

    assert_equal :ca_path, @http.ca_path
    assert_equal 1, @http.ssl_generation
  end

  def test_can_retry_eh_change_requests
    post = Net::HTTP::Post.new '/'

    refute @http.can_retry? post

    @http.retry_change_requests = true

    assert @http.can_retry? post
  end

  def test_can_retry_eh_idempotent
    head = Net::HTTP::Head.new '/'

    refute @http.can_retry? head

    post = Net::HTTP::Post.new '/'

    refute @http.can_retry? post
  end

  def test_cert_store_equals
    @http.cert_store = :cert_store

    assert_equal :cert_store, @http.cert_store
    assert_equal 1, @http.ssl_generation
  end

  def test_certificate_equals
    @http.certificate = :cert

    assert_equal :cert, @http.certificate
    assert_equal 1, @http.ssl_generation
  end

  def test_ciphers_equals
    @http.ciphers = :ciphers

    assert_equal :ciphers, @http.ciphers
    assert_equal 1, @http.ssl_generation
  end

  def test_connection_for
    @http.open_timeout = 123
    @http.read_timeout = 321
    @http.idle_timeout = 42

    used = @http.connection_for @uri do |c|
      assert_kind_of Net::HTTP, c.http

      assert c.http.started?
      refute c.http.proxy?

      assert_equal 123, c.http.open_timeout
      assert_equal 321, c.http.read_timeout
      assert_equal 42, c.http.keep_alive_timeout

      c
    end

    stored = @http.pool.checkout ['example.com', 80, nil, nil, nil, nil]

    assert_same used, stored
  end

  def test_connection_for_cached
    cached = basic_connection
    cached.http.start

    @http.read_timeout = 5

    @http.connection_for @uri do |c|
      assert c.http.started?

      assert_equal 5, c.http.read_timeout

      assert_same cached, c
    end
  end

  def test_connection_for_closed
    cached = basic_connection
    cached.http.start
    if Socket.const_defined? :TCP_NODELAY then
      io = Object.new
      def io.setsockopt(*a) raise IOError, 'closed stream' end
      cached.instance_variable_set :@socket, Net::BufferedIO.new(io)
    end

    @http.connection_for @uri do |c|
      assert c.http.started?

      socket = c.http.instance_variable_get :@socket

      refute_includes socket.io.instance_variables, :@setsockopt
      refute_includes socket.io.instance_variables, '@setsockopt'
    end
  end

  def test_connection_for_debug_output
    io = StringIO.new
    @http.debug_output = io

    @http.connection_for @uri do |c|
      assert c.http.started?
      assert_equal io, c.http.instance_variable_get(:@debug_output)
    end
  end

  def test_connection_for_cached_expire_always
    cached = basic_connection
    cached.http.start
    cached.requests = 10
    cached.last_use = Time.now # last used right now

    @http.idle_timeout = 0

    @http.connection_for @uri do |c|
      assert c.http.started?

      assert_same cached, c

      assert_equal 0, c.requests, 'connection reset due to timeout'
    end
  end

  def test_connection_for_cached_expire_never
    cached = basic_connection
    cached.http.start
    cached.requests = 10
    cached.last_use = Time.now # last used right now

    @http.idle_timeout = nil

    @http.connection_for @uri do |c|
      assert c.http.started?

      assert_same cached, c

      assert_equal 10, c.requests, 'connection reset despite no timeout'
    end
  end

  def test_connection_for_cached_expired
    cached = basic_connection
    cached.http.start
    cached.requests = 10
    cached.last_use = Time.now - 3600

    @http.connection_for @uri do |c|
      assert c.http.started?

      assert_same cached, c
      assert_equal 0, cached.requests, 'connection not reset due to timeout'
    end
  end

  def test_connection_for_finished_ssl
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    uri = URI.parse 'https://example.com/path'

    @http.connection_for uri do |c|
      assert c.http.started?
      assert c.http.use_ssl?

      @http.finish c

      refute c.http.started?
    end

    @http.connection_for uri do |c2|
      assert c2.http.started?
    end
  end

  def test_connection_for_ipv6
    uri = URI.parse 'https://[::1]/'

    @http.connection_for uri do |c|
      assert_equal '::1', c.http.address
    end
  end

  def test_connection_for_host_down
    c = basic_connection
    def (c.http).start; raise Errno::EHOSTDOWN end
    def (c.http).started?; false end

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.connection_for @uri do end
    end

    assert_equal 'host down: example.com:80', e.message
  end

  def test_connection_for_http_class_with_fakeweb
    Object.send :const_set, :FakeWeb, nil

    @http.connection_for @uri do |c|
      assert_instance_of Net::HTTP, c.http
    end
  ensure
    if Object.const_defined?(:FakeWeb) then
      Object.send :remove_const, :FakeWeb
    end
  end

  def test_connection_for_http_class_with_webmock
    Object.send :const_set, :WebMock, nil
    @http.connection_for @uri do |c|
      assert_instance_of Net::HTTP, c.http
    end
  ensure
    if Object.const_defined?(:WebMock) then
      Object.send :remove_const, :WebMock
    end
  end

  def test_connection_for_http_class_with_artifice
    Object.send :const_set, :Artifice, nil
    @http.connection_for @uri do |c|
      assert_instance_of Net::HTTP, c.http
    end
  ensure
    if Object.const_defined?(:Artifice) then
      Object.send :remove_const, :Artifice
    end
  end

  def test_connection_for_name
    http = Net::HTTP::Persistent.new name: 'name'
    uri = URI.parse 'http://example/'

    http.connection_for uri do |c|
      assert c.http.started?
    end
  end

  def test_connection_for_proxy
    uri = URI.parse 'http://proxy.example'
    uri.user     = 'johndoe'
    uri.password = 'muffins'

    http = Net::HTTP::Persistent.new proxy: uri

    used = http.connection_for @uri do |c|
      assert c.http.started?
      assert c.http.proxy?

      c
    end

    stored = http.pool.checkout ['example.com', 80,
                                 'proxy.example', 80,
                                 'johndoe', 'muffins']

    assert_same used, stored
  end

  def test_connection_for_proxy_unescaped
    uri = URI.parse 'http://proxy.example'
    uri.user = 'john%40doe'
    uri.password = 'muf%3Afins'
    uri.freeze

    http = Net::HTTP::Persistent.new proxy: uri

    http.connection_for @uri do end

    stored = http.pool.checkout ['example.com', 80,
                                 'proxy.example', 80,
                                 'john@doe', 'muf:fins']

    assert stored
  end

  def test_connection_for_proxy_host_down
    Net::HTTP.use_connect :host_down_connect

    uri = URI.parse 'http://proxy.example'
    uri.user     = 'johndoe'
    uri.password = 'muffins'

    http = Net::HTTP::Persistent.new proxy: uri

    e = assert_raises Net::HTTP::Persistent::Error do
      http.connection_for @uri do end
    end

    assert_equal 'host down: proxy.example:80', e.message
  end

  def test_connection_for_proxy_refused
    Net::HTTP.use_connect :refused_connect

    uri = URI.parse 'http://proxy.example'
    uri.user     = 'johndoe'
    uri.password = 'muffins'

    http = Net::HTTP::Persistent.new proxy: uri

    e = assert_raises Net::HTTP::Persistent::Error do
      http.connection_for @uri do end
    end

    assert_equal 'connection refused: proxy.example:80', e.message
  end

  def test_connection_for_no_proxy
    uri = URI.parse 'http://proxy.example'
    uri.user     = 'johndoe'
    uri.password = 'muffins'
    uri.query    = 'no_proxy=example.com'

    http = Net::HTTP::Persistent.new proxy: uri

    http.connection_for @uri do |c|
      assert c.http.started?
      refute c.http.proxy?
    end

    stored = http.pool.checkout ['example.com', 80]

    assert stored
  end

  def test_connection_for_no_proxy_from_env
    ENV['http_proxy'] = 'proxy.example'
    ENV['no_proxy'] = 'localhost, example.com,'
    ENV['proxy_user'] = 'johndoe'
    ENV['proxy_password'] = 'muffins'

    http = Net::HTTP::Persistent.new proxy: :ENV

    http.connection_for @uri do |c|
      assert c.http.started?
      refute c.http.proxy?
      refute c.http.proxy_from_env?
    end
  end

  def test_connection_for_refused
    Net::HTTP.use_connect :refused_connect

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.connection_for @uri do end
    end

    assert_equal 'connection refused: example.com:80', e.message
  end

  def test_connection_for_ssl
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    uri = URI.parse 'https://example.com/path'

    @http.connection_for uri do |c|
      assert c.http.started?
      assert c.http.use_ssl?
    end
  end

  def test_connection_for_ssl_cached
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    @uri = URI.parse 'https://example.com/path'

    cached = ssl_connection

    @http.connection_for @uri do |c|
      assert_same cached, c
    end
  end

  def test_connection_for_ssl_cached_reconnect
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    @uri = URI.parse 'https://example.com/path'

    cached = ssl_connection

    ssl_generation = @http.ssl_generation

    @http.reconnect_ssl

    @http.connection_for @uri do |c|
      assert_same cached, c
      refute_equal ssl_generation, c.ssl_generation
    end
  end

  def test_connection_for_ssl_case
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    uri = URI.parse 'HTTPS://example.com/path'
    @http.connection_for uri do |c|
      assert c.http.started?
      assert c.http.use_ssl?
    end
  end

  def test_connection_for_timeout
    cached = basic_connection
    cached.http.start
    cached.requests = 10
    cached.last_use = Time.now - 6

    @http.connection_for @uri do |c|
      assert c.http.started?
      assert_equal 0, c.requests

      assert_same cached, c
    end
  end

  def test_error_message
    c = basic_connection
    c.last_use = Time.now - 1
    c.requests = 5

    message = @http.error_message c
    assert_match %r%after 4 requests on #{c.http.object_id}%, message
    assert_match %r%, last used [\d.]+ seconds ago%, message
  end

  def test_escape
    assert_nil @http.escape nil

    assert_equal '+%3F', @http.escape(' ?')
  end

  def test_unescape
    assert_nil @http.unescape nil

    assert_equal ' ?', @http.unescape('+%3F')
  end

  def test_expired_eh
    c = basic_connection
    c.requests = 0
    c.last_use = Time.now - 11

    @http.idle_timeout = 0
    assert @http.expired? c

    @http.idle_timeout = 10
    assert @http.expired? c

    @http.idle_timeout = 11
    assert @http.expired? c

    @http.idle_timeout = 12
    refute @http.expired? c

    @http.idle_timeout = nil
    refute @http.expired? c
  end

  def test_expired_due_to_max_requests
    c = basic_connection
    c.requests = 0
    c.last_use = Time.now

    refute @http.expired? c

    c.requests = 10
    refute @http.expired? c

    @http.max_requests = 10
    assert @http.expired? c

    c.requests = 9
    refute @http.expired? c
  end

  def test_finish
    c = basic_connection
    c.requests = 5

    @http.finish c

    refute c.http.started?
    assert c.http.finished?

    assert_equal 0, c.requests
    assert_equal Net::HTTP::Persistent::EPOCH, c.last_use
  end

  def test_finish_io_error
    c = basic_connection
    def (c.http).finish; @finished += 1; raise IOError end
    c.requests = 5

    @http.finish c

    refute c.http.started?
    assert c.http.finished?
  end

  def test_finish_ssl_no_session_reuse
    http = Net::HTTP.new 'localhost', 443, ssl: true
    http.instance_variable_set :@ssl_session, :something

    c = Net::HTTP::Persistent::Connection.allocate
    c.instance_variable_set :@http, http

    @http.reuse_ssl_sessions = false

    @http.finish c

    assert_nil c.http.instance_variable_get :@ssl_session
  end

  def test_http_version
    assert_nil @http.http_version @uri

    connection

    @http.request @uri

    assert_equal '1.1', @http.http_version(@uri)
  end

  def test_idempotent_eh
    assert @http.idempotent? Net::HTTP::Delete.new '/'
    assert @http.idempotent? Net::HTTP::Get.new '/'
    assert @http.idempotent? Net::HTTP::Head.new '/'
    assert @http.idempotent? Net::HTTP::Options.new '/'
    assert @http.idempotent? Net::HTTP::Put.new '/'
    assert @http.idempotent? Net::HTTP::Trace.new '/'

    assert @http.idempotent? Net::HTTPGenericRequest.new('DELETE', false, true, '/')
    assert @http.idempotent? Net::HTTPGenericRequest.new('GET', false, true, '/')
    assert @http.idempotent? Net::HTTPGenericRequest.new('HEAD', false, false, '/')
    assert @http.idempotent? Net::HTTPGenericRequest.new('OPTIONS', false, false, '/')
    assert @http.idempotent? Net::HTTPGenericRequest.new('PUT', true, true, '/')
    assert @http.idempotent? Net::HTTPGenericRequest.new('TRACE', false, true, '/')

    refute @http.idempotent? Net::HTTP::Post.new '/'

    refute @http.idempotent? Net::HTTPGenericRequest.new('POST', true, true, '/')
  end

  def test_normalize_uri
    assert_equal 'http://example',  @http.normalize_uri('example')
    assert_equal 'http://example',  @http.normalize_uri('http://example')
    assert_equal 'https://example', @http.normalize_uri('https://example')
  end

  def test_override_haeders
    assert_empty @http.override_headers

    @http.override_headers['User-Agent'] = 'MyCustomAgent'

    expected = { 'User-Agent' => 'MyCustomAgent' }

    assert_equal expected, @http.override_headers
  end

  def test_pipeline
    skip 'net-http-pipeline not installed' unless defined?(Net::HTTP::Pipeline)

    cached = basic_connection
    cached.http.start

    requests = [
      Net::HTTP::Get.new((@uri + '1').request_uri),
      Net::HTTP::Get.new((@uri + '2').request_uri),
    ]

    responses = @http.pipeline @uri, requests

    assert_equal 2, responses.length
    assert_equal '/1', responses.first
    assert_equal '/2', responses.last
  end

  def test_private_key_equals
    @http.private_key = :private_key

    assert_equal :private_key, @http.private_key
    assert_equal 1, @http.ssl_generation
  end

  def test_proxy_equals_env
    ENV['http_proxy'] = 'proxy.example'

    @http.proxy = :ENV

    assert_equal URI.parse('http://proxy.example'), @http.proxy_uri

    assert_equal 1, @http.generation, 'generation'
    assert_equal 1, @http.ssl_generation, 'ssl_generation'
  end

  def test_proxy_equals_nil
    @http.proxy = nil

    assert_nil @http.proxy_uri

    assert_equal 1, @http.generation, 'generation'
    assert_equal 1, @http.ssl_generation, 'ssl_generation'
  end

  def test_proxy_equals_uri
    proxy_uri = URI.parse 'http://proxy.example'

    @http.proxy = proxy_uri

    assert_equal proxy_uri, @http.proxy_uri
  end

  def test_proxy_from_env
    ENV['http_proxy']      = 'proxy.example'
    ENV['http_proxy_user'] = 'johndoe'
    ENV['http_proxy_pass'] = 'muffins'
    ENV['NO_PROXY']        = 'localhost,example.com'

    uri = @http.proxy_from_env

    expected = URI.parse 'http://proxy.example'
    expected.user     = 'johndoe'
    expected.password = 'muffins'
    expected.query    = 'no_proxy=localhost%2Cexample.com'

    assert_equal expected, uri
  end

  def test_proxy_from_env_lower
    ENV['http_proxy']      = 'proxy.example'
    ENV['http_proxy_user'] = 'johndoe'
    ENV['http_proxy_pass'] = 'muffins'
    ENV['no_proxy']        = 'localhost,example.com'

    uri = @http.proxy_from_env

    expected = URI.parse 'http://proxy.example'
    expected.user     = 'johndoe'
    expected.password = 'muffins'
    expected.query    = 'no_proxy=localhost%2Cexample.com'

    assert_equal expected, uri
  end

  def test_proxy_from_env_nil
    uri = @http.proxy_from_env

    assert_nil uri

    ENV['http_proxy'] = ''

    uri = @http.proxy_from_env

    assert_nil uri
  end

  def test_proxy_from_env_no_proxy_star
    uri = @http.proxy_from_env

    assert_nil uri

    ENV['http_proxy'] = 'proxy.example'
    ENV['no_proxy'] = '*'

    uri = @http.proxy_from_env

    assert_nil uri
  end

  def test_proxy_bypass
    ENV['http_proxy'] = 'proxy.example'
    ENV['no_proxy'] = 'localhost,example.com:80'

    @http.proxy = :ENV

    assert @http.proxy_bypass? 'localhost', 80
    assert @http.proxy_bypass? 'localhost', 443
    assert @http.proxy_bypass? 'LOCALHOST', 80
    assert @http.proxy_bypass? 'example.com', 80
    refute @http.proxy_bypass? 'example.com', 443
    assert @http.proxy_bypass? 'www.example.com', 80
    refute @http.proxy_bypass? 'www.example.com', 443
    assert @http.proxy_bypass? 'endingexample.com', 80
    refute @http.proxy_bypass? 'example.org', 80
  end

  def test_proxy_bypass_space
    ENV['http_proxy'] = 'proxy.example'
    ENV['no_proxy'] = 'localhost, example.com'

    @http.proxy = :ENV

    assert @http.proxy_bypass? 'example.com', 80
    refute @http.proxy_bypass? 'example.org', 80
  end

  def test_proxy_bypass_trailing
    ENV['http_proxy'] = 'proxy.example'
    ENV['no_proxy'] = 'localhost,example.com,'

    @http.proxy = :ENV

    assert @http.proxy_bypass? 'example.com', 80
    refute @http.proxy_bypass? 'example.org', 80
  end

  def test_proxy_bypass_double_comma
    ENV['http_proxy'] = 'proxy.example'
    ENV['no_proxy'] = 'localhost,,example.com'

    @http.proxy = :ENV

    assert @http.proxy_bypass? 'example.com', 80
    refute @http.proxy_bypass? 'example.org', 80
  end

  def test_reconnect
    result = @http.reconnect

    assert_equal 1, result
  end

  def test_reconnect_ssl
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    @uri = URI 'https://example.com'
    now = Time.now

    ssl_http = ssl_connection

    def (ssl_http.http).finish
      @started = 0
    end

    used1 = @http.connection_for @uri do |c|
      c.requests = 1
      c.last_use = now
      c
    end

    assert_equal OpenSSL::SSL::VERIFY_PEER, used1.http.verify_mode

    @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    @http.reconnect_ssl

    used2 = @http.connection_for @uri do |c|
      c
    end

    assert_same used1, used2

    assert_equal OpenSSL::SSL::VERIFY_NONE, used2.http.verify_mode,
                 'verify mode must change'
    assert_equal 0, used2.requests
    assert_equal Net::HTTP::Persistent::EPOCH, used2.last_use
  end

  def test_request
    @http.override_headers['user-agent'] = 'test ua'
    @http.headers['accept'] = 'text/*'
    c = connection

    res = @http.request @uri
    req = c.http.req

    assert_kind_of Net::HTTPResponse, res

    assert_kind_of Net::HTTP::Get, req
    assert_equal '/path',      req.path

    assert_equal 'test ua',    req['user-agent']
    assert_match %r%text/\*%,  req['accept']

    assert_equal 'keep-alive', req['connection']
    assert_equal '30',         req['keep-alive']

    assert_in_delta Time.now, c.last_use

    assert_equal 1, c.requests
  end

  def test_request_ETIMEDOUT
    c = basic_connection
    def (c.http).request(*a) raise Errno::ETIMEDOUT, "timed out" end

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.request @uri
    end

    assert_equal 0, c.requests
    assert_match %r%too many connection resets%, e.message
  end

  def test_request_bad_response
    c = basic_connection
    def (c.http).request(*a) raise Net::HTTPBadResponse end

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.request @uri
    end

    assert_equal 0, c.requests
    assert_match %r%too many bad responses%, e.message
  end

  def test_request_bad_response_retry
    c = basic_connection
    def (c.http).request(*a)
      raise Net::HTTPBadResponse
    end

    assert_raises Net::HTTP::Persistent::Error do
      @http.request @uri
    end

    assert c.http.finished?
  end

  def test_request_bad_response_unsafe
    c = basic_connection
    def (c.http).request(*a)
      if instance_variable_defined? :@request then
        raise 'POST must not be retried'
      else
        @request = true
        raise Net::HTTPBadResponse
      end
    end

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.request @uri, Net::HTTP::Post.new(@uri.path)
    end

    assert_equal 0, c.requests
    assert_match %r%too many bad responses%, e.message
  end

  def test_request_block
    @http.headers['user-agent'] = 'test ua'
    c = connection
    body = nil

    res = @http.request @uri do |r|
      body = r.read_body
    end

    req = c.http.req

    assert_kind_of Net::HTTPResponse, res
    refute_nil body

    assert_kind_of Net::HTTP::Get, req
    assert_equal '/path',      req.path
    assert_equal 'keep-alive', req['connection']
    assert_equal '30',         req['keep-alive']
    assert_match %r%test ua%,  req['user-agent']

    assert_equal 1, c.requests
  end

  def test_request_close_1_0
    c = connection

    class << c.http
      remove_method :request
    end

    def (c.http).request req
      @req = req
      r = Net::HTTPResponse.allocate
      r.instance_variable_set :@header, {}
      def r.http_version() '1.0' end
      def r.read_body() :read_body end
      yield r if block_given?
      r
    end

    request = Net::HTTP::Get.new @uri.request_uri

    res = @http.request @uri, request
    req = c.http.req

    assert_kind_of Net::HTTPResponse, res

    assert_kind_of Net::HTTP::Get, req
    assert_equal '/path',      req.path
    assert_equal 'keep-alive', req['connection']
    assert_equal '30',         req['keep-alive']

    assert c.http.finished?
  end

  def test_request_connection_close_request
    c = connection

    request = Net::HTTP::Get.new @uri.request_uri
    request['connection'] = 'close'

    res = @http.request @uri, request
    req = c.http.req

    assert_kind_of Net::HTTPResponse, res

    assert_kind_of Net::HTTP::Get, req
    assert_equal '/path',      req.path
    assert_equal 'close',      req['connection']
    assert_nil req['keep-alive']

    assert c.http.finished?
  end

  def test_request_connection_close_response
    c = connection

    class << c.http
      remove_method :request
    end

    def (c.http).request req
      @req = req
      r = Net::HTTPResponse.allocate
      r.instance_variable_set :@header, {}
      r['connection'] = 'close'
      def r.http_version() '1.1' end
      def r.read_body() :read_body end
      yield r if block_given?
      r
    end

    request = Net::HTTP::Get.new @uri.request_uri

    res = @http.request @uri, request
    req = c.http.req

    assert_kind_of Net::HTTPResponse, res

    assert_kind_of Net::HTTP::Get, req
    assert_equal '/path',      req.path
    assert_equal 'keep-alive', req['connection']
    assert_equal '30',         req['keep-alive']

    assert c.http.finished?
  end

  def test_request_exception
    c = basic_connection
    def (c.http).request(*a)
      raise Exception, "very bad things happened"
    end

    assert_raises Exception do
      @http.request @uri
    end

    assert_equal 0, c.requests
    assert c.http.finished?
  end

  def test_request_invalid
    c = basic_connection
    def (c.http).request(*a) raise Errno::EINVAL, "write" end

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.request @uri
    end

    assert_equal 0, c.requests
    assert_match %r%too many connection resets%, e.message
  end

  def test_request_post
    c = connection

    post = Net::HTTP::Post.new @uri.path

    @http.request @uri, post
    req = c.http.req

    assert_same post, req
  end

  def test_request_reset
    c = basic_connection
    def (c.http).request(*a) raise Errno::ECONNRESET end

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.request @uri
    end

    assert_equal 0, c.requests
    assert_match %r%too many connection resets%, e.message
  end

  def test_request_reset_retry
    c = basic_connection
    c.last_use = Time.now

    def (c.http).request(*a)
      raise Errno::ECONNRESET
    end

    assert_raises Net::HTTP::Persistent::Error do
      @http.request @uri
    end

    refute (c.http).reset?
    assert (c.http).finished?
  end

  def test_request_reset_unsafe
    c = basic_connection
    def (c.http).request(*a)
      if instance_variable_defined? :@request then
        raise 'POST must not be retried'
      else
        @request = true
        raise Errno::ECONNRESET
      end
    end

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.request @uri, Net::HTTP::Post.new(@uri.path)
    end

    assert_equal 0, c.requests
    assert_match %r%too many connection resets%, e.message
  end

  def test_request_ssl_error
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    uri = URI.parse 'https://example.com/path'
    @http.connection_for uri do |c|
      def (c.http).request(*)
        raise OpenSSL::SSL::SSLError, "SSL3_WRITE_PENDING:bad write retry"
      end

      e = assert_raises Net::HTTP::Persistent::Error do
        @http.request uri
      end
      assert_match %r%bad write retry%, e.message
    end
  end

  def test_request_setup
    @http.override_headers['user-agent'] = 'test ua'
    @http.headers['accept'] = 'text/*'

    input = Net::HTTP::Post.new '/path'

    req = @http.request_setup input

    assert_same input,         req
    assert_equal '/path',      req.path

    assert_equal 'test ua',    req['user-agent']
    assert_match %r%text/\*%,  req['accept']

    assert_equal 'keep-alive', req['connection']
    assert_equal '30',         req['keep-alive']
  end

  def test_request_string
    @http.override_headers['user-agent'] = 'test ua'
    @http.headers['accept'] = 'text/*'
    c = connection

    res = @http.request @uri.to_s
    req = c.http.req

    assert_kind_of Net::HTTPResponse, res

    assert_kind_of Net::HTTP::Get, req
    assert_equal '/path',      req.path

    assert_equal 1, c.requests
  end

  def test_request_setup_uri
    uri = @uri + '?a=b'

    req = @http.request_setup uri

    assert_kind_of Net::HTTP::Get, req
    assert_equal '/path?a=b',  req.path
  end

  def test_request_failed
    c = basic_connection
    c.requests = 1
    c.last_use = Time.now

    original = nil

    begin
      raise 'original'
    rescue => original
    end

    req = Net::HTTP::Get.new '/'

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.request_failed original, req, c
    end

    assert_match "too many connection resets (due to original - RuntimeError)",
                 e.message

    assert_equal original.backtrace, e.backtrace
  end

  def test_reset
    c = basic_connection
    c.http.start
    c.last_use = Time.now
    c.requests  = 5

    @http.reset c

    assert c.http.started?
    assert c.http.finished?
    assert c.http.reset?
    assert_equal 0, c.requests
    assert_equal Net::HTTP::Persistent::EPOCH, c.last_use
  end

  def test_reset_host_down
    c = basic_connection
    c.last_use = Time.now
    def (c.http).start; raise Errno::EHOSTDOWN end
    c.requests = 5

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.reset c
    end

    assert_match %r%host down%, e.message
    assert_match __FILE__, e.backtrace.first
  end

  def test_reset_io_error
    c = basic_connection
    c.last_use = Time.now
    c.requests = 5

    @http.reset c

    assert c.http.started?
    assert c.http.finished?
  end

  def test_reset_refused
    c = basic_connection
    c.last_use = Time.now
    def (c.http).start; raise Errno::ECONNREFUSED end
    c.requests = 5

    e = assert_raises Net::HTTP::Persistent::Error do
      @http.reset c
    end

    assert_match %r%connection refused%, e.message
    assert_match __FILE__, e.backtrace.first
  end

  def test_retry_change_requests_equals
    get  = Net::HTTP::Get.new('/')
    post = Net::HTTP::Post.new('/')

    refute @http.retry_change_requests

    refute @http.can_retry?(get)
    refute @http.can_retry?(post)

    @http.retry_change_requests = true

    assert @http.retry_change_requests

    refute @http.can_retry?(get)
    assert @http.can_retry?(post)
  end

  def test_shutdown
    c = connection

    orig = @http
    @http = Net::HTTP::Persistent.new name: 'name'
    c2 = connection

    orig.shutdown

    @http = orig

    assert c.http.finished?, 'last-generation connection must be finished'
    refute c2.http.finished?, 'present generation connection must not be finished'
  end

  def test_ssl
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    @http.verify_callback = :callback
    c = Net::HTTP.new 'localhost', 80

    @http.ssl c

    assert c.use_ssl?
    assert_equal OpenSSL::SSL::VERIFY_PEER, c.verify_mode
    assert_kind_of OpenSSL::X509::Store,    c.cert_store
    assert_nil c.verify_callback
  end

  def test_ssl_ca_file
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    @http.ca_file = 'ca_file'
    @http.verify_callback = :callback
    c = Net::HTTP.new 'localhost', 80

    @http.ssl c

    assert c.use_ssl?
    assert_equal OpenSSL::SSL::VERIFY_PEER, c.verify_mode
    assert_equal :callback, c.verify_callback
  end

  def test_ssl_ca_path
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    @http.ca_path = 'ca_path'
    @http.verify_callback = :callback
    c = Net::HTTP.new 'localhost', 80

    @http.ssl c

    assert c.use_ssl?
    assert_equal OpenSSL::SSL::VERIFY_PEER, c.verify_mode
    assert_equal :callback, c.verify_callback
  end

  def test_ssl_cert_store
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    store = OpenSSL::X509::Store.new
    @http.cert_store = store

    c = Net::HTTP.new 'localhost', 80

    @http.ssl c

    assert c.use_ssl?
    assert_equal store, c.cert_store
  end

  def test_ssl_cert_store_default
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    @http.verify_mode = OpenSSL::SSL::VERIFY_PEER

    c = Net::HTTP.new 'localhost', 80

    @http.ssl c

    assert c.use_ssl?
    assert c.cert_store
  end

  def test_ssl_certificate
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    @http.certificate = :cert
    @http.private_key = :key
    c = Net::HTTP.new 'localhost', 80

    @http.ssl c

    assert c.use_ssl?
    assert_equal :cert, c.cert
    assert_equal :key,  c.key
  end

  def test_ssl_verify_mode
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    c = Net::HTTP.new 'localhost', 80

    @http.ssl c

    assert c.use_ssl?
    assert_equal OpenSSL::SSL::VERIFY_NONE, c.verify_mode
  end

  def test_ssl_warning
    skip 'OpenSSL is missing' unless HAVE_OPENSSL

    begin
      orig_verify_peer = OpenSSL::SSL::VERIFY_PEER
      OpenSSL::SSL.send :remove_const, :VERIFY_PEER
      OpenSSL::SSL.send :const_set, :VERIFY_PEER, OpenSSL::SSL::VERIFY_NONE

      c = Net::HTTP.new 'localhost', 80

      out, err = capture_io do
        @http.ssl c
      end

      assert_empty out

      assert_match %r%localhost:80%, err
      assert_match %r%I_KNOW_THAT_OPENSSL%, err

      Object.send :const_set, :I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG, nil

      assert_silent do
        @http.ssl c
      end
    ensure
      OpenSSL::SSL.send :remove_const, :VERIFY_PEER
      OpenSSL::SSL.send :const_set, :VERIFY_PEER, orig_verify_peer
      if Object.const_defined?(:I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG) then
        Object.send :remove_const, :I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG
      end
    end
  end

  def test_ssl_timeout_equals
    @http.ssl_timeout = :ssl_timeout

    assert_equal :ssl_timeout, @http.ssl_timeout
    assert_equal 1, @http.ssl_generation
  end

  def test_ssl_version_equals
    @http.ssl_version = :ssl_version

    assert_equal :ssl_version, @http.ssl_version
    assert_equal 1, @http.ssl_generation
  end

  def test_min_version_equals
    @http.min_version = :min_version

    assert_equal :min_version, @http.min_version
    assert_equal 1, @http.ssl_generation
  end

  def test_max_version_equals
    @http.max_version = :max_version

    assert_equal :max_version, @http.max_version
    assert_equal 1, @http.ssl_generation
  end

  def test_start
    c = basic_connection
    c = c.http

    @http.socket_options << [Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1]
    @http.debug_output = $stderr
    @http.open_timeout = 6

    @http.start c

    assert_equal $stderr, c.debug_output
    assert_equal 6,       c.open_timeout

    socket = c.instance_variable_get :@socket

    expected = []
    expected << [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1] if
      Socket.const_defined? :TCP_NODELAY
    expected << [Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1]

    assert_equal expected, socket.io.instance_variable_get(:@setsockopts)
  end

  def test_verify_callback_equals
    @http.verify_callback = :verify_callback

    assert_equal :verify_callback, @http.verify_callback
    assert_equal 1, @http.ssl_generation
  end

  def test_verify_depth_equals
    @http.verify_depth = :verify_depth

    assert_equal :verify_depth, @http.verify_depth
    assert_equal 1, @http.ssl_generation
  end

  def test_verify_mode_equals
    @http.verify_mode = :verify_mode

    assert_equal :verify_mode, @http.verify_mode
    assert_equal 1, @http.ssl_generation
  end

end

