require 'spec_helper'
require 'puppet/http'

describe Puppet::HTTP::Client do
  let(:uri) { URI.parse('https://www.example.com') }
  let(:puppet_context) { Puppet::SSL::SSLContext.new }
  let(:system_context) { Puppet::SSL::SSLContext.new }
  let(:client) { described_class.new(ssl_context: puppet_context, system_ssl_context: system_context) }
  let(:credentials) { ['user', 'pass'] }

  it 'creates unique sessions' do
    expect(client.create_session).to_not eq(client.create_session)
  end

  context "when connecting" do
    it 'connects to HTTP URLs' do
      uri = URI.parse('http://www.example.com')

      client.connect(uri) do |http|
        expect(http.address).to eq('www.example.com')
        expect(http.port).to eq(80)
        expect(http).to_not be_use_ssl
      end
    end

    it 'connects to HTTPS URLs' do
      client.connect(uri) do |http|
        expect(http.address).to eq('www.example.com')
        expect(http.port).to eq(443)
        expect(http).to be_use_ssl
      end
    end

    it 'raises ConnectionError if the connection is refused' do
      allow_any_instance_of(Net::HTTP).to receive(:start).and_raise(Errno::ECONNREFUSED)

      expect {
        client.connect(uri)
      }.to raise_error(Puppet::HTTP::ConnectionError, %r{^Request to https://www.example.com failed after .* seconds: (Connection refused|No connection could be made because the target machine actively refused it)})
    end

    it 'raises ConnectionError if the connect times out' do
      allow_any_instance_of(Net::HTTP).to receive(:start).and_raise(Net::OpenTimeout)

      expect {
        client.connect(uri)
      }.to raise_error(Puppet::HTTP::ConnectionError, %r{^Request to https://www.example.com timed out connect operation after .* seconds})
    end

    it 'connects using the default ssl context' do
      expect(client.pool).to receive(:with_connection) do |_, verifier|
        expect(verifier.ssl_context).to equal(puppet_context)
      end

      client.connect(uri)
    end

    it 'connects using a specified ssl context' do
      other_context = Puppet::SSL::SSLContext.new

      expect(client.pool).to receive(:with_connection) do |_, verifier|
        expect(verifier.ssl_context).to equal(other_context)
      end

      client.connect(uri, options: {ssl_context: other_context})
    end

    it 'connects using the system store' do
      expect(client.pool).to receive(:with_connection) do |_, verifier|
        expect(verifier.ssl_context).to equal(system_context)
      end

      client.connect(uri, options: {include_system_store: true})
    end

    it 'does not create a verifier for HTTP connections' do
      expect(client.pool).to receive(:with_connection) do |_, verifier|
        expect(verifier).to be_nil
      end

      client.connect(URI.parse('http://www.example.com'))
    end

    it 'raises an HTTPError if both are specified' do
      expect {
        client.connect(uri, options: {ssl_context: puppet_context, include_system_store: true})
      }.to raise_error(Puppet::HTTP::HTTPError, /The ssl_context and include_system_store parameters are mutually exclusive/)
    end
  end

  context 'after connecting' do
    def expect_http_error(cause, expected_message)
      expect {
        client.connect(uri) do |_|
          raise cause, 'whoops'
        end
      }.to raise_error(Puppet::HTTP::HTTPError, expected_message)
    end

    it 're-raises HTTPError' do
      expect_http_error(Puppet::HTTP::HTTPError, 'whoops')
    end

    it 'raises HTTPError if connection is interrupted while reading' do
      expect_http_error(EOFError, %r{^Request to https://www.example.com interrupted after .* seconds})
    end

    it 'raises HTTPError if connection times out' do
      expect_http_error(Net::ReadTimeout, %r{^Request to https://www.example.com timed out read operation after .* seconds})
    end

    it 'raises HTTPError if connection fails' do
      expect_http_error(ArgumentError, %r{^Request to https://www.example.com failed after .* seconds})
    end
  end

  context "when closing" do
    it "closes all connections in the pool" do
      expect(client.pool).to receive(:close)

      client.close
    end

    it 'reloads the default ssl context' do
      expect(client.pool).to receive(:with_connection) do |_, verifier|
        expect(verifier.ssl_context).to_not equal(puppet_context)
      end

      client.close
      client.connect(uri)
    end

    it 'reloads the default system ssl context' do
      expect(client.pool).to receive(:with_connection) do |_, verifier|
        expect(verifier.ssl_context).to_not equal(system_context)
      end

      client.close
      client.connect(uri, options: {include_system_store: true})
    end
  end

  context "for GET requests" do
    it "includes default HTTP headers" do
      stub_request(:get, uri).with do |request|
        expect(request.headers).to include({'X-Puppet-Version' => /./, 'User-Agent' => /./})
        expect(request.headers).to_not include('X-Puppet-Profiling')
      end

      client.get(uri)
    end

    it "stringifies keys and encodes values in the query" do
      stub_request(:get, uri).with(query: "foo=bar%3Dbaz")

      client.get(uri, params: {:foo => "bar=baz"})
    end

    it "fails if a user passes in an invalid param type" do
      environment = Puppet::Node::Environment.create(:testing, [])

      expect{client.get(uri, params: {environment: environment})}.to raise_error(Puppet::HTTP::SerializationError, /HTTP REST queries cannot handle values of type/)
    end

    it "merges custom headers with default ones" do
      stub_request(:get, uri).with(headers: { 'X-Foo' => 'Bar', 'X-Puppet-Version' => /./, 'User-Agent' => /./ })

      client.get(uri, headers: {'X-Foo' => 'Bar'})
    end

    it "returns the response" do
      stub_request(:get, uri)

      response = client.get(uri)
      expect(response).to be_a(Puppet::HTTP::Response)
      expect(response).to be_success
      expect(response.code).to eq(200)
    end

    it "returns the entire response body" do
      stub_request(:get, uri).to_return(body: "abc")

      expect(client.get(uri).body).to eq("abc")
    end

    it "streams the response body when a block is given" do
      stub_request(:get, uri).to_return(body: "abc")

      io = StringIO.new
      client.get(uri) do |response|
        response.read_body do |data|
          io.write(data)
        end
      end

      expect(io.string).to eq("abc")
    end

    context 'when connecting' do
      it 'uses a specified ssl context' do
        stub_request(:get, uri).to_return(body: "abc")

        other_context = Puppet::SSL::SSLContext.new

        client.get(uri, options: {ssl_context: other_context})
      end

      it 'uses the system store' do
        stub_request(:get, uri).to_return(body: "abc")

        client.get(uri, options: {include_system_store: true})
      end

      it 'raises an HTTPError if both are specified' do
        expect {
          client.get(uri, options: {ssl_context: puppet_context, include_system_store: true})
        }.to raise_error(Puppet::HTTP::HTTPError, /The ssl_context and include_system_store parameters are mutually exclusive/)
      end
    end
  end

  context "for HEAD requests" do
    it "includes default HTTP headers" do
      stub_request(:head, uri).with(headers: {'X-Puppet-Version' => /./, 'User-Agent' => /./})

      client.head(uri)
    end

    it "stringifies keys and encodes values in the query" do
      stub_request(:head, uri).with(query: "foo=bar%3Dbaz")

      client.head(uri, params: {:foo => "bar=baz"})
    end

    it "merges custom headers with default ones" do
      stub_request(:head, uri).with(headers: { 'X-Foo' => 'Bar', 'X-Puppet-Version' => /./, 'User-Agent' => /./ })

      client.head(uri, headers: {'X-Foo' => 'Bar'})
    end

    it "returns the response" do
      stub_request(:head, uri)

      response = client.head(uri)
      expect(response).to be_a(Puppet::HTTP::Response)
      expect(response).to be_success
      expect(response.code).to eq(200)
    end

    it "returns the entire response body" do
      stub_request(:head, uri).to_return(body: "abc")

      expect(client.head(uri).body).to eq("abc")
    end

    context 'when connecting' do
      it 'uses a specified ssl context' do
        stub_request(:head, uri)

        other_context = Puppet::SSL::SSLContext.new

        client.head(uri, options: {ssl_context: other_context})
      end

      it 'uses the system store' do
        stub_request(:head, uri)

        client.head(uri, options: {include_system_store: true})
      end

      it 'raises an HTTPError if both are specified' do
        expect {
          client.head(uri, options: {ssl_context: puppet_context, include_system_store: true})
        }.to raise_error(Puppet::HTTP::HTTPError, /The ssl_context and include_system_store parameters are mutually exclusive/)
      end
    end
  end

  context "for PUT requests" do
    it "includes default HTTP headers" do
      stub_request(:put, uri).with do |request|
        expect(request.headers).to include({'X-Puppet-Version' => /./, 'User-Agent' => /./})
        expect(request.headers).to_not include('X-Puppet-Profiling')
      end

      client.put(uri, "", headers: {'Content-Type' => 'text/plain'})
    end

    it "stringifies keys and encodes values in the query" do
      stub_request(:put, "https://www.example.com").with(query: "foo=bar%3Dbaz")

      client.put(uri, "", params: {:foo => "bar=baz"}, headers: {'Content-Type' => 'text/plain'})
    end

    it "includes custom headers" do
      stub_request(:put, "https://www.example.com").with(headers: { 'X-Foo' => 'Bar' })

      client.put(uri, "", headers: {'X-Foo' => 'Bar', 'Content-Type' => 'text/plain'})
    end

    it "returns the response" do
      stub_request(:put, uri)

      response = client.put(uri, "", headers: {'Content-Type' => 'text/plain'})
      expect(response).to be_a(Puppet::HTTP::Response)
      expect(response).to be_success
      expect(response.code).to eq(200)
    end

    it "sets content-length and content-type for the body" do
      stub_request(:put, uri).with(headers: {"Content-Length" => "5", "Content-Type" => "text/plain"})

      client.put(uri, "hello", headers: {'Content-Type' => 'text/plain'})
    end

     it 'raises an ArgumentError if `body` is missing' do
       expect {
         client.put(uri, nil, headers: {'Content-Type' => 'text/plain'})
       }.to raise_error(ArgumentError, /'put' requires a string 'body' argument/)
     end

     it 'raises an ArgumentError if `content_type` is missing from the headers hash' do
       expect {
         client.put(uri, '')
       }.to raise_error(ArgumentError, /'put' requires a 'content-type' header/)
     end

    context 'when connecting' do
      it 'uses a specified ssl context' do
        stub_request(:put, uri)

        other_context = Puppet::SSL::SSLContext.new

        client.put(uri, "", headers: {'Content-Type' => 'text/plain'}, options: {ssl_context: other_context})
      end

      it 'uses the system store' do
        stub_request(:put, uri)

        client.put(uri, "", headers: {'Content-Type' => 'text/plain'}, options: {include_system_store: true})
      end

      it 'raises an HTTPError if both are specified' do
        expect {
          client.put(uri, "", headers: {'Content-Type' => 'text/plain'}, options: {ssl_context: puppet_context, include_system_store: true})
        }.to raise_error(Puppet::HTTP::HTTPError, /The ssl_context and include_system_store parameters are mutually exclusive/)
      end
    end
  end

  context "for POST requests" do
    it "includes default HTTP headers" do
      stub_request(:post, uri).with(headers: {'X-Puppet-Version' => /./, 'User-Agent' => /./})

      client.post(uri, "", headers: {'Content-Type' => 'text/plain'})
    end

    it "stringifies keys and encodes values in the query" do
      stub_request(:post, "https://www.example.com").with(query: "foo=bar%3Dbaz")

      client.post(uri, "", params: {:foo => "bar=baz"}, headers: {'Content-Type' => 'text/plain'})
    end

    it "includes custom headers" do
      stub_request(:post, "https://www.example.com").with(headers: { 'X-Foo' => 'Bar' })

      client.post(uri, "", headers: {'X-Foo' => 'Bar', 'Content-Type' => 'text/plain'})
    end

    it "returns the response" do
      stub_request(:post, uri)

      response = client.post(uri, "", headers: {'Content-Type' => 'text/plain'})
      expect(response).to be_a(Puppet::HTTP::Response)
      expect(response).to be_success
      expect(response.code).to eq(200)
    end

    it "sets content-length and content-type for the body" do
      stub_request(:post, uri).with(headers: {"Content-Length" => "5", "Content-Type" => "text/plain"})

      client.post(uri, "hello", headers: {'Content-Type' => 'text/plain'})
    end

    it "streams the response body when a block is given" do
      stub_request(:post, uri).to_return(body: "abc")

      io = StringIO.new
      client.post(uri, "", headers: {'Content-Type' => 'text/plain'}) do |response|
        response.read_body do |data|
          io.write(data)
        end
      end

      expect(io.string).to eq("abc")
    end

    it 'raises an ArgumentError if `body` is missing' do
      expect {
        client.post(uri, nil, headers: {'Content-Type' => 'text/plain'})
      }.to raise_error(ArgumentError, /'post' requires a string 'body' argument/)
    end

    it 'raises an ArgumentError if `content_type` is missing from the headers hash' do
      expect {
        client.post(uri, "")
      }.to raise_error(ArgumentError, /'post' requires a 'content-type' header/)
    end

    context 'when connecting' do
      it 'uses a specified ssl context' do
        stub_request(:post, uri)

        other_context = Puppet::SSL::SSLContext.new

        client.post(uri, "", headers: {'Content-Type' => 'text/plain'}, options: {body: "", ssl_context: other_context})
      end

      it 'uses the system store' do
        stub_request(:post, uri)

        client.post(uri, "", headers: {'Content-Type' => 'text/plain'}, options: {include_system_store: true})
      end

      it 'raises an HTTPError if both are specified' do
        expect {
          client.post(uri, "", headers: {'Content-Type' => 'text/plain'}, options: {ssl_context: puppet_context, include_system_store: true})
        }.to raise_error(Puppet::HTTP::HTTPError, /The ssl_context and include_system_store parameters are mutually exclusive/)
      end
    end
  end

  context "for DELETE requests" do
    it "includes default HTTP headers" do
      stub_request(:delete, uri).with(headers: {'X-Puppet-Version' => /./, 'User-Agent' => /./})

      client.delete(uri)
    end

    it "merges custom headers with default ones" do
      stub_request(:delete, uri).with(headers: { 'X-Foo' => 'Bar', 'X-Puppet-Version' => /./, 'User-Agent' => /./ })

      client.delete(uri, headers: {'X-Foo' => 'Bar'})
    end

    it "stringifies keys and encodes values in the query" do
      stub_request(:delete, "https://www.example.com").with(query: "foo=bar%3Dbaz")

      client.delete(uri, params: {:foo => "bar=baz"})
    end

    it "returns the response" do
      stub_request(:delete, uri)

      response = client.delete(uri)
      expect(response).to be_a(Puppet::HTTP::Response)
      expect(response).to be_success
      expect(response.code).to eq(200)
    end

    it "returns the entire response body" do
      stub_request(:delete, uri).to_return(body: "abc")

      expect(client.delete(uri).body).to eq("abc")
    end

    context 'when connecting' do
      it 'uses a specified ssl context' do
        stub_request(:delete, uri)

        other_context = Puppet::SSL::SSLContext.new

        client.delete(uri, options: {ssl_context: other_context})
      end

      it 'uses the system store' do
        stub_request(:delete, uri)

        client.delete(uri, options: {include_system_store: true})
      end

      it 'raises an HTTPError if both are specified' do
        expect {
          client.delete(uri, options: {ssl_context: puppet_context, include_system_store: true})
        }.to raise_error(Puppet::HTTP::HTTPError, /The ssl_context and include_system_store parameters are mutually exclusive/)
      end
    end
  end

  context "Basic Auth" do
    it "submits credentials for GET requests" do
      stub_request(:get, uri).with(basic_auth: credentials)

      client.get(uri, options: {basic_auth: {user: 'user', password: 'pass'}})
    end

    it "submits credentials for PUT requests" do
      stub_request(:put, uri).with(basic_auth: credentials)

      client.put(uri, "hello", headers: {'Content-Type' => 'text/plain'}, options: {basic_auth: {user: 'user', password: 'pass'}})
    end

    it "returns response containing access denied" do
      stub_request(:get, uri).with(basic_auth: credentials).to_return(status: [403, "Ye Shall Not Pass"])

      response = client.get(uri, options: {basic_auth: {user: 'user', password: 'pass'}})
      expect(response.code).to eq(403)
      expect(response.reason).to eq("Ye Shall Not Pass")
      expect(response).to_not be_success
    end

    it 'includes basic auth if user is nil' do
      stub_request(:get, uri).with do |req|
        expect(req.headers).to include('Authorization')
      end

      client.get(uri, options: {basic_auth: {user: nil, password: 'pass'}})
    end

    it 'includes basic auth if password is nil' do
      stub_request(:get, uri).with do |req|
        expect(req.headers).to include('Authorization')
      end

      client.get(uri, options: {basic_auth: {user: 'user', password: nil}})
    end

    it 'observes userinfo in the URL' do
      stub_request(:get, uri).with(basic_auth: credentials)

      client.get(URI("https://user:pass@www.example.com"))
    end

    it 'prefers explicit basic_auth credentials' do
      uri = URI("https://ignored_user:ignored_pass@www.example.com")

      stub_request(:get, "https://www.example.com").with(basic_auth: credentials)

      client.get(uri, options: {basic_auth: {user: 'user', password: 'pass'}})
    end
  end

  context "when redirecting" do
    let(:start_url)  { URI("https://www.example.com:8140/foo") }
    let(:bar_url)  { "https://www.example.com:8140/bar" }
    let(:baz_url) { "https://www.example.com:8140/baz" }
    let(:other_host)  { "https://other.example.com:8140/qux" }

    def redirect_to(status: 302, url:)
      { status: status, headers: { 'Location' => url }, body: "Redirected to #{url}" }
    end

    it "preserves GET method" do
      stub_request(:get, start_url).to_return(redirect_to(url: bar_url))
      stub_request(:get, bar_url).to_return(status: 200)

      response = client.get(start_url)
      expect(response).to be_success
    end

    it "preserves PUT method" do
      stub_request(:put, start_url).to_return(redirect_to(url: bar_url))
      stub_request(:put, bar_url).to_return(status: 200)

      response = client.put(start_url, "", headers: {'Content-Type' => 'text/plain'})
      expect(response).to be_success
    end

    it "updates the Host header from the Location host and port" do
      stub_request(:get, start_url).with(headers: { 'Host' => 'www.example.com:8140' })
        .to_return(redirect_to(url: other_host))
      stub_request(:get, other_host).with(headers: { 'Host' => 'other.example.com:8140' })
        .to_return(status: 200)

      response = client.get(start_url)
      expect(response).to be_success
    end

    it "omits the default HTTPS port from the Host header" do
      stub_request(:get, start_url).with(headers: { 'Host' => 'www.example.com:8140' })
        .to_return(redirect_to(url: "https://other.example.com/qux"))
      stub_request(:get, "https://other.example.com/qux").with(headers: { 'Host' => 'other.example.com' })
        .to_return(status: 200)

      response = client.get(start_url)
      expect(response).to be_success
    end

    it "omits the default HTTP port from the Host header" do
      stub_request(:get, start_url).with(headers: { 'Host' => 'www.example.com:8140' })
        .to_return(redirect_to(url: "http://other.example.com/qux"))
      stub_request(:get, "http://other.example.com/qux").with(headers: { 'Host' => 'other.example.com' })
        .to_return(status: 200)

      response = client.get(start_url)
      expect(response).to be_success
    end

    it "applies query parameters from the location header" do
      query = { 'redirected' => false }

      stub_request(:get, start_url).with(query: query).to_return(redirect_to(url: "#{bar_url}?redirected=true"))
      stub_request(:get, bar_url).with(query: {'redirected' => 'true'}).to_return(status: 200)

      response = client.get(start_url, params: query)
      expect(response).to be_success
    end

    it "preserves custom and default headers when redirecting" do
      headers = { 'X-Foo' => 'Bar', 'X-Puppet-Version' => Puppet.version }
      stub_request(:get, start_url).with(headers: headers).to_return(redirect_to(url: bar_url))
      stub_request(:get, bar_url).with(headers: headers).to_return(status: 200)

      response = client.get(start_url, headers: headers)
      expect(response).to be_success
    end

    it "does not preserve basic authorization when redirecting to different hosts" do
      stub_request(:get, start_url).with(basic_auth: credentials).to_return(redirect_to(url: other_host))
      stub_request(:get, other_host).to_return(status: 200)

      client.get(start_url, options: {basic_auth: {user: 'user', password: 'pass'}})
      expect(a_request(:get, other_host).
      with{ |req| !req.headers.key?('Authorization')}).to have_been_made
    end

    it "does preserve basic authorization when redirecting to the same hosts" do
      stub_request(:get, start_url).with(basic_auth: credentials).to_return(redirect_to(url: bar_url))
      stub_request(:get, bar_url).with(basic_auth: credentials).to_return(status: 200)

      client.get(start_url, options: {basic_auth: {user: 'user', password: 'pass'}})
      expect(a_request(:get, bar_url).
      with{ |req| req.headers.key?('Authorization')}).to have_been_made
    end

    it "does not preserve cookie header when redirecting to different hosts" do
      headers = { 'Cookie' => 'TEST_COOKIE'}

      stub_request(:get, start_url).with(headers: headers).to_return(redirect_to(url: other_host))
      stub_request(:get, other_host).to_return(status: 200)

      client.get(start_url, headers: headers)
      expect(a_request(:get, other_host).
      with{ |req| !req.headers.key?('Cookie')}).to have_been_made
    end

    it "does preserve cookie header when redirecting to the same hosts" do
      headers = { 'Cookie' => 'TEST_COOKIE'}

      stub_request(:get, start_url).with(headers: headers).to_return(redirect_to(url: bar_url))
      stub_request(:get, bar_url).with(headers: headers).to_return(status: 200)

      client.get(start_url, headers: headers)
      expect(a_request(:get, bar_url).
      with{ |req| req.headers.key?('Cookie')}).to have_been_made
    end

    it "does preserves cookie header and basic authentication when Puppet[:location_trusted] is true redirecting to different hosts" do
      headers = { 'cookie' => 'TEST_COOKIE'}
      Puppet[:location_trusted] = true

      stub_request(:get, start_url).with(headers: headers, basic_auth: credentials).to_return(redirect_to(url: other_host))
      stub_request(:get, other_host).with(headers: headers, basic_auth: credentials).to_return(status: 200)

      client.get(start_url, headers: headers, options: {basic_auth: {user: 'user', password: 'pass'}})
      expect(a_request(:get, other_host).
      with{ |req| req.headers.key?('Authorization') && req.headers.key?('Cookie')}).to have_been_made
    end

    it "treats hosts as case-insensitive" do
      start_url = URI("https://www.EXAmple.com:8140/Start")
      bar_url = "https://www.example.com:8140/bar"

      stub_request(:get, start_url).with(basic_auth: credentials).to_return(redirect_to(url: bar_url))
      stub_request(:get, bar_url).with(basic_auth: credentials).to_return(status: 200)

      client.get(start_url, options: {basic_auth: {user: 'user', password: 'pass'}})
      expect(a_request(:get, bar_url).
      with{ |req| req.headers.key?('Authorization')}).to have_been_made
    end

    it "redirects given a relative location" do
      relative_url = "/people.html"
      stub_request(:get, start_url).to_return(redirect_to(url: relative_url))
      stub_request(:get, "https://www.example.com:8140/people.html").to_return(status: 200)

      response = client.get(start_url)
      expect(response).to be_success
    end

    it "applies query parameters from the location header" do
      relative_url = "/people.html"
      query = { 'redirected' => false }
      stub_request(:get, start_url).with(query: query).to_return(redirect_to(url: "#{relative_url}?redirected=true"))
      stub_request(:get, "https://www.example.com:8140/people.html").with(query: {'redirected' => 'true'}).to_return(status: 200)

      response = client.get(start_url, params: query)
      expect(response).to be_success
    end

    it "removes dot segments from a relative location" do
      # from https://tools.ietf.org/html/rfc3986#section-5.4.2
      base_url = URI("http://a/b/c/d;p?q")
      relative_url = "../../../../g"
      stub_request(:get, base_url).to_return(redirect_to(url: relative_url))
      stub_request(:get, "http://a/g").to_return(status: 200)

      response = client.get(base_url)
      expect(response).to be_success
    end

    it "preserves request body for each request" do
      data = 'some data'
      stub_request(:put, start_url).with(body: data).to_return(redirect_to(url: bar_url))
      stub_request(:put, bar_url).with(body: data).to_return(status: 200)

      response = client.put(start_url, data, headers: {'Content-Type' => 'text/plain'})
      expect(response).to be_success
    end

    it "returns the body from the final response" do
      stub_request(:get, start_url).to_return(redirect_to(url: bar_url))
      stub_request(:get, bar_url).to_return(status: 200, body: 'followed')

      response = client.get(start_url)
      expect(response.body).to eq('followed')
    end

    [301, 307].each do |code|
      it "also redirects on #{code}" do
        stub_request(:get, start_url).to_return(redirect_to(status: code, url: bar_url))
        stub_request(:get, bar_url).to_return(status: 200)

        response = client.get(start_url)
        expect(response).to be_success
      end
    end

    [303, 308].each do |code|
      it "returns an error on #{code}" do
        stub_request(:get, start_url).to_return(redirect_to(status: code, url: bar_url))

        response = client.get(start_url)
        expect(response.code).to eq(code)
        expect(response).to_not be_success
      end
    end

    it "raises an error if the Location header is missing" do
      stub_request(:get, start_url).to_return(status: 302)

      expect {
        client.get(start_url)
      }.to raise_error(Puppet::HTTP::ProtocolError, "Location response header is missing")
    end

    it "raises an error if the Location header is invalid" do
      stub_request(:get, start_url).to_return(redirect_to(status: 302, url: 'http://foo"bar'))

      expect {
        client.get(start_url)
      }.to raise_error(Puppet::HTTP::ProtocolError, /Location URI is invalid/)
    end

    it "raises an error if limit is 0 and we're asked to follow" do
      stub_request(:get, start_url).to_return(redirect_to(url: bar_url))

      client = described_class.new(redirect_limit: 0)
      expect {
        client.get(start_url)
      }.to raise_error(Puppet::HTTP::TooManyRedirects, %r{Too many HTTP redirections for https://www.example.com:8140})
    end

    it "raises an error if asked to follow redirects more times than the limit" do
      stub_request(:get, start_url).to_return(redirect_to(url: bar_url))
      stub_request(:get, bar_url).to_return(redirect_to(url: baz_url))

      client = described_class.new(redirect_limit: 1)
      expect {
        client.get(start_url)
      }.to raise_error(Puppet::HTTP::TooManyRedirects, %r{Too many HTTP redirections for https://www.example.com:8140})
    end

    it "follows multiple redirects if equal to or less than the redirect limit" do
      stub_request(:get, start_url).to_return(redirect_to(url: bar_url))
      stub_request(:get, bar_url).to_return(redirect_to(url: baz_url))
      stub_request(:get, baz_url).to_return(status: 200)

      client = described_class.new(redirect_limit: 2)
      response = client.get(start_url)
      expect(response).to be_success
    end

    it "redirects to a different host" do
      stub_request(:get, start_url).to_return(redirect_to(url: other_host))
      stub_request(:get, other_host).to_return(status: 200)

      response = client.get(start_url)
      expect(response).to be_success
    end

    it "redirects from http to https" do
      http = URI("http://example.com/foo")
      https = URI("https://example.com/bar")

      stub_request(:get, http).to_return(redirect_to(url: https))
      stub_request(:get, https).to_return(status: 200)

      response = client.get(http)
      expect(response).to be_success
    end

    it "redirects from https to http" do
      http = URI("http://example.com/foo")
      https = URI("https://example.com/bar")

      stub_request(:get, https).to_return(redirect_to(url: http))
      stub_request(:get, http).to_return(status: 200)

      response = client.get(https)
      expect(response).to be_success
    end
  end

  context "when response indicates an overloaded server" do
    def retry_after(datetime)
      stub_request(:get, uri)
        .to_return(status: [503, 'Service Unavailable'], headers: {'Retry-After' => datetime}).then
        .to_return(status: 200)
    end

    it "returns a 503 response if Retry-After is not set" do
      stub_request(:get, uri).to_return(status: [503, 'Service Unavailable'])

      expect(client.get(uri).code).to eq(503)
    end

    it "raises if Retry-After is not convertible to an Integer or RFC 2822 Date" do
      stub_request(:get, uri).to_return(status: [503, 'Service Unavailable'], headers: {'Retry-After' => 'foo'})

      expect {
        client.get(uri)
      }.to raise_error(Puppet::HTTP::ProtocolError, /Failed to parse Retry-After header 'foo' as an integer or RFC 2822 date/)
    end

    it "should close the connection before sleeping" do
      retry_after('42')

      site = Puppet::HTTP::Site.from_uri(uri)

      http1 = Net::HTTP.new(site.host, site.port)
      http1.use_ssl = true
      allow(http1).to receive(:started?).and_return(true)

      http2 = Net::HTTP.new(site.host, site.port)
      http2.use_ssl = true
      allow(http2).to receive(:started?).and_return(true)

      pool = Puppet::HTTP::Pool.new(15)
      client = Puppet::HTTP::Client.new(pool: pool)

      # The "with_connection" method is required to yield started connections
      allow(pool).to receive(:with_connection).and_yield(http1).and_yield(http2)

      expect(http1).to receive(:finish).ordered
      expect(::Kernel).to receive(:sleep).with(42).ordered

      client.get(uri)
    end

    it "should sleep and retry if Retry-After is an Integer" do
      retry_after('42')

      expect(::Kernel).to receive(:sleep).with(42)

      client.get(uri)
    end

    it "should sleep and retry if Retry-After is an RFC 2822 Date" do
      retry_after('Wed, 13 Apr 2005 15:18:05 GMT')

      now = DateTime.new(2005, 4, 13, 8, 17, 5, '-07:00')
      allow(DateTime).to receive(:now).and_return(now)

      expect(::Kernel).to receive(:sleep).with(60)

      client.get(uri)
    end

    it "should sleep for no more than the Puppet runinterval" do
      retry_after('60')
      Puppet[:runinterval] = 30

      expect(::Kernel).to receive(:sleep).with(30)

      client.get(uri)
    end

    it "should sleep for 0 seconds if the RFC 2822 date has past" do
      retry_after('Wed, 13 Apr 2005 15:18:05 GMT')

      expect(::Kernel).to receive(:sleep).with(0)

      client.get(uri)
    end
  end

  context "persistent connections" do
    before :each do
      stub_request(:get, uri)
    end

    it 'defaults keepalive to http_keepalive_timeout' do
      expect(client.pool.keepalive_timeout).to eq(Puppet[:http_keepalive_timeout])
    end

    it 'reuses a cached connection' do
      allow(Puppet).to receive(:debug)
      expect(Puppet).to receive(:debug).with(/^Creating new connection/)
      expect(Puppet).to receive(:debug).with(/^Using cached connection/)

      client.get(uri)
      client.get(uri)
    end

    it 'can be disabled' do
      Puppet[:http_keepalive_timeout] = 0

      allow(Puppet).to receive(:debug)
      expect(Puppet).to receive(:debug).with(/^Creating new connection/).twice
      expect(Puppet).to receive(:debug).with(/^Using cached connection/).never

      client.get(uri)
      client.get(uri)
    end
  end
end
