# frozen_string_literal: true
require 'spec_helper'
describe Grape::Middleware::Error do
let(:exception_app) do
Class.new do
class << self
def call(_env)
raise 'rain!'
end
end
end
end
let(:other_exception_app) do
Class.new do
class << self
def call(_env)
raise NotImplementedError, 'snow!'
end
end
end
end
let(:custom_error_app) do
Class.new do
class << self
class CustomError < Grape::Exceptions::Base; end
def call(_env)
raise CustomError.new(status: 400, message: 'failed validation')
end
end
end
end
let(:error_hash_app) do
Class.new do
class << self
def error!(message, status)
throw :error, message: { error: message, detail: 'missing widget' }, status: status
end
def call(_env)
error!('rain!', 401)
end
end
end
end
let(:access_denied_app) do
Class.new do
class << self
def error!(message, status)
throw :error, message: message, status: status
end
def call(_env)
error!('Access Denied', 401)
end
end
end
end
let(:app) do
builder = Rack::Builder.new
builder.use Spec::Support::EndpointFaker
if options.any?
builder.use described_class, options
else
builder.use described_class
end
builder.run running_app
builder.to_app
end
context 'with defaults' do
let(:running_app) { exception_app }
let(:options) { {} }
it 'does not trap errors by default' do
expect { get '/' }.to raise_error(RuntimeError, 'rain!')
end
end
context 'with rescue_all' do
context 'StandardError exception' do
let(:running_app) { exception_app }
let(:options) { { rescue_all: true } }
it 'sets the message appropriately' do
get '/'
expect(last_response.body).to eq('rain!')
end
it 'defaults to a 500 status' do
get '/'
expect(last_response.status).to eq(500)
end
end
context 'Non-StandardError exception' do
let(:running_app) { other_exception_app }
let(:options) { { rescue_all: true } }
it 'does not trap errors other than StandardError' do
expect { get '/' }.to raise_error(NotImplementedError, 'snow!')
end
end
end
context 'Non-StandardError exception with a provided rescue handler' do
context 'default error response' do
let(:running_app) { other_exception_app }
let(:options) { { rescue_handlers: { NotImplementedError => nil } } }
it 'rescues the exception using the default handler' do
get '/'
expect(last_response.body).to eq('snow!')
end
end
context 'custom error response' do
let(:running_app) { other_exception_app }
let(:options) { { rescue_handlers: { NotImplementedError => -> { Rack::Response.new('rescued', 200, {}) } } } }
it 'rescues the exception using the provided handler' do
get '/'
expect(last_response.body).to eq('rescued')
end
end
end
context do
let(:running_app) { exception_app }
let(:options) { { rescue_all: true, default_status: 500 } }
it 'is possible to specify a different default status code' do
get '/'
expect(last_response.status).to eq(500)
end
end
context do
let(:running_app) { exception_app }
let(:options) { { rescue_all: true, format: :json } }
it 'is possible to return errors in json format' do
get '/'
expect(last_response.body).to eq('{"error":"rain!"}')
end
end
context do
let(:running_app) { error_hash_app }
let(:options) { { rescue_all: true, format: :json } }
it 'is possible to return hash errors in json format' do
get '/'
expect(['{"error":"rain!","detail":"missing widget"}',
'{"detail":"missing widget","error":"rain!"}']).to include(last_response.body)
end
end
context do
let(:running_app) { exception_app }
let(:options) { { rescue_all: true, format: :jsonapi } }
it 'is possible to return errors in jsonapi format' do
get '/'
expect(last_response.body).to eq('{"error":"rain!"}')
end
end
context do
let(:running_app) { error_hash_app }
let(:options) { { rescue_all: true, format: :jsonapi } }
it 'is possible to return hash errors in jsonapi format' do
get '/'
expect(['{"error":"rain!","detail":"missing widget"}',
'{"detail":"missing widget","error":"rain!"}']).to include(last_response.body)
end
end
context do
let(:running_app) { exception_app }
let(:options) { { rescue_all: true, format: :xml } }
it 'is possible to return errors in xml format' do
get '/'
expect(last_response.body).to eq("\n\n rain!\n\n")
end
end
context do
let(:running_app) { error_hash_app }
let(:options) { { rescue_all: true, format: :xml } }
it 'is possible to return hash errors in xml format' do
get '/'
expect(["\n\n missing widget\n rain!\n\n",
"\n\n rain!\n missing widget\n\n"]).to include(last_response.body)
end
end
context do
let(:running_app) { exception_app }
let(:options) do
{
rescue_all: true,
format: :custom,
error_formatters: {
custom: lambda do |message, _backtrace, _options, _env, _original_exception|
{ custom_formatter: message }.inspect
end
}
}
end
it 'is possible to specify a custom formatter' do
get '/'
expect(last_response.body).to eq('{:custom_formatter=>"rain!"}')
end
end
context do
let(:running_app) { access_denied_app }
let(:options) { {} }
it 'does not trap regular error! codes' do
get '/'
expect(last_response.status).to eq(401)
end
end
context do
let(:running_app) { custom_error_app }
let(:options) { { rescue_all: false } }
it 'responds to custom Grape exceptions appropriately' do
get '/'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('failed validation')
end
end
context 'with rescue_options :backtrace and :exception set to true' do
let(:running_app) { exception_app }
let(:options) do
{
rescue_all: true,
format: :json,
rescue_options: { backtrace: true, original_exception: true }
}
end
it 'is possible to return the backtrace and the original exception in json format' do
get '/'
expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original_exception', 'RuntimeError')
end
end
context do
let(:running_app) { exception_app }
let(:options) do
{
rescue_all: true,
format: :xml,
rescue_options: { backtrace: true, original_exception: true }
}
end
it 'is possible to return the backtrace and the original exception in xml format' do
get '/'
expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original-exception', 'RuntimeError')
end
end
context do
let(:running_app) { exception_app }
let(:options) do
{
rescue_all: true,
format: :txt,
rescue_options: { backtrace: true, original_exception: true }
}
end
it 'is possible to return the backtrace and the original exception in txt format' do
get '/'
expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original exception', 'RuntimeError')
end
end
end