* Coverage added for StaticContentHandler.

* Add an accessor to StaticContentHandler -- possible use: dynamic static resource
  directory selection based on arbitrary conditionals!!! YEAH.  A new para-dig-im!
3 files changed, 135 insertions(+), 7 deletions(-)

M lib/thingfish/exceptions.rb
M lib/thingfish/handler/staticcontent.rb
M spec/staticcontent_spec.rb
M lib/thingfish/exceptions.rb +3 -1
@@ 43,7 43,9 @@ module ThingFish
 	class FileStoreQuotaError < ThingFish::FileStoreError; end
 
 	# Something was wrong with a request
-	class RequestError < ThingFish::Error; end
+	class RequestError < ThingFish::Error
+		
+   	end
 
 	# Something was wrong with a response
 	class ResponseError < ThingFish::Error; end

          
M lib/thingfish/handler/staticcontent.rb +13 -4
@@ 57,11 57,16 @@ class ThingFish::StaticContentHandler < 
 	def initialize( basedir )
 		@basedir = basedir.expand_path
 	end
-
+	
 
-	#########
-	protected
-	#########
+	######
+	public
+	######
+
+	# The directory this handler should use as its base
+	# :TODO: Document use-case of changing this on the fly based on Host: header.
+	attr_accessor :basedir
+	
 
 	### Handle a GET request
 	def handle_get_request( request, response )

          
@@ 129,6 134,10 @@ class ThingFish::StaticContentHandler < 
 	end
 
 
+	#########
+	protected
+	#########
+
 	### Given a +path+, try to map it to a file under the handler's base directory.  
 	### Return a Pathname object to the requested file if valid, +nil+ if not.
 	def get_safe_path( path )

          
M spec/staticcontent_spec.rb +119 -2
@@ 37,8 37,8 @@ include ThingFish::TestHelpers
 describe ThingFish::StaticContentHandler do
 
 	before( :each ) do
-		resdir = Pathname.new( __FILE__ ).expand_path.dirname.parent + 'var/www'
-		@handler = ThingFish::Handler.create( 'static', 'resource_dir' => resdir )
+		resdir = Pathname.new( __FILE__ ).expand_path.dirname.parent + '/tmp'
+		@handler = ThingFish::Handler.create( 'staticcontent', resdir )
 
 		@listener = mock( "thingfish daemon", :null_object => true )
 		@handler.listener = @listener

          
@@ 50,12 50,129 @@ describe ThingFish::StaticContentHandler
 
 		@request.stub!( :headers ).and_return( @request_headers )
 		@response.stub!( :headers ).and_return( @response_headers )
+
+		@pathname = mock( "Pathname", :null_object => true )
 	end	
 	
 	### Shared behaviors
 	it_should_behave_like "A Handler"
 
 
+	it "does nothing if the requested path doesn't exist" do
+		@handler.stub!( :get_safe_path ).and_return( @pathname )
+		@pathname.should_receive( :exist? ).and_return( false )
+
+		@response.should_not_receive( :status= )
+		@handler.handle_get_request( @request, @response )
+	end
+
+
+	it "refuses to serve directories" do
+		@handler.stub!( :get_safe_path ).and_return( @pathname )
+		@pathname.should_receive( :exist? ).and_return( true )
+		@pathname.should_receive( :file? ).and_return( false )
+
+		@response.should_receive( :status= ).with( HTTP::FORBIDDEN )
+		@handler.handle_get_request( @request, @response )
+	end
+
+
+	it "refuses to serve files outside of its resource directory" do
+		@request.should_receive( :path_info ).
+			and_return( '../../etc/shadow' )
+
+		@response.should_not_receive( :status= )
+		@handler.handle_get_request( @request, @response )
+	end
+
+
+	it "respects file access permissions" do
+		@handler.stub!( :get_safe_path ).and_return( @pathname )
+		@pathname.should_receive( :exist? ).and_return( true )
+		@pathname.should_receive( :file? ).and_return( true )
+		@pathname.should_receive( :readable? ).and_return( false )
+
+		@response.should_receive( :status= ).with( HTTP::FORBIDDEN )
+		@handler.handle_get_request( @request, @response )
+	end
+
+
+	it "sets the appropriate content type for the file" do
+		@handler.stub!( :get_safe_path ).and_return( @pathname )
+
+		@pathname.should_receive( :extname ).and_return( '.html' )
+		@response_headers.
+			should_receive( :[]= ).
+			with( :content_type, 'text/html' )
+
+		@pathname.should_receive( :size ).and_return( 7 )
+		@response_headers.
+			should_receive( :[]= ).
+			with( :content_length, 7 )
+
+		@response.should_receive( :status= ).with( HTTP::OK )
+
+		@pathname.should_receive( :open ).with('r').and_return( :an_IO )
+		@response.should_receive( :body= ).with( :an_IO )
+
+		@handler.handle_get_request( @request, @response )
+	end
+
+
+	it "defaults to a generic content type as fallback" do
+		@handler.stub!( :get_safe_path ).and_return( @pathname )
+
+		@pathname.should_receive( :extname ).and_return( '.floop' )
+		@response_headers.
+			should_receive( :[]= ).
+			with( :content_type, ThingFish::StaticContentHandler::DEFAULT_CONTENT_TYPE )
+
+		@pathname.should_receive( :size ).and_return( 7938844 )
+		@response_headers.
+			should_receive( :[]= ).
+			with( :content_length, 7938844 )
+
+		@response.should_receive( :status= ).with( HTTP::OK )
+
+		@pathname.should_receive( :open ).with('r').and_return( :an_IO )
+		@response.should_receive( :body= ).with( :an_IO )
+
+		@handler.handle_get_request( @request, @response )
+	end
+
+
+	INDEX_FILE = ThingFish::StaticContentHandler::DEFAULT_INDEX_FILE
+
+	it "serves index files" do
+		@request.should_receive( :path_info ).
+			and_return( 'this_is_a_directory_with_an_index_file' )
+		@handler.stub!( :get_safe_path ).and_return( @pathname )
+		
+		@pathname.should_receive( :directory? ).and_return( true )
+		@indexpath = mock( "index file Pathname object" )
+		@pathname.should_receive( :+ ).with( INDEX_FILE ).and_return( @indexpath )
+
+		@indexpath.should_receive( :exist? ).and_return( true )
+		@indexpath.should_receive( :file? ).and_return( true )
+		@indexpath.should_receive( :readable? ).and_return( true )
+
+		@indexpath.should_receive( :extname ).and_return( '.floop' )
+		@response_headers.
+			should_receive( :[]= ).
+			with( :content_type, ThingFish::StaticContentHandler::DEFAULT_CONTENT_TYPE )
+
+		@indexpath.should_receive( :size ).and_return( 7938844 )
+		@response_headers.
+			should_receive( :[]= ).
+			with( :content_length, 7938844 )
+
+		@response.should_receive( :status= ).with( HTTP::OK )
+
+		@indexpath.should_receive( :open ).with('r').and_return( :an_IO )
+		@response.should_receive( :body= ).with( :an_IO )
+
+		@handler.handle_get_request( @request, @response )
+	end
 end