378b4fedb1
- https://github.com/rails/rails/pull/35604 introduced a vulnerability fix to raise an error in case the `HTTP_ACCEPT` headers contains malformated mime type. This will cause applications to throw a 500 if a User Agent sends an invalid header. This PR adds the `InvalidMimeType` in the default `rescue_responses` from the ExceptionWrapper and will return a 406. I looked up the HTTP/1.1 RFC and it doesn't stand what should be returned when the UA sends malformated mime type. Decided to get 406 as it seemed to be the status the better suited for this.
593 lines
20 KiB
Ruby
593 lines
20 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, format: :html, locals: [])
|
|
|
|
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 "/invalid_mimetype"
|
|
raise Mime::Type::InvalidMimeType
|
|
when "/not_found_original_exception"
|
|
begin
|
|
raise AbstractController::ActionNotFound.new
|
|
rescue
|
|
raise ActionView::Template::Error.new(template)
|
|
end
|
|
when "/cause_mapped_to_rescue_responses"
|
|
begin
|
|
raise ActionController::ParameterMissing, :missing_param_key
|
|
rescue
|
|
raise NameError.new("uninitialized constant Userr")
|
|
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)
|
|
|
|
get "/invalid_mimetype", headers: { "Accept" => "text/html,*", "action_dispatch.show_exceptions" => true }
|
|
assert_response 406
|
|
assert_match(/Mime::Type::InvalidMimeType/, 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 if the last exception is TemplateError" do
|
|
@app = DevelopmentApp
|
|
|
|
get "/not_found_original_exception", headers: { "action_dispatch.show_exceptions" => true }
|
|
assert_response 404
|
|
assert_match %r{AbstractController::ActionNotFound}, body
|
|
assert_match %r{Showing <i>.*test/dispatch/debug_exceptions_test.rb</i>}, body
|
|
end
|
|
|
|
test "show the last exception and cause even when the cause is mapped to resque_responses" do
|
|
@app = DevelopmentApp
|
|
|
|
get "/cause_mapped_to_rescue_responses", headers: { "action_dispatch.show_exceptions" => true }
|
|
assert_response 500
|
|
assert_match %r{ActionController::ParameterMissing}, body
|
|
assert_match %r{NameError}, 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
|
|
assert_match %r{Showing <i>.*test/dispatch/debug_exceptions_test.rb</i>}, body
|
|
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
|