Reputation: 1502
I'm using in my application_controller : .consider_all_requests_local
like
unless Rails.application.config.consider_all_requests_local
rescue_from ActionController::InvalidCrossOriginRequest, :with => :render_404
end
This return 404 if ActionController::InvalidCrossOriginRequest is raised. In local environnement it's not raised, it's good for debugging. For this part it's working. But I would like to test it with rspec.
I've tried something like
describe 'ActionController::InvalidCrossOriginRequest render 404' do
before { Rails.application.config.consider_all_requests_local = false }
controller do
def index
raise ActionController::InvalidCrossOriginRequest
end
end
subject { xhr :get, :index, format: :js }
its(:status) { is_expected.to eq 404 }
end
Two things. I probably not raise in the proper way. Locally the error occurs when mywebsite.com/editor/fckeditor.js is called. Didn't found a way to call a specific url.
Second problem, the before doesn't change the Rails.application.config.consider_all_requests_local
state.
I get :
1) ApplicationController ActionController::InvalidCrossOriginRequest render 404 status
Failure/Error: raise ActionController::InvalidCrossOriginRequest
ActionController::InvalidCrossOriginRequest:
ActionController::InvalidCrossOriginRequest
Upvotes: 2
Views: 1477
Reputation: 1821
The above option mentioned by @tyler-rick works great for ad-hoc switching between full integration tests more specific details about failures rather than getting a simple 400 or 500.
I was hardly looking for a way to use rspec meta tags to toggle between
Rails.application.config.action_dispatch.show_exceptions = true # or false
The idea was that I commonly want to switch between black box testing where I only care about I ended up reusing his solution to achieve just that with:
config.before(:each, :blackbox, type: :request) do
method = Rails.application.method(:env_config)
allow(Rails.application).to receive(:env_config).with(no_args) do
method.call.merge(
'action_dispatch.show_exceptions' => true,
'action_dispatch.show_detailed_exceptions' => false,
'consider_all_requests_local' => true
)
end
end
This now allows me to run tests like this:
# does not raise any exception and simply renders 400 :bad_request
# allowing me to ensure an Event record was not created
it 'does not create a new Event when Name is missing', :blackbox do
expect {
post webhook_url, params: { name: '' }, headers: auth_header
}.to_not change(Event, :count)
end
# it does raise an exception and allows me to test it is complaining
# about the right Name parameter and not any other without resorting to
# error message testing.
it "responds with bad request when Name is empty" do
expect {
post webhook_url, params: { name: '' }, headers: auth_header
}.to raise_error(ActionController::ParameterMissing).with_message(
/param is missing or the value is empty: :name/
)
end
It is also great when you are testing API endpoints responses and you get a 500 rendered, you can simply disable the blackbox mode temporarily to see what's failing or add a unit test to ensure it doesn't reoccur and enable it back again. This should actually be a feature on the rails test suite but I guess this is more often used for API :request tests and not so useful for :feature testing.
Upvotes: 0
Reputation: 9491
I used to have a guard around my rescue_from
config, too, like:
unless Rails.application.config.consider_all_requests_local
rescue_from Exception, with: :render_error
…
end
... which worked fine, until I was trying to figure out how to make it handle errors and show pretty custom error pages (like it does in production) in some tests. @Aaron K's answer was helpful in explaining why the check can't be evaluated within the class definition, and has to be checked within the actual error handler (at run time) instead. But that only solved part of the problem for me.
Here's what I did...
In ApplicationController
, remember to re-raise any errors if the show_detailed_exceptions
flag (a more appropriate check than consider_all_requests_local
) is true. In other words, only do the production error handling if the app/request is configured to handle errors for production; otherwise "pass" and re-raise the error.
rescue_from Exception, with: :render_error
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
rescue_from ActionController::RoutingError, with: :render_not_found
rescue_from AbstractController::ActionNotFound, with: :render_not_found
def show_detailed_exceptions?
# Rails.application.config.consider_all_requests_local causes this to be set to true as well.
request.get_header("action_dispatch.show_detailed_exceptions")
end
def render_not_found(exception = nil, template = 'errors/not_found')
raise exception if show_detailed_exceptions?
logger.error exception if exception
render template, formats: [:html], status: :not_found
end
def render_error(exception)
raise exception if show_detailed_exceptions?
deliver_exception_notification(exception)
logger.error exception
# Prevent AbstractController::DoubleRenderError in case we've already rendered something
method(:response_body=).super_method.call(nil)
respond_to do |format|
format.html { render 'errors/internal_server_error', formats: [:html], status: :internal_server_error }
format.any { raise exception }
end
end
Add to spec/support/handle_exceptions_like_production.rb
:
shared_context 'handle_exceptions_like_production', handle_exceptions_like_production: true do
before do |example|
case example.metadata[:type]
when :feature
method = Rails.application.method(:env_config)
allow(Rails.application).to receive(:env_config).with(no_args) do
method.call.merge(
'action_dispatch.show_exceptions' => true,
'action_dispatch.show_detailed_exceptions' => false,
'consider_all_requests_local' => true
)
end
when :controller
# In controller tests, we can only test *controller* behavior, not middleware behavior. We
# can disable show_detailed_exceptions here but we can *only* test any behaviors that depend
# on it that are defined in our *controller* (ApplicationController). Because the request
# doesn't go through the middleware (DebugExceptions, ShowExceptions) — which is what actually
# renders the production error pages — in controller tests, we may not see the exact same
# behavior as we would in production. Feature (end-to-end) tests may be needed to more
# accurately simulate a full production stack with middlewares.
request.set_header 'action_dispatch.show_detailed_exceptions', false
else
raise "expected example.metadata[:type] to be one of :feature or :controller but was #{example.metadata[:type]}"
end
end
end
RSpec.configure do |config|
config.include_context 'handle_exceptions_like_production', :handle_exceptions_like_production
end
Then, in end-to-end (feature) tests where you want it to handle exceptions like it does in production (in other words, to not treat it like a local request), just add :handle_exceptions_like_production
to your example group:
describe 'something', :handle_exceptions_like_production do
it …
end
For example:
spec/features/exception_handling_spec.rb
:
describe 'exception handling', js: false do
context 'default behavior' do
it do |example|
expect(example.metadata[:handle_exceptions_like_production]).to eq nil
end
describe 'ActiveRecord::RecordNotFound' do
it do
expect {
visit '/users/0'
}.to raise_exception(ActiveRecord::RecordNotFound)
end
end
describe 'ActionController::RoutingError' do
it do
expect {
visit '/advertisers/that_track_you_and_show_you_personalized_ads/'
}.to raise_exception(ActionController::RoutingError)
end
end
describe 'RuntimeError => raised' do
it do
expect {
visit '/test/exception'
}.to raise_exception(RuntimeError, 'A test exception')
end
end
end
context 'when :handle_exceptions_like_production is true', :handle_exceptions_like_production do
describe 'ActiveRecord::RecordNotFound => production not_found page' do
it do
expect {
visit '/users/0'
}.to_not raise_exception
expect_not_found
end
end
describe 'ActionController::RoutingError => production not_found page' do
it do
visit '/advertisers/that_track_you_and_show_you_personalized_ads/'
expect_not_found
end
end
describe 'RuntimeError => production not_found page' do
it do
visit '/test/exception'
expect_application_error
end
end
end
end
It can also be used in controller tests — if you have production error-handling defined in your ApplicationController
. spec/controllers/exception_handling_spec.rb
:
describe 'exception handling' do
context 'default behavior' do
describe UsersController do
it do
expect {
get 'show', params: {id: 0}
}.to raise_exception(ActiveRecord::RecordNotFound)
end
end
describe TestController do
it do
expect {
get 'exception'
}.to raise_exception(RuntimeError, 'A test exception')
end
end
end
context 'when handle_exceptions_like_production: true', :handle_exceptions_like_production do
describe UsersController do
it do
expect {
get 'show', params: {id: 0}
}.to_not raise_exception
expect(response).to render_template('errors/not_found')
end
end
describe TestController do
it do
expect {
get 'exception'
}.to_not raise_exception
expect(response).to render_template('errors/internal_server_error')
end
end
end
end
Tested with: rspec 3.9
, rails 5.2
Upvotes: 2
Reputation: 6961
The issue looks to be caused by your unless
check being performed at class load time. This means the very first time the class is loaded the value in the application config is checked and the rescue_from
is either set or not set.
At the most basic workaround, you would need to use load
to cause that file to get re-read once the setting has been changed. However, as is, once the rescue_from
is turned on, loading the file again won't cause it to turn off.
The next alternative is to use rescue_from(with:)
which delegates to a helper or the block form. You can use this helper to check the value and either handle the condition or not. However, considering this looks to be something you only want to do in a non-production environment, you could combine the two. Use the unless
to verify that you are not in production, then use the with to check the config each time.
Something like:
class ApplicationController < ActionController::Base
unless Rails.env.production?
rescue_from ActionController::InvalidCrossOriginRequest do
unless Rails.application.config.consider_all_requests_local
render_404
end
end
end
end
Upvotes: 3
Reputation: 2390
Try mocking it instead of setting:
before { Rails.stub_chain('application.config.consider_all_requests_local').and_return(false) }
More info here
This syntax is deprecated, so you can either turn off the deprecation warning or use the new 'workaround'
allow(object).to receive_message_chain(:one, :two, :three).and_return(:four)
expect(object.one.two.three).to eq(:four)
as posted here
Upvotes: 1