Testing BetterErrors Middleware Implementation in BetterErrors
This test suite evaluates the BetterErrors middleware functionality, covering error handling, CSRF protection, and request processing. It verifies middleware behavior for both normal requests and error scenarios, ensuring proper security measures and response handling.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
bettererrors/better_errors
spec/better_errors/middleware_spec.rb
require "spec_helper"
module BetterErrors
describe Middleware do
let(:app) { Middleware.new(->env { ":)" }) }
let(:exception) { RuntimeError.new("oh no :(") }
let(:status) { response_env[0] }
let(:headers) { response_env[1] }
let(:body) { response_env[2].join }
context 'when the application raises no exception' do
it "passes non-error responses through" do
expect(app.call({})).to eq(":)")
end
end
it "calls the internal methods" do
expect(app).to receive :internal_call
app.call("PATH_INFO" => "/__better_errors/1/preform_awesomness")
end
it "calls the internal methods on any subfolder path" do
expect(app).to receive :internal_call
app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/1/preform_awesomness")
end
it "shows the error page" do
expect(app).to receive :show_error_page
app.call("PATH_INFO" => "/__better_errors/")
end
it "doesn't show the error page to a non-local address" do
expect(app).not_to receive :better_errors_call
app.call("REMOTE_ADDR" => "1.2.3.4")
end
it "shows to a whitelisted IP" do
BetterErrors::Middleware.allow_ip! '77.55.33.11'
expect(app).to receive :better_errors_call
app.call("REMOTE_ADDR" => "77.55.33.11")
end
it "shows to a whitelisted IPAddr" do
BetterErrors::Middleware.allow_ip! IPAddr.new('77.55.33.0/24')
expect(app).to receive :better_errors_call
app.call("REMOTE_ADDR" => "77.55.33.11")
end
it "respects the X-Forwarded-For header" do
expect(app).not_to receive :better_errors_call
app.call(
"REMOTE_ADDR" => "127.0.0.1",
"HTTP_X_FORWARDED_FOR" => "1.2.3.4",
)
end
it "doesn't blow up when given a blank REMOTE_ADDR" do
expect { app.call("REMOTE_ADDR" => " ") }.to_not raise_error
end
it "doesn't blow up when given an IP address with a zone index" do
expect { app.call("REMOTE_ADDR" => "0:0:0:0:0:0:0:1%0" ) }.to_not raise_error
end
context "when /__better_errors is requested directly" do
let(:response_env) { app.call("PATH_INFO" => "/__better_errors") }
context "when no error has been recorded since startup" do
it "shows that no errors have been recorded" do
expect(body).to match /No errors have been recorded yet./
end
it 'does not attempt to use ActionDispatch::ExceptionWrapper on the nil exception' do
ad_ew = double("ActionDispatch::ExceptionWrapper")
stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
expect(ad_ew).to_not receive :new
response_env
end
context 'when requested inside a subfolder path' do
let(:response_env) { app.call("PATH_INFO" => "/any_sub/folder/__better_errors") }
it "shows that no errors have been recorded" do
expect(body).to match /No errors have been recorded yet./
end
end
end
context 'when an error has been recorded' do
let(:app) {
Middleware.new(->env do
# Only raise on the first request
raise exception unless @already_raised
@already_raised = true
end)
}
before do
app.call({})
end
it 'returns the information of the most recent error' do
expect(body).to include("oh no :(")
end
it 'does not attempt to use ActionDispatch::ExceptionWrapper' do
ad_ew = double("ActionDispatch::ExceptionWrapper")
stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
expect(ad_ew).to_not receive :new
response_env
end
context 'when inside a subfolder path' do
let(:response_env) { app.call("PATH_INFO" => "/any_sub/folder/__better_errors") }
it "shows the error page on any subfolder path" do
expect(app).to receive :show_error_page
app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/")
end
end
end
end
context "when handling an error" do
let(:app) { Middleware.new(->env { raise exception }) }
let(:response_env) { app.call({}) }
it "returns status 500" do
expect(status).to eq(500)
end
context "when the exception has a cause" do
before do
pending "This Ruby does not support `cause`" unless Exception.new.respond_to?(:cause)
end
let(:app) {
Middleware.new(->env {
begin
raise "First Exception"
rescue
raise "Second Exception"
end
})
}
it "shows the exception as-is" do
expect(status).to eq(500)
expect(body).to match(/\nSecond Exception\n/)
expect(body).not_to match(/\nFirst Exception\n/)
end
end
context "when the exception responds to #original_exception" do
class OriginalExceptionException < Exception
attr_reader :original_exception
def initialize(message, original_exception = nil)
super(message)
@original_exception = original_exception
end
end
context 'and has one' do
let(:app) {
Middleware.new(->env {
raise OriginalExceptionException.new("Second Exception", Exception.new("First Exception"))
})
}
it "shows the original exception instead of the last-raised one" do
expect(status).to eq(500)
expect(body).not_to match(/Second Exception/)
expect(body).to match(/First Exception/)
end
end
context 'and does not have one' do
let(:app) {
Middleware.new(->env {
raise OriginalExceptionException.new("The Exception")
})
}
it "shows the exception as-is" do
expect(status).to eq(500)
expect(body).to match(/The Exception/)
end
end
end
it "returns ExceptionWrapper's status_code" do
ad_ew = double("ActionDispatch::ExceptionWrapper")
allow(ad_ew).to receive('new').with(anything, exception) { double("ExceptionWrapper", status_code: 404) }
stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
expect(status).to eq(404)
end
it "returns UTF-8 error pages" do
expect(headers["Content-Type"]).to match /charset=utf-8/
end
it "returns text content by default" do
expect(headers["Content-Type"]).to match /text\/plain/
end
context 'when a CSRF token cookie is not specified' do
it 'includes a newly-generated CSRF token cookie' do
expect(headers).to include(
'Set-Cookie' => /BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=[-a-z0-9]+; path=\/; HttpOnly; SameSite=Strict/,
)
end
end
context 'when a CSRF token cookie is specified' do
let(:response_env) { app.call({ 'HTTP_COOKIE' => "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=abc123" }) }
it 'does not set a new CSRF token cookie' do
expect(headers).not_to include('Set-Cookie')
end
end
context 'when the Accept header specifies HTML first' do
let(:response_env) { app.call("HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*") }
it "returns HTML content" do
expect(headers["Content-Type"]).to match /text\/html/
end
it 'includes the newly-generated CSRF token in the body of the page' do
matches = headers['Set-Cookie'].match(/BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=(?<tok>[-a-z0-9]+); path=\/; HttpOnly; SameSite=Strict/)
expect(body).to include(matches[:tok])
end
context 'when a CSRF token cookie is specified' do
let(:response_env) {
app.call({
'HTTP_COOKIE' => "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=csrfTokenGHI",
"HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*",
})
}
it 'includes that CSRF token in the body of the page' do
expect(body).to include('csrfTokenGHI')
end
end
end
context 'the logger' do
let(:logger) { double('logger', fatal: nil) }
before do
allow(BetterErrors).to receive(:logger).and_return(logger)
end
it "receives the exception as a fatal message" do
expect(logger).to receive(:fatal).with(/RuntimeError/)
response_env
end
context 'when Rails is being used' do
before do
skip("Rails not included in this run") unless defined? Rails
end
it "receives the exception without filtered backtrace frames" do
expect(logger).to receive(:fatal) do |message|
expect(message).to_not match(/rspec-core/)
end
response_env
end
end
context 'when Rails is not being used' do
before do
skip("Rails is included in this run") if defined? Rails
end
it "receives the exception with all backtrace frames" do
expect(logger).to receive(:fatal) do |message|
expect(message).to match(/rspec-core/)
end
response_env
end
end
end
end
context "requesting the variables for a specific frame" do
let(:env) { {} }
let(:response_env) {
app.call(request_env)
}
let(:request_env) {
Rack::MockRequest.env_for("/__better_errors/#{id}/variables", input: StringIO.new(JSON.dump(request_body_data)))
}
let(:request_body_data) { { "index" => 0 } }
let(:json_body) { JSON.parse(body) }
let(:id) { 'abcdefg' }
context 'when no errors have been recorded' do
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'No exception information available',
'explanation' => /application has been restarted/,
)
end
context 'when Middleman is in use' do
let!(:middleman) { class_double("Middleman").as_stubbed_const }
it 'returns a JSON error' do
expect(json_body['explanation'])
.to match(/Middleman reloads all dependencies/)
end
end
context 'when Shotgun is in use' do
let!(:shotgun) { class_double("Shotgun").as_stubbed_const }
it 'returns a JSON error' do
expect(json_body['explanation'])
.to match(/The shotgun gem/)
end
context 'when Hanami is also in use' do
let!(:hanami) { class_double("Hanami").as_stubbed_const }
it 'returns a JSON error' do
expect(json_body['explanation'])
.to match(/--no-code-reloading/)
end
end
end
end
context 'when an error has been recorded' do
let(:error_page) { ErrorPage.new(exception, env) }
before do
app.instance_variable_set('@error_page', error_page)
end
context 'but it does not match the request' do
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'Session expired',
'explanation' => /no longer available in memory/,
)
end
end
context 'and its ID matches the requested ID' do
let(:id) { error_page.id }
context 'when the body csrfToken matches the CSRF token cookie' do
let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
before do
request_env["HTTP_COOKIE"] = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=csrfToken123"
end
context 'when the Content-Type of the request is application/json' do
before do
request_env['CONTENT_TYPE'] = 'application/json'
end
it 'returns JSON containing the HTML content' do
expect(error_page).to receive(:do_variables).and_return(html: "<content>")
expect(json_body).to match(
'html' => '<content>',
)
end
end
context 'when the Content-Type of the request is application/json' do
before do
request_env['HTTP_CONTENT_TYPE'] = 'application/json'
end
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'Request not acceptable',
'explanation' => /did not match an acceptable content type/,
)
end
end
end
context 'when the body csrfToken does not match the CSRF token cookie' do
let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
before do
request_env["HTTP_COOKIE"] = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=csrfToken456"
end
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'Invalid CSRF Token',
'explanation' => /session might have been cleared/,
)
end
end
context 'when there is no CSRF token in the request' do
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'Invalid CSRF Token',
'explanation' => /session might have been cleared/,
)
end
end
end
end
end
context "requesting eval for a specific frame" do
let(:env) { {} }
let(:response_env) {
app.call(request_env)
}
let(:request_env) {
Rack::MockRequest.env_for("/__better_errors/#{id}/eval", input: StringIO.new(JSON.dump(request_body_data)))
}
let(:request_body_data) { { "index" => 0, source: "do_a_thing" } }
let(:json_body) { JSON.parse(body) }
let(:id) { 'abcdefg' }
context 'when no errors have been recorded' do
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'No exception information available',
'explanation' => /application has been restarted/,
)
end
context 'when Middleman is in use' do
let!(:middleman) { class_double("Middleman").as_stubbed_const }
it 'returns a JSON error' do
expect(json_body['explanation'])
.to match(/Middleman reloads all dependencies/)
end
end
context 'when Shotgun is in use' do
let!(:shotgun) { class_double("Shotgun").as_stubbed_const }
it 'returns a JSON error' do
expect(json_body['explanation'])
.to match(/The shotgun gem/)
end
context 'when Hanami is also in use' do
let!(:hanami) { class_double("Hanami").as_stubbed_const }
it 'returns a JSON error' do
expect(json_body['explanation'])
.to match(/--no-code-reloading/)
end
end
end
end
context 'when an error has been recorded' do
let(:error_page) { ErrorPage.new(exception, env) }
before do
app.instance_variable_set('@error_page', error_page)
end
context 'but it does not match the request' do
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'Session expired',
'explanation' => /no longer available in memory/,
)
end
end
context 'and its ID matches the requested ID' do
let(:id) { error_page.id }
context 'when the body csrfToken matches the CSRF token cookie' do
let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
before do
request_env["HTTP_COOKIE"] = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=csrfToken123"
end
context 'when the Content-Type of the request is application/json' do
before do
request_env['CONTENT_TYPE'] = 'application/json'
end
it 'returns JSON containing the eval result' do
expect(error_page).to receive(:do_eval).and_return(prompt: '#', result: "much_stuff_here")
expect(json_body).to match(
'prompt' => '#',
'result' => 'much_stuff_here',
)
end
end
context 'when the Content-Type of the request is application/json' do
before do
request_env['HTTP_CONTENT_TYPE'] = 'application/json'
end
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'Request not acceptable',
'explanation' => /did not match an acceptable content type/,
)
end
end
end
context 'when the body csrfToken does not match the CSRF token cookie' do
let(:request_body_data) { { "index" => 0, "csrfToken" => "csrfToken123" } }
before do
request_env["HTTP_COOKIE"] = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token=csrfToken456"
end
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'Invalid CSRF Token',
'explanation' => /session might have been cleared/,
)
end
end
context 'when there is no CSRF token in the request' do
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'Invalid CSRF Token',
'explanation' => /session might have been cleared/,
)
end
end
end
end
end
context "requesting an invalid internal method" do
let(:env) { {} }
let(:response_env) {
app.call(request_env)
}
let(:request_env) {
Rack::MockRequest.env_for("/__better_errors/#{id}/invalid", input: StringIO.new(JSON.dump(request_body_data)))
}
let(:request_body_data) { { "index" => 0 } }
let(:json_body) { JSON.parse(body) }
let(:id) { 'abcdefg' }
it 'returns a JSON error' do
expect(json_body).to match(
'error' => 'Not found',
'explanation' => /recognized internal call/,
)
end
end
end
end