module Html module Test class InvalidUrl < RuntimeError; end class UrlChecker attr_accessor :request, :response, :params include Html::Test::UrlSelector def initialize(controller) self.request = controller.request self.response = controller.response self.params = controller.params end def check_urls_resolve urls_to_check.each do |url| check_url_resolves(url) end end def check_redirects_resolve if response.headers['Status'] =~ /302/ if response.redirected_to.is_a?(Hash) # redirected_to={:action=>"foobar"} url = url_from_params(response.redirected_to) else # redirected_to="http://test.host/foobar" url = response.redirected_to[%r{test.host(.+)$}, 1] end check_url_resolves(url) end end protected def urls_to_check anchor_urls + image_urls + form_urls end def check_url_resolves(url) return if url.blank? or skip_url?(url) or external_http?(url) url = strip_anchor(remove_query(make_absolute(url))) return if public_file_exists?(url) check_action_exists(url) end def public_file_exists?(url) public_path = File.join(rails_public_path, url) File.exists?(public_path) || File.exists?(public_path + ".html") end def rails_public_path File.join(RAILS_ROOT, "public") end # Make relative URLs absolute, i.e. relative to the site root def make_absolute(url) return url if url =~ %r{^/} current_url = request.request_uri || url_from_params current_url = File.dirname(current_url) if current_url !~ %r{/$} url = File.join(current_url, url) end def remove_query(url) url =~ /\?/ ? url[/^(.+?)\?/, 1] : url end def strip_anchor(url) url =~ /\#/ ? url[/^(.+?)\#/, 1] : url end def check_action_exists(url) route = route_from_url(url) controller = "#{route[:controller].camelize}Controller".constantize if !controller.public_instance_methods.include?(route[:action]) && !template_file_exists?(route, controller) raise Html::Test::InvalidUrl.new("No action or template for url '#{url}' body=#{response.body}") end end def template_file_exists?(route, controller) # Workaround for Rails 1.2 that doesn't have the view_paths method template_dirs = controller.respond_to?(:view_paths) ? controller.view_paths : [controller.view_root] template_dirs.each do |template_dir| template_file = File.join(template_dir, controller.controller_path, "#{route[:action]}.*") return true if !Dir.glob(template_file).empty? end false end # This is a special case where on my site I had a catch all route for 404s. If you have # such a route, you can override this method and check for it, i.e. you could do something # like this: # # if params[:action] == "rescue_404" # raise Html::Test::InvalidUrl.new("Action rescue_404 invoked for url '#{url}'") # end def check_not_404(url, params) # This method is unimplemented by default end def route_from_url(url) [:get, :post, :put, :delete].each do |method| begin # Need to specify the method here for RESTful resource routes to work, i.e. # for /posts/1 to be recognized as the show action etc. params = ::ActionController::Routing::Routes.recognize_path(url, {:method => method}) check_not_404(url, params) return params rescue # Could not find a route with that method, let's try the next method in the list. # Some routes may have stuff like :conditions => {:method => :post} end end raise Html::Test::InvalidUrl.new("Cannot find a route for url '#{url}'") end def url_from_params(options = params) return "/" if params.empty? options[:controller] ||= params[:controller] ::ActionController::Routing::Routes.generate_extras(symbolize_hash(options))[0] end # Convert all keys in the hash to symbols. Not sure why this is needed. def symbolize_hash(hash) hash.keys.inject({}) { |h, k| h[k.to_sym] = hash[k]; h } end end end end