diff --git a/lib/puppet/file_serving/configuration.rb b/lib/puppet/file_serving/configuration.rb index de5995d7e..608924c8b 100644 --- a/lib/puppet/file_serving/configuration.rb +++ b/lib/puppet/file_serving/configuration.rb @@ -1,122 +1,118 @@ # # Created by Luke Kanies on 2007-10-16. # Copyright (c) 2007. All rights reserved. require 'puppet' require 'puppet/file_serving' require 'puppet/file_serving/mount' require 'puppet/file_serving/mount/file' require 'puppet/file_serving/mount/modules' require 'puppet/file_serving/mount/plugins' require 'puppet/util/cacher' -require 'puppet/util/uri_helper' class Puppet::FileServing::Configuration - include Puppet::Util::URIHelper require 'puppet/file_serving/configuration/parser' class << self include Puppet::Util::Cacher cached_attr(:configuration) { new() } end Mount = Puppet::FileServing::Mount # Create our singleton configuration. def self.create configuration end private_class_method :new attr_reader :mounts #private :mounts # Find the right mount. Does some shenanigans to support old-style module # mounts. def find_mount(mount_name, node) # Reparse the configuration if necessary. readconfig if mount = mounts[mount_name] return mount end if mounts["modules"].environment(node).module(mount_name) Puppet.warning "DEPRECATION NOTICE: Found module '%s' without using the 'modules' mount; please prefix path with 'modules/'" % mount_name return mounts["modules"] end # This can be nil. mounts[mount_name] end def initialize @mounts = {} @config_file = nil # We don't check to see if the file is modified the first time, # because we always want to parse at first. readconfig(false) end # Is a given mount available? def mounted?(name) @mounts.include?(name) end # Split the path into the separate mount point and path. def split_path(request) # Reparse the configuration if necessary. readconfig - uri = key2uri(request.key) - - mount_name, path = uri.path.sub(/^\//, '').split(File::Separator, 2) + mount_name, path = request.key.split(File::Separator, 2) raise(ArgumentError, "Cannot find file: Invalid path '%s'" % mount_name) unless mount_name =~ %r{^[-\w]+$} return nil unless mount = find_mount(mount_name, request.options[:node]) if mount.name == "modules" and mount_name != "modules" # yay backward-compatibility path = "%s/%s" % [mount_name, path] end if path == "" path = nil elsif path # Remove any double slashes that might have occurred path = path.gsub(/\/+/, "/") end return mount, path end def umount(name) @mounts.delete(name) if @mounts.include? name end private # Read the configuration file. def readconfig(check = true) config = Puppet[:fileserverconfig] return unless FileTest.exists?(config) @parser ||= Puppet::FileServing::Configuration::Parser.new(config) if check and ! @parser.changed? return end # Don't assign the mounts hash until we're sure the parsing succeeded. begin newmounts = @parser.parse @mounts = newmounts rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Error parsing fileserver configuration: %s; using old configuration" % detail end end end diff --git a/lib/puppet/indirector/direct_file_server.rb b/lib/puppet/indirector/direct_file_server.rb index bcda92366..f69f9e14b 100644 --- a/lib/puppet/indirector/direct_file_server.rb +++ b/lib/puppet/indirector/direct_file_server.rb @@ -1,25 +1,23 @@ # # Created by Luke Kanies on 2007-10-24. # Copyright (c) 2007. All rights reserved. require 'puppet/file_serving/terminus_helper' -require 'puppet/util/uri_helper' require 'puppet/indirector/terminus' class Puppet::Indirector::DirectFileServer < Puppet::Indirector::Terminus - include Puppet::Util::URIHelper include Puppet::FileServing::TerminusHelper def find(request) return nil unless FileTest.exists?(request.key) instance = model.new(request.key) instance.links = request.options[:links] if request.options[:links] return instance end def search(request) return nil unless FileTest.exists?(request.key) path2instances(request, request.key) end end diff --git a/lib/puppet/indirector/file_content/rest.rb b/lib/puppet/indirector/file_content/rest.rb index 31df7626d..7b3cade8e 100644 --- a/lib/puppet/indirector/file_content/rest.rb +++ b/lib/puppet/indirector/file_content/rest.rb @@ -1,12 +1,11 @@ # # Created by Luke Kanies on 2007-10-18. # Copyright (c) 2007. All rights reserved. require 'puppet/file_serving/content' -require 'puppet/util/uri_helper' require 'puppet/indirector/file_content' require 'puppet/indirector/rest' class Puppet::Indirector::FileContent::Rest < Puppet::Indirector::REST desc "Retrieve file contents via a REST HTTP interface." end diff --git a/lib/puppet/indirector/file_metadata/rest.rb b/lib/puppet/indirector/file_metadata/rest.rb index 0f3d9c6fd..8cbf91049 100644 --- a/lib/puppet/indirector/file_metadata/rest.rb +++ b/lib/puppet/indirector/file_metadata/rest.rb @@ -1,12 +1,11 @@ # # Created by Luke Kanies on 2007-10-18. # Copyright (c) 2007. All rights reserved. require 'puppet/file_serving/metadata' -require 'puppet/util/uri_helper' require 'puppet/indirector/file_metadata' require 'puppet/indirector/rest' class Puppet::Indirector::FileMetadata::Rest < Puppet::Indirector::REST desc "Retrieve file metadata via a REST HTTP interface." end diff --git a/lib/puppet/indirector/request.rb b/lib/puppet/indirector/request.rb index c6f268ab2..539cd0e62 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -1,116 +1,121 @@ +require 'uri' require 'puppet/indirector' # This class encapsulates all of the information you need to make an # Indirection call, and as a a result also handles REST calls. It's somewhat # analogous to an HTTP Request object, except tuned for our Indirector. class Puppet::Indirector::Request attr_accessor :indirection_name, :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus attr_accessor :server, :port, :uri, :protocol # Is this an authenticated request? def authenticated? # Double negative, so we just get true or false ! ! authenticated end + def escaped_key + URI.escape(key) + end + # LAK:NOTE This is a messy interface to the cache, and it's only # used by the Configurer class. I decided it was better to implement # it now and refactor later, when we have a better design, than # to spend another month coming up with a design now that might # not be any better. def ignore_cache? ignore_cache end def ignore_terminus? ignore_terminus end def initialize(indirection_name, method, key, options = {}) options ||= {} raise ArgumentError, "Request options must be a hash, not %s" % options.class unless options.is_a?(Hash) @indirection_name, @method = indirection_name, method @options = options.inject({}) do |result, ary| param, value = ary if respond_to?(param.to_s + "=") send(param.to_s + "=", value) else result[param] = value end result end if key.is_a?(String) or key.is_a?(Symbol) # If the request key is a URI, then we need to treat it specially, # because it rewrites the key. We could otherwise strip server/port/etc # info out in the REST class, but it seemed bad design for the REST # class to rewrite the key. if key.to_s =~ /^\w+:\/\// # it's a URI set_uri_key(key) else @key = key end else @instance = key @key = @instance.name end end # Look up the indirection based on the name provided. def indirection Puppet::Indirector::Indirection.instance(indirection_name) end # Should we allow use of the cached object? def use_cache? if defined?(@use_cache) ! ! use_cache else true end end # Are we trying to interact with multiple resources, or just one? def plural? method == :search end def to_s return uri if uri return "/%s/%s" % [indirection_name, key] end private # Parse the key as a URI, setting attributes appropriately. def set_uri_key(key) @uri = key begin uri = URI.parse(URI.escape(key)) rescue => detail raise ArgumentError, "Could not understand URL %s: %s" % [source, detail.to_s] end # Just short-circuit these to full paths if uri.scheme == "file" - @key = uri.path + @key = URI.unescape(uri.path) return end @server = uri.host if uri.host # If the URI class can look up the scheme, it will provide a port, # otherwise it will default to '0'. if uri.port.to_i == 0 and uri.scheme == "puppet" @port = Puppet.settings[:masterport].to_i else @port = uri.port.to_i end @protocol = uri.scheme - @key = uri.path.sub(/^\//, '') + @key = URI.unescape(uri.path.sub(/^\//, '')) end end diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb index 2d0799286..ce459b905 100644 --- a/lib/puppet/indirector/rest.rb +++ b/lib/puppet/indirector/rest.rb @@ -1,107 +1,107 @@ 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.key}#{query_string(request)}", headers) + 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.key}#{query_string(request)}" + 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.key}", headers) + 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 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 9bc94a037..cce679864 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,198 +1,201 @@ 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" end result[param.to_sym] = value result end end end diff --git a/lib/puppet/util/uri_helper.rb b/lib/puppet/util/uri_helper.rb deleted file mode 100644 index cb9320387..000000000 --- a/lib/puppet/util/uri_helper.rb +++ /dev/null @@ -1,22 +0,0 @@ -# -# Created by Luke Kanies on 2007-10-16. -# Copyright (c) 2007. All rights reserved. - -require 'uri' -require 'puppet/util' - -# Helper methods for dealing with URIs. -module Puppet::Util::URIHelper - def key2uri(key) - # Return it directly if it's fully qualified. - if key =~ /^#{::File::SEPARATOR}/ - key = "file://" + key - end - - begin - uri = URI.parse(URI.escape(key)) - rescue => detail - raise ArgumentError, "Could not understand URI %s: %s" % [key, detail.to_s] - end - end -end diff --git a/spec/unit/file_serving/configuration.rb b/spec/unit/file_serving/configuration.rb index c2d2e5014..e62423caf 100755 --- a/spec/unit/file_serving/configuration.rb +++ b/spec/unit/file_serving/configuration.rb @@ -1,208 +1,208 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/file_serving/configuration' describe Puppet::FileServing::Configuration do it "should make :new a private method" do proc { Puppet::FileServing::Configuration.new }.should raise_error end it "should return the same configuration each time :create is called" do Puppet::FileServing::Configuration.create.should equal(Puppet::FileServing::Configuration.create) end it "should have a method for removing the current configuration instance" do old = Puppet::FileServing::Configuration.create Puppet::Util::Cacher.expire Puppet::FileServing::Configuration.create.should_not equal(old) end after do Puppet::Util::Cacher.expire end end describe Puppet::FileServing::Configuration do before :each do @path = "/path/to/configuration/file.conf" Puppet.settings.stubs(:value).with(:trace).returns(false) Puppet.settings.stubs(:value).with(:fileserverconfig).returns(@path) end after :each do Puppet::Util::Cacher.expire end describe "when initializing" do it "should work without a configuration file" do FileTest.stubs(:exists?).with(@path).returns(false) proc { Puppet::FileServing::Configuration.create }.should_not raise_error end it "should parse the configuration file if present" do FileTest.stubs(:exists?).with(@path).returns(true) @parser = mock 'parser' @parser.expects(:parse).returns({}) Puppet::FileServing::Configuration::Parser.stubs(:new).returns(@parser) Puppet::FileServing::Configuration.create end it "should determine the path to the configuration file from the Puppet settings" do Puppet::FileServing::Configuration.create end end describe "when parsing the configuration file" do before do FileTest.stubs(:exists?).with(@path).returns(true) @parser = mock 'parser' Puppet::FileServing::Configuration::Parser.stubs(:new).returns(@parser) end it "should set the mount list to the results of parsing" do @parser.expects(:parse).returns("one" => mock("mount")) config = Puppet::FileServing::Configuration.create config.mounted?("one").should be_true end it "should not raise exceptions" do @parser.expects(:parse).raises(ArgumentError) proc { Puppet::FileServing::Configuration.create }.should_not raise_error end it "should replace the existing mount list with the results of reparsing" do @parser.expects(:parse).returns("one" => mock("mount")) config = Puppet::FileServing::Configuration.create config.mounted?("one").should be_true # Now parse again @parser.expects(:parse).returns("two" => mock('other')) config.send(:readconfig, false) config.mounted?("one").should be_false config.mounted?("two").should be_true end it "should not replace the mount list until the file is entirely parsed successfully" do @parser.expects(:parse).returns("one" => mock("mount")) @parser.expects(:parse).raises(ArgumentError) config = Puppet::FileServing::Configuration.create # Now parse again, so the exception gets thrown config.send(:readconfig, false) config.mounted?("one").should be_true end end describe "when finding the specified mount" do it "should choose the named mount if one exists" do config = Puppet::FileServing::Configuration.create config.expects(:mounts).returns("one" => "foo") config.find_mount("one", "mynode").should == "foo" end it "should modules mount's environment to find a matching module if the named module cannot be found" do config = Puppet::FileServing::Configuration.create mod = mock 'module' env = mock 'environment' env.expects(:module).with("foo").returns mod mount = mock 'mount' mount.expects(:environment).with("mynode").returns env config.stubs(:mounts).returns("modules" => mount) config.find_mount("foo", "mynode").should equal(mount) end it "should return nil if there is no such named mount and no module with the same name exists" do config = Puppet::FileServing::Configuration.create env = mock 'environment' env.expects(:module).with("foo").returns nil mount = mock 'mount' mount.expects(:environment).with("mynode").returns env config.stubs(:mounts).returns("modules" => mount) config.find_mount("foo", "mynode").should be_nil end end describe "when finding the mount name and relative path in a request key" do before do @config = Puppet::FileServing::Configuration.create @config.stubs(:find_mount) - @request = stub 'request', :key => "puppet:///foo/bar/baz", :options => {} + @request = stub 'request', :key => "foo/bar/baz", :options => {} end it "should reread the configuration" do @config.expects(:readconfig) @config.split_path(@request) end it "should treat the first field of the URI path as the mount name" do @config.expects(:find_mount).with { |name, node| name == "foo" } @config.split_path(@request) end it "should fail if the mount name is not alpha-numeric" do - @request.expects(:key).returns "puppet:///foo&bar/asdf" + @request.expects(:key).returns "foo&bar/asdf" lambda { @config.split_path(@request) }.should raise_error(ArgumentError) end it "should support dashes in the mount name" do - @request.expects(:key).returns "puppet:///foo-bar/asdf" + @request.expects(:key).returns "foo-bar/asdf" lambda { @config.split_path(@request) }.should_not raise_error(ArgumentError) end it "should use the mount name and node to find the mount" do @config.expects(:find_mount).with { |name, node| name == "foo" and node == "mynode" } @request.options[:node] = "mynode" @config.split_path(@request) end it "should return nil if the mount cannot be found" do @config.expects(:find_mount).returns nil @config.split_path(@request).should be_nil end it "should return the mount and the relative path if the mount is found" do mount = stub 'mount', :name => "foo" @config.expects(:find_mount).returns mount @config.split_path(@request).should == [mount, "bar/baz"] end it "should remove any double slashes" do - @request.stubs(:key).returns "puppet:///foo/bar//baz" + @request.stubs(:key).returns "foo/bar//baz" mount = stub 'mount', :name => "foo" @config.expects(:find_mount).returns mount @config.split_path(@request).should == [mount, "bar/baz"] end it "should return the relative path as nil if it is an empty string" do - @request.expects(:key).returns "puppet:///foo" + @request.expects(:key).returns "foo" mount = stub 'mount', :name => "foo" @config.expects(:find_mount).returns mount @config.split_path(@request).should == [mount, nil] end it "should add 'modules/' to the relative path if the modules mount is used but not specified, for backward compatibility" do - @request.expects(:key).returns "puppet:///foo/bar" + @request.expects(:key).returns "foo/bar" mount = stub 'mount', :name => "modules" @config.expects(:find_mount).returns mount @config.split_path(@request).should == [mount, "foo/bar"] end end end diff --git a/spec/unit/indirector/request.rb b/spec/unit/indirector/request.rb index dc1e4e39d..fc3ed44e6 100755 --- a/spec/unit/indirector/request.rb +++ b/spec/unit/indirector/request.rb @@ -1,174 +1,180 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/indirector/request' describe Puppet::Indirector::Request do describe "when initializing" do it "should require an indirection name, a key, and a method" do lambda { Puppet::Indirector::Request.new }.should raise_error(ArgumentError) end it "should use provided value as the key if it is a string" do Puppet::Indirector::Request.new(:ind, :method, "mykey").key.should == "mykey" end it "should use provided value as the key if it is a symbol" do Puppet::Indirector::Request.new(:ind, :method, :mykey).key.should == :mykey end it "should use the name of the provided instance as its key if an instance is provided as the key instead of a string" do instance = mock 'instance', :name => "mykey" request = Puppet::Indirector::Request.new(:ind, :method, instance) request.key.should == "mykey" request.instance.should equal(instance) end it "should support options specified as a hash" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key, :one => :two) }.should_not raise_error(ArgumentError) end it "should support nil options" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key, nil) }.should_not raise_error(ArgumentError) end it "should support unspecified options" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key) }.should_not raise_error(ArgumentError) end it "should fail if options are specified as anything other than nil or a hash" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key, [:one, :two]) }.should raise_error(ArgumentError) end it "should use an empty options hash if nil was provided" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).options.should == {} end it "should default to a nil node" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).node.should be_nil end it "should set its node attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :node => "foo.com").node.should == "foo.com" end it "should default to a nil ip" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).ip.should be_nil end it "should set its ip attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :ip => "192.168.0.1").ip.should == "192.168.0.1" end it "should default to being unauthenticated" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_authenticated end it "should set be marked authenticated if configured in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :authenticated => "eh").should be_authenticated end it "should keep its options as a hash even if a node is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, :node => "eh").options.should be_instance_of(Hash) end it "should keep its options as a hash even if another option is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, :foo => "bar").options.should be_instance_of(Hash) end describe "and the request key is a URI" do describe "and the URI is a 'file' URI" do before do - @request = Puppet::Indirector::Request.new(:ind, :method, "file:///my/file") + @request = Puppet::Indirector::Request.new(:ind, :method, "file:///my/file with spaces") end - it "should set the request key to the full file path" do @request.key.should == "/my/file" end + it "should set the request key to the unescaped full file path" do + @request.key.should == "/my/file with spaces" + end it "should not set the protocol" do @request.protocol.should be_nil end it "should not set the port" do @request.port.should be_nil end it "should not set the server" do @request.server.should be_nil end end it "should set the protocol to the URI scheme" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").protocol.should == "http" end it "should set the server if a server is provided" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").server.should == "host" end it "should set the server and port if both are provided" do Puppet::Indirector::Request.new(:ind, :method, "http://host:543/stuff").port.should == 543 end it "should default to the masterport if the URI scheme is 'puppet'" do Puppet.settings.expects(:value).with(:masterport).returns "321" Puppet::Indirector::Request.new(:ind, :method, "puppet://host/stuff").port.should == 321 end it "should use the provided port if the URI scheme is not 'puppet'" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").port.should == 80 end - it "should set the request key to the unqualified path from the URI" do - Puppet::Indirector::Request.new(:ind, :method, "http:///stuff").key.should == "stuff" + it "should set the request key to the unescaped unqualified path from the URI" do + Puppet::Indirector::Request.new(:ind, :method, "http:///stuff with spaces").key.should == "stuff with spaces" end it "should set the :uri attribute to the full URI" do Puppet::Indirector::Request.new(:ind, :method, "http:///stuff").uri.should == "http:///stuff" end end it "should allow indication that it should not read a cached instance" do Puppet::Indirector::Request.new(:ind, :method, :key, :ignore_cache => true).should be_ignore_cache end it "should default to not ignoring the cache" do Puppet::Indirector::Request.new(:ind, :method, :key).should_not be_ignore_cache end it "should allow indication that it should not not read an instance from the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key, :ignore_terminus => true).should be_ignore_terminus end it "should default to not ignoring the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key).should_not be_ignore_terminus end end it "should look use the Indirection class to return the appropriate indirection" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key) request.indirection.should equal(ind) end it "should have a method for determining if the request is plural or singular" do Puppet::Indirector::Request.new(:myind, :method, :key).should respond_to(:plural?) end it "should be considered plural if the method is 'search'" do Puppet::Indirector::Request.new(:myind, :search, :key).should be_plural end it "should not be considered plural if the method is not 'search'" do Puppet::Indirector::Request.new(:myind, :find, :key).should_not be_plural end it "should use its uri, if it has one, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "foo://bar/baz").to_s.should == "foo://bar/baz" end it "should use its indirection name and key, if it has no uri, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "key") == "/myind/key" end + + it "should be able to return the URI-escaped key" do + Puppet::Indirector::Request.new(:myind, :find, "my key").escaped_key.should == URI.escape("my key") + end end diff --git a/spec/unit/indirector/rest.rb b/spec/unit/indirector/rest.rb index aa6d51b6a..4fa30b20a 100755 --- a/spec/unit/indirector/rest.rb +++ b/spec/unit/indirector/rest.rb @@ -1,426 +1,427 @@ #!/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 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 - @request = stub 'request', :key => 'foo', :options => {} + # 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 request key to create the path" do + 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', :key => 'foo', :options => {} + @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 request key to create the path if the request key is set" do + 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', :key => 'foo', :options => {} + @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 request key to create the path" do + 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/handler.rb b/spec/unit/network/http/handler.rb index ed0f25121..6f1a57b8b 100755 --- a/spec/unit/network/http/handler.rb +++ b/spec/unit/network/http/handler.rb @@ -1,483 +1,499 @@ #!/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 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(: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" @handler.should_not be_singular(@request) end 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.should be_plural(@request) 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" @handler.should_not be_plural(@request) end 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_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(: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') @model_class.stubs(:find).returns @result @format = stub 'format', :suitable? => true Puppet::Network::FormatHandler.stubs(:format).returns @format end - it "should fail to find model if key is not specified" do + 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" + end.returns @result + @handler.do_find(@request, @response) + end + it "should use a common method for determining the request parameters" do @handler.stubs(:params).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) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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') @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) @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 a request key if one is provided" do - @handler.expects(:request_key).with(@request).returns "foo" - @model_class.expects(:search).with { |key, args| key == "foo" }.returns @result + 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) 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) 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) 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) 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) 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) 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') @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") + @model_class.expects(:destroy).with do |key, args| + key == "foo bar" + end + @handler.do_destroy(@request, @response) + end + it "should use a common method for determining the request parameters" do @handler.stubs(:params).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) 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) 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) 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') @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) 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) end it "should use a common method for determining the request parameters" do @handler.stubs(:params).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) 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) 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) end it "should set the content to yaml" do @handler.expects(:set_content_type).with(@response, "yaml") @handler.do_save(@request, @response) end end end end diff --git a/spec/unit/util/uri_helper.rb b/spec/unit/util/uri_helper.rb deleted file mode 100755 index f454a2ced..000000000 --- a/spec/unit/util/uri_helper.rb +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env ruby -# -# Created by Luke Kanies on 2007-10-18. -# Copyright (c) 2007. All rights reserved. - -require File.dirname(__FILE__) + '/../../spec_helper' - -require 'puppet/util/uri_helper' - -describe Puppet::Util::URIHelper, " when converting a key to a URI" do - before do - @helper = Object.new - @helper.extend(Puppet::Util::URIHelper) - end - - it "should return the URI instance" do - URI.expects(:parse).with("file:///myhost/blah").returns(:myuri) - @helper.key2uri("/myhost/blah").should == :myuri - end - - it "should escape the key before parsing" do - URI.expects(:escape).with("mykey").returns("http://myhost/blah") - URI.expects(:parse).with("http://myhost/blah").returns(:myuri) - @helper.key2uri("mykey").should == :myuri - end - - it "should use the URI class to parse the key" do - URI.expects(:parse).with("http://myhost/blah").returns(:myuri) - @helper.key2uri("http://myhost/blah").should == :myuri - end - - it "should set the scheme to 'file' if the key is a fully qualified path" do - URI.expects(:parse).with("file:///myhost/blah").returns(:myuri) - @helper.key2uri("/myhost/blah").should == :myuri - end - - it "should set the host to 'nil' if the key is a fully qualified path" do - URI.expects(:parse).with("file:///myhost/blah").returns(:myuri) - @helper.key2uri("/myhost/blah").should == :myuri - end -end