diff options
-rw-r--r-- | lib/puppet/network/http/handler.rb | 90 | ||||
-rwxr-xr-x | spec/unit/network/http/handler.rb | 411 |
2 files changed, 478 insertions, 23 deletions
diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index a862bb85e..a09375a67 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,4 +1,19 @@ +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) + accept_header(request).split(/,\s*/)[0] + end def initialize_for_puppet(args = {}) raise ArgumentError unless @server = args[:server] @@ -17,41 +32,82 @@ module Puppet::Network::HTTP::Handler return do_exception(request, response, e) end - private + # Are we interacting with a singular instance? + def singular?(request) + %r{/#{handler.to_s}$}.match(path(request)) + end - def model - @model + # 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 + + # Execute our find. def do_find(request, response) key = request_key(request) || raise(ArgumentError, "Could not locate lookup key in request path [#{path(request)}]") args = params(request) - result = model.find(key, args).to_yaml - encode_result(request, response, result) + result = model.find(key, args) + + # 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) result = model.search(args).collect {|result| result.to_yaml }.to_yaml - encode_result(request, response, result) + + # LAK:FAIL This doesn't work. + format = format_to_use(request) + set_content_type(response, format) + + set_response(response, 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)}]") args = params(request) result = model.destroy(key, args) - encode_result(request, response, YAML.dump(result)) + + 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) - obj = model.from_yaml(data) - result = save_object(obj, args).to_yaml - encode_result(request, response, result) + + 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) @@ -59,7 +115,7 @@ module Puppet::Network::HTTP::Handler end def do_exception(request, response, exception, status=400) - encode_result(request, response, exception.to_s, status) + set_response(response, exception.to_s, status) end def find_model_for_handler(handler) @@ -79,14 +135,6 @@ module Puppet::Network::HTTP::Handler http_method(request) == 'DELETE' end - def singular?(request) - %r{/#{@handler.to_s}$}.match(path(request)) - end - - def plural?(request) - %r{/#{@handler.to_s}s$}.match(path(request)) - end - # methods to be overridden by the including web server class def register_handler @@ -112,8 +160,4 @@ module Puppet::Network::HTTP::Handler def params(request) raise NotImplementedError end - - def encode_result(request, response, result, status = 200) - raise NotImplementedError - end end diff --git a/spec/unit/network/http/handler.rb b/spec/unit/network/http/handler.rb new file mode 100755 index 000000000..36e566624 --- /dev/null +++ b/spec/unit/network/http/handler.rb @@ -0,0 +1,411 @@ +#!/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 + + 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 + end + + it "should fail to find model if 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 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 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 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(:search).returns(@result) + 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 |args| + args[:foo] == :baz and args[:bar] == :xyzzy + end.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 + pending "I have not figured out how to do this yet" + @result1.expects(:render).returns "result1" + @result2.expects(:render).returns "result2" + + @model_class.stubs(:search).returns(@result) + @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 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) + 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 |