summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/puppet/network/http/handler.rb90
-rwxr-xr-xspec/unit/network/http/handler.rb411
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