require 'rubygems' require 'test/unit' require 'file_permissions' require 'action_controller' require 'action_controller/dispatcher' # Include mongrel stuff if you have mongrel gem installed. begin require 'mongrel' require 'file_permissions/mongrel' rescue end RAILS_ROOT = Dir.pwd FP = FilePermissions PUBLIC_JPG = "#{RAILS_ROOT}/public/public.jpg" ################################################################################ # Minimal implementation of request object for testing Rails dispatcher. class TestRequest < ActionController::AbstractRequest attr_accessor :body attr_accessor :query_parameters attr_accessor :request_parameters attr_accessor :cookies attr_accessor :session attr_accessor :session_options def initialize(env) self.query_parameters = env[:params] || {} self.request_parameters = env[:params] || {} self.cookies = env[:cookies] || {} self.session = env[:session] || {} env['REQUEST_METHOD'] ||= 'GET' @env = env end end ################################################################################ # Minimal implementation of response object for testing Rails dispatcher. class TestResponse < ActionController::AbstractResponse def out(val); end end ################################################################################ # Minimal controller that everything gets routed to. class TestController < ActionController::Base before_filter :file_permissions # Register all JPEGs, giving permission only to public.jpg. Let # file_permissions do default rendering based on return value. def self.setup_test1 FP.clear FP.register(/(\w+).jpg$/) { |c,f,m| m[1] == 'public' } end # Register all JPEGs, giving permission only to public.jpg, again. This time # do the rendering ourselves. Pass the optional args along to render_file(). def self.setup_test2(args={}) FP.clear FP.register(/(\w+).jpg$/) do |c,f,m| if m[1] == 'public' c.render_file(f, args) else c.render(:inline => 'Keep out, no Gurls.') end end end end ################################################################################ class FilePermissionsTest < Test::Unit::TestCase # This helper sets up a basic request, and tells Rails to dispatch it. # It returns the response object. def make_request(url, args={}) args = args.clone.merge({'REQUEST_URI' => url}) request = TestRequest.new(args) response = TestResponse.new() ActionController::Dispatcher.new($stdout, request, response).dispatch return response end # Test internal workings of FilePermissions.register method. def test_register FP.clear FP.register('/testone') {|c,f| "testone: #{f}"} FP.register('/testtwo') {|c,f| "testtwo: #{f}"} FP.register(/testthree/) {|c,f| "testthree: #{f}"} FP.register(/testfour/) {|c,f| "testfour: #{f}"} assert_equal(["#{RAILS_ROOT}/testone", "#{RAILS_ROOT}/testtwo"], FP.dirs.keys.sort) assert_equal([/testfour/, /testthree/], FP.regexen.keys.sort {|x,y| x.to_s <=> y.to_s}) assert_equal('testone: foo', FP.dirs["#{RAILS_ROOT}/testone"].call(0,'foo')) assert_equal('testtwo: bar', FP.dirs["#{RAILS_ROOT}/testtwo"].call(0,'bar')) assert_equal('testthree: tweedle', FP.regexen[/testthree/].call(0,'tweedle',1)) assert_equal('testfour: dum', FP.regexen[/testfour/].call(0,'dum',1)) assert(FP.pattern.match("#{RAILS_ROOT}/testone/image.jpg")) assert(FP.pattern.match("#{RAILS_ROOT}/testtwo/image.jpg")) assert(FP.pattern.match("#{RAILS_ROOT}/anything-with-testthree-in-it")) assert(FP.pattern.match("#{RAILS_ROOT}/anything-with-testfour-in-it")) assert(!FP.pattern.match("#{RAILS_ROOT}/subdir/testone/image.jpg")) assert(!FP.pattern.match("#{RAILS_ROOT}/blahdeblah.pdf")) end # Test a basic request for some files. def test_request # Allow browser to see public.jpg but not private.jpg. TestController.setup_test1 # Make sure we serve public.jpg. response = make_request('/public.jpg') assert_equal(200, response.headers['Status'].to_i) # Make sure we forbid private.jpg. response = make_request('/private.jpg') assert_equal(403, response.headers['Status'].to_i) # Same files, but do customized rendering. TestController.setup_test2( :private => true, :max_age => 0, :must_revalidate => true ) # This time check everything about response. response = make_request('/public.jpg') assert_equal(200, response.headers['Status'].to_i) assert_equal('max-age=0, must-revalidate, private', response.headers['Cache-Control']) assert_equal('image/jpeg', response.content_type) assert_equal( File.new(PUBLIC_JPG, 'rb').read, response.body ) # Likewise, check everything about "no access" response. response = make_request('/private.jpg') assert_equal(200, response.headers['Status'].to_i) assert_equal('text/html', response.content_type) assert_equal('Keep out, no Gurls.', response.body) end # Test cache logic. def test_cache TestController.setup_test1 mtime = File.stat(PUBLIC_JPG).mtime # Should let browser use cache since public.jpg has not been modified # since now(!) (status 304 = no change) response = make_request('/public.jpg', 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate) assert_equal(304, response.headers['Status'].to_i) assert_equal(mtime.httpdate, response.headers['Last-Modified']) assert_equal(mtime.to_i.to_s, response.headers['ETag']) # Should not let browser use cache since public.jpg has been modified # one hour after the date the browser gives us. Which means we serve # the file (status 200). response = make_request('/public.jpg', 'HTTP_IF_MODIFIED_SINCE' => (mtime - 1.hour).httpdate) assert_equal(200, response.headers['Status'].to_i) assert_equal(mtime.httpdate, response.headers['Last-Modified']) assert_equal(mtime.to_i.to_s, response.headers['ETag']) # Same thing using etag: etag we use is just file's age in seconds. # First check that it gives "no change" if browser has correct etag. response = make_request('/public.jpg', 'HTTP_IF_NONE_MATCH' => mtime.to_i.to_s) assert_equal(304, response.headers['Status'].to_i) assert_equal(mtime.httpdate, response.headers['Last-Modified']) assert_equal(mtime.to_i.to_s, response.headers['ETag']) # Now check that it serves file if etag is different. response = make_request('/public.jpg', 'HTTP_IF_NONE_MATCH' => (mtime - 1.hour).to_i.to_s) assert_equal(200, response.headers['Status'].to_i) assert_equal(mtime.httpdate, response.headers['Last-Modified']) assert_equal(mtime.to_i.to_s, response.headers['ETag']) end # Test hack to Mongrel's DirHandler.can_serve method. if Object.const_defined?(:Mongrel) def test_mongrel dir_handler = Mongrel::DirHandler.new('public') FP.clear FP.register('/public/private') do |c,f| end assert(!dir_handler.can_serve('/private.jpg')) assert(dir_handler.can_serve('/rails.png')) FP.clear FP.register(/(\w+).jpg$/) do |c,f,m| end assert(!dir_handler.can_serve('/public.jpg')) assert(!dir_handler.can_serve('/private.jpg')) assert(dir_handler.can_serve('/rails.png')) end end end