require 'abstract_unit' class DebugExceptionsTest < ActionDispatch::IntegrationTest class Boomer attr_accessor :closed def initialize(detailed = false) @detailed = detailed @closed = false end # We're obliged to implement this (even though it doesn't actually # get called here) to properly comply with the Rack SPEC def each end def close @closed = true end def method_that_raises raise StandardError.new 'error in framework' end def call(env) env['action_dispatch.show_detailed_exceptions'] = @detailed req = ActionDispatch::Request.new(env) case req.path when %r{/pass} [404, { "X-Cascade" => "pass" }, self] when %r{/not_found} raise AbstractController::ActionNotFound when %r{/runtime_error} raise RuntimeError when %r{/method_not_allowed} raise ActionController::MethodNotAllowed when %r{/unknown_http_method} raise ActionController::UnknownHttpMethod when %r{/not_implemented} raise ActionController::NotImplemented when %r{/unprocessable_entity} raise ActionController::InvalidAuthenticityToken when %r{/not_found_original_exception} begin raise AbstractController::ActionNotFound.new rescue raise ActionView::Template::Error.new('template') end when %r{/missing_template} raise ActionView::MissingTemplate.new(%w(foo), 'foo/index', %w(foo), false, 'mailer') when %r{/bad_request} raise ActionController::BadRequest when %r{/missing_keys} raise ActionController::UrlGenerationError, "No route matches" when %r{/parameter_missing} raise ActionController::ParameterMissing, :missing_param_key when %r{/original_syntax_error} eval 'broke_syntax =' # `eval` need for raise native SyntaxError at runtime when %r{/syntax_error_into_view} begin eval 'broke_syntax =' rescue Exception template = ActionView::Template.new(File.read(__FILE__), __FILE__, ActionView::Template::Handlers::Raw.new, {}) raise ActionView::Template::Error.new(template) end when %r{/framework_raises} method_that_raises else raise "puke!" end end end RoutesApp = Struct.new(:routes).new(SharedTestRoutes) ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp) DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp) test 'skip diagnosis if not showing detailed exceptions' do @app = ProductionApp assert_raise RuntimeError do get "/", headers: { 'action_dispatch.show_exceptions' => true } end end test 'skip diagnosis if not showing exceptions' do @app = DevelopmentApp assert_raise RuntimeError do get "/", headers: { 'action_dispatch.show_exceptions' => false } end end test 'raise an exception on cascade pass' do @app = ProductionApp assert_raise ActionController::RoutingError do get "/pass", headers: { 'action_dispatch.show_exceptions' => true } end end test 'closes the response body on cascade pass' do boomer = Boomer.new(false) @app = ActionDispatch::DebugExceptions.new(boomer) assert_raise ActionController::RoutingError do get "/pass", headers: { 'action_dispatch.show_exceptions' => true } end assert boomer.closed, "Expected to close the response body" end test 'displays routes in a table when a RoutingError occurs' do @app = DevelopmentApp get "/pass", headers: { 'action_dispatch.show_exceptions' => true } routing_table = body[/route_table.*<.table>/m] assert_match '/:controller(/:action)(.:format)', routing_table assert_match ':controller#:action', routing_table assert_no_match '<|>', routing_table, "there should not be escaped html in the output" end test 'displays request and response info when a RoutingError occurs' do @app = DevelopmentApp get "/pass", headers: { 'action_dispatch.show_exceptions' => true } assert_select 'h2', /Request/ assert_select 'h2', /Response/ end test "rescue with diagnostics message" do @app = DevelopmentApp get "/", headers: { 'action_dispatch.show_exceptions' => true } assert_response 500 assert_match(/puke/, body) get "/not_found", headers: { 'action_dispatch.show_exceptions' => true } assert_response 404 assert_match(/#{AbstractController::ActionNotFound.name}/, body) get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true } assert_response 405 assert_match(/ActionController::MethodNotAllowed/, body) get "/unknown_http_method", headers: { 'action_dispatch.show_exceptions' => true } assert_response 405 assert_match(/ActionController::UnknownHttpMethod/, body) get "/bad_request", headers: { 'action_dispatch.show_exceptions' => true } assert_response 400 assert_match(/ActionController::BadRequest/, body) get "/parameter_missing", headers: { 'action_dispatch.show_exceptions' => true } assert_response 400 assert_match(/ActionController::ParameterMissing/, body) end test "rescue with text error for xhr request" do @app = DevelopmentApp xhr_request_env = {'action_dispatch.show_exceptions' => true, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'} get "/", headers: xhr_request_env assert_response 500 assert_no_match(/
/, body) assert_no_match(//, body) assert_equal "text/plain", response.content_type assert_match(/RuntimeError\npuke/, body) Rails.stub :root, Pathname.new('.') do get "/", headers: xhr_request_env assert_response 500 assert_match 'Extracted source (around line #', body assert_select 'pre', { count: 0 }, body end get "/not_found", headers: xhr_request_env assert_response 404 assert_no_match(//, body) assert_equal "text/plain", response.content_type assert_match(/#{AbstractController::ActionNotFound.name}/, body) get "/method_not_allowed", headers: xhr_request_env assert_response 405 assert_no_match(//, body) assert_equal "text/plain", response.content_type assert_match(/ActionController::MethodNotAllowed/, body) get "/unknown_http_method", headers: xhr_request_env assert_response 405 assert_no_match(//, body) assert_equal "text/plain", response.content_type assert_match(/ActionController::UnknownHttpMethod/, body) get "/bad_request", headers: xhr_request_env assert_response 400 assert_no_match(//, body) assert_equal "text/plain", response.content_type assert_match(/ActionController::BadRequest/, body) get "/parameter_missing", headers: xhr_request_env assert_response 400 assert_no_match(//, body) assert_equal "text/plain", response.content_type assert_match(/ActionController::ParameterMissing/, body) end test "rescue with JSON error for JSON API request" do @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) get "/", headers: { 'action_dispatch.show_exceptions' => true }, as: :json assert_response 500 assert_no_match(/
/, body) assert_no_match(//, body) assert_equal "application/json", response.content_type assert_match(/RuntimeError: puke/, body) get "/not_found", headers: { 'action_dispatch.show_exceptions' => true }, as: :json assert_response 404 assert_no_match(//, body) assert_equal "application/json", response.content_type assert_match(/#{AbstractController::ActionNotFound.name}/, body) get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true }, as: :json assert_response 405 assert_no_match(//, body) assert_equal "application/json", response.content_type assert_match(/ActionController::MethodNotAllowed/, body) get "/unknown_http_method", headers: { 'action_dispatch.show_exceptions' => true }, as: :json assert_response 405 assert_no_match(//, body) assert_equal "application/json", response.content_type assert_match(/ActionController::UnknownHttpMethod/, body) get "/bad_request", headers: { 'action_dispatch.show_exceptions' => true }, as: :json assert_response 400 assert_no_match(//, body) assert_equal "application/json", response.content_type assert_match(/ActionController::BadRequest/, body) get "/parameter_missing", headers: { 'action_dispatch.show_exceptions' => true }, as: :json assert_response 400 assert_no_match(//, body) assert_equal "application/json", response.content_type assert_match(/ActionController::ParameterMissing/, body) end test "rescue with HTML format for HTML API request" do @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) get "/index.html", headers: { 'action_dispatch.show_exceptions' => true } assert_response 500 assert_match(/
/, body) assert_match(//, body) assert_equal "text/html", response.content_type assert_match(/puke/, body) end test "rescue with XML format for XML API requests" do @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) get "/index.xml", headers: { 'action_dispatch.show_exceptions' => true } assert_response 500 assert_equal "application/xml", response.content_type assert_match(/RuntimeError: puke/, body) end test "rescue with JSON format as fallback if API request format is not supported" do begin Mime::Type.register 'text/wibble', :wibble ActionDispatch::IntegrationTest.register_encoder(:wibble, param_encoder: -> params { params }) @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) get "/index", headers: { 'action_dispatch.show_exceptions' => true }, as: :wibble assert_response 500 assert_equal "application/json", response.content_type assert_match(/RuntimeError: puke/, body) ensure Mime::Type.unregister :wibble end end test "does not show filtered parameters" do @app = DevelopmentApp get "/", params: { "foo"=>"bar" }, headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.parameter_filter' => [:foo] } assert_response 500 assert_match(""foo"=>"[FILTERED]"", body) end test "show registered original exception for wrapped exceptions" do @app = DevelopmentApp get "/not_found_original_exception", headers: { 'action_dispatch.show_exceptions' => true } assert_response 404 assert_match(/AbstractController::ActionNotFound/, body) end test "named urls missing keys raise 500 level error" do @app = DevelopmentApp get "/missing_keys", headers: { 'action_dispatch.show_exceptions' => true } assert_response 500 assert_match(/ActionController::UrlGenerationError/, body) end test "show the controller name in the diagnostics template when controller name is present" do @app = DevelopmentApp get("/runtime_error", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.request.parameters' => { 'action' => 'show', 'id' => 'unknown', 'controller' => 'featured_tile' } }) assert_response 500 assert_match(/RuntimeError\n\s+in FeaturedTileController/, body) end test "show formatted params" do @app = DevelopmentApp params = { 'id' => 'unknown', 'someparam' => { 'foo' => 'bar', 'abc' => 'goo' } } get("/runtime_error", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.request.parameters' => { 'action' => 'show', 'controller' => 'featured_tile' }.merge(params) }) assert_response 500 assert_includes(body, CGI.escapeHTML(PP.pp(params, "", 200))) end test "sets the HTTP charset parameter" do @app = DevelopmentApp get "/", headers: { 'action_dispatch.show_exceptions' => true } assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] end test 'uses logger from env' do @app = DevelopmentApp output = StringIO.new get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => Logger.new(output) } assert_match(/puke/, output.rewind && output.read) end test 'logs only what is necessary' do @app = DevelopmentApp io = StringIO.new logger = ActiveSupport::Logger.new(io) _old, ActionView::Base.logger = ActionView::Base.logger, logger begin get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => logger } ensure ActionView::Base.logger = _old end output = io.rewind && io.read lines = output.lines # Other than the first three... assert_equal([" \n", "RuntimeError (puke!):\n", " \n"], lines.slice!(0, 3)) lines.each do |line| # .. all the remaining lines should be from the backtrace assert_match(/:\d+:in /, line) end end test 'uses backtrace cleaner from env' do @app = DevelopmentApp backtrace_cleaner = ActiveSupport::BacktraceCleaner.new backtrace_cleaner.stub :clean, ['passed backtrace cleaner'] do get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.backtrace_cleaner' => backtrace_cleaner } assert_match(/passed backtrace cleaner/, body) end end test 'logs exception backtrace when all lines silenced' do output = StringIO.new backtrace_cleaner = ActiveSupport::BacktraceCleaner.new backtrace_cleaner.add_silencer { true } env = {'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => Logger.new(output), 'action_dispatch.backtrace_cleaner' => backtrace_cleaner} get "/", headers: env assert_operator((output.rewind && output.read).lines.count, :>, 10) end test 'display backtrace when error type is SyntaxError' do @app = DevelopmentApp get '/original_syntax_error', headers: { 'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new } assert_response 500 assert_select '#Application-Trace' do assert_select 'pre code', /syntax error, unexpected/ end end test 'display backtrace on template missing errors' do @app = DevelopmentApp get "/missing_template" assert_select "header h1", /Template is missing/ assert_select "#container h2", /^Missing template/ assert_select '#Application-Trace' assert_select '#Framework-Trace' assert_select '#Full-Trace' assert_select 'h2', /Request/ end test 'display backtrace when error type is SyntaxError wrapped by ActionView::Template::Error' do @app = DevelopmentApp get '/syntax_error_into_view', headers: { 'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new } assert_response 500 assert_select '#Application-Trace' do assert_select 'pre code', /syntax error, unexpected/ end end test 'debug exceptions app shows user code that caused the error in source view' do @app = DevelopmentApp Rails.stub :root, Pathname.new('.') do cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc| bc.add_silencer { |line| line =~ /method_that_raises/ } bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} } end get '/framework_raises', headers: { 'action_dispatch.backtrace_cleaner' => cleaner } # Assert correct error assert_response 500 assert_select 'h2', /error in framework/ # assert source view line is the call to method_that_raises assert_select 'div.source:not(.hidden)' do assert_select 'pre .line.active', /method_that_raises/ end # assert first source view (hidden) that throws the error assert_select 'div.source:first' do assert_select 'pre .line.active', /raise StandardError\.new/ end # assert application trace refers to line that calls method_that_raises is first assert_select '#Application-Trace' do assert_select 'pre code a:first', %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call} end # assert framework trace that threw the error is first assert_select '#Framework-Trace' do assert_select 'pre code a:first', /method_that_raises/ end end end end