diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb index 6429b22b7..38ecdb758 100644 --- a/lib/puppet/indirector/rest.rb +++ b/lib/puppet/indirector/rest.rb @@ -1,95 +1,95 @@ require 'net/http' require 'uri' require 'puppet/network/http_pool' +require 'puppet/network/http/handler' # Access objects via REST class Puppet::Indirector::REST < Puppet::Indirector::Terminus + include Puppet::Network::HTTP::Handler class << self attr_reader :server_setting, :port_setting end # Specify the setting that we should use to get the server name. def self.use_server_setting(setting) @server_setting = setting end def self.server return Puppet.settings[server_setting || :server] end # Specify the setting that we should use to get the port. def self.use_port_setting(setting) @port_setting = setting end def self.port return Puppet.settings[port_setting || :masterport].to_i end # Figure out the content type, turn that into a format, and use the format # to extract the body of the response. def deserialize(response, multiple = false) case response.code when "404" return nil when /^2/ unless response['content-type'] raise "No content type in http response; cannot parse" end # Convert the response to a deserialized object. if multiple model.convert_from_multiple(response['content-type'], response.body) else model.convert_from(response['content-type'], response.body) end else # Raise the http error if we didn't get a 'success' of some kind. message = "Server returned %s: %s" % [response.code, response.message] raise Net::HTTPError.new(message, response) end end # Provide appropriate headers. def headers {"Accept" => model.supported_formats.join(", ")} end def network(request) Puppet::Network::HttpPool.http_instance(request.server || self.class.server, request.port || self.class.port) end def find(request) - deserialize network(request).get("/#{environment}/#{indirection.name}/#{request.escaped_key}#{request.query_string}", headers) + p model + p indirection + p indirection.model + deserialize network(request).get(indirection2uri(request), headers) end def search(request) - if request.key - path = "/#{environment}/#{indirection.name}s/#{request.escaped_key}#{request.query_string}" - else - path = "/#{environment}/#{indirection.name}s#{request.query_string}" - end - unless result = deserialize(network(request).get(path, headers), true) + unless result = deserialize(network(request).get(indirection2uri(request), headers), true) return [] end return result end def destroy(request) raise ArgumentError, "DELETE does not accept options" unless request.options.empty? - deserialize network(request).delete("/#{environment}/#{indirection.name}/#{request.escaped_key}", headers) + deserialize network(request).delete(indirection2uri(request), headers) end def save(request) raise ArgumentError, "PUT does not accept options" unless request.options.empty? - deserialize network(request).put("/#{environment}/#{indirection.name}/", request.instance.render, headers) + deserialize network(request).put(indirection2uri(request), request.instance.render, headers) end private def environment Puppet::Node::Environment.new end end diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index 1e5281695..610aa0a3f 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,249 +1,237 @@ module Puppet::Network::HTTP end module Puppet::Network::HTTP::Handler # How we map http methods and the indirection name in the URI # to an indirection method. METHOD_MAP = { "GET" => { :plural => :search, :singular => :find }, "PUT" => { :singular => :save }, "DELETE" => { :singular => :destroy } } attr_reader :model, :server, :handler # Retrieve the accept header from the http request. def accept_header(request) raise NotImplementedError end # Which format to use when serializing our response. Just picks # the first value in the accept header, at this point. 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 name end raise "No specified acceptable formats (%s) are functional on this machine" % header end def initialize_for_puppet(args = {}) raise ArgumentError unless @server = args[:server] raise ArgumentError unless @handler = args[:handler] @model = find_model_for_handler(@handler) end # handle an HTTP request def process(request, response) - return do_find(request, response) if get?(request) and singular?(request) - return do_search(request, response) if get?(request) and plural?(request) - return do_destroy(request, response) if delete?(request) and singular?(request) - return do_save(request, response) if put?(request) and singular?(request) - raise ArgumentError, "Did not understand HTTP #{http_method(request)} request for '#{path(request)}'" + indirection_request = uri2indirection(path(request), params(request), http_method(request)) + + send("do_%s" % indirection_request.method, indirection_request, request, response) rescue Exception => e return do_exception(response, e) end def uri2indirection(http_method, uri, params) environment, indirection, key = uri.split("/", 4)[1..-1] # the first field is always nil because of the leading slash raise ArgumentError, "The environment must be purely alphanumeric, not '%s'" % environment unless environment =~ /^\w+$/ raise ArgumentError, "The indirection name must be purely alphanumeric, not '%s'" % indirection unless indirection =~ /^\w+$/ - plurality = (indirection == handler.to_s + "s") ? :plural : :singular - - unless METHOD_MAP[http_method] - raise ArgumentError, "No support for http method %s" % http_method - end - - unless method = METHOD_MAP[http_method][plurality] - raise ArgumentError, "No support for plural %s operations" % http_method - end - - indirection.sub!(/s$/, '') if plurality == :plural + method = indirection_method(http_method, indirection) params[:environment] = environment + raise ArgumentError, "No request key specified in %s" % uri if key == "" or key.nil? + key = URI.unescape(key) Puppet::Indirector::Request.new(indirection, method, key, params) end def indirection2uri(request) indirection = request.method == :search ? request.indirection_name.to_s + "s" : request.indirection_name.to_s "/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}#{request.query_string}" end - # Are we interacting with a singular instance? - def singular?(request) - %r{/#{handler.to_s}$}.match(path(request)) + def indirection_method(http_method, indirection) + unless METHOD_MAP[http_method] + raise ArgumentError, "No support for http method %s" % http_method + end + + unless method = METHOD_MAP[http_method][plurality(indirection)] + raise ArgumentError, "No support for plural %s operations" % http_method + end + + return method end - # Are we interacting with multiple instances? - def plural?(request) - %r{/#{handler.to_s}s$}.match(path(request)) + def plurality(indirection) + result = (indirection == handler.to_s + "s") ? :plural : :singular + + indirection.sub!(/s$/, '') if result + + result 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?(Exception) puts exception.backtrace if Puppet[:trace] Puppet.err(exception) end set_content_type(response, "text/plain") set_response(response, exception.to_s, status) end # Execute our find. - def do_find(request, response) - key = request_key(request) || raise(ArgumentError, "Could not locate lookup key in request path [#{path(request)}]") - key = URI.unescape(key) - args = params(request) - unless result = model.find(key, args) - return do_exception(response, "Could not find %s %s" % [model.name, key], 404) + def do_find(indirection_request, request, response) + unless result = model.find(indirection_request.key, indirection_request.options) + return do_exception(response, "Could not find %s %s" % [model.name, indirection_request.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) set_response(response, result.render(format)) end # Execute our search. - def do_search(request, response) - args = params(request) - if key = request_key(request) - key = URI.unescape(key) - result = model.search(key, args) - else - result = model.search(args) - end + def do_search(indirection_request, request, response) + result = model.search(indirection_request.key, indirection_request.options) + if result.nil? or (result.is_a?(Array) and result.empty?) - return do_exception(response, "Could not find instances in %s with '%s'" % [model.name, args.inspect], 404) + return do_exception(response, "Could not find instances in %s with '%s'" % [model.name, indirection_request.options.inspect], 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(request, response) - key = request_key(request) || raise(ArgumentError, "Could not locate lookup key in request path [#{path(request)}]") - key = URI.unescape(key) - args = params(request) - result = model.destroy(key, args) + def do_destroy(indirection_request, request, response) + result = model.destroy(indirection_request.key, indirection_request.options) set_content_type(response, "yaml") set_response(response, result.to_yaml) end # Execute our save. - def do_save(request, response) + def do_save(indirection_request, request, response) data = body(request).to_s raise ArgumentError, "No data to save" if !data or data.empty? - args = params(request) format = format_to_use(request) obj = model.convert_from(format_to_use(request), data) - result = save_object(obj, args) + result = save_object(indirection_request, obj) set_content_type(response, "yaml") set_response(response, result.to_yaml) end private # LAK:NOTE This has to be here for testing; it's a stub-point so # we keep infinite recursion from happening. - def save_object(object, args) - object.save(args) + def save_object(ind_request, object) + object.save(ind_request.options) end def find_model_for_handler(handler) Puppet::Indirector::Indirection.model(handler) || raise(ArgumentError, "Cannot locate indirection [#{handler}].") 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 decode_params(params) params.inject({}) do |result, ary| param, value = ary value = URI.unescape(value) if value =~ /^---/ value = YAML.load(value) 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.to_sym] = value result end end end diff --git a/spec/unit/indirector/rest.rb b/spec/unit/indirector/rest.rb index 361412a54..9305a7109 100755 --- a/spec/unit/indirector/rest.rb +++ b/spec/unit/indirector/rest.rb @@ -1,384 +1,363 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/indirector/rest' describe "a REST http call", :shared => true do it "should accept a path" do lambda { @search.send(@method, *@arguments) }.should_not raise_error(ArgumentError) end it "should require a path" do lambda { @searcher.send(@method) }.should raise_error(ArgumentError) end it "should return the results of deserializing the response to the request" do conn = mock 'connection' conn.stubs(:put).returns @response conn.stubs(:delete).returns @response conn.stubs(:get).returns @response Puppet::Network::HttpPool.stubs(:http_instance).returns conn @searcher.expects(:deserialize).with(@response).returns "myobject" @searcher.send(@method, *@arguments).should == 'myobject' end end describe Puppet::Indirector::REST do before do Puppet::Indirector::Terminus.stubs(:register_terminus_class) @model = stub('model', :supported_formats => %w{}, :convert_from => nil) @instance = stub('model instance') @indirection = stub('indirection', :name => :mystuff, :register_terminus_type => nil, :model => @model) Puppet::Indirector::Indirection.stubs(:instance).returns(@indirection) @rest_class = Class.new(Puppet::Indirector::REST) do def self.to_s "This::Is::A::Test::Class" end end @response = stub('mock response', :body => 'result', :code => "200") @response.stubs(:[]).with('content-type').returns "text/plain" @searcher = @rest_class.new + @searcher.stubs(:model).returns @model + end + + it "should include the Http Handler module" do + Puppet::Indirector::REST.ancestors.should be_include(Puppet::Network::HTTP::Handler) end it "should have a method for specifying what setting a subclass should use to retrieve its server" do @rest_class.should respond_to(:use_server_setting) end it "should use any specified setting to pick the server" do @rest_class.expects(:server_setting).returns :servset Puppet.settings.expects(:value).with(:servset).returns "myserver" @rest_class.server.should == "myserver" end it "should default to :server for the server setting" do @rest_class.expects(:server_setting).returns nil Puppet.settings.expects(:value).with(:server).returns "myserver" @rest_class.server.should == "myserver" end it "should have a method for specifying what setting a subclass should use to retrieve its port" do @rest_class.should respond_to(:use_port_setting) end it "should use any specified setting to pick the port" do @rest_class.expects(:port_setting).returns :servset Puppet.settings.expects(:value).with(:servset).returns "321" @rest_class.port.should == 321 end it "should default to :port for the port setting" do @rest_class.expects(:port_setting).returns nil Puppet.settings.expects(:value).with(:masterport).returns "543" @rest_class.port.should == 543 end describe "when deserializing responses" do it "should return nil if the response code is 404" do response = mock 'response' response.expects(:code).returns "404" @searcher.deserialize(response).should be_nil end it "should fail if the response code is not in the 200s" do @model.expects(:convert_from).never response = mock 'response' response.stubs(:code).returns "300" response.stubs(:message).returns "There was a problem" lambda { @searcher.deserialize(response) }.should raise_error(Net::HTTPError) end it "should return the results of converting from the format specified by the content-type header if the response code is in the 200s" do @model.expects(:convert_from).with("myformat", "mydata").returns "myobject" response = mock 'response' response.stubs(:[]).with("content-type").returns "myformat" response.stubs(:body).returns "mydata" response.stubs(:code).returns "200" @searcher.deserialize(response).should == "myobject" end it "should convert and return multiple instances if the return code is in the 200s and 'multiple' is specified" do @model.expects(:convert_from_multiple).with("myformat", "mydata").returns "myobjects" response = mock 'response' response.stubs(:[]).with("content-type").returns "myformat" response.stubs(:body).returns "mydata" response.stubs(:code).returns "200" @searcher.deserialize(response, true).should == "myobjects" end end describe "when creating an HTTP client" do before do Puppet.settings.stubs(:value).returns("rest_testing") end 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 @searcher.class.expects(:port).returns 321 @searcher.class.expects(:server).returns "myserver" Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" @searcher.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 @searcher.class.stubs(:port).returns 321 Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" @searcher.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 @searcher.class.stubs(:server).returns "myserver" Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" @searcher.network(@request).should == "myconn" end end describe "when doing a find" do before :each do @connection = stub('mock http connection', :get => @response) @searcher.stubs(:network).returns(@connection) # neuter the network connection # Use a key with spaces, so we can test escaping - @request = stub 'request', :escaped_key => 'foo', :query_string => "" + @request = Puppet::Indirector::Request.new(:foo, :find, "foo bar") end it "should call the GET http method on a network connection" do @searcher.expects(:network).returns @connection @connection.expects(:get).returns @response @searcher.find(@request) end it "should deserialize and return the http response" do @connection.expects(:get).returns @response @searcher.expects(:deserialize).with(@response).returns "myobject" @searcher.find(@request).should == 'myobject' end - it "should use the environment, indirection name, and escaped request key to create the path" do - should_path = "/%s/%s/%s" % [Puppet::Node::Environment.new, @indirection.name.to_s, "foo"] - @connection.expects(:get).with { |path, args| path == should_path }.returns(@response) - @searcher.find(@request) - end - - it "should include the query string" do - @request.expects(:query_string).with().returns "?my_string" - @connection.expects(:get).with { |path, args| path.include?("?my_string") }.returns(@response) + it "should use the URI generated by the Handler module" do + @searcher.expects(:indirection2uri).with(@request).returns "/my/uri" + @connection.expects(:get).with { |path, args| path == "/my/uri" }.returns(@response) @searcher.find(@request) end it "should provide an Accept header containing the list of supported formats joined with commas" do @connection.expects(:get).with { |path, args| args["Accept"] == "supported, formats" }.returns(@response) @searcher.model.expects(:supported_formats).returns %w{supported formats} @searcher.find(@request) end it "should deserialize and return the network response" do @searcher.expects(:deserialize).with(@response).returns @instance @searcher.find(@request).should equal(@instance) end it "should generate an error when result data deserializes fails" do @searcher.expects(:deserialize).raises(ArgumentError) lambda { @searcher.find(@request) }.should raise_error(ArgumentError) end end describe "when doing a search" do before :each do @connection = stub('mock http connection', :get => @response) @searcher.stubs(:network).returns(@connection) # neuter the network connection @model.stubs(:convert_from_multiple) - @request = stub 'request', :escaped_key => 'foo', :query_string => "", :key => "bar" + @request = Puppet::Indirector::Request.new(:foo, :search, "foo bar") end it "should call the GET http method on a network connection" do @searcher.expects(:network).returns @connection @connection.expects(:get).returns @response @searcher.search(@request) end it "should deserialize as multiple instances and return the http response" do @connection.expects(:get).returns @response @searcher.expects(:deserialize).with(@response, true).returns "myobject" @searcher.search(@request).should == 'myobject' end - it "should use the environment and the plural indirection name as the path if there is no request key" do - should_path = "/%s/%ss" % [Puppet::Node::Environment.new, @indirection.name.to_s] - @request.stubs(:key).returns nil - @connection.expects(:get).with { |path, args| path == should_path }.returns(@response) - @searcher.search(@request) - end - - it "should use the envrironment, the plural indirection name, and the escaped request key to create the path if the request key is set" do - should_path = "/%s/%ss/%s" % [Puppet::Node::Environment.new, @indirection.name.to_s, "foo"] - @connection.expects(:get).with { |path, args| path == should_path }.returns(@response) - @searcher.search(@request) - end - - it "should include the query string" do - @request.expects(:query_string).with().returns "?my_string" - @connection.expects(:get).with { |path, args| path.include?("?my_string") }.returns(@response) + it "should use the URI generated by the Handler module" do + @searcher.expects(:indirection2uri).with(@request).returns "/mys/uri" + @connection.expects(:get).with { |path, args| path == "/mys/uri" }.returns(@response) @searcher.search(@request) end it "should provide an Accept header containing the list of supported formats joined with commas" do @connection.expects(:get).with { |path, args| args["Accept"] == "supported, formats" }.returns(@response) @searcher.model.expects(:supported_formats).returns %w{supported formats} @searcher.search(@request) end it "should return an empty array if serialization returns nil" do @model.stubs(:convert_from_multiple).returns nil @searcher.search(@request).should == [] end it "should generate an error when result data deserializes fails" do @searcher.expects(:deserialize).raises(ArgumentError) lambda { @searcher.search(@request) }.should raise_error(ArgumentError) end end describe "when doing a destroy" do before :each do @connection = stub('mock http connection', :delete => @response) @searcher.stubs(:network).returns(@connection) # neuter the network connection - @request = stub 'request', :escaped_key => 'foo', :query_string => "", :options => {} + @request = Puppet::Indirector::Request.new(:foo, :destroy, "foo bar") end it "should call the DELETE http method on a network connection" do @searcher.expects(:network).returns @connection @connection.expects(:delete).returns @response @searcher.destroy(@request) end it "should fail if any options are provided, since DELETE apparently does not support query options" do @request.stubs(:options).returns(:one => "two", :three => "four") lambda { @searcher.destroy(@request) }.should raise_error(ArgumentError) end it "should deserialize and return the http response" do @connection.expects(:delete).returns @response @searcher.expects(:deserialize).with(@response).returns "myobject" @searcher.destroy(@request).should == 'myobject' end - it "should use the environment, the indirection name, and the escaped request key to create the path" do - should_path = "/%s/%s/%s" % [Puppet::Node::Environment.new, @indirection.name.to_s, "foo"] - @connection.expects(:delete).with { |path, args| path == should_path }.returns(@response) + it "should use the URI generated by the Handler module" do + @searcher.expects(:indirection2uri).with(@request).returns "/my/uri" + @connection.expects(:delete).with { |path, args| path == "/my/uri" }.returns(@response) @searcher.destroy(@request) end it "should not include the query string" do - @request.expects(:query_string).never @connection.stubs(:delete).returns @response @searcher.destroy(@request) end it "should provide an Accept header containing the list of supported formats joined with commas" do @connection.expects(:delete).with { |path, args| args["Accept"] == "supported, formats" }.returns(@response) @searcher.model.expects(:supported_formats).returns %w{supported formats} @searcher.destroy(@request) end it "should deserialize and return the network response" do @searcher.expects(:deserialize).with(@response).returns @instance @searcher.destroy(@request).should equal(@instance) end it "should generate an error when result data deserializes fails" do @searcher.expects(:deserialize).raises(ArgumentError) lambda { @searcher.destroy(@request) }.should raise_error(ArgumentError) end end describe "when doing a save" do before :each do @connection = stub('mock http connection', :put => @response) @searcher.stubs(:network).returns(@connection) # neuter the network connection @instance = stub 'instance', :render => "mydata" - @request = stub 'request', :instance => @instance, :query_string => "", :options => {} + @request = Puppet::Indirector::Request.new(:foo, :save, "foo bar") + @request.instance = @instance end it "should call the PUT http method on a network connection" do @searcher.expects(:network).returns @connection @connection.expects(:put).returns @response @searcher.save(@request) end it "should fail if any options are provided, since DELETE apparently does not support query options" do @request.stubs(:options).returns(:one => "two", :three => "four") lambda { @searcher.save(@request) }.should raise_error(ArgumentError) end - it "should use the environment and the indirection name as the path for the request" do - path = "/%s/%s/" % [Puppet::Node::Environment.new, @indirection.name] - @connection.expects(:put).with { |path, data, args| path == path }.returns @response - - @searcher.save(@request) - end - - it "should not include the query string" do - @request.expects(:query_string).never - @connection.stubs(:put).returns @response + it "should use the URI generated by the Handler module" do + @searcher.expects(:indirection2uri).with(@request).returns "/my/uri" + @connection.expects(:put).with { |path, args| path == "/my/uri" }.returns(@response) @searcher.save(@request) 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 { |path, data, args| data == "serial_instance" }.returns @response @searcher.save(@request) end it "should deserialize and return the http response" do @connection.expects(:put).returns @response @searcher.expects(:deserialize).with(@response).returns "myobject" @searcher.save(@request).should == 'myobject' end it "should provide an Accept header containing the list of supported formats joined with commas" do @connection.expects(:put).with { |path, data, args| args["Accept"] == "supported, formats" }.returns(@response) @searcher.model.expects(:supported_formats).returns %w{supported formats} @searcher.save(@request) end it "should deserialize and return the network response" do @searcher.expects(:deserialize).with(@response).returns @instance @searcher.save(@request).should equal(@instance) end it "should generate an error when result data deserializes fails" do @searcher.expects(:deserialize).raises(ArgumentError) lambda { @searcher.save(@request) }.should raise_error(ArgumentError) end end end diff --git a/spec/unit/network/http/handler.rb b/spec/unit/network/http/handler.rb index fc73cc131..aa3e13df7 100755 --- a/spec/unit/network/http/handler.rb +++ b/spec/unit/network/http/handler.rb @@ -1,600 +1,461 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/network/http/handler' class HttpHandled include Puppet::Network::HTTP::Handler end describe Puppet::Network::HTTP::Handler do before do @handler = HttpHandled.new end it "should be able to convert a URI into a request" do @handler.should respond_to(:uri2indirection) end it "should be able to convert a request into a URI" do @handler.should respond_to(:indirection2uri) end describe "when converting a URI into a request" do before do @handler.stubs(:handler).returns "foo" end it "should require the http method, the URI, and the query parameters" do # Not a terribly useful test, but an important statement for the spec lambda { @handler.uri2indirection("/foo") }.should raise_error(ArgumentError) end it "should use the first field of the URI as the environment" do @handler.uri2indirection("GET", "/env/foo/bar", {}).environment.should == Puppet::Node::Environment.new("env") end it "should fail if the environment is not alphanumeric" do lambda { @handler.uri2indirection("GET", "/env ness/foo/bar", {}) }.should raise_error(ArgumentError) end it "should use the environment from the URI even if one is specified in the parameters" do @handler.uri2indirection("GET", "/env/foo/bar", {:environment => "otherenv"}).environment.should == Puppet::Node::Environment.new("env") end it "should use the second field of the URI as the indirection name" do @handler.uri2indirection("GET", "/env/foo/bar", {}).indirection_name.should == :foo end it "should fail if the indirection name is not alphanumeric" do lambda { @handler.uri2indirection("GET", "/env/foo ness/bar", {}) }.should raise_error(ArgumentError) end it "should use the remainder of the URI as the indirection key" do @handler.uri2indirection("GET", "/env/foo/bar", {}).key.should == "bar" end it "should support the indirection key being a /-separated file path" do @handler.uri2indirection("GET", "/env/foo/bee/baz/bomb", {}).key.should == "bee/baz/bomb" end + it "should fail if no indirection key is specified" do + lambda { @handler.uri2indirection("GET", "/env/foo/", {}) }.should raise_error(ArgumentError) + lambda { @handler.uri2indirection("GET", "/env/foo", {}) }.should raise_error(ArgumentError) + end + it "should choose 'find' as the indirection method if the http method is a GET and the indirection name is singular" do @handler.uri2indirection("GET", "/env/foo/bar", {}).method.should == :find end it "should choose 'search' as the indirection method if the http method is a GET and the indirection name is plural" do @handler.uri2indirection("GET", "/env/foos/bar", {}).method.should == :search end it "should choose 'delete' as the indirection method if the http method is a DELETE and the indirection name is singular" do @handler.uri2indirection("DELETE", "/env/foo/bar", {}).method.should == :destroy end it "should choose 'save' as the indirection method if the http method is a PUT and the indirection name is singular" do @handler.uri2indirection("PUT", "/env/foo/bar", {}).method.should == :save end it "should fail if an indirection method cannot be picked" do lambda { @handler.uri2indirection("UPDATE", "/env/foo/bar", {}) }.should raise_error(ArgumentError) end it "should URI unescape the indirection key" do escaped = URI.escape("foo bar") @handler.uri2indirection("GET", "/env/foo/#{escaped}", {}).key.should == "foo bar" end end describe "when converting a request into a URI" do before do @request = Puppet::Indirector::Request.new(:foo, :find, "with spaces", :foo => :bar, :environment => "myenv") end it "should use the environment as the first field of the URI" do @handler.indirection2uri(@request).split("/")[1].should == "myenv" end it "should use the indirection as the second field of the URI" do @handler.indirection2uri(@request).split("/")[2].should == "foo" end it "should pluralize the indirection name if the method is 'search'" do @request.stubs(:method).returns :search @handler.indirection2uri(@request).split("/")[2].should == "foos" end it "should use the escaped key as the remainder of the URI" do escaped = URI.escape("with spaces") @handler.indirection2uri(@request).split("/")[3].sub(/\?.+/, '').should == escaped end it "should add the query string to the URI" do @request.expects(:query_string).returns "?query" @handler.indirection2uri(@request).should =~ /\?query$/ end end it "should have a method for initializing" do @handler.should respond_to(:initialize_for_puppet) end describe "when initializing" do before do Puppet::Indirector::Indirection.stubs(:model).returns "eh" end it "should fail when no server type has been provided" do lambda { @handler.initialize_for_puppet :handler => "foo" }.should raise_error(ArgumentError) end it "should fail when no handler has been provided" do lambda { @handler.initialize_for_puppet :server => "foo" }.should raise_error(ArgumentError) end it "should set the handler and server type" do @handler.initialize_for_puppet :server => "foo", :handler => "bar" @handler.server.should == "foo" @handler.handler.should == "bar" end it "should use the indirector to find the appropriate model" do Puppet::Indirector::Indirection.expects(:model).with("bar").returns "mymodel" @handler.initialize_for_puppet :server => "foo", :handler => "bar" @handler.model.should == "mymodel" end end it "should be able to process requests" do @handler.should respond_to(:process) end describe "when processing a request" do before do @request = stub('http request') @request.stubs(:[]).returns "foo" @response = stub('http response') @model_class = stub('indirected model class') @result = stub 'result', :render => "mytext" @handler.stubs(:model).returns @model_class @handler.stubs(:handler).returns :my_handler stub_server_interface end # Stub out the interface we require our including classes to # implement. def stub_server_interface @handler.stubs(:accept_header ).returns "format_one,format_two" @handler.stubs(:set_content_type).returns "my_result" @handler.stubs(:set_response ).returns "my_result" - @handler.stubs(:path ).returns "/my_handler" - @handler.stubs(:request_key ).returns "my_result" + @handler.stubs(:path ).returns "/my_handler/my_result" + @handler.stubs(:http_method ).returns("GET") @handler.stubs(:params ).returns({}) @handler.stubs(:content_type ).returns("text/plain") end - it "should consider the request singular if the path is equal to '/' plus the handler name" do - @handler.expects(:path).with(@request).returns "/foo" - @handler.expects(:handler).returns "foo" - - @handler.should be_singular(@request) - end - - it "should not consider the request singular unless the path is equal to '/' plus the handler name" do - @handler.expects(:path).with(@request).returns "/foo" - @handler.expects(:handler).returns "bar" + it "should create an indirection request from the path, parameters, and http method" do + @handler.expects(:path).with(@request).returns "mypath" + @handler.expects(:http_method).with(@request).returns "mymethod" + @handler.expects(:params).with(@request).returns "myparams" - @handler.should_not be_singular(@request) - end + @handler.expects(:uri2indirection).with("mypath", "myparams", "mymethod").returns stub("request", :method => :find) - it "should consider the request plural if the path is equal to '/' plus the handler name plus 's'" do - @handler.expects(:path).with(@request).returns "/foos" - @handler.expects(:handler).returns "foo" + @handler.stubs(:do_find) - @handler.should be_plural(@request) + @handler.process(@request, @response) end - it "should not consider the request plural unless the path is equal to '/' plus the handler name plus 's'" do - @handler.expects(:path).with(@request).returns "/foos" - @handler.expects(:handler).returns "bar" + it "should call the 'do' method associated with the indirection method" do + request = stub 'request' + @handler.expects(:uri2indirection).returns request - @handler.should_not be_plural(@request) - end + request.expects(:method).returns "mymethod" - it "should call the model find method if the request represents a singular HTTP GET" do - @handler.expects(:http_method).returns('GET') - @handler.expects(:singular?).returns(true) + @handler.expects(:do_mymethod).with(request, @request, @response) - @handler.expects(:do_find).with(@request, @response) @handler.process(@request, @response) end it "should serialize a controller exception when an exception is thrown while finding the model instance" do - @handler.expects(:http_method).returns('GET') - @handler.expects(:singular?).returns(true) + @handler.expects(:uri2indirection).returns stub("request", :method => :find) @handler.expects(:do_find).raises(ArgumentError, "The exception") @handler.expects(:set_response).with { |response, body, status| body == "The exception" and status == 400 } @handler.process(@request, @response) end - it "should call the model search method if the request represents a plural HTTP GET" do - @handler.stubs(:http_method).returns('GET') - @handler.stubs(:singular?).returns(false) - @handler.stubs(:plural?).returns(true) - - @handler.expects(:do_search).with(@request, @response) - @handler.process(@request, @response) - end - - it "should serialize a controller exception when an exception is thrown by search" do - @handler.stubs(:http_method).returns('GET') - @handler.stubs(:singular?).returns(false) - @handler.stubs(:plural?).returns(true) - - @model_class.expects(:search).raises(ArgumentError) - @handler.expects(:set_response).with { |response, data, status| status == 400 } - @handler.process(@request, @response) - end - - it "should call the model destroy method if the request represents an HTTP DELETE" do - @handler.stubs(:http_method).returns('DELETE') - @handler.stubs(:singular?).returns(true) - @handler.stubs(:plural?).returns(false) - - @handler.expects(:do_destroy).with(@request, @response) - - @handler.process(@request, @response) - end - - it "should serialize a controller exception when an exception is thrown by destroy" do - @handler.stubs(:http_method).returns('DELETE') - @handler.stubs(:singular?).returns(true) - @handler.stubs(:plural?).returns(false) - - @handler.expects(:do_destroy).with(@request, @response).raises(ArgumentError, "The exception") - @handler.expects(:set_response).with { |response, body, status| body == "The exception" and status == 400 } - - @handler.process(@request, @response) - end - - it "should call the model save method if the request represents an HTTP PUT" do - @handler.stubs(:http_method).returns('PUT') - @handler.stubs(:singular?).returns(true) - - @handler.expects(:do_save).with(@request, @response) - - @handler.process(@request, @response) - end - - it "should serialize a controller exception when an exception is thrown by save" do - @handler.stubs(:http_method).returns('PUT') - @handler.stubs(:singular?).returns(true) - @handler.stubs(:body).raises(ArgumentError) - - @handler.expects(:set_response).with { |response, body, status| status == 400 } - @handler.process(@request, @response) - end - - it "should fail if the HTTP method isn't supported" do - @handler.stubs(:http_method).returns('POST') - @handler.stubs(:singular?).returns(true) - @handler.stubs(:plural?).returns(false) - - @handler.expects(:set_response).with { |response, body, status| status == 400 } - @handler.process(@request, @response) - end - - it "should fail if delete request's pluralization is wrong" do - @handler.stubs(:http_method).returns('DELETE') - @handler.stubs(:singular?).returns(false) - @handler.stubs(:plural?).returns(true) - - @handler.expects(:set_response).with { |response, body, status| status == 400 } - @handler.process(@request, @response) - end - - it "should fail if put request's pluralization is wrong" do - @handler.stubs(:http_method).returns('PUT') - @handler.stubs(:singular?).returns(false) - @handler.stubs(:plural?).returns(true) - - @handler.expects(:set_response).with { |response, body, status| status == 400 } - @handler.process(@request, @response) - end - - it "should fail if the request is for an unknown path" do - @handler.stubs(:http_method).returns('GET') - @handler.expects(:singular?).returns false - @handler.expects(:plural?).returns false - - @handler.expects(:set_response).with { |response, body, status| status == 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 describe "when finding a model instance" do before do - @handler.stubs(:http_method).returns('GET') - @handler.stubs(:path).returns('/my_handler') - @handler.stubs(:singular?).returns(true) - @handler.stubs(:request_key).returns('key') + @irequest = stub 'indirection_request', :method => :find, :indirection_name => "my_handler", :options => {}, :key => "my_result" + @model_class.stubs(:find).returns @result @format = stub 'format', :suitable? => true Puppet::Network::FormatHandler.stubs(:format).returns @format end - it "should fail if the key is not specified" do - @handler.stubs(:request_key).returns(nil) - - lambda { @handler.do_find(@request, @response) }.should raise_error(ArgumentError) - end - it "should use the escaped request key" do - @handler.stubs(:request_key).returns URI.escape("my key") @model_class.expects(:find).with do |key, args| - key == "my key" + key == "my_result" end.returns @result - @handler.do_find(@request, @response) + @handler.do_find(@irequest, @request, @response) end it "should use a common method for determining the request parameters" do - @handler.stubs(:params).returns(:foo => :baz, :bar => :xyzzy) + @irequest.stubs(:options).returns(:foo => :baz, :bar => :xyzzy) @model_class.expects(:find).with do |key, args| args[:foo] == :baz and args[:bar] == :xyzzy end.returns @result - @handler.do_find(@request, @response) + @handler.do_find(@irequest, @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, "one") - @handler.do_find(@request, @response) + @handler.do_find(@irequest, @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(@request, @response) }.should raise_error(ArgumentError) + lambda { @handler.do_find(@irequest, @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(@request, @response) }.should raise_error(RuntimeError) + lambda { @handler.do_find(@irequest, @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(@request, @response) + @handler.do_find(@irequest, @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("one") - @handler.do_find(@request, @response) + @handler.do_find(@irequest, @request, @response) end it "should use the default status when a model find call succeeds" do @handler.expects(:set_response).with { |response, body, status| status.nil? } - @handler.do_find(@request, @response) + @handler.do_find(@irequest, @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 { |response, body, status| body == "my_rendered_object" } @model_class.stubs(:find).returns(@model_instance) - @handler.do_find(@request, @response) + @handler.do_find(@irequest, @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 { |response, body, status| status == 404 } @model_class.stubs(:find).returns(nil) - @handler.do_find(@request, @response) + @handler.do_find(@irequest, @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 "one" @model_instance.expects(:render).with("one").returns "my_rendered_object" @model_class.stubs(:find).returns(@model_instance) - @handler.do_find(@request, @response) + @handler.do_find(@irequest, @request, @response) end end describe "when searching for model instances" do before do - @handler.stubs(:http_method).returns('GET') - @handler.stubs(:path).returns('/my_handlers') - @handler.stubs(:singular?).returns(false) - @handler.stubs(:plural?).returns(true) - @handler.stubs(:request_key).returns('key') + @irequest = stub 'indirection_request', :method => :find, :indirection_name => "my_handler", :options => {}, :key => "key" @result1 = mock 'result1' @result2 = mock 'results' @result = [@result1, @result2] @model_class.stubs(:render_multiple).returns "my rendered instances" @model_class.stubs(:search).returns(@result) @format = stub 'format', :suitable? => true Puppet::Network::FormatHandler.stubs(:format).returns @format end it "should use a common method for determining the request parameters" do - @handler.stubs(:params).returns(:foo => :baz, :bar => :xyzzy) + @irequest.stubs(:options).returns(:foo => :baz, :bar => :xyzzy) @model_class.expects(:search).with do |key, args| args[:foo] == :baz and args[:bar] == :xyzzy end.returns @result - @handler.do_search(@request, @response) - end - - it "should use an escaped request key if one is provided" do - @handler.expects(:request_key).with(@request).returns URI.escape("foo bar") - @model_class.expects(:search).with { |key, args| key == "foo bar" }.returns @result - @handler.do_search(@request, @response) - end - - it "should work with no request key if none is provided" do - @handler.expects(:request_key).with(@request).returns nil - @model_class.expects(:search).with { |args| args.is_a?(Hash) }.returns @result - @handler.do_search(@request, @response) + @handler.do_search(@irequest, @request, @response) end it "should use the default status when a model search call succeeds" do @model_class.stubs(:search).returns(@result) - @handler.do_search(@request, @response) + @handler.do_search(@irequest, @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, "one") - @handler.do_search(@request, @response) + @handler.do_search(@irequest, @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" @model_class.stubs(:search).returns(@result) @model_class.expects(:render_multiple).with("one", @result).returns "my rendered instances" @handler.expects(:set_response).with { |response, data| data == "my rendered instances" } - @handler.do_search(@request, @response) + @handler.do_search(@irequest, @request, @response) end it "should return a 404 when searching returns an empty array" do @model_class.stubs(:name).returns "my name" @handler.expects(:set_response).with { |response, body, status| status == 404 } @model_class.stubs(:search).returns([]) - @handler.do_search(@request, @response) + @handler.do_search(@irequest, @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 { |response, body, status| status == 404 } @model_class.stubs(:search).returns([]) - @handler.do_search(@request, @response) + @handler.do_search(@irequest, @request, @response) end end describe "when destroying a model instance" do before do - @handler.stubs(:http_method).returns('DELETE') - @handler.stubs(:path).returns('/my_handler/key') - @handler.stubs(:singular?).returns(true) - @handler.stubs(:request_key).returns('key') + @irequest = stub 'indirection_request', :method => :destroy, :indirection_name => "my_handler", :options => {}, :key => "key" @result = stub 'result', :render => "the result" @model_class.stubs(:destroy).returns @result end - it "should fail to destroy model if key is not specified" do - @handler.expects(:request_key).returns nil - lambda { @handler.do_destroy(@request, @response) }.should raise_error(ArgumentError) - end - it "should use the escaped request key to destroy the instance in the model" do - @handler.expects(:request_key).returns URI.escape("foo bar") + @irequest.expects(:key).returns "foo bar" @model_class.expects(:destroy).with do |key, args| key == "foo bar" end - @handler.do_destroy(@request, @response) + @handler.do_destroy(@irequest, @request, @response) end it "should use a common method for determining the request parameters" do - @handler.stubs(:params).returns(:foo => :baz, :bar => :xyzzy) + @irequest.stubs(:options).returns(:foo => :baz, :bar => :xyzzy) @model_class.expects(:destroy).with do |key, args| args[:foo] == :baz and args[:bar] == :xyzzy end - @handler.do_destroy(@request, @response) + @handler.do_destroy(@irequest, @request, @response) end it "should use the default status code a model destroy call succeeds" do @handler.expects(:set_response).with { |response, body, status| status.nil? } - @handler.do_destroy(@request, @response) + @handler.do_destroy(@irequest, @request, @response) end it "should return a yaml-encoded result when a model destroy call succeeds" do @result = stub 'result', :to_yaml => "the result" @model_class.expects(:destroy).returns(@result) @handler.expects(:set_response).with { |response, body, status| body == "the result" } - @handler.do_destroy(@request, @response) + @handler.do_destroy(@irequest, @request, @response) end end describe "when saving a model instance" do before do - @handler.stubs(:http_method).returns('PUT') - @handler.stubs(:path).returns('/my_handler/key') - @handler.stubs(:singular?).returns(true) - @handler.stubs(:request_key).returns('key') + @irequest = stub 'indirection_request', :method => :save, :indirection_name => "my_handler", :options => {}, :key => "key" @handler.stubs(:body).returns('my stuff') @result = stub 'result', :render => "the result" @model_instance = stub('indirected model instance', :save => true) @model_class.stubs(:convert_from).returns(@model_instance) @format = stub 'format', :suitable? => true Puppet::Network::FormatHandler.stubs(:format).returns @format end it "should use the 'body' hook to retrieve the body of the request" do @handler.expects(:body).returns "my body" @model_class.expects(:convert_from).with { |format, body| body == "my body" }.returns @model_instance - @handler.do_save(@request, @response) + @handler.do_save(@irequest, @request, @response) end it "should fail to save model if data is not specified" do @handler.stubs(:body).returns('') - lambda { @handler.do_save(@request, @response) }.should raise_error(ArgumentError) + lambda { @handler.do_save(@irequest, @request, @response) }.should raise_error(ArgumentError) end it "should use a common method for determining the request parameters" do - @handler.stubs(:params).returns(:foo => :baz, :bar => :xyzzy) + @irequest.stubs(:options).returns(:foo => :baz, :bar => :xyzzy) @model_instance.expects(:save).with do |args| args[:foo] == :baz and args[:bar] == :xyzzy end - @handler.do_save(@request, @response) + @handler.do_save(@irequest, @request, @response) end it "should use the default status when a model save call succeeds" do @handler.expects(:set_response).with { |response, body, status| status.nil? } - @handler.do_save(@request, @response) + @handler.do_save(@irequest, @request, @response) end it "should return the yaml-serialized result when a model save call succeeds" do @model_instance.stubs(:save).returns(@model_instance) @model_instance.expects(:to_yaml).returns('foo') - @handler.do_save(@request, @response) + @handler.do_save(@irequest, @request, @response) end it "should set the content to yaml" do @handler.expects(:set_content_type).with(@response, "yaml") - @handler.do_save(@request, @response) + @handler.do_save(@irequest, @request, @response) end end end end