6922ba6cb6
In this case statement, there are two patterns that start with the same line: when %r{/not_found} ... when %r{/not_found_original_exception} ... Because the string "/not_found_original_exception" does match the first one, it is never routed to what it is supposed to be routed, causing one of the tests for DebugExceptions to happen to be passing. After changing the regex expressions back to strings, I noticed that the test setup is not complete (the template object needs to be a proper template object). Once I fixed it all the tests started padding.
570 lines
19 KiB
Ruby
570 lines
19 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "abstract_unit"
|
|
|
|
class DebugExceptionsTest < ActionDispatch::IntegrationTest
|
|
InterceptedErrorInstance = StandardError.new
|
|
|
|
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 raise_nested_exceptions
|
|
raise "First error"
|
|
rescue
|
|
begin
|
|
raise "Second error"
|
|
rescue
|
|
raise "Third error"
|
|
end
|
|
end
|
|
|
|
def call(env)
|
|
env["action_dispatch.show_detailed_exceptions"] = @detailed
|
|
req = ActionDispatch::Request.new(env)
|
|
template = ActionView::Template.new(File.read(__FILE__), __FILE__, ActionView::Template::Handlers::Raw.new, {})
|
|
|
|
case req.path
|
|
when "/pass"
|
|
[404, { "X-Cascade" => "pass" }, self]
|
|
when "/not_found"
|
|
raise AbstractController::ActionNotFound
|
|
when "/runtime_error"
|
|
raise RuntimeError
|
|
when "/method_not_allowed"
|
|
raise ActionController::MethodNotAllowed
|
|
when "/intercepted_error"
|
|
raise InterceptedErrorInstance
|
|
when "/unknown_http_method"
|
|
raise ActionController::UnknownHttpMethod
|
|
when "/not_implemented"
|
|
raise ActionController::NotImplemented
|
|
when "/unprocessable_entity"
|
|
raise ActionController::InvalidAuthenticityToken
|
|
when "/not_found_original_exception"
|
|
begin
|
|
raise AbstractController::ActionNotFound.new
|
|
rescue
|
|
raise ActionView::Template::Error.new(template)
|
|
end
|
|
when "/missing_template"
|
|
raise ActionView::MissingTemplate.new(%w(foo), "foo/index", %w(foo), false, "mailer")
|
|
when "/bad_request"
|
|
raise ActionController::BadRequest
|
|
when "/missing_keys"
|
|
raise ActionController::UrlGenerationError, "No route matches"
|
|
when "/parameter_missing"
|
|
raise ActionController::ParameterMissing, :missing_param_key
|
|
when "/original_syntax_error"
|
|
eval "broke_syntax =" # `eval` need for raise native SyntaxError at runtime
|
|
when "/syntax_error_into_view"
|
|
begin
|
|
eval "broke_syntax ="
|
|
rescue Exception
|
|
raise ActionView::Template::Error.new(template)
|
|
end
|
|
when "/framework_raises"
|
|
method_that_raises
|
|
when "/nested_exceptions"
|
|
raise_nested_exceptions
|
|
else
|
|
raise "puke!"
|
|
end
|
|
end
|
|
end
|
|
|
|
Interceptor = proc { |request, exception| request.set_header("int", exception) }
|
|
BadInterceptor = proc { |request, exception| raise "bad" }
|
|
RoutesApp = Struct.new(:routes).new(SharedTestRoutes)
|
|
ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp)
|
|
DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp)
|
|
InterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [Interceptor])
|
|
BadInterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [BadInterceptor])
|
|
|
|
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
|
|
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
|
|
|
|
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 "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-0" do
|
|
assert_select "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-0"
|
|
assert_select "#Framework-Trace-0"
|
|
assert_select "#Full-Trace-0"
|
|
|
|
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-0" do
|
|
assert_select "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-0" do
|
|
assert_select "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-0" do
|
|
assert_select "code a:first", /method_that_raises/
|
|
end
|
|
end
|
|
end
|
|
|
|
test "invoke interceptors before rendering" do
|
|
@app = InterceptedApp
|
|
get "/intercepted_error", headers: { "action_dispatch.show_exceptions" => true }
|
|
|
|
assert_equal InterceptedErrorInstance, request.get_header("int")
|
|
end
|
|
|
|
test "bad interceptors doesn't debug exceptions" do
|
|
@app = BadInterceptedApp
|
|
|
|
get "/puke", headers: { "action_dispatch.show_exceptions" => true }
|
|
|
|
assert_response 500
|
|
assert_match(/puke/, body)
|
|
end
|
|
|
|
test "debug exceptions app shows all the nested exceptions in source view" do
|
|
@app = DevelopmentApp
|
|
Rails.stub :root, Pathname.new(".") do
|
|
cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
|
|
bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
|
|
end
|
|
|
|
get "/nested_exceptions", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
|
|
|
|
# Assert correct error
|
|
assert_response 500
|
|
assert_select "h2", /Third error/
|
|
|
|
# assert source view line shows the last error
|
|
assert_select "div.source:not(.hidden)" do
|
|
assert_select "pre .line.active", /raise "Third error"/
|
|
end
|
|
|
|
# assert application trace refers to line that raises the last exception
|
|
assert_select "#Application-Trace-0" do
|
|
assert_select "code a:first", %r{in `rescue in rescue in raise_nested_exceptions'}
|
|
end
|
|
|
|
# assert the second application trace refers to the line that raises the second exception
|
|
assert_select "#Application-Trace-1" do
|
|
assert_select "code a:first", %r{in `rescue in raise_nested_exceptions'}
|
|
end
|
|
|
|
# assert the third application trace refers to the line that raises the first exception
|
|
assert_select "#Application-Trace-2" do
|
|
assert_select "code a:first", %r{in `raise_nested_exceptions'}
|
|
end
|
|
end
|
|
end
|
|
end
|