Devise: Writing specs for routes defined with authenticated

Created on 20 Feb 2012  路  19Comments  路  Source: heartcombo/devise

Greetings,

I'm trying to use the method "authenticated" when defining a route only for authenticated users and is working. I'm having something like this in my routes:

authenticated :users do
resources :client-leads, :controller => 'authenticated/coach_match_leads'
end

However, I'm trying to write test for these routes and is not working. The test is the following:

{:get => "/client-leads"}.should route_to("authenticated/coach_match_leads#index")

But I'm getting I'm exception:

NoMethodError Exception: undefined method `authenticate?' for nil:NilClass

Do you have any ideas why is this happening? Is important to notice that this is happening only for the routes defined with authenticated. All the other specs for the routes are working properly (even the ones for other devise routes).

Thanks in advanced for your help.

Most helpful comment

this kinda helped me:

module DeviseRoutingHelpers
  # workaround for this issue https://github.com/plataformatec/devise/issues/1670
  def mock_warden_for_route_tests!
    warden = double
    allow_any_instance_of(ActionDispatch::Request).to receive(:env).
      and_wrap_original { |orig, *args|
      env = orig.call(*args)
      env['warden'] = warden
      env
    }
    allow(warden).to receive(:authenticate!).and_return(true)
  end
end

RSpec.configure do |config|
  config.include DeviseRoutingHelpers, type: :routing

  config.before(:each, type: :routing) do
    mock_warden_for_route_tests!
  end
end

All 19 comments

This is a rails limitation. the only way for you to test this is via integration tests.

Jose, is this why my functional tests are failing? I use for routes:

authenticate :admin do
  namespace :admin do
    resources :contracts
  end
end

and getting this error when using assert_routing:

  1) Error:
test: with an admin user routing should route GET /admin/contracts to/from {:action=>"index", :controller=>"admin/contracts"}. (Admin::ContractsControllerTest):
NoMethodError: undefined method `authenticate!' for nil:NilClass

(BTW, I'm using Test::Unit, not Rspec.) If this is a Rails 3/Rack limitation, I suggest adding this note in the Wiki or Readme. As always, THANKS THANKS THANKS for a brilliant library!!! :D

Could you please update the wiki yourself? Thanks!

I'd be glad to update it but first it would help to know the scope of the limitation. May I have more info or a hint at where to look in the Rails code?

As I understand, it's because Rack is first to run and checks the routes, but why is the environment information not available to Rack at that point during our functional tests? request.env['warden'] can be set before #assert_routing is called, but why is Rack unable to access it?

There is no request.env in functional tests because the functional tests are supposed to remain at the controller level. For this reason, Devise::TestHelper is forced to mock and define a warden proxy so everything works as expected.

So @request.env in Devise::TestHelper is fake so when Rack runs the real state information is of course nil. Okay, this information will help people who write controller tests, thanks.

I am having this same exact issue, but when writing cucumber integration tests.

I have this in my step definition:

Given(/^I visit my new favorite page$/) do
  visit new_person_favorite_path(@person)
end

But that throws an error:

NoMethodError: undefined method `authenticate?' for nil:NilClass
from /Users/bfults/.rvm/gems/ruby-1.9.3-p448/gems/devise-2.2.4/lib/devise/rails/routes.rb:425:in `block in constraints_for'

As far as I know this should be a full integration test. Is there something else special I have to do to get capybara & cucumber to have the right warden setup in the request env?

  • devise (2.2.4)
  • cucumber (1.3.5)
  • cucumber-rails (1.3.1)
  • capybara (2.1.0)
  • rails (3.2.13)

This is weird. The warden middleware is supposed to be there, before it reaches your routes, and it should set warden in the environment.

I think my environment in the test is in a strange state. request might not be tied into Rack properly?

From: /Users/bfults/Sites/unc/uncommon/features/step_definitions/favorite_steps.rb @ line 10 :

     5:   fill_in 'person_password', with: @person.password
     6:   click_button 'Log in'
     7: end
     8: 
     9: Given(/^I visit my new favorite page$/) do
 => 10:   binding.pry
    11:   visit new_person_favorite_path(@person)
    12: end
    13: 
    14: Given(/^I have (\d+) favorites?$/) do |num|
    15:   @person.favorites = []

[1] pry(#<Cucumber::Rails::World>)> request
ArgumentError: wrong number of arguments (0 for 1)
from /Users/bfults/.rvm/gems/ruby-1.9.3-p448/gems/rack-test-0.6.2/lib/rack/test.rb:121:in `request'

[2] pry(#<Cucumber::Rails::World>)> method(:request).source
=> "      def \#{ali}(*args, &block)\n        begin\n          \#{accessor}.__send__(:\#{method}, *args, &block)\n        rescue Exception\n          [email protected]_if{|s| %r\"\#{Regexp.quote(__FILE__)}\"o =~ s} unless Forwardable::debug\n          ::Kernel::raise\n        end\n"

That is actually correct. Capybara doesnt expose a request object. Does rake middleware in test shows the warden middleware?

Yes:

$ rake middleware
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007fc460176f70>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Remotipart::Middleware
use ActionDispatch::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionDispatch::BestStandardsSupport
use Warden::Manager
use Rack::Pjax
run Uncommon::Application.routes

I'm expecting that this should work:

[6] pry(#<Cucumber::Rails::World>)> Uncommon::Application.routes.recognize_path("/#{@person.slug}/favorites/new")
NoMethodError: undefined method `authenticate?' for nil:NilClass

I should back up a bit:

[1] pry(#<Cucumber::Rails::World>)> @person
=> #<Person id: 74, email: "[email protected]", role: "member", status: "active", inviter_id: nil, created_at: "2013-08-04 18:00:21", updated_at: "2013-08-04 18:00:21", encrypted_password: "$2a$04$DhJ3h6RSBOHwQIre5zvgR.YjSAe.4Xr/xtE4lxgSEuai...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil, confirmation_token: nil, confirmed_at: "2013-08-04 18:00:21", confirmation_sent_at: nil, unconfirmed_email: nil, time_zone: "">
[2] pry(#<Cucumber::Rails::World>)> new_person_favorite_path(@person)
=> "/bertram-gutmann/favorites/new"
[3] pry(#<Cucumber::Rails::World>)> visit new_person_favorite_path(@person)
=> nil

But this shows in the logs:

Started GET "/bertram-gutmann/favorites/new" for 127.0.0.1 at 2013-08-04 13:00:49 -0500
Started GET "/bertram-gutmann%2Ffavorites%2Fnew" for 127.0.0.1 at 2013-08-04 13:00:49 -0500
Processing by ApplicationController#not_found as HTML
  Parameters: {"nonexistent"=>"bertram-gutmann/favorites/new"}

So I assumed (maybe incorrectly) that the authenticated routes were to blame, which are set up thusly:

  authenticated do
    constraints(slug: Profile::SLUG_FORMAT) do
      get '/:slug/favorites/new' => 'favorites#new', as: 'new_person_favorite'
    end
  end
  # ...
  get '*nonexistent' => 'application#not_found'

And this works fine in the browser, hitting a Rails server. It just fails in the cucumber test environment.

I'm experiencing the same problem. Did anyone find a workaround for it?

EDIT: I don't recommend using it!

@dpisarewski here is my workaround:

# spec/support/devise.rb

module Devise::RoutingTestHelpers
  attr_accessor :request

  def initialize(*args)
    request
    super(*args)
  end

  def request
    @request ||= Hashie::Mash.new env: {}
  end

  def setup_controller(controller=subject)
    request.env['action_controller.instance'] = @controller = controller
  end

  def sign_in!
    NilClass.any_instance.unstub(:authenticate?)
    NilClass.any_instance.stubs(:authenticate?).returns(true)
  end

  def sign_out!
    NilClass.any_instance.stubs(:authenticate?).returns(false)
  end
end

RSpec.configure do |config|
  config.include Devise::TestHelpers, type: :controller
  config.include Devise::RoutingTestHelpers, type: :routing
  config.include Devise::TestHelpers, type: :routing

  config.before(:each, type: :routing) do
    setup_controller
    sign_out!
  end
end

next you use it in your routing specs like this:

# spec/routing/home_controller_spec.rb

describe HomeController do
  describe 'root path' do
    context 'when signed out' do
      it 'routes to #landing' do
        expect(get: '/').to route_to controller: 'home', action: 'landing'
      end
    end

    context 'when signed in' do
      before { sign_in! }

      it 'routes to #index' do
        expect(get: '/').to route_to controller: 'home', action: 'index'
      end
    end
  end
end

@waterlink stubbing methods on NilClass is a bad idea鈥攜ou can easily break something later and your tests might be green where an object or logic was missing/bad.

@seanknox Yes, you are right. That was a very bad workaround. Stubbing methods on NilClass can be compared to switching to dark side of force. Powerful but comes with a huge price.

I highly don't recommend using my workaround from 1 year ago.

This worked for me, @seanknox.

  let(:warden) do
    instance_double('Warden::Proxy').tap do |warden|
      allow(warden).to receive(:authenticate!).with(scope: :user)
        .and_return(authenticated?)
      allow(warden).to receive(:user).with(:user).and_return(user)
    end
  end
  let(:user) { instance_double(User) }
  let(:authenticated?) { true }

  def simulate_running_with_devise
    stub_const(
      'Rack::MockRequest::DEFAULT_ENV',
      Rack::MockRequest::DEFAULT_ENV.merge('warden' => warden),
    )
  end

I had to change ":authenticate!" in @ClayShentrup 's answer to ":authenticate?" to get it working, like this:

    let(:warden) do
        instance_double('Warden::Proxy').tap do |warden|
            allow(warden).to receive(:authenticate?).with(scope: :user)
                .and_return(authenticated?)
            allow(warden).to receive(:user).with(:user).and_return(user)
        end
    end
    let(:user) { instance_double(User) }
    let(:authenticated?) { true }

    def simulate_running_with_devise
        stub_const(
            'Rack::MockRequest::DEFAULT_ENV',
            Rack::MockRequest::DEFAULT_ENV.merge('warden' => warden),
        )
    end

this kinda helped me:

module DeviseRoutingHelpers
  # workaround for this issue https://github.com/plataformatec/devise/issues/1670
  def mock_warden_for_route_tests!
    warden = double
    allow_any_instance_of(ActionDispatch::Request).to receive(:env).
      and_wrap_original { |orig, *args|
      env = orig.call(*args)
      env['warden'] = warden
      env
    }
    allow(warden).to receive(:authenticate!).and_return(true)
  end
end

RSpec.configure do |config|
  config.include DeviseRoutingHelpers, type: :routing

  config.before(:each, type: :routing) do
    mock_warden_for_route_tests!
  end
end
Was this page helpful?
0 / 5 - 0 ratings