diff --git a/lib/puppet/indirector/memory.rb b/lib/puppet/indirector/memory.rb index c44d62de2..0e3afaef7 100644 --- a/lib/puppet/indirector/memory.rb +++ b/lib/puppet/indirector/memory.rb @@ -1,21 +1,30 @@ require 'puppet/indirector/terminus' # Manage a memory-cached list of instances. class Puppet::Indirector::Memory < Puppet::Indirector::Terminus def initialize @instances = {} end def destroy(request) raise ArgumentError.new("Could not find #{request.key} to destroy") unless @instances.include?(request.key) @instances.delete(request.key) end def find(request) @instances[request.key] end + def search(request) + found_keys = @instances.keys.find_all { |key| key.include?(request.key) } + found_keys.collect { |key| @instances[key] } + end + + def head(request) + not find(request).nil? + end + def save(request) @instances[request.key] = request.instance end end diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index 2634e8e2b..b4cf2b89a 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,329 +1,323 @@ 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 + class HTTPNotFoundError < HTTPError + def initialize(message) + super("Not Found: " + message, 404) + end + end + attr_reader :server, :handler # 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 HTTPError => e + return do_exception(response, e.message, e.status) 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 + else + Puppet.notice(exception.to_s) 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) + model_class = model(indirection_name) + unless result = model_class.indirection.find(key, params) + raise HTTPNotFoundError, "Could not find #{indirection_name} #{key}" 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) + format = accepted_response_formatter_for(model_class, 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) + 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) + raise HTTPNotFoundError, "Could not find #{indirection_name} #{key}" 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) + raise HTTPNotFoundError, "Could not find instances in #{indirection_name} with '#{key}'" end - format = format_to_use(request) + format = accepted_response_formatter_for(model, 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) model_class = model(indirection_name) - formatter = response_formatter_for(model_class, request) + formatter = accepted_response_formatter_or_yaml_for(model_class, request) result = model_class.indirection.destroy(key, params) set_content_type(response, formatter) set_response(response, formatter.render(result)) end # Execute our save. def do_save(indirection_name, key, params, request, response) model_class = model(indirection_name) - formatter = response_formatter_for(model_class, request) + formatter = accepted_response_formatter_or_yaml_for(model_class, request) sent_object = read_body_into_model(model_class, request) 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) + def accepted_response_formatter_for(model_class, request) + accepted_formats = accept_header(request) or raise HTTPNotAcceptableError, "Missing required Accept header" + response_formatter_for(model_class, request, accepted_formats) + end + + def accepted_response_formatter_or_yaml_for(model_class, request) accepted_formats = accept_header(request) || "yaml" + response_formatter_for(model_class, request, accepted_formats) + end + + def response_formatter_for(model_class, request, accepted_formats) 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 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 in network requests is deprecated and will be removed in a future version. See http://links.puppetlabs.com/deprecate_yaml_on_network") 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/network/http/handler_spec.rb b/spec/unit/network/http/handler_spec.rb index f2277a29c..6d16bd799 100755 --- a/spec/unit/network/http/handler_spec.rb +++ b/spec/unit/network/http/handler_spec.rb @@ -1,602 +1,545 @@ #! /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 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 :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 + { + :accept_header => "pson", + :content_type_header => "text/yaml", + :http_method => "HEAD", + :path => "/production/#{indirection.name}/unknown", + :params => {}, + :client_cert => nil, + :headers => {}, + :body => nil + } + end + + def a_request_that_heads(data, request = {}) + { + :accept_header => request[:accept_header], + :content_type_header => "text/yaml", + :http_method => "HEAD", + :path => "/production/#{indirection.name}/#{data.name}", + :params => {}, + :client_cert => nil, + :body => nil + } + end + def a_request_that_submits(data, request = {}) { :accept_header => request[:accept_header], :content_type_header => "text/yaml", - :http_method => "GET", - :path => "/#{indirection.name}/#{data.name}", + :http_method => "PUT", + :path => "/production/#{indirection.name}/#{data.name}", :params => {}, :client_cert => nil, :body => data.render("text/yaml") } end def a_request_that_destroys(data, request = {}) { :accept_header => request[:accept_header], :content_type_header => "text/yaml", :http_method => "DELETE", - :path => "/#{indirection.name}/#{data.name}", + :path => "/production/#{indirection.name}/#{data.name}", + :params => {}, + :client_cert => nil, + :body => '' + } + end + + def a_request_that_finds(data, request = {}) + { + :accept_header => request[:accept_header], + :content_type_header => "text/yaml", + :http_method => "GET", + :path => "/production/#{indirection.name}/#{data.name}", + :params => {}, + :client_cert => nil, + :body => '' + } + end + + def a_request_that_searches(key, request = {}) + { + :accept_header => request[:accept_header], + :content_type_header => "text/yaml", + :http_method => "GET", + :path => "/production/#{indirection.name}s/#{key}", :params => {}, :client_cert => nil, :body => '' } end 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 + request = a_request 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 = a_request 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 = a_request 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 = a_request 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 + request = a_request 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 + request = a_request 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 + request = a_request 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") + data = Puppet::TestModel.new("not_found", "not found") + request = a_request_that_finds(data, :accept_header => "pson") + error = Puppet::Network::HTTP::Handler::HTTPNotFoundError.new("Could not find test_model not_found") handler.expects(:set_response).with(response, error.to_s, error.status) - handler.do_exception(response, error) + handler.process(request, response) 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 + request = a_request 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 "uses the first supported format for the response" do + data = Puppet::TestModel.new("my data", "some data") + indirection.save(data, "my data") + request = a_request_that_finds(data, :accept_header => "unknown, pson, yaml") - 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 + handler.expects(:set_response).with(response, data.render(:pson)) + handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) - 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) + handler.do_find(indirection.name, "my data", {}, request, response) 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 + it "responds with a 406 error when no accept header is provided" do + data = Puppet::TestModel.new("my data", "some data") + indirection.save(data, "my data") + request = a_request_that_finds(data, :accept_header => nil) - handler.do_find("my_handler", "my_result", {}, request, response) + expect do + handler.do_find(indirection.name, "my data", {}, request, response) + end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotAcceptableError) 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) + it "raises an error when no accepted formats are known" do + data = Puppet::TestModel.new("my data", "some data") + indirection.save(data, "my data") + request = a_request_that_finds(data, :accept_header => "unknown, also/unknown") - handler.do_find("my_handler", "my_result", {}, request, response) + expect do + handler.do_find(indirection.name, "my data", {}, request, response) + end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotAcceptableError) 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 + data = Puppet::TestModel.new("my data", "some data") + data_string = "my data string" + request = a_request_that_finds(data, :accept_header => "pson") + indirection.expects(:find).returns(data_string) - 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(response, data_string) + handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) - 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) + handler.do_find(indirection.name, "my data", {}, 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') + data = Puppet::TestModel.new("my data", "some data") + request = a_request_that_finds(data, :accept_header => "unknown, pson, yaml") - 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) + expect do + handler.do_find(indirection.name, "my data", {}, request, response) + end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotFoundError) 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 + data = Puppet::TestModel.new("my data", "some data") + indirection.save(data, "my data") + request = a_request_that_heads(data) + 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) + data = Puppet::TestModel.new("my data", "data not there") + request = a_request_that_heads(data) + + handler.expects(:set_response).with(response, "Not Found: Could not find test_model my data", 404) + 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) + it "uses the first supported format for the response" do + data = Puppet::TestModel.new("my data", "some data") + indirection.save(data, "my data") + request = a_request_that_searches("my", :accept_header => "unknown, pson, yaml") - @model_class.expects(:render_multiple).with(@oneformat, @result).returns "my rendered instances" + handler.expects(:set_response).with(response, Puppet::TestModel.render_multiple(:pson, [data])) + handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) - handler.expects(:set_response).with(anything, "my rendered instances") - handler.do_search("my_handler", "my_result", {}, request, response) + handler.do_search(indirection.name, "my", {}, 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 "[]" + request = a_request_that_searches("nothing", :accept_header => "unknown, pson, yaml") + handler.expects(:set_response).with(response, Puppet::TestModel.render_multiple(:pson, [])) + handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) - handler.expects(:set_response).with(anything, "[]") - handler.do_search("my_handler", "my_result", {}, request, response) + handler.do_search(indirection.name, "nothing", {}, 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) + request = a_request_that_searches("nothing", :accept_header => "unknown, pson, yaml") + indirection.expects(:search).returns(nil) + + expect do + handler.do_search(indirection.name, "nothing", {}, request, response) + end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotFoundError) end end describe "when destroying a model instance" do it "destroys the data indicated in the request" do data = Puppet::TestModel.new("my data", "some data") indirection.save(data, "my data") request = a_request_that_destroys(data) handler.do_destroy(indirection.name, "my data", {}, request, response) Puppet::TestModel.indirection.find("my data").should be_nil end it "responds with yaml when no Accept header is given" do data = Puppet::TestModel.new("my data", "some data") indirection.save(data, "my data") request = a_request_that_destroys(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_destroy(indirection.name, "my data", {}, request, response) end it "uses the first supported format for the response" do data = Puppet::TestModel.new("my data", "some data") indirection.save(data, "my data") request = a_request_that_destroys(data, :accept_header => "unknown, pson, yaml") handler.expects(:set_response).with(response, data.render(:pson)) handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) handler.do_destroy(indirection.name, "my data", {}, request, response) end it "raises an error and does not destory when no accepted formats are known" do data = Puppet::TestModel.new("my data", "some data") indirection.save(data, "my data") request = a_request_that_submits(data, :accept_header => "unknown, also/unknown") expect do handler.do_destroy(indirection.name, "my data", {}, request, response) end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotAcceptableError) Puppet::TestModel.indirection.find("my data").should_not be_nil end end describe "when saving a model instance" do it "should fail to save model if data is not specified" do + data = Puppet::TestModel.new("my data", "some data") + request = a_request_that_submits(data) request[:body] = '' expect { handler.do_save("my_handler", "my_result", {}, request, response) }.to raise_error(ArgumentError) end 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 "responds with yaml when no Accept header is given" do data = Puppet::TestModel.new("my data", "some 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 "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_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 + + def headers(request) + request[:headers] || {} + end end end