require 'spec_helper'
require 'puppet/util/json'
require 'puppet/indirector'
require 'puppet/indirector/errors'
require 'puppet/indirector/rest'
require 'puppet/util/psych_support'

HTTP_ERROR_CODES = [300, 400, 500]

# Just one from each category since the code makes no real distinctions
shared_examples_for "a REST terminus method" do |terminus_method|

  describe "when handling the response" do
    let(:response) do
      mock_response(200, 'OK')
    end

    it "falls back to pson for future requests" do
      allow(response).to receive(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).and_return("4.10.1")
      terminus.send(terminus_method, request)

      expect(Puppet[:preferred_serialization_format]).to eq("pson")
    end

    it "doesn't change the serialization format if the X-Puppet-Version header is missing" do
      allow(response).to receive(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).and_return(nil)

      terminus.send(terminus_method, request)

      expect(Puppet[:preferred_serialization_format]).to eq("json")
    end

    it "doesn't change the serialization format if the server major version is 5" do
      allow(response).to receive(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).and_return("5.0.3")

      terminus.send(terminus_method, request)

      expect(Puppet[:preferred_serialization_format]).to eq("json")
    end

    it "doesn't change the serialization format if the current format is already pson" do
      allow(response).to receive(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).and_return("4.10.1")
      Puppet[:preferred_serialization_format] = "pson"
      terminus.send(terminus_method, request)

      expect(Puppet[:preferred_serialization_format]).to eq("pson")
    end
  end

  HTTP_ERROR_CODES.each do |code|
    describe "when the response code is #{code}" do
      let(:message) { 'error messaged!!!' }
      let(:body) do
        Puppet::Util::Json.dump({
          :issue_kind => 'server-error',
          :message    => message
        })
      end
      let(:response) { mock_response(code, body, 'application/json') }

      describe "when the response is plain text" do
        let(:response) { mock_response(code, message) }

        it "raises an http error with the body of the response when plain text" do

          expect {
            terminus.send(terminus_method, request)
          }.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{message}")
        end
      end

      it "raises an http error with the body's message field when json" do
        expect {
          terminus.send(terminus_method, request)
        }.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{message}")
      end

      it "does not attempt to deserialize the response into a model" do
        expect(model).not_to receive(:convert_from)

        expect {
          terminus.send(terminus_method, request)
        }.to raise_error(Net::HTTPError)
      end

      # I'm not sure what this means or if it's used
      it "if the body is empty raises an http error with the response header" do
        allow(response).to receive(:body).and_return("")
        allow(response).to receive(:message).and_return("fhqwhgads")

        expect {
          terminus.send(terminus_method, request)
        }.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{response.message}")
      end

      describe "and the body is compressed" do
        it "raises an http error with the decompressed body of the response" do
          compressed_body = Zlib::Deflate.deflate(body)

          compressed_response = mock_response(code, compressed_body, 'application/json', 'deflate')
          expect(connection).to receive(http_method).and_return(compressed_response)

          expect {
            terminus.send(terminus_method, request)
          }.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{message}")
        end
      end
    end
  end
end

shared_examples_for "a deserializing terminus method" do |terminus_method|
  describe "when the response has no content-type" do
    let(:response) { mock_response(200, "body", nil, nil) }
    it "raises an error" do
      expect {
        terminus.send(terminus_method, request)
      }.to raise_error(RuntimeError, "No content type in http response; cannot parse")
    end
  end

  it "doesn't catch errors in deserialization" do
    expect(model).to receive(:convert_from).and_raise(Puppet::Error, "Whoa there")

    expect { terminus.send(terminus_method, request) }.to raise_error(Puppet::Error, "Whoa there")
  end
end

describe Puppet::Indirector::REST do
  before :all do
    class Puppet::TestModel
      include Puppet::Util::PsychSupport
      extend Puppet::Indirector
      indirects :test_model
      attr_accessor :name, :data
      def initialize(name = "name", data = '')
        @name = name
        @data = data
      end

      def self.convert_from(format, string)
        new('', string)
      end

      def self.convert_from_multiple(format, string)
        string.split(',').collect { |s| convert_from(format, s) }
      end

      def to_data_hash
        { 'name' => @name, 'data' => @data }
      end

      def ==(other)
        other.is_a? Puppet::TestModel and other.name == name and other.data == data
      end
    end

    # The subclass must not be all caps even though the superclass is
    class Puppet::TestModel::Rest < Puppet::Indirector::REST
    end

    Puppet::TestModel.indirection.terminus_class = :rest
  end

  after :all do
    Puppet::TestModel.indirection.delete
    # Remove the class, unlinking it from the rest of the system.
    Puppet.send(:remove_const, :TestModel)
  end

  let(:terminus_class) { Puppet::TestModel::Rest }
  let(:terminus) { Puppet::TestModel.indirection.terminus(:rest) }
  let(:indirection) { Puppet::TestModel.indirection }
  let(:model) { Puppet::TestModel }
  let(:url_prefix) { "#{Puppet::Network::HTTP::MASTER_URL_PREFIX}/v3"}

  around(:each) do |example|
    Puppet.override(:current_environment => Puppet::Node::Environment.create(:production, [])) do
      example.run
    end
  end

  def mock_response(code, body, content_type='text/plain', encoding=nil)
    obj = double('http 200 ok', :code => code.to_s, :body => body)
    allow(obj).to receive(:[]).with('content-type').and_return(content_type)
    allow(obj).to receive(:[]).with('content-encoding').and_return(encoding)
    allow(obj).to receive(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).and_return(Puppet.version)
    obj
  end

  def find_request(key, options={})
    Puppet::Indirector::Request.new(:test_model, :find, key, nil, options)
  end

  def head_request(key, options={})
    Puppet::Indirector::Request.new(:test_model, :head, key, nil, options)
  end

  def search_request(key, options={})
    Puppet::Indirector::Request.new(:test_model, :search, key, nil, options)
  end

  def delete_request(key, options={})
    Puppet::Indirector::Request.new(:test_model, :destroy, key, nil, options)
  end

  def save_request(key, instance, options={})
    Puppet::Indirector::Request.new(:test_model, :save, key, instance, options)
  end

  it "should have a method for specifying what setting a subclass should use to retrieve its server" do
    expect(terminus_class).to respond_to(:use_server_setting)
  end

  it "should use any specified setting to pick the server" do
    expect(terminus_class).to receive(:server_setting).and_return(:ca_server)
    Puppet[:ca_server] = "myserver"
    expect(terminus_class.server).to eq("myserver")
  end

  it "should default to :server for the server setting" do
    expect(terminus_class).to receive(:server_setting).and_return(nil)
    Puppet[:server] = "myserver"
    expect(terminus_class.server).to eq("myserver")
  end

  it "should have a method for specifying what setting a subclass should use to retrieve its port" do
    expect(terminus_class).to respond_to(:use_port_setting)
  end

  it "should use any specified setting to pick the port" do
    expect(terminus_class).to receive(:port_setting).and_return(:ca_port)
    Puppet[:ca_port] = "321"
    expect(terminus_class.port).to eq(321)
  end

  it "should default to :port for the port setting" do
    expect(terminus_class).to receive(:port_setting).and_return(nil)
    Puppet[:masterport] = "543"
    expect(terminus_class.port).to eq(543)
  end

  it "should use a failover-selected server if set" do
    expect(terminus_class).to receive(:server_setting).and_return(nil)
    Puppet.override(:server => "myserver") do
      expect(terminus_class.server).to eq("myserver")
    end
  end

  it "should use a failover-selected port if set" do
    expect(terminus_class).to receive(:port_setting).and_return(nil)
    Puppet.override(:serverport => 321) do
      expect(terminus_class.port).to eq(321)
    end
  end

  it "should use server_list for server when available" do
    expect(terminus_class).to receive(:server_setting).and_return(nil)
    Puppet[:server_list] = [["foo", "123"]]
    expect(terminus_class.server).to eq("foo")
  end

  it "should prefer failover-selected server from server list" do
    expect(terminus_class).to receive(:server_setting).and_return(nil)
    Puppet[:server_list] = [["foo", "123"],["bar", "321"]]
    Puppet.override(:server => "bar") do
      expect(terminus_class.server).to eq("bar")
    end
  end

  it "should use server_list for port when available" do
    expect(terminus_class).to receive(:port_setting).and_return(nil)
    Puppet[:server_list] = [["foo", "123"]]
    expect(terminus_class.port).to eq(123)
  end

  it "should prefer failover-selected port from server list" do
    expect(terminus_class).to receive(:port_setting).and_return(nil)
    Puppet[:server_list] = [["foo", "123"],["bar", "321"]]
    Puppet.override(:serverport => "321") do
      expect(terminus_class.port).to eq(321)
    end
  end

  it "should use an explicitly specified more-speciic server when failover is active" do
    expect(terminus_class).to receive(:server_setting).and_return(:ca_server)
    Puppet[:ca_server] = "myserver"
    Puppet.override(:server => "anotherserver") do
      expect(terminus_class.server).to eq("myserver")
    end
  end

  it "should use an explicitly specified more-specific port when failover is active" do
    expect(terminus_class).to receive(:port_setting).and_return(:ca_port)
    Puppet[:ca_port] = 321
    Puppet.override(:serverport => 543) do
      expect(terminus_class.port).to eq(321)
    end
  end

  it "should use a default port when a more-specific server is set" do
    expect(terminus_class).to receive(:server_setting).and_return(:ca_server)
    expect(terminus_class).to receive(:port_setting).and_return(:ca_port)
    Puppet[:ca_server] = "myserver"
    Puppet.override(:server => "anotherserver", :port => 666) do
      expect(terminus_class.port).to eq(8140)
    end
  end

  it 'should default to :puppet for the srv_service' do
    expect(Puppet::Indirector::REST.srv_service).to eq(:puppet)
  end

  it 'excludes yaml from the Accept header' do
    expect(model).to receive(:supported_formats).and_return([:json, :pson, :yaml, :binary])

    expect(terminus.headers['Accept']).to eq('application/json, text/pson, application/octet-stream')
  end

  it 'excludes b64_zlib_yaml from the Accept header' do
    expect(model).to receive(:supported_formats).and_return([:json, :pson, :b64_zlib_yaml])

    expect(terminus.headers['Accept']).to eq('application/json, text/pson')
  end

  it 'excludes dot from the Accept header' do
    expect(model).to receive(:supported_formats).and_return([:json, :dot])

    expect(terminus.headers['Accept']).to eq('application/json')
  end

  describe "when creating an HTTP client" do
    it "should use the class's server and port if the indirection request provides neither" do
      @request = double('request', :key => "foo", :server => nil, :port => nil)
      expect(terminus.class).to receive(:port).and_return(321)
      expect(terminus.class).to receive(:server).and_return("myserver")
      expect(Puppet::Network::HttpPool).to receive(:http_instance).with("myserver", 321).and_return("myconn")
      expect(terminus.network(@request)).to eq("myconn")
    end

    it "should use the server from the indirection request if one is present" do
      @request = double('request', :key => "foo", :server => "myserver", :port => nil)
      allow(terminus.class).to receive(:port).and_return(321)
      expect(Puppet::Network::HttpPool).to receive(:http_instance).with("myserver", 321).and_return("myconn")
      expect(terminus.network(@request)).to eq("myconn")
    end

    it "should use the port from the indirection request if one is present" do
      @request = double('request', :key => "foo", :server => nil, :port => 321)
      allow(terminus.class).to receive(:server).and_return("myserver")
      expect(Puppet::Network::HttpPool).to receive(:http_instance).with("myserver", 321).and_return("myconn")
      expect(terminus.network(@request)).to eq("myconn")
    end
  end

  describe "#find" do
    let(:http_method) { :get }
    let(:response) { mock_response(200, 'body') }
    let(:connection) { double('mock http connection', :get => response, :verify_callback= => nil) }
    let(:request) { find_request('foo') }

    before :each do
      allow(terminus).to receive(:network).and_return(connection)
    end

    it_behaves_like 'a REST terminus method', :find
    it_behaves_like 'a deserializing terminus method', :find

    describe "with a long set of parameters" do
      it "calls post on the connection with the query params in the body" do
        params = {}
        'aa'.upto('zz') do |s|
          params[s] = 'foo'
        end

        # The request special-cases this parameter, and it
        # won't be passed on to the server, so we remove it here
        # to avoid a failure.
        params.delete('ip')

        params["environment"] = "production"

        request = find_request('whoa', params)

        expect(connection).to receive(:post) do |_,req,_|
          expect(req.split("&").flatten).to match_array(params.map {|key,value| "#{key}=#{value}"})

          mock_response(200, 'body')
        end

        terminus.find(request)
      end
    end

    describe "with no parameters" do
      it "calls get on the connection" do
        request = find_request('foo bar')

        expect(connection).to receive(:get).with("#{url_prefix}/test_model/foo%20bar?environment=production&", anything).and_return(mock_response('200', 'response body'))

        expect(terminus.find(request)).to eq(model.new('foo bar', 'response body'))
      end
    end

    it "returns nil on 404" do
      response = mock_response('404', nil)

      expect(connection).to receive(:get).and_return(response)

      expect(terminus.find(request)).to eq(nil)
    end

    it 'raises no warning for a 404 (when not asked to do so)' do
      response = mock_response('404', 'this is the notfound you are looking for')
      expect(connection).to receive(:get).and_return(response)
      expect{terminus.find(request)}.to_not raise_error()
    end

    context 'when fail_on_404 is used in request' do
      it 'raises an error for a 404 when asked to do so' do
        request = find_request('foo', :fail_on_404 => true)
        response = mock_response('404', 'this is the notfound you are looking for')
        expect(connection).to receive(:get).and_return(response)

        expect do
          terminus.find(request)
        end.to raise_error(
          Puppet::Error,
          "Find #{url_prefix}/test_model/foo?environment=production&fail_on_404=true resulted in 404 with the message: this is the notfound you are looking for")
      end

      it 'truncates the URI when it is very long' do
        request = find_request('foo', :fail_on_404 => true, :long_param => ('A' * 100) + 'B')
        response = mock_response('404', 'this is the notfound you are looking for')
        expect(connection).to receive(:get).and_return(response)

        expect do
          terminus.find(request)
        end.to raise_error(
          Puppet::Error,
          /\/test_model\/foo.*\?environment=production&.*long_param=A+\.\.\..*resulted in 404 with the message/)
      end

      it 'does not truncate the URI when logging debug information' do
        Puppet.debug = true
        request = find_request('foo', :fail_on_404 => true, :long_param => ('A' * 100) + 'B')
        response = mock_response('404', 'this is the notfound you are looking for')
        expect(connection).to receive(:get).and_return(response)

        expect do
          terminus.find(request)
        end.to raise_error(
          Puppet::Error,
          /\/test_model\/foo.*\?environment=production&.*long_param=A+B.*resulted in 404 with the message/)
      end
    end

    it "asks the model to deserialize the response body and sets the name on the resulting object to the find key" do
      expect(connection).to receive(:get).and_return(response)

      expect(model).to receive(:convert_from).with(response['content-type'], response.body).and_return(
        model.new('overwritten', 'decoded body')
      )

      expect(terminus.find(request)).to eq(model.new('foo', 'decoded body'))
    end

    it "doesn't require the model to support name=" do
      class Puppet::TestModel2 < Puppet::TestModel
        undef_method :name=
      end

      expect(connection).to receive(:get).and_return(response)
      instance = Puppet::TestModel2.new('name', 'decoded body')

      expect(model).to receive(:convert_from).with(response['content-type'], response.body).and_return(instance)

      expect(terminus.find(request)).to eq(model.new('name', 'decoded body'))
    end

    it "provides an Accept header containing the list of supported mime types joined with commas" do
      expect(connection).to receive(:get).with(anything, hash_including("Accept" => "application/json, text/pson")).and_return(response)

      expect(terminus.model).to receive(:supported_formats).and_return([:json, :pson])
      terminus.find(request)
    end

    it "provides a version header with the current puppet version" do
      expect(connection).to receive(:get).with(anything, hash_including(Puppet::Network::HTTP::HEADER_PUPPET_VERSION => Puppet.version)).and_return(response)

      terminus.find(request)
    end

    it "adds an Accept-Encoding header" do
      expect(terminus).to receive(:add_accept_encoding).and_return({"accept-encoding" => "gzip"})

      expect(connection).to receive(:get).with(anything, hash_including("accept-encoding" => "gzip")).and_return(response)

      terminus.find(request)
    end

    it "uses only the mime-type from the content-type header when asking the model to deserialize" do
      response = mock_response('200', 'mydata', "text/plain; charset=utf-8")
      expect(connection).to receive(:get).and_return(response)

      expect(model).to receive(:convert_from).with("text/plain", "mydata").and_return("myobject")

      expect(terminus.find(request)).to eq("myobject")
    end

    it "decompresses the body before passing it to the model for deserialization" do
      uncompressed_body = "Why hello there"
      compressed_body = Zlib::Deflate.deflate(uncompressed_body)

      response = mock_response('200', compressed_body, 'text/plain', 'deflate')
      expect(connection).to receive(:get).and_return(response)

      expect(model).to receive(:convert_from).with("text/plain", uncompressed_body).and_return("myobject")

      expect(terminus.find(request)).to eq("myobject")
    end
  end

  describe "#head" do
    let(:http_method) { :head }
    let(:response) { mock_response(200, nil) }
    let(:connection) { double('mock http connection', :head => response, :verify_callback= => nil) }
    let(:request) { head_request('foo') }

    before :each do
      allow(terminus).to receive(:network).and_return(connection)
    end

    it_behaves_like 'a REST terminus method', :head

    it "returns true if there was a successful http response" do
      expect(connection).to receive(:head).and_return(mock_response('200', nil))

      expect(terminus.head(request)).to eq(true)
    end

    it "returns false on a 404 response" do
      expect(connection).to receive(:head).and_return(mock_response('404', nil))

      expect(terminus.head(request)).to eq(false)
    end

    it "provides a version header with the current puppet version" do
      expect(connection).to receive(:head).with(anything, hash_including(Puppet::Network::HTTP::HEADER_PUPPET_VERSION => Puppet.version)).and_return(response)

      terminus.head(request)
    end
  end

  describe "#search" do
    let(:http_method) { :get }
    let(:response) { mock_response(200, 'data1,data2,data3') }
    let(:connection) { double('mock http connection', :get => response, :verify_callback= => nil) }
    let(:request) { search_request('foo') }

    before :each do
      allow(terminus).to receive(:network).and_return(connection)
    end

    it_behaves_like 'a REST terminus method', :search
    it_behaves_like 'a deserializing terminus method', :search

    it "should call the GET http method on a network connection" do
      expect(connection).to receive(:get).with("#{url_prefix}/test_models/foo?environment=production&", hash_including('Accept' => anything)).and_return(mock_response(200, 'data3, data4'))

      terminus.search(request)
    end

    it "returns an empty list on 404" do
      response = mock_response('404', nil)

      expect(connection).to receive(:get).and_return(response)

      expect(terminus.search(request)).to eq([])
    end

    it "asks the model to deserialize the response body into multiple instances" do
      expect(terminus.search(request)).to eq([model.new('', 'data1'), model.new('', 'data2'), model.new('', 'data3')])
    end

    it "should provide an Accept header containing the list of supported formats joined with commas" do
      expect(connection).to receive(:get).with(anything, hash_including("Accept" => "application/json, text/pson")).and_return(mock_response(200, ''))

      expect(terminus.model).to receive(:supported_formats).and_return([:json, :pson])
      terminus.search(request)
    end

    it "provides a version header with the current puppet version" do
      expect(connection).to receive(:get).with(anything, hash_including(Puppet::Network::HTTP::HEADER_PUPPET_VERSION => Puppet.version)).and_return(mock_response(200, ''))

      terminus.search(request)
    end

    it "should return an empty array if serialization returns nil" do
      allow(model).to receive(:convert_from_multiple).and_return(nil)

      expect(terminus.search(request)).to eq([])
    end
  end

  describe "#destroy" do
    let(:http_method) { :delete }
    let(:response) { mock_response(200, 'body') }
    let(:connection) { double('mock http connection', :delete => response, :verify_callback= => nil) }
    let(:request) { delete_request('foo') }

    before :each do
      allow(terminus).to receive(:network).and_return(connection)
    end

    it_behaves_like 'a REST terminus method', :destroy
    it_behaves_like 'a deserializing terminus method', :destroy

    it "should call the DELETE http method on a network connection" do
      expect(connection).to receive(:delete).with("#{url_prefix}/test_model/foo?environment=production&", hash_including('Accept' => anything)).and_return(response)

      terminus.destroy(request)
    end

    it "should fail if any options are provided, since DELETE apparently does not support query options" do
      request = delete_request('foo', :one => "two", :three => "four")

      expect { terminus.destroy(request) }.to raise_error(ArgumentError)
    end

    it "should deserialize and return the http response" do
      expect(connection).to receive(:delete).and_return(response)

      expect(terminus.destroy(request)).to eq(model.new('', 'body'))
    end

    it "returns nil on 404" do
      response = mock_response('404', nil)

      expect(connection).to receive(:delete).and_return(response)

      expect(terminus.destroy(request)).to eq(nil)
    end

    it "should provide an Accept header containing the list of supported formats joined with commas" do
      expect(connection).to receive(:delete).with(anything, hash_including("Accept" => "application/json, text/pson")).and_return(response)

      expect(terminus.model).to receive(:supported_formats).and_return([:json, :pson])
      terminus.destroy(request)
    end

    it "provides a version header with the current puppet version" do
      expect(connection).to receive(:delete).with(anything, hash_including(Puppet::Network::HTTP::HEADER_PUPPET_VERSION => Puppet.version)).and_return(response)

      terminus.destroy(request)
    end
  end

  describe "#save" do
    let(:http_method) { :put }
    let(:response) { mock_response(200, 'body') }
    let(:connection) { double('mock http connection', :put => response, :verify_callback= => nil) }
    let(:instance) { model.new('the thing', 'some contents') }
    let(:request) { save_request(instance.name, instance) }

    before :each do
      allow(terminus).to receive(:network).and_return(connection)
    end

    it_behaves_like 'a REST terminus method', :save

    it "should call the PUT http method on a network connection" do
      expect(connection).to receive(:put).with("#{url_prefix}/test_model/the%20thing?environment=production&", anything, hash_including("Content-Type")).and_return(response)

      terminus.save(request)
    end

    it "should fail if any options are provided, since PUT apparently does not support query options" do
      request = save_request(instance.name, instance, :one => "two", :three => "four")

      expect { terminus.save(request) }.to raise_error(ArgumentError)
    end

    it "should serialize the instance using the default format and pass the result as the body of the request" do
      expect(instance).to receive(:render).and_return("serial_instance")
      expect(connection).to receive(:put).with(anything, "serial_instance", anything).and_return(response)

      terminus.save(request)
    end

    it "returns nil on 404" do
      response = mock_response('404', nil)

      expect(connection).to receive(:put).and_return(response)

      expect(terminus.save(request)).to eq(nil)
    end

    it "returns nil" do
      expect(connection).to receive(:put).and_return(response)

      expect(terminus.save(request)).to be_nil
    end

    it "should provide an Accept header containing the list of supported formats joined with commas" do
      expect(connection).to receive(:put).with(anything, anything, hash_including("Accept" => "application/json, text/pson")).and_return(response)

      expect(instance).to receive(:render).and_return('')
      expect(model).to receive(:supported_formats).and_return([:json, :pson])
      expect(instance).to receive(:mime).and_return("supported")

      terminus.save(request)
    end

    it "provides a version header with the current puppet version" do
      expect(connection).to receive(:put).with(anything, anything, hash_including(Puppet::Network::HTTP::HEADER_PUPPET_VERSION => Puppet.version)).and_return(response)

      terminus.save(request)
    end

    it "should provide a Content-Type header containing the mime-type of the sent object" do
      expect(instance).to receive(:mime).and_return("mime")
      expect(connection).to receive(:put).with(anything, anything, hash_including('Content-Type' => "mime")).and_return(response)

      terminus.save(request)
    end
  end

  describe '#handle_response' do
    # There are multiple request types to choose from, this may not be the one I want for this situation
    let(:response) { mock_response(200, 'body') }
    let(:connection) { double('mock http connection', :put => response, :verify_callback= => nil) }
    let(:instance) { model.new('the thing', 'some contents') }
    let(:request) { save_request(instance.name, instance) }

    before :each do
      allow(terminus).to receive(:network).and_return(connection)
    end

    it 'adds server_agent_version to the context if not already set' do
      expect(Puppet).to receive(:push_context).with(:server_agent_version => Puppet.version)
      terminus.handle_response(request, response)
    end

    it 'does not add server_agent_version to the context if it is already set' do
      Puppet.override(:server_agent_version => "5.3.4") do
        expect(Puppet).not_to receive(:push_context)
        terminus.handle_response(request, response)
      end
    end

    it 'downgrades to pson and emits a warning' do
      allow(response).to receive(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).and_return('4.2.8')
      Puppet[:preferred_serialization_format] = 'other'

      expect(Puppet).to receive(:warning).with('Downgrading to PSON for future requests')

      terminus.handle_response(request, response)

      expect(Puppet[:preferred_serialization_format]).to eq('pson')
    end

    it 'preserves the set serialization format' do
      Puppet[:preferred_serialization_format] = 'other'

      expect(Puppet[:preferred_serialization_format]).to eq('other')

      terminus.handle_response(request, response)
    end
  end

  context 'dealing with SRV settings' do
    [
      :destroy,
      :find,
      :head,
      :save,
      :search
    ].each do |method|
      it "##{method} passes the SRV service, and fall-back server & port to the request's do_request method" do
        request = Puppet::Indirector::Request.new(:indirection, method, 'key', nil)
        stub_response = mock_response('200', 'body')

        expect(request).to receive(:do_request).with(terminus.class.srv_service, terminus.class.server, terminus.class.port).and_return(stub_response)

        terminus.send(method, request)
      end
    end
  end
end
