diff --git a/lib/puppet/network/format_handler.rb b/lib/puppet/network/format_handler.rb index cfce5e777..54f09c21b 100644 --- a/lib/puppet/network/format_handler.rb +++ b/lib/puppet/network/format_handler.rb @@ -1,68 +1,92 @@ require 'yaml' require 'puppet/network' require 'puppet/network/format' module Puppet::Network::FormatHandler class FormatError < Puppet::Error; end @formats = {} def self.create(*args, &block) instance = Puppet::Network::Format.new(*args, &block) @formats[instance.name] = instance instance end def self.create_serialized_formats(name,options = {},&block) ["application/x-#{name}", "application/#{name}", "text/x-#{name}", "text/#{name}"].each { |mime_type| create name, {:mime => mime_type}.update(options), &block } end def self.format(name) @formats[name.to_s.downcase.intern] end def self.format_for(name) name = format_to_canonical_name(name) format(name) end def self.format_by_extension(ext) @formats.each do |name, format| return format if format.extension == ext end nil end # Provide a list of all formats. def self.formats @formats.keys end # Return a format capable of handling the provided mime type. def self.mime(mimetype) mimetype = mimetype.to_s.downcase @formats.values.find { |format| format.mime == mimetype } end # Return a format name given: # * a format name # * a mime-type # * a format instance def self.format_to_canonical_name(format) case format when Puppet::Network::Format out = format when %r{\w+/\w+} out = mime(format) else out = format(format) end raise ArgumentError, "No format match the given format name or mime-type (#{format})" if out.nil? out.name end + + # Determine which of the accepted formats should be used given what is supported. + # + # @param accepted [Array] the accepted formats in a form a + # that can be understood by #format_to_canonical_name and is in order of + # preference (most preferred is first) + # @param supported [Array] the names of the supported formats (order + # does not matter) + # @return [Puppet::Network::Format, nil] the most suitable format + # @api private + def self.most_suitable_format_for(accepted, supported) + format_name = accepted.find do |accepted| + begin + format_name = format_to_canonical_name(accepted) + supported.include?(format_name) + rescue ArgumentError + false + end + end + + if format_name + format_for(format_name) + end + end end require 'puppet/network/formats' diff --git a/lib/puppet/network/format_support.rb b/lib/puppet/network/format_support.rb index c02d3d541..7cc6cc001 100644 --- a/lib/puppet/network/format_support.rb +++ b/lib/puppet/network/format_support.rb @@ -1,100 +1,106 @@ require 'puppet/network/format_handler' # Provides network serialization support when included module Puppet::Network::FormatSupport def self.included(klass) klass.extend(ClassMethods) end module ClassMethods def convert_from(format, data) get_format(format).intern(self, data) rescue => err raise Puppet::Network::FormatHandler::FormatError, "Could not intern from #{format}: #{err}", err.backtrace end def convert_from_multiple(format, data) get_format(format).intern_multiple(self, data) rescue => err raise Puppet::Network::FormatHandler::FormatError, "Could not intern_multiple from #{format}: #{err}", err.backtrace end def render_multiple(format, instances) get_format(format).render_multiple(instances) rescue => err raise Puppet::Network::FormatHandler::FormatError, "Could not render_multiple to #{format}: #{err}", err.backtrace end def default_format supported_formats[0] end def support_format?(name) Puppet::Network::FormatHandler.format(name).supported?(self) end def supported_formats - result = format_handler.formats.collect { |f| format_handler.format(f) }.find_all { |f| f.supported?(self) }.collect { |f| f.name }.sort do |a, b| + result = format_handler.formats.collect do |f| + format_handler.format(f) + end.find_all do |f| + f.supported?(self) + end.sort do |a, b| # It's an inverse sort -- higher weight formats go first. - format_handler.format(b).weight <=> format_handler.format(a).weight + b.weight <=> a.weight + end.collect do |f| + f.name end result = put_preferred_format_first(result) - Puppet.debug "#{friendly_name} supports formats: #{result.map{ |f| f.to_s }.sort.join(' ')}; using #{result.first}" + Puppet.debug "#{friendly_name} supports formats: #{result.join(' ')}" result end # @api private def get_format(format_name) format_handler.format_for(format_name) end private def format_handler Puppet::Network::FormatHandler end def friendly_name if self.respond_to? :indirection indirection.name else self end end def put_preferred_format_first(list) preferred_format = Puppet.settings[:preferred_serialization_format].to_sym if list.include?(preferred_format) list.delete(preferred_format) list.unshift(preferred_format) else Puppet.debug "Value of 'preferred_serialization_format' (#{preferred_format}) is invalid for #{friendly_name}, using default (#{list.first})" end list end end def render(format = nil) format ||= self.class.default_format self.class.get_format(format).render(self) rescue => err raise Puppet::Network::FormatHandler::FormatError, "Could not render to #{format}: #{err}", err.backtrace end def mime(format = nil) format ||= self.class.default_format self.class.get_format(format).mime rescue => err raise Puppet::Network::FormatHandler::FormatError, "Could not mime to #{format}: #{err}", err.backtrace end def support_format?(name) self.class.support_format?(name) end end diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index d070c1264..7c7fa4f8d 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,294 +1,333 @@ module Puppet::Network::HTTP end require 'puppet/network/http' require 'puppet/network/http/api/v1' require 'puppet/network/authorization' require 'puppet/network/authentication' require 'puppet/network/rights' require 'puppet/util/profiler' require 'resolv' module Puppet::Network::HTTP::Handler include Puppet::Network::HTTP::API::V1 include Puppet::Network::Authorization include Puppet::Network::Authentication + class HTTPError < Exception + attr_reader :status + + def initialize(message, status) + super(message) + @status = status + end + end + + class HTTPNotAcceptableError < HTTPError + def initialize(message) + super("Not Acceptable: " + message, 406) + end + end + attr_reader :server, :handler YAML_DEPRECATION = "YAML in network requests is deprecated and will be removed in a future version. See http://links.puppetlabs.com/deprecate_yaml_on_network" # Retrieve all headers from the http request, as a hash with the header names # (lower-cased) as the keys def headers(request) raise NotImplementedError end # Retrieve the accept header from the http request. def accept_header(request) raise NotImplementedError end # Retrieve the Content-Type header from the http request. def content_type_header(request) raise NotImplementedError end # Which format to use when serializing our response or interpreting the request. # IF the client provided a Content-Type use this, otherwise use the Accept header # and just pick the first value. def format_to_use(request) unless header = accept_header(request) raise ArgumentError, "An Accept header must be provided to pick the right format" end format = nil header.split(/,\s*/).each do |name| next unless format = Puppet::Network::FormatHandler.format(name) next unless format.suitable? return format end raise "No specified acceptable formats (#{header}) are functional on this machine" end def request_format(request) if header = content_type_header(request) header.gsub!(/\s*;.*$/,'') # strip any charset format = Puppet::Network::FormatHandler.mime(header) raise "Client sent a mime-type (#{header}) that doesn't correspond to a format we support" if format.nil? return format.name.to_s if format.suitable? end raise "No Content-Type header was received, it isn't possible to unserialize the request" end def format_to_mime(format) format.is_a?(Puppet::Network::Format) ? format.mime : format end def initialize_for_puppet(server) @server = server end # handle an HTTP request def process(request, response) request_headers = headers(request) request_params = params(request) request_method = http_method(request) request_path = path(request) configure_profiler(request_headers, request_params) Puppet::Util::Profiler.profile("Processed request #{request_method} #{request_path}") do indirection, method, key, params = uri2indirection(request_method, request_path, request_params) check_authorization(indirection, method, key, params) warn_if_near_expiration(client_cert(request)) send("do_#{method}", indirection, key, params, request, response) end rescue SystemExit,NoMemoryError raise rescue Exception => e return do_exception(response, e) ensure cleanup(request) end # Set the response up, with the body and status. def set_response(response, body, status = 200) raise NotImplementedError end # Set the specified format as the content type of the response. def set_content_type(response, format) raise NotImplementedError end def do_exception(response, exception, status=400) if exception.is_a?(Puppet::Network::AuthorizationError) # make sure we return the correct status code # for authorization issues status = 403 if status == 400 end if exception.is_a?(Exception) Puppet.log_exception(exception) end + + if exception.respond_to?(:status) + status = exception.status + end + set_content_type(response, "text/plain") set_response(response, exception.to_s, status) end def model(indirection_name) raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless indirection = Puppet::Indirector::Indirection.instance(indirection_name.to_sym) indirection.model end # Execute our find. def do_find(indirection_name, key, params, request, response) unless result = model(indirection_name).indirection.find(key, params) Puppet.info("Could not find #{indirection_name} for '#{key}'") return do_exception(response, "Could not find #{indirection_name} #{key}", 404) end # The encoding of the result must include the format to use, # and it needs to be used for both the rendering and as # the content type. format = format_to_use(request) set_content_type(response, format) rendered_result = result if result.respond_to?(:render) Puppet::Util::Profiler.profile("Rendered result in #{format}") do rendered_result = result.render(format) end end Puppet::Util::Profiler.profile("Sent response") do set_response(response, rendered_result) end end # Execute our head. def do_head(indirection_name, key, params, request, response) unless self.model(indirection_name).indirection.head(key, params) Puppet.info("Could not find #{indirection_name} for '#{key}'") return do_exception(response, "Could not find #{indirection_name} #{key}", 404) end # No need to set a response because no response is expected from a # HEAD request. All we need to do is not die. end # Execute our search. def do_search(indirection_name, key, params, request, response) model = self.model(indirection_name) result = model.indirection.search(key, params) if result.nil? return do_exception(response, "Could not find instances in #{indirection_name} with '#{key}'", 404) end format = format_to_use(request) set_content_type(response, format) set_response(response, model.render_multiple(format, result)) end # Execute our destroy. def do_destroy(indirection_name, key, params, request, response) result = model(indirection_name).indirection.destroy(key, params) return_yaml_response(response, result) end # Execute our save. def do_save(indirection_name, key, params, request, response) - data = body(request).to_s - raise ArgumentError, "No data to save" if !data or data.empty? + model_class = model(indirection_name) + formatter = response_formatter_for(model_class, request) + sent_object = read_body_into_model(model_class, request) - format = request_format(request) - if format == 'yaml' - Puppet.deprecation_warning(YAML_DEPRECATION) - end - obj = model(indirection_name).convert_from(format, data) - result = model(indirection_name).indirection.save(obj, key) - return_yaml_response(response, result) + result = model_class.indirection.save(sent_object, key) + + set_content_type(response, formatter) + set_response(response, formatter.render(result)) end # resolve node name from peer's ip address # this is used when the request is unauthenticated def resolve_node(result) begin return Resolv.getname(result[:ip]) rescue => detail Puppet.err "Could not resolve #{result[:ip]}: #{detail}" end result[:ip] end private + def response_formatter_for(model_class, request) + accepted_formats = accept_header(request) || "yaml" + formatter = Puppet::Network::FormatHandler.most_suitable_format_for( + accepted_formats.split(/\s*,\s*/), + model_class.supported_formats) + + if formatter.nil? + raise HTTPNotAcceptableError, "No supported formats are acceptable (Accept: #{accepted_formats})" + end + + formatter + end + + def read_body_into_model(model_class, request) + data = body(request).to_s + raise ArgumentError, "No data to save" if !data or data.empty? + + format = request_format(request) + model_class.convert_from(format, data) + end + def return_yaml_response(response, body) Puppet.deprecation_warning(YAML_DEPRECATION) set_content_type(response, Puppet::Network::FormatHandler.format("yaml")) set_response(response, body.to_yaml) end def get?(request) http_method(request) == 'GET' end def put?(request) http_method(request) == 'PUT' end def delete?(request) http_method(request) == 'DELETE' end # methods to be overridden by the including web server class def http_method(request) raise NotImplementedError end def path(request) raise NotImplementedError end def request_key(request) raise NotImplementedError end def body(request) raise NotImplementedError end def params(request) raise NotImplementedError end def client_cert(request) raise NotImplementedError end def cleanup(request) # By default, there is nothing to cleanup. end def decode_params(params) params.inject({}) do |result, ary| param, value = ary next result if param.nil? || param.empty? param = param.to_sym # These shouldn't be allowed to be set by clients # in the query string, for security reasons. next result if param == :node next result if param == :ip value = CGI.unescape(value) if value =~ /^---/ Puppet.debug("Found YAML while processing request parameter #{param} (value: <#{value}>)") Puppet.deprecation_warning(YAML_DEPRECATION) value = YAML.load(value, :safe => true, :deserialize_symbols => true) else value = true if value == "true" value = false if value == "false" value = Integer(value) if value =~ /^\d+$/ value = value.to_f if value =~ /^\d+\.\d+$/ end result[param] = value result end end def configure_profiler(request_headers, request_params) if (request_headers.has_key?(Puppet::Network::HTTP::HEADER_ENABLE_PROFILING.downcase) or Puppet[:profile]) Puppet::Util::Profiler.current = Puppet::Util::Profiler::WallClock.new(Puppet.method(:debug), request_params.object_id) else Puppet::Util::Profiler.current = Puppet::Util::Profiler::NONE end end end diff --git a/spec/unit/indirector/rest_spec.rb b/spec/unit/indirector/rest_spec.rb index ff7f98a1b..8a3fbca7f 100755 --- a/spec/unit/indirector/rest_spec.rb +++ b/spec/unit/indirector/rest_spec.rb @@ -1,515 +1,516 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/indirector' require 'puppet/indirector/errors' require 'puppet/indirector/rest' # Just one from each category since the code makes no real distinctions HTTP_ERROR_CODES = [300, 400, 500] shared_examples_for "a REST terminus method" do |terminus_method| HTTP_ERROR_CODES.each do |code| describe "when the response code is #{code}" do let(:response) { mock_response(code, 'error messaged!!!') } it "raises an http error with the body of the response" do expect { terminus.send(terminus_method, request) }.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{response.body}") end it "does not attempt to deserialize the response" do model.expects(:convert_from).never 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 response.stubs(:body).returns "" response.stubs(:message).returns "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 uncompressed_body = "why" compressed_body = Zlib::Deflate.deflate(uncompressed_body) response = mock_response(code, compressed_body, 'text/plain', 'deflate') connection.expects(http_method).returns(response) expect { terminus.send(terminus_method, request) }.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{uncompressed_body}") 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 model.expects(:convert_from).raises(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 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 ==(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 } def mock_response(code, body, content_type='text/plain', encoding=nil) obj = stub('http 200 ok', :code => code.to_s, :body => body) obj.stubs(:[]).with('content-type').returns(content_type) obj.stubs(:[]).with('content-encoding').returns(encoding) 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 include the v1 REST API module" do Puppet::Indirector::REST.ancestors.should be_include(Puppet::Network::HTTP::API::V1) end it "should have a method for specifying what setting a subclass should use to retrieve its server" do terminus_class.should respond_to(:use_server_setting) end it "should use any specified setting to pick the server" do terminus_class.expects(:server_setting).returns :inventory_server Puppet[:inventory_server] = "myserver" terminus_class.server.should == "myserver" end it "should default to :server for the server setting" do terminus_class.expects(:server_setting).returns nil Puppet[:server] = "myserver" terminus_class.server.should == "myserver" end it "should have a method for specifying what setting a subclass should use to retrieve its port" do terminus_class.should respond_to(:use_port_setting) end it "should use any specified setting to pick the port" do terminus_class.expects(:port_setting).returns :ca_port Puppet[:ca_port] = "321" terminus_class.port.should == 321 end it "should default to :port for the port setting" do terminus_class.expects(:port_setting).returns nil Puppet[:masterport] = "543" terminus_class.port.should == 543 end it 'should default to :puppet for the srv_service' do Puppet::Indirector::REST.srv_service.should == :puppet 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 = stub 'request', :key => "foo", :server => nil, :port => nil terminus.class.expects(:port).returns 321 terminus.class.expects(:server).returns "myserver" Puppet::Network::HTTP::Connection.expects(:new).with("myserver", 321).returns "myconn" terminus.network(@request).should == "myconn" end it "should use the server from the indirection request if one is present" do @request = stub 'request', :key => "foo", :server => "myserver", :port => nil terminus.class.stubs(:port).returns 321 Puppet::Network::HTTP::Connection.expects(:new).with("myserver", 321).returns "myconn" terminus.network(@request).should == "myconn" end it "should use the port from the indirection request if one is present" do @request = stub 'request', :key => "foo", :server => nil, :port => 321 terminus.class.stubs(:server).returns "myserver" Puppet::Network::HTTP::Connection.expects(:new).with("myserver", 321).returns "myconn" terminus.network(@request).should == "myconn" end end describe "#find" do let(:http_method) { :get } let(:response) { mock_response(200, 'body') } let(:connection) { stub('mock http connection', :get => response, :verify_callback= => nil) } let(:request) { find_request('foo') } before :each do terminus.stubs(:network).returns(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') request = find_request('whoa', params) connection.expects(:post).with do |uri, body| body.split("&").sort == params.map {|key,value| "#{key}=#{value}"}.sort end.returns(mock_response(200, 'body')) terminus.find(request) end end describe "with no parameters" do it "calls get on the connection" do request = find_request('foo bar') connection.expects(:get).with('/production/test_model/foo%20bar?', anything).returns(mock_response('200', 'response body')) terminus.find(request).should == model.new('foo bar', 'response body') end end it "returns nil on 404" do response = mock_response('404', nil) connection.expects(:get).returns(response) terminus.find(request).should == nil end it "asks the model to deserialize the response body and sets the name on the resulting object to the find key" do connection.expects(:get).returns response model.expects(:convert_from).with(response['content-type'], response.body).returns( model.new('overwritten', 'decoded body') ) terminus.find(request).should == model.new('foo', 'decoded body') end it "doesn't require the model to support name=" do connection.expects(:get).returns response instance = model.new('name', 'decoded body') model.expects(:convert_from).with(response['content-type'], response.body).returns(instance) instance.expects(:respond_to?).with(:name=).returns(false) instance.expects(:name=).never terminus.find(request).should == model.new('name', 'decoded body') end it "provides an Accept header containing the list of supported formats joined with commas" do connection.expects(:get).with(anything, has_entry("Accept" => "supported, formats")).returns(response) terminus.model.expects(:supported_formats).returns %w{supported formats} terminus.find(request) end it "adds an Accept-Encoding header" do terminus.expects(:add_accept_encoding).returns({"accept-encoding" => "gzip"}) connection.expects(:get).with(anything, has_entry("accept-encoding" => "gzip")).returns(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") connection.expects(:get).returns(response) model.expects(:convert_from).with("text/plain", "mydata").returns "myobject" terminus.find(request).should == "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') connection.expects(:get).returns(response) model.expects(:convert_from).with("text/plain", uncompressed_body).returns "myobject" terminus.find(request).should == "myobject" end end describe "#head" do let(:http_method) { :head } let(:response) { mock_response(200, nil) } let(:connection) { stub('mock http connection', :head => response, :verify_callback= => nil) } let(:request) { head_request('foo') } before :each do terminus.stubs(:network).returns(connection) end it_behaves_like 'a REST terminus method', :head it "returns true if there was a successful http response" do connection.expects(:head).returns mock_response('200', nil) terminus.head(request).should == true end it "returns false on a 404 response" do connection.expects(:head).returns mock_response('404', nil) terminus.head(request).should == false end end describe "#search" do let(:http_method) { :get } let(:response) { mock_response(200, 'data1,data2,data3') } let(:connection) { stub('mock http connection', :get => response, :verify_callback= => nil) } let(:request) { search_request('foo') } before :each do terminus.stubs(:network).returns(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 connection.expects(:get).with('/production/test_models/foo', has_key('Accept')).returns mock_response(200, 'data3, data4') terminus.search(request) end it "returns an empty list on 404" do response = mock_response('404', nil) connection.expects(:get).returns(response) terminus.search(request).should == [] end it "asks the model to deserialize the response body into multiple instances" do terminus.search(request).should == [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 connection.expects(:get).with(anything, has_entry("Accept" => "supported, formats")).returns(mock_response(200, '')) terminus.model.expects(:supported_formats).returns %w{supported formats} terminus.search(request) end it "should return an empty array if serialization returns nil" do model.stubs(:convert_from_multiple).returns nil terminus.search(request).should == [] end end describe "#destroy" do let(:http_method) { :delete } let(:response) { mock_response(200, 'body') } let(:connection) { stub('mock http connection', :delete => response, :verify_callback= => nil) } let(:request) { delete_request('foo') } before :each do terminus.stubs(:network).returns(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 connection.expects(:delete).with('/production/test_model/foo', has_key('Accept')).returns(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 connection.expects(:delete).returns response terminus.destroy(request).should == model.new('', 'body') end it "returns nil on 404" do response = mock_response('404', nil) connection.expects(:delete).returns(response) terminus.destroy(request).should == nil end it "should provide an Accept header containing the list of supported formats joined with commas" do connection.expects(:delete).with(anything, has_entry("Accept" => "supported, formats")).returns(response) terminus.model.expects(:supported_formats).returns %w{supported formats} terminus.destroy(request) end end describe "#save" do let(:http_method) { :put } let(:response) { mock_response(200, 'body') } let(:connection) { stub('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 terminus.stubs(:network).returns(connection) end it_behaves_like 'a REST terminus method', :save it "should call the PUT http method on a network connection" do connection.expects(:put).with('/production/test_model/the%20thing', anything, has_key("Content-Type")).returns 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 instance.expects(:render).returns "serial_instance" connection.expects(:put).with(anything, "serial_instance", anything).returns response terminus.save(request) end it "returns nil on 404" do response = mock_response('404', nil) connection.expects(:put).returns(response) terminus.save(request).should == nil end it "returns nil" do connection.expects(:put).returns response terminus.save(request).should be_nil end it "should provide an Accept header containing the list of supported formats joined with commas" do connection.expects(:put).with(anything, anything, has_entry("Accept" => "supported, formats")).returns(response) instance.expects(:render).returns('') model.expects(:supported_formats).returns %w{supported formats} instance.expects(:mime).returns "supported" terminus.save(request) end it "should provide a Content-Type header containing the mime-type of the sent object" do instance.expects(:mime).returns "mime" connection.expects(:put).with(anything, anything, has_entry('Content-Type' => "mime")).returns(response) terminus.save(request) 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') request.expects(:do_request).with(terminus.class.srv_service, terminus.class.server, terminus.class.port).returns(stub_response) terminus.send(method, request) end end end end diff --git a/spec/unit/network/format_handler_spec.rb b/spec/unit/network/format_handler_spec.rb index 631435018..51a684dae 100755 --- a/spec/unit/network/format_handler_spec.rb +++ b/spec/unit/network/format_handler_spec.rb @@ -1,52 +1,86 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/network/format_handler' describe Puppet::Network::FormatHandler do before(:each) do @saved_formats = Puppet::Network::FormatHandler.instance_variable_get(:@formats).dup Puppet::Network::FormatHandler.instance_variable_set(:@formats, {}) end after(:each) do Puppet::Network::FormatHandler.instance_variable_set(:@formats, @saved_formats) end describe "when creating formats" do it "should instance_eval any block provided when creating a format" do - format = Puppet::Network::FormatHandler.create(:instance_eval) do + format = Puppet::Network::FormatHandler.create(:test_format) do def asdfghjkl; end end format.should respond_to(:asdfghjkl) end end describe "when retrieving formats" do let!(:format) { Puppet::Network::FormatHandler.create(:the_format, :extension => "foo", :mime => "foo/bar") } it "should be able to retrieve a format by name" do Puppet::Network::FormatHandler.format(:the_format).should equal(format) end it "should be able to retrieve a format by extension" do Puppet::Network::FormatHandler.format_by_extension("foo").should equal(format) end it "should return nil if asked to return a format by an unknown extension" do Puppet::Network::FormatHandler.format_by_extension("yayness").should be_nil end it "should be able to retrieve formats by name irrespective of case" do Puppet::Network::FormatHandler.format(:The_Format).should equal(format) end it "should be able to retrieve a format by mime type" do Puppet::Network::FormatHandler.mime("foo/bar").should equal(format) end it "should be able to retrieve a format by mime type irrespective of case" do Puppet::Network::FormatHandler.mime("Foo/Bar").should equal(format) end end + + describe "#most_suitable_format_for" do + before :each do + Puppet::Network::FormatHandler.create(:one, :extension => "foo", :mime => "text/one") + Puppet::Network::FormatHandler.create(:two, :extension => "bar", :mime => "text/two") + end + + let(:format_one) { Puppet::Network::FormatHandler.format(:one) } + let(:format_two) { Puppet::Network::FormatHandler.format(:two) } + + def suitable_in_setup_formats(accepted) + Puppet::Network::FormatHandler.most_suitable_format_for(accepted, [:one, :two]) + end + + it "finds no format when none are acceptable" do + suitable_in_setup_formats(["three"]).should be_nil + end + + it "skips unsupported, but accepted, formats" do + suitable_in_setup_formats(["three", "two"]).should == format_two + end + + it "gives the first acceptable and suitable format" do + suitable_in_setup_formats(["three", "one", "two"]).should == format_one + end + + it "allows specifying acceptable formats by mime type" do + suitable_in_setup_formats(["text/one"]).should == format_one + end + + it "allows specifying acceptable formats by canonical name" do + suitable_in_setup_formats([:one]).should == format_one + end + end end diff --git a/spec/unit/network/format_support_spec.rb b/spec/unit/network/format_support_spec.rb index a7383558c..8e43ae135 100644 --- a/spec/unit/network/format_support_spec.rb +++ b/spec/unit/network/format_support_spec.rb @@ -1,199 +1,199 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/network/format_handler' require 'puppet/network/format_support' class FormatTester include Puppet::Network::FormatSupport end describe Puppet::Network::FormatHandler do before(:each) do @saved_formats = Puppet::Network::FormatHandler.instance_variable_get(:@formats).dup Puppet::Network::FormatHandler.instance_variable_set(:@formats, {}) end after(:each) do Puppet::Network::FormatHandler.instance_variable_set(:@formats, @saved_formats) end describe "when listing formats" do before(:each) do one = Puppet::Network::FormatHandler.create(:one, :weight => 1) one.stubs(:supported?).returns(true) two = Puppet::Network::FormatHandler.create(:two, :weight => 6) two.stubs(:supported?).returns(true) three = Puppet::Network::FormatHandler.create(:three, :weight => 2) three.stubs(:supported?).returns(true) four = Puppet::Network::FormatHandler.create(:four, :weight => 8) four.stubs(:supported?).returns(false) end it "should return all supported formats in decreasing order of weight" do FormatTester.supported_formats.should == [:two, :three, :one] end end it "should return the first format as the default format" do FormatTester.expects(:supported_formats).returns [:one, :two] FormatTester.default_format.should == :one end describe "with a preferred serialization format setting" do before do one = Puppet::Network::FormatHandler.create(:one, :weight => 1) one.stubs(:supported?).returns(true) two = Puppet::Network::FormatHandler.create(:two, :weight => 6) two.stubs(:supported?).returns(true) end describe "that is supported" do before do Puppet[:preferred_serialization_format] = :one end it "should return the preferred serialization format first" do FormatTester.supported_formats.should == [:one, :two] end end describe "that is not supported" do before do Puppet[:preferred_serialization_format] = :unsupported end it "should return the default format first" do FormatTester.supported_formats.should == [:two, :one] end it "should log a debug message" do Puppet.expects(:debug).with("Value of 'preferred_serialization_format' (unsupported) is invalid for FormatTester, using default (two)") - Puppet.expects(:debug).with("FormatTester supports formats: one two; using two") + Puppet.expects(:debug).with("FormatTester supports formats: two one") FormatTester.supported_formats end end end describe "when using formats" do let(:format) { Puppet::Network::FormatHandler.create(:my_format, :mime => "text/myformat") } it "should use the Format to determine whether a given format is supported" do format.expects(:supported?).with(FormatTester) FormatTester.support_format?(:my_format) end it "should call the format-specific converter when asked to convert from a given format" do format.expects(:intern).with(FormatTester, "mydata") FormatTester.convert_from(:my_format, "mydata") end it "should call the format-specific converter when asked to convert from a given format by mime-type" do format.expects(:intern).with(FormatTester, "mydata") FormatTester.convert_from("text/myformat", "mydata") end it "should call the format-specific converter when asked to convert from a given format by format instance" do format.expects(:intern).with(FormatTester, "mydata") FormatTester.convert_from(format, "mydata") end it "should raise a FormatError when an exception is encountered when converting from a format" do format.expects(:intern).with(FormatTester, "mydata").raises "foo" expect do FormatTester.convert_from(:my_format, "mydata") end.to raise_error( Puppet::Network::FormatHandler::FormatError, 'Could not intern from my_format: foo' ) end it "should be able to use a specific hook for converting into multiple instances" do format.expects(:intern_multiple).with(FormatTester, "mydata") FormatTester.convert_from_multiple(:my_format, "mydata") end it "should raise a FormatError when an exception is encountered when converting multiple items from a format" do format.expects(:intern_multiple).with(FormatTester, "mydata").raises "foo" expect do FormatTester.convert_from_multiple(:my_format, "mydata") end.to raise_error(Puppet::Network::FormatHandler::FormatError, 'Could not intern_multiple from my_format: foo') end it "should be able to use a specific hook for rendering multiple instances" do format.expects(:render_multiple).with("mydata") FormatTester.render_multiple(:my_format, "mydata") end it "should raise a FormatError when an exception is encountered when rendering multiple items into a format" do format.expects(:render_multiple).with("mydata").raises "foo" expect do FormatTester.render_multiple(:my_format, "mydata") end.to raise_error(Puppet::Network::FormatHandler::FormatError, 'Could not render_multiple to my_format: foo') end end describe "when an instance" do let(:format) { Puppet::Network::FormatHandler.create(:foo, :mime => "text/foo") } it "should list as supported a format that reports itself supported" do format.expects(:supported?).returns true FormatTester.new.support_format?(:foo).should be_true end it "should raise a FormatError when a rendering error is encountered" do tester = FormatTester.new format.expects(:render).with(tester).raises "eh" expect do tester.render(:foo) end.to raise_error(Puppet::Network::FormatHandler::FormatError, 'Could not render to foo: eh') end it "should call the format-specific converter when asked to convert to a given format" do tester = FormatTester.new format.expects(:render).with(tester).returns "foo" tester.render(:foo).should == "foo" end it "should call the format-specific converter when asked to convert to a given format by mime-type" do tester = FormatTester.new format.expects(:render).with(tester).returns "foo" tester.render("text/foo").should == "foo" end it "should call the format converter when asked to convert to a given format instance" do tester = FormatTester.new format.expects(:render).with(tester).returns "foo" tester.render(format).should == "foo" end it "should render to the default format if no format is provided when rendering" do FormatTester.expects(:default_format).returns :foo tester = FormatTester.new format.expects(:render).with(tester) tester.render end it "should call the format-specific converter when asked for the mime-type of a given format" do tester = FormatTester.new format.expects(:mime).returns "text/foo" tester.mime(:foo).should == "text/foo" end it "should return the default format mime-type if no format is provided" do FormatTester.expects(:default_format).returns :foo tester = FormatTester.new format.expects(:mime).returns "text/foo" tester.mime.should == "text/foo" end end end diff --git a/spec/unit/network/http/handler_spec.rb b/spec/unit/network/http/handler_spec.rb index b03138e59..37831067e 100755 --- a/spec/unit/network/http/handler_spec.rb +++ b/spec/unit/network/http/handler_spec.rb @@ -1,543 +1,581 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/network/http' require 'puppet/network/http/handler' require 'puppet/network/authorization' require 'puppet/network/authentication' require 'puppet/indirector/memory' describe Puppet::Network::HTTP::Handler do let(:handler) { TestingHandler.new } it "should include the v1 REST API" do Puppet::Network::HTTP::Handler.ancestors.should be_include(Puppet::Network::HTTP::API::V1) end it "should include the Rest Authorization system" do Puppet::Network::HTTP::Handler.ancestors.should be_include(Puppet::Network::Authorization) end describe "when initializing" do it "should fail when no server type has been provided" do lambda { handler.initialize_for_puppet }.should raise_error(ArgumentError) end it "should set server type" do handler.initialize_for_puppet("foo") handler.server.should == "foo" end end describe "when processing a request" do let(:request) do { :accept_header => "format_one,format_two", :content_type_header => "text/yaml", :http_method => "GET", :path => "/my_handler/my_result", :params => {}, :client_cert => nil } end let(:response) { mock('http response') } before do @model_class = stub('indirected model class') @indirection = stub('indirection') @model_class.stubs(:indirection).returns(@indirection) @result = stub 'result', :render => "mytext" request[:headers] = { "Content-Type" => request[:content_type_header], "Accept" => request[:accept_header] } handler.stubs(:check_authorization) handler.stubs(:warn_if_near_expiration) handler.stubs(:headers).returns(request[:headers]) end it "should check the client certificate for upcoming expiration" do cert = mock 'cert' handler.stubs(:uri2indirection).returns(["facts", :mymethod, "key", {:node => "name"}]) handler.expects(:client_cert).returns(cert).with(request) handler.expects(:warn_if_near_expiration).with(cert) handler.process(request, response) end it "should setup a profiler when the puppet-profiling header exists" do request[:headers][Puppet::Network::HTTP::HEADER_ENABLE_PROFILING.downcase] = "true" handler.process(request, response) Puppet::Util::Profiler.current.should be_kind_of(Puppet::Util::Profiler::WallClock) end it "should not setup profiler when the profile parameter is missing" do request[:params] = { } handler.process(request, response) Puppet::Util::Profiler.current.should == Puppet::Util::Profiler::NONE end it "should create an indirection request from the path, parameters, and http method" do request[:path] = "mypath" request[:http_method] = "mymethod" request[:params] = { :params => "mine" } handler.expects(:uri2indirection).with("mymethod", "mypath", { :params => "mine" }).returns stub("request", :method => :find) handler.stubs(:do_find) handler.process(request, response) end it "should call the 'do' method and delegate authorization to the authorization layer" do handler.expects(:uri2indirection).returns(["facts", :mymethod, "key", {:node => "name"}]) handler.expects(:do_mymethod).with("facts", "key", {:node => "name"}, request, response) handler.expects(:check_authorization).with("facts", :mymethod, "key", {:node => "name"}) handler.process(request, response) end it "should return 403 if the request is not authorized" do handler.expects(:uri2indirection).returns(["facts", :mymethod, "key", {:node => "name"}]) handler.expects(:do_mymethod).never handler.expects(:check_authorization).with("facts", :mymethod, "key", {:node => "name"}).raises(Puppet::Network::AuthorizationError.new("forbidden")) handler.expects(:set_response).with(anything, anything, 403) handler.process(request, response) end it "should serialize a controller exception when an exception is thrown while finding the model instance" do handler.expects(:uri2indirection).returns(["facts", :find, "key", {:node => "name"}]) handler.expects(:do_find).raises(ArgumentError, "The exception") handler.expects(:set_response).with(anything, "The exception", 400) handler.process(request, response) end it "should set the format to text/plain when serializing an exception" do handler.expects(:set_content_type).with(response, "text/plain") + + handler.do_exception(response, "A test", 404) + end + + it "sends an exception string with the given status" do + handler.expects(:set_response).with(response, "A test", 404) + handler.do_exception(response, "A test", 404) end + it "sends an exception error with the exception's status" do + error = Puppet::Network::HTTP::Handler::HTTPNotAcceptableError.new("test message") + + handler.expects(:set_response).with(response, error.to_s, error.status) + + handler.do_exception(response, error) + end + it "should raise an error if the request is formatted in an unknown format" do handler.stubs(:content_type_header).returns "unknown format" lambda { handler.request_format(request) }.should raise_error end it "should still find the correct format if content type contains charset information" do handler.stubs(:content_type_header).returns "text/plain; charset=UTF-8" handler.request_format(request).should == "s" end it "should deserialize YAML parameters" do params = {'my_param' => [1,2,3].to_yaml} decoded_params = handler.send(:decode_params, params) decoded_params.should == {:my_param => [1,2,3]} end it "should ignore tags on YAML parameters" do params = {'my_param' => "--- !ruby/object:Array {}"} decoded_params = handler.send(:decode_params, params) decoded_params[:my_param].should be_a(Hash) end describe "when finding a model instance" do before do @indirection.stubs(:find).returns @result Puppet::Indirector::Indirection.expects(:instance).with(:my_handler).returns( stub "indirection", :model => @model_class ) @format = stub 'format', :suitable? => true, :mime => "text/format", :name => "format" Puppet::Network::FormatHandler.stubs(:format).returns @format @oneformat = stub 'one', :suitable? => true, :mime => "text/one", :name => "one" Puppet::Network::FormatHandler.stubs(:format).with("one").returns @oneformat end it "should use the indirection request to find the model class" do handler.do_find("my_handler", "my_result", {}, request, response) end it "should use the escaped request key" do @indirection.expects(:find).with("my_result", anything).returns @result handler.do_find("my_handler", "my_result", {}, request, response) end it "should use a common method for determining the request parameters" do @indirection.expects(:find).with(anything, has_entries(:foo => :baz, :bar => :xyzzy)).returns @result handler.do_find("my_handler", "my_result", {:foo => :baz, :bar => :xyzzy}, request, response) end it "should set the content type to the first format specified in the accept header" do handler.expects(:accept_header).with(request).returns "one,two" handler.expects(:set_content_type).with(response, @oneformat) handler.do_find("my_handler", "my_result", {}, request, response) end it "should fail if no accept header is provided" do handler.expects(:accept_header).with(request).returns nil lambda { handler.do_find("my_handler", "my_result", {}, request, response) }.should raise_error(ArgumentError) end it "should fail if the accept header does not contain a valid format" do handler.expects(:accept_header).with(request).returns "" lambda { handler.do_find("my_handler", "my_result", {}, request, response) }.should raise_error(RuntimeError) end it "should not use an unsuitable format" do handler.expects(:accept_header).with(request).returns "foo,bar" foo = mock 'foo', :suitable? => false bar = mock 'bar', :suitable? => true Puppet::Network::FormatHandler.expects(:format).with("foo").returns foo Puppet::Network::FormatHandler.expects(:format).with("bar").returns bar handler.expects(:set_content_type).with(response, bar) # the suitable one handler.do_find("my_handler", "my_result", {}, request, response) end it "should render the result using the first format specified in the accept header" do handler.expects(:accept_header).with(request).returns "one,two" @result.expects(:render).with(@oneformat) handler.do_find("my_handler", "my_result", {}, request, response) end it "should pass the result through without rendering it if the result is a string" do @indirection.stubs(:find).returns "foo" handler.expects(:set_response).with(response, "foo") handler.do_find("my_handler", "my_result", {}, request, response) end it "should use the default status when a model find call succeeds" do handler.expects(:set_response).with(anything, anything, nil) handler.do_find("my_handler", "my_result", {}, request, response) end it "should return a serialized object when a model find call succeeds" do @model_instance = stub('model instance') @model_instance.expects(:render).returns "my_rendered_object" handler.expects(:set_response).with(anything, "my_rendered_object", anything) @indirection.stubs(:find).returns(@model_instance) handler.do_find("my_handler", "my_result", {}, request, response) end it "should return a 404 when no model instance can be found" do @model_class.stubs(:name).returns "my name" handler.expects(:set_response).with(anything, anything, 404) @indirection.stubs(:find).returns(nil) handler.do_find("my_handler", "my_result", {}, request, response) end it "should write a log message when no model instance can be found" do @model_class.stubs(:name).returns "my name" @indirection.stubs(:find).returns(nil) Puppet.expects(:info).with("Could not find my_handler for 'my_result'") handler.do_find("my_handler", "my_result", {}, request, response) end it "should serialize the result in with the appropriate format" do @model_instance = stub('model instance') handler.expects(:format_to_use).returns(@oneformat) @model_instance.expects(:render).with(@oneformat).returns "my_rendered_object" @indirection.stubs(:find).returns(@model_instance) handler.do_find("my_handler", "my_result", {}, request, response) end end describe "when performing head operation" do before do handler.stubs(:model).with("my_handler").returns(stub 'model', :indirection => @model_class) request[:http_method] = "HEAD" request[:path] = "/production/my_handler/my_result" request[:params] = {} @model_class.stubs(:head).returns true end it "should use the escaped request key" do @model_class.expects(:head).with("my_result", anything).returns true handler.process(request, response) end it "should not generate a response when a model head call succeeds" do handler.expects(:set_response).never handler.process(request, response) end it "should return a 404 when the model head call returns false" do handler.expects(:set_response).with(anything, anything, 404) @model_class.stubs(:head).returns(false) handler.process(request, response) end end describe "when searching for model instances" do before do Puppet::Indirector::Indirection.expects(:instance).with(:my_handler).returns( stub "indirection", :model => @model_class ) result1 = mock 'result1' result2 = mock 'results' @result = [result1, result2] @model_class.stubs(:render_multiple).returns "my rendered instances" @indirection.stubs(:search).returns(@result) @format = stub 'format', :suitable? => true, :mime => "text/format", :name => "format" Puppet::Network::FormatHandler.stubs(:format).returns @format @oneformat = stub 'one', :suitable? => true, :mime => "text/one", :name => "one" Puppet::Network::FormatHandler.stubs(:format).with("one").returns @oneformat end it "should use the indirection request to find the model" do handler.do_search("my_handler", "my_result", {}, request, response) end it "should use a common method for determining the request parameters" do @indirection.expects(:search).with(anything, has_entries(:foo => :baz, :bar => :xyzzy)).returns @result handler.do_search("my_handler", "my_result", {:foo => :baz, :bar => :xyzzy}, request, response) end it "should use the default status when a model search call succeeds" do @indirection.stubs(:search).returns(@result) handler.do_search("my_handler", "my_result", {}, request, response) end it "should set the content type to the first format returned by the accept header" do handler.expects(:accept_header).with(request).returns "one,two" handler.expects(:set_content_type).with(response, @oneformat) handler.do_search("my_handler", "my_result", {}, request, response) end it "should return a list of serialized objects when a model search call succeeds" do handler.expects(:accept_header).with(request).returns "one,two" @indirection.stubs(:search).returns(@result) @model_class.expects(:render_multiple).with(@oneformat, @result).returns "my rendered instances" handler.expects(:set_response).with(anything, "my rendered instances") handler.do_search("my_handler", "my_result", {}, request, response) end it "should return [] when searching returns an empty array" do handler.expects(:accept_header).with(request).returns "one,two" @indirection.stubs(:search).returns([]) @model_class.expects(:render_multiple).with(@oneformat, []).returns "[]" handler.expects(:set_response).with(anything, "[]") handler.do_search("my_handler", "my_result", {}, request, response) end it "should return a 404 when searching returns nil" do @model_class.stubs(:name).returns "my name" handler.expects(:set_response).with(anything, anything, 404) @indirection.stubs(:search).returns(nil) handler.do_search("my_handler", "my_result", {}, request, response) end end describe "when destroying a model instance" do before do Puppet::Indirector::Indirection.expects(:instance).with(:my_handler).returns( stub "indirection", :model => @model_class ) @result = stub 'result', :render => "the result" @indirection.stubs(:destroy).returns @result end it "should use the indirection request to find the model" do handler.do_destroy("my_handler", "my_result", {}, request, response) end it "should use the escaped request key to destroy the instance in the model" do @indirection.expects(:destroy).with("foo bar", anything) handler.do_destroy("my_handler", "foo bar", {}, request, response) end it "should use a common method for determining the request parameters" do @indirection.expects(:destroy).with(anything, has_entries(:foo => :baz, :bar => :xyzzy)) handler.do_destroy("my_handler", "my_result", {:foo => :baz, :bar => :xyzzy}, request, response) end it "should use the default status code a model destroy call succeeds" do handler.expects(:set_response).with(anything, anything, nil) handler.do_destroy("my_handler", "my_result", {}, request, response) end it "should return a yaml-encoded result when a model destroy call succeeds" do @result = stub 'result', :to_yaml => "the result" @indirection.expects(:destroy).returns(@result) handler.expects(:set_response).with(anything, "the result", anything) handler.do_destroy("my_handler", "my_result", {}, request, response) end end describe "when saving a model instance" do - before :all do + before :each do class Puppet::TestModel extend Puppet::Indirector indirects :test_model attr_accessor :name, :data def initialize(name = "name", data = '') @name = name @data = data end def self.from_pson(pson) new(pson["name"], pson["data"]) end + def to_pson + { + "name" => @name, + "data" => @data + }.to_pson + 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::Memory < Puppet::Indirector::Memory end Puppet::TestModel.indirection.terminus_class = :memory end - after :all do + after :each 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::Memory } let(:terminus) { Puppet::TestModel.indirection.terminus(:memory) } let(:indirection) { Puppet::TestModel.indirection } let(:model) { Puppet::TestModel } def a_request_that_submits(data, request = {}) { - :accept_header => request[:accept_header] || "yaml", + :accept_header => request[:accept_header], :content_type_header => "text/yaml", :http_method => "GET", :path => "/#{indirection.name}/#{data.name}", :params => {}, :client_cert => nil, :body => data.render("text/yaml") } end it "should fail to save model if data is not specified" do request[:body] = '' expect { handler.do_save("my_handler", "my_result", {}, request, response) }.to raise_error(ArgumentError) end - it "should use the default status when a model save call succeeds" do - request = a_request_that_submits(Puppet::TestModel.new("my data", "some data")) - - handler.expects(:set_response).with(anything, anything, nil) + it "saves the data sent in the request" do + data = Puppet::TestModel.new("my data", "some data") + request = a_request_that_submits(data) handler.do_save(indirection.name, "my data", {}, request, response) + + Puppet::TestModel.indirection.find("my data").should == data end - it "should return the yaml-serialized result when a model save call succeeds" do + it "responds with yaml when no Accept header is given" do data = Puppet::TestModel.new("my data", "some data") - request = a_request_that_submits(data) + request = a_request_that_submits(data, :accept_header => nil) handler.expects(:set_response).with(response, data.render(:yaml)) + handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:yaml)) handler.do_save(indirection.name, "my data", {}, request, response) end - it "should set the content to yaml" do - request = a_request_that_submits(Puppet::TestModel.new("my data", "some data")) + it "uses the first supported format for the response" do + data = Puppet::TestModel.new("my data", "some data") + request = a_request_that_submits(data, :accept_header => "unknown, pson, yaml") - handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format("yaml")) + handler.expects(:set_response).with(response, data.render(:pson)) + handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) handler.do_save(indirection.name, "my data", {}, request, response) end + + it "raises an error and does not save when no accepted formats are known" do + data = Puppet::TestModel.new("my data", "some data") + request = a_request_that_submits(data, :accept_header => "unknown, also/unknown") + + expect do + handler.do_save(indirection.name, "my data", {}, request, response) + end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotAcceptableError) + + Puppet::TestModel.indirection.find("my data").should be_nil + end end end describe "when resolving node" do it "should use a look-up from the ip address" do Resolv.expects(:getname).with("1.2.3.4").returns("host.domain.com") handler.resolve_node(:ip => "1.2.3.4") end it "should return the look-up result" do Resolv.stubs(:getname).with("1.2.3.4").returns("host.domain.com") handler.resolve_node(:ip => "1.2.3.4").should == "host.domain.com" end it "should return the ip address if resolving fails" do Resolv.stubs(:getname).with("1.2.3.4").raises(RuntimeError, "no such host") handler.resolve_node(:ip => "1.2.3.4").should == "1.2.3.4" end end class TestingHandler include Puppet::Network::HTTP::Handler def accept_header(request) request[:accept_header] end def content_type_header(request) request[:content_type_header] end def set_content_type(response, format) "my_result" end def set_response(response, body, status = 200) "my_result" end def http_method(request) request[:http_method] end def path(request) request[:path] end def params(request) request[:params] end def client_cert(request) request[:client_cert] end def body(request) request[:body] end end end