diff options
Diffstat (limited to 'actionpack/test/dispatch/debug_exceptions_test.rb')
-rw-r--r-- | actionpack/test/dispatch/debug_exceptions_test.rb | 502 |
1 files changed, 502 insertions, 0 deletions
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb new file mode 100644 index 0000000000..60acba0616 --- /dev/null +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -0,0 +1,502 @@ +# frozen_string_literal: true + +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(/<header>/, body) + assert_no_match(/<body>/, 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>/, 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>/, 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>/, 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>/, 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>/, 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(/<header>/, body) + assert_no_match(/<body>/, 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>/, 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>/, 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>/, 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>/, 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>/, 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(/<header>/, body) + assert_match(/<body>/, 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, "".dup, 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 "logs with non active support loggers" do + @app = DevelopmentApp + io = StringIO.new + logger = Logger.new(io) + + _old, ActionView::Base.logger = ActionView::Base.logger, logger + begin + assert_nothing_raised do + get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => logger } + end + ensure + ActionView::Base.logger = _old + end + + assert_match(/puke/, io.rewind && io.read) + 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 |