Mio
Mio

Reputation: 1502

Testing with rspec .consider_all_requests_local = false

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

Answers (4)

jasoares
jasoares

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

Tyler Rick
Tyler Rick

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

Aaron K
Aaron K

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

Piotr Kruczek
Piotr Kruczek

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

Related Questions