diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb index ce459b905..23ed56dee 100644 --- a/lib/puppet/indirector/rest.rb +++ b/lib/puppet/indirector/rest.rb @@ -1,107 +1,108 @@ require 'net/http' require 'uri' require 'puppet/network/http_pool' # Access objects via REST class Puppet::Indirector::REST < Puppet::Indirector::Terminus 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("/#{indirection.name}/#{request.escaped_key}#{query_string(request)}", headers) end def search(request) if request.key path = "/#{indirection.name}s/#{request.escaped_key}#{query_string(request)}" else path = "/#{indirection.name}s#{query_string(request)}" end unless result = deserialize(network(request).get(path, 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("/#{indirection.name}/#{request.escaped_key}", headers) end def save(request) raise ArgumentError, "PUT does not accept options" unless request.options.empty? deserialize network(request).put("/#{indirection.name}/", request.instance.render, headers) end # Create the query string, if options are present. def query_string(request) return "" unless request.options and ! request.options.empty? "?" + request.options.collect do |key, value| case value when nil; next when true, false; value = value.to_s + when Fixnum, Bignum, Float; value = value # nothing when String; value = URI.escape(value) when Symbol; value = URI.escape(value.to_s) when Array; value = URI.escape(YAML.dump(value)) else raise ArgumentError, "HTTP REST queries cannot handle values of type '%s'" % value.class end "%s=%s" % [key, value] end.join("&") end end diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index cce679864..7f6bd4111 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,201 +1,203 @@ module Puppet::Network::HTTP end module Puppet::Network::HTTP::Handler 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)}'" rescue Exception => e return do_exception(response, e) end # Are we interacting with a singular instance? def singular?(request) %r{/#{handler.to_s}$}.match(path(request)) end # Are we interacting with multiple instances? def plural?(request) %r{/#{handler.to_s}s$}.match(path(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?(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) 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 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) 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) set_content_type(response, "yaml") set_response(response, result.to_yaml) end # Execute our save. def do_save(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) 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) 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 4fa30b20a..8ed50d300 100755 --- a/spec/unit/indirector/rest.rb +++ b/spec/unit/indirector/rest.rb @@ -1,427 +1,437 @@ #!/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 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 building a query string from request options" do it "should return an empty query string if there are no options" do @request.stubs(:options).returns nil @searcher.query_string(@request).should == "" end it "should return an empty query string if the options are empty" do @request.stubs(:options).returns({}) @searcher.query_string(@request).should == "" end it "should prefix the query string with '?'" do @request.stubs(:options).returns(:one => "two") @searcher.query_string(@request).should =~ /^\?/ end it "should include all options in the query string, separated by '&'" do @request.stubs(:options).returns(:one => "two", :three => "four") @searcher.query_string(@request).sub(/^\?/, '').split("&").sort.should == %w{one=two three=four}.sort end it "should ignore nil options" do @request.stubs(:options).returns(:one => "two", :three => nil) @searcher.query_string(@request).should_not be_include("three") end it "should convert 'true' option values into strings" do @request.stubs(:options).returns(:one => true) @searcher.query_string(@request).should == "?one=true" end it "should convert 'false' option values into strings" do @request.stubs(:options).returns(:one => false) @searcher.query_string(@request).should == "?one=false" end it "should URI-escape all option values that are strings" do escaping = URI.escape("one two") @request.stubs(:options).returns(:one => "one two") @searcher.query_string(@request).should == "?one=#{escaping}" end it "should YAML-dump and URI-escape arrays" do escaping = URI.escape(YAML.dump(%w{one two})) @request.stubs(:options).returns(:one => %w{one two}) @searcher.query_string(@request).should == "?one=#{escaping}" end + it "should convert to a string all option values that are integers" do + @request.stubs(:options).returns(:one => 50) + @searcher.query_string(@request).should == "?one=50" + end + + it "should convert to a string all option values that are floating point numbers" do + @request.stubs(:options).returns(:one => 1.2) + @searcher.query_string(@request).should == "?one=1.2" + end + it "should convert to a string and URI-escape all option values that are symbols" do escaping = URI.escape("sym bol") @request.stubs(:options).returns(:one => :"sym bol") @searcher.query_string(@request).should == "?one=#{escaping}" end it "should fail if options other than booleans or strings are provided" do @request.stubs(:options).returns(:one => {:one => :two}) lambda { @searcher.query_string(@request) }.should raise_error(ArgumentError) 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', :options => {} 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 indirection name and escaped request key to create the path" do should_path = "/%s/%s" % [@indirection.name.to_s, "foo"] @connection.expects(:get).with { |path, args| path == should_path }.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', :options => {}, :key => "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 plural indirection name as the path if there is no request key" do should_path = "/%ss" % [@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 plural indirection name and escaped request key to create the path if the request key is set" do should_path = "/%ss/%s" % [@indirection.name.to_s, "foo"] @connection.expects(:get).with { |path, args| path == should_path }.returns(@response) @searcher.search(@request) end it "should include all options in the query string" do @request.stubs(:options).returns(:one => "two", :three => "four") should_path = "/%s/%s" % [@indirection.name.to_s, "foo"] @connection.expects(:get).with { |path, args| path =~ /\?one=two&three=four$/ or path =~ /\?three=four&one=two$/ }.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', :options => {} 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 indirection name and escaped request key to create the path" do should_path = "/%s/%s" % [@indirection.name.to_s, "foo"] @connection.expects(:delete).with { |path, args| path == should_path }.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, :options => {} 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 indirection name as the path for the request" do @connection.expects(:put).with { |path, data, args| path == "/#{@indirection.name.to_s}/" }.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/mongrel/rest.rb b/spec/unit/network/http/mongrel/rest.rb index e72d4f540..c03698dbd 100755 --- a/spec/unit/network/http/mongrel/rest.rb +++ b/spec/unit/network/http/mongrel/rest.rb @@ -1,193 +1,205 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../../spec_helper' require 'puppet/network/http' describe "Puppet::Network::HTTP::MongrelREST" do confine "Mongrel is not available" => Puppet.features.mongrel? before do require 'puppet/network/http/mongrel/rest' end it "should include the Puppet::Network::HTTP::Handler module" do Puppet::Network::HTTP::MongrelREST.ancestors.should be_include(Puppet::Network::HTTP::Handler) end describe "when initializing" do it "should call the Handler's initialization hook with its provided arguments as the server and handler" do Puppet::Network::HTTP::MongrelREST.any_instance.expects(:initialize_for_puppet).with(:server => "my", :handler => "arguments") Puppet::Network::HTTP::MongrelREST.new(:server => "my", :handler => "arguments") end end describe "when receiving a request" do before do @params = {} @request = stub('mongrel http request', :params => @params) @head = stub('response head') @body = stub('response body', :write => true) @response = stub('mongrel http response') @response.stubs(:start).yields(@head, @body) @model_class = stub('indirected model class') @mongrel = stub('mongrel http server', :register => true) Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@model_class) @handler = Puppet::Network::HTTP::MongrelREST.new(:server => @mongrel, :handler => :foo) end describe "and using the HTTP Handler interface" do it "should return the HTTP_ACCEPT parameter as the accept header" do @params.expects(:[]).with("HTTP_ACCEPT").returns "myaccept" @handler.accept_header(@request).should == "myaccept" end it "should use the REQUEST_METHOD as the http method" do @params.expects(:[]).with(Mongrel::Const::REQUEST_METHOD).returns "mymethod" @handler.http_method(@request).should == "mymethod" end it "should use the first part of the request path as the path" do @params.expects(:[]).with(Mongrel::Const::REQUEST_PATH).returns "/foo/bar" @handler.path(@request).should == "/foo" end it "should use the remainder of the request path as the request key" do @params.expects(:[]).with(Mongrel::Const::REQUEST_PATH).returns "/foo/bar/baz" @handler.request_key(@request).should == "bar/baz" end it "should return nil as the request key if no second field is present" do @params.expects(:[]).with(Mongrel::Const::REQUEST_PATH).returns "/foo" @handler.request_key(@request).should be_nil end it "should return the request body as the body" do @request.expects(:body).returns "mybody" @handler.body(@request).should == "mybody" end it "should set the response's content-type header when setting the content type" do @header = mock 'header' @response.expects(:header).returns @header @header.expects(:[]=).with('Content-Type', "mytype") @handler.set_content_type(@response, "mytype") end it "should set the status and write the body when setting the response for a successful request" do head = mock 'head' body = mock 'body' @response.expects(:start).with(200).yields(head, body) body.expects(:write).with("mybody") @handler.set_response(@response, "mybody", 200) end it "should set the status and reason and write the body when setting the response for a successful request" do head = mock 'head' body = mock 'body' @response.expects(:start).with(400, false, "mybody").yields(head, body) body.expects(:write).with("mybody") @handler.set_response(@response, "mybody", 400) end end describe "and determining the request parameters" do before do @request.stubs(:params).returns({}) end it "should include the HTTP request parameters, with the keys as symbols" do @request.expects(:params).returns('QUERY_STRING' => 'foo=baz&bar=xyzzy') result = @handler.params(@request) result[:foo].should == "baz" result[:bar].should == "xyzzy" end it "should URI-decode the HTTP parameters" do encoding = URI.escape("foo bar") @request.expects(:params).returns('QUERY_STRING' => "foo=#{encoding}") result = @handler.params(@request) result[:foo].should == "foo bar" end it "should convert the string 'true' to the boolean" do @request.expects(:params).returns('QUERY_STRING' => 'foo=true') result = @handler.params(@request) result[:foo].should be_true end it "should convert the string 'false' to the boolean" do @request.expects(:params).returns('QUERY_STRING' => 'foo=false') result = @handler.params(@request) result[:foo].should be_false end + it "should convert integer arguments to Integers" do + @request.expects(:params).returns('QUERY_STRING' => 'foo=15') + result = @handler.params(@request) + result[:foo].should == 15 + end + + it "should convert floating point arguments to Floats" do + @request.expects(:params).returns('QUERY_STRING' => 'foo=1.5') + result = @handler.params(@request) + result[:foo].should == 1.5 + end + it "should YAML-load and URI-decode values that are YAML-encoded" do escaping = URI.escape(YAML.dump(%w{one two})) @request.expects(:params).returns('QUERY_STRING' => "foo=#{escaping}") result = @handler.params(@request) result[:foo].should == %w{one two} end it "should pass the client's ip address to model find" do @request.stubs(:params).returns("REMOTE_ADDR" => "ipaddress") @handler.params(@request)[:ip].should == "ipaddress" end it "should use the :ssl_client_header to determine the parameter when looking for the certificate" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:ssl_client_header).returns "myheader" @request.stubs(:params).returns("myheader" => "/CN=host.domain.com") @handler.params(@request) end it "should retrieve the hostname by matching the certificate parameter" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:ssl_client_header).returns "myheader" @request.stubs(:params).returns("myheader" => "/CN=host.domain.com") @handler.params(@request)[:node].should == "host.domain.com" end it "should use the :ssl_client_header to determine the parameter for checking whether the host certificate is valid" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.expects(:value).with(:ssl_client_verify_header).returns "myheader" @request.stubs(:params).returns("myheader" => "SUCCESS", "certheader" => "/CN=host.domain.com") @handler.params(@request) end it "should consider the host authenticated if the validity parameter contains 'SUCCESS'" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.stubs(:value).with(:ssl_client_verify_header).returns "myheader" @request.stubs(:params).returns("myheader" => "SUCCESS", "certheader" => "/CN=host.domain.com") @handler.params(@request)[:authenticated].should be_true end it "should consider the host unauthenticated if the validity parameter does not contain 'SUCCESS'" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.stubs(:value).with(:ssl_client_verify_header).returns "myheader" @request.stubs(:params).returns("myheader" => "whatever", "certheader" => "/CN=host.domain.com") @handler.params(@request)[:authenticated].should be_false end it "should consider the host unauthenticated if no certificate information is present" do Puppet.settings.stubs(:value).with(:ssl_client_header).returns "certheader" Puppet.settings.stubs(:value).with(:ssl_client_verify_header).returns "myheader" @request.stubs(:params).returns("myheader" => nil, "certheader" => "SUCCESS") @handler.params(@request)[:authenticated].should be_false end it "should not pass a node name to model method if no certificate information is present" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:ssl_client_header).returns "myheader" @request.stubs(:params).returns("myheader" => nil) @handler.params(@request).should_not be_include(:node) end end end end