Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Devise::TestHelpers inclusion in Rails 5 #3913

Closed
holman opened this issue Jan 24, 2016 · 25 comments
Closed

Devise::TestHelpers inclusion in Rails 5 #3913

holman opened this issue Jan 24, 2016 · 25 comments

Comments

@holman
Copy link

holman commented Jan 24, 2016

I'm running into an inconsistency in how Devise suggests doing controller tests in Rails 5.0.0.beta1.

I set up a minimal app on Devise b97b3e6 for you to reproduce, if you'd like:

git clone https://github.com/holman/devise-helpers-test
cd devise-helpers-test && rake

In master's README, we're currently suggesting this to be added to test/test_helper.rb:

class ActionController::TestCase
  include Devise::TestHelpers
end

Running the tests will result in:

NoMethodError: undefined method `sign_in' for #<HomeControllerTest:0x007fa1433cf668>
    test/controllers/home_controller_test.rb:5:in `block in <class:HomeControllerTest>'

Greenfield Rails 5 generated controllers now inherit from ActionDispatch::IntegrationTest:

class HomeControllerTest < ActionDispatch::IntegrationTest
  test "home" do
    sign_in User.create(:email => "#{rand(50000)}@example.com")
    get root_url
    assert_response :success
  end
end

Modifying the test_helper.rb to include from within the new class causes another problem, though:

NoMethodError: undefined method `env' for nil:NilClass
    /Users/holman/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/devise-b97b3e6e3b57/lib/devise/test_helpers.rb:27:in `setup_controller_for_warden'

That's currently this line, which doesn't have the @request ivar available.

Likely missing something obvious here! If I am, we should likely update the README to account for bonehead developers like myself. 🎆

@josevalim
Copy link
Contributor

Excellent bug report, @holman! ❤️

It seems controller tests in Rails 5 are integration tests, so we may not have a "backdoor" to sign the user in. We will either need to go explicitly through the sign in page OR build a session cookie that we'll store directly in the request. We will explore the options but I hope this is enough information for you to progress! If not, just ask!

@holman
Copy link
Author

holman commented Jan 26, 2016

Thanks, @josevalim! Real helpful.

For those of you who may be following along at home, I'm tossing this in test_helper.rb in the meantime:

class ActionDispatch::IntegrationTest
  def sign_in(user)
    post user_session_path \
      "user[email]"    => user.email,
      "user[password]" => user.password
  end
end

Seems to be the easiest thing to do that allows future drop-in support. Small app too so far, so not a huge deal to add the extra overhead for me.

@CollinGraves
Copy link

@holman are you actively working on this? Let me know if there's anything I can do to help. Currently working on several Rails 5 apps and have done the same workaround as you posted in your last comment. Would love to help patch this.

@holman
Copy link
Author

holman commented Feb 2, 2016

Probbbbbably not. Don't think I know enough of the internals to jump in and figure out which direction to take this. (If there's a consensus on what approach makes the most sense I can probably hop in and build it out though).

@greyman888
Copy link

Thanks @holman for posting the work around, it helped me a lot.

Although I don't have a solution I thought I'd add a little more information that might help someone else out. I'm pretty new to testing and rails in general so sorry if this isn't much use.

It looks like in Rails 5 you can add gem 'rails-controller-testing' to keep using the old controller tests. I haven't tried this but it might help someone. I'd like to make the integration tests work though because @eileencodes and @tenderlove put so much work into making them faster (https://youtu.be/oT74HLvDo_A).

When I tried the solution @holman posted here, it didn't work without providing a password. I don't really understand why. Here is what did work for me eventually:

My user.rb file:

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable, :omniauthable
end

My test_helper.rb file:

ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require "minitest/reporters"
Minitest::Reporters.use!

class ActiveSupport::TestCase
#  fixtures :all

  def sign_in(user:, password:)
    post user_session_path \
      "user[email]"    => user.email,
      "user[password]" => password
  end
end

My first test file login_test.rb (These tests all pass without generating errors):

require 'test_helper'

class LoginTest < ActionDispatch::IntegrationTest

  def setup
    @password = "password"
    @confirmed_user = User.create(email: "#{rand(50000)}@example.com", 
                                  password: @password, 
                                  confirmed_at: "2016-02-01 11:35:56")
    @unconfirmed_user = User.create(email: "#{rand(50000)}@example.com", 
                                    password: @password,
                                    confirmed_at: "")
  end

  test "successful login of confirmed user" do
    sign_in(user: @confirmed_user, password: @password)
    assert_redirected_to home_url
  end

  test "unsuccessful login of unconfirmed user" do
    sign_in(user: @unconfirmed_user, password: @password)
    get home_path
    assert_response :redirect
  end

  test "unsuccessful login of confirmed user with wrong password" do
    sign_in(user: @confirmed_user, password: "wrong password")
    get home_path
    assert_response :redirect
  end

end

I hope it helps a little.

@lachlanjc
Copy link

I'm working on a Rails 5 app as well and ran in to these issues. I slotted in @holman's code to my test_helper.rb:

ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'

class ActiveSupport::TestCase
  fixtures :all

  def sign_in(user)
    post user_session_path \
      'user[email]'    => user.email,
      'user[password]' => user.password
  end
end

And then I've got a controller test:

require 'test_helper'

class KlassesControllerTest < ActionDispatch::IntegrationTest
  setup do
    sign_in users(:one)
    @klass = klasses(:one)
  end

  test 'should get index' do
    get klasses_url
    assert_response :success
  end
end

My user fixture:

one:
  name: 'Lachlan'
  email: 'atlachlanjc@gmail.com'
  encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>

When I run the test, however, I get this:

Error:
KlassesControllerTest#test_should_get_index:
ArgumentError: wrong number of arguments (given 0, expected 1..2)
    test/test_helper.rb:9:in `sign_in'
    test/controllers/klasses_controller_test.rb:5:in `block in <class:KlassesControllerTest>'

How can I fix this issue (even temporarily) so that I can test my app? Thanks 😀

@holman
Copy link
Author

holman commented Feb 15, 2016

I could have sworn I ran into that problem when I was initially looking into this, but for the life of me I can't remember what the specifics were or how to reproduce it. Guessing it might have something to do with your routes, maybe?

@lachlanjc
Copy link

Hmm. My routes are pretty straightforward:

devise_for :users, controllers: { registrations: 'registrations' }
as :user do
  get '/login', to: 'devise/sessions#new'
  get '/signin', to: 'devise/sessions#new'
  get '/signup', to: 'devise/registrations#new'
  get '/settings', to: 'devise/registrations#edit'
end

And in case you were wondering, the registrations controller:

class RegistrationsController < Devise::RegistrationsController
  protected

  def update_resource(resource, params)
    resource.update_without_password(params)
  end
end

Really confused as to my next step 😬 — thanks for helping out, though!

@twe4ked
Copy link

twe4ked commented Feb 18, 2016

@lachlanjc is the sign_in method definitely the one you've defined or is something else overriding it. Try method(:sign_in).source_location.

@lachlanjc
Copy link

@twe4ked Source location is confirming the method is from test/test_helper.rb (note that I haven't changed it from what I posted above).

@greyman888
Copy link

greyman888 commented Feb 19, 2016 via email

@lachlanjc
Copy link

Ok, so I've updated my sign_in method to create a user right there:

def sign_in(user)
  @user = User.create(email: "#{rand(50000)}@example.com",
                      password: 'password')
  post user_registration_path, params: { 'user[email]': user.email, 'user[password]': 'password' }
  puts current_user.email # Line 12 — test authentication state
end

But this isn't actually working:

Error:
KlassesControllerTest#test_should_get_index:
NameError: undefined local variable or method `current_user' for #<KlassesControllerTest:0x007ff9b5e7e878>
    test/test_helper.rb:12:in `sign_in'
    test/controllers/klasses_controller_test.rb:10:in `block in <class:KlassesControllerTest>'

@twe4ked
Copy link

twe4ked commented Feb 21, 2016

You should have access to a @controller or controller variable in the test. Try @controller.current_user.email? You might get more luck over at http://stackoverflow.com.

@lachlanjc
Copy link

@twe4ked The sign_in method is in test/test_helper.rb because I need/will need it in many different controller tests. Is there a better place for it? Also, I'm still getting an error if I set @controller = KlassesController before running sign_in with the change you proposed. Thanks for helping out though!

@twe4ked
Copy link

twe4ked commented Feb 21, 2016

@lachlanjc Try controller.current_user.email. I suggest creating a question on Stack Overflow and linking to it from here. There are a bunch of people getting notifications for each comment here and it's getting a bit off topic.

@lachlanjc
Copy link

@twe4ked Yeah, that took a while! When I created a user on the fly instead of using a fixture everything worked, but then the user_ids in my fixtures didn't match up and so authenticated pages wouldn't work. I'm now creating a user on the fly, and then changing the user ID in the database for the subject so that everything works smoothly. Here's my final solution for anyone that needs it:

class ActionDispatch::IntegrationTest
  def sign_in(user)
    post user_session_path \
      "user[email]"    => user.email,
      "user[password]" => user.password
  end

  def sign_in_for(subject)
    @user = User.create(email: "#{rand(50000)}@example.com", password: 'password')
    sign_in @user
    subject.update_attribute(:user_id, @user.id)
  end
end

And an example for using it (likely inside the setup of the test):

setup do
  @post = posts(:one)
  sign_in_for(@post)
end

@cwsaylor
Copy link

This worked for me and I wanted to get some feedback on the solution. I used Warden's test helpers directly as instructed here, https://github.com/hassox/warden/wiki/Testing.

In test_helper.rb

class ActionDispatch::IntegrationTest
  include Warden::Test::Helper
end

In fixtures/users.yml

alice:
  email: test@host.com
  encrypted_password: <%= Devise::Encryptor.digest(User, "12345678") %>
  confirmed_at: <%= Time.now %>

In my controller test:

class FooControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:alice)
    Warden.test_mode!
    login_as(@user, scope: :user)
  end

  teardown do
    Warden.test_reset!
  end
end

@lucasmazza
Copy link
Contributor

@cwsaylor I'm using a similar implementation on a small here, I think we can wrap this up inside a Devise::IntegrationHelper module for 4.1.

@lucasmazza
Copy link
Contributor

Merged #4071 introducing a new IntegrationHelpers module that uses the Warden testing API and moved the old TestHelpers to ControllerHelpers - I still want to finish a few other things before a 4.2.0 release, but anyone is welcome to try out these modules from master and let us know how it goes 😄

@tirdadc
Copy link

tirdadc commented Jul 18, 2016

Not trying to re-animate this thread, but is there any documentation on how to get sign_in to work with Minitest following the upgrade to Rails 5? I basically am stopped at:

undefined method `sign_in'

@lucasmazza
Copy link
Contributor

@tirdadc have you checked the updated Test helpers section from the README? If it doesn't work for you, please create a new issue following our CONTRIBUTING.md recommendations.

trendwithin added a commit to XiaoA/trivia_app that referenced this issue Aug 11, 2016
refer to this issue before modification;
heartcombo/devise#3913
@rodrigo-puente
Copy link

Hey @tirdadc, this might be a bit late but can help others: According to the README, in RAILS 5 you should do this:

class ActionDispatch::IntegrationTest
    include Devise::Test::IntegrationHelpers
end

Instead of how we use to do it on RAILS 4 ( ActionController::TestCase will be deprecated in RAILS 5)

class ActionController::TestCase
  include Devise::Test::ControllerHelpers
end

@kakoni
Copy link

kakoni commented Oct 25, 2017

Also, very late to the party, but if you use gem 'rails-controller-testing' with rails 5, you should setup test_helper like in rails 4 aka

class ActionController::TestCase
  include Devise::Test::ControllerHelpers
end

otherwise you will start getting these undefined method `sign_in'errors.

@tomgrim
Copy link

tomgrim commented Nov 7, 2017

In case anyone finds this useful, I have a slightly different approach to logging in users during testing. If you wish to skip over the devise functionality you can use some middleware, similar to how the clearance gem works. This prevents you from having to make a whole other request to sign in and should speed up specs.

spec/support/back_door.rb

# Usage:
#
#   visit new_feedback_path(as: user)
class BackDoor
  def initialize(app, &block)
    @app = app
    @block = block
  end

  def call(env)
    sign_in_through_the_back_door(env)
    @app.call(env)
  end

  private

  def sign_in_through_the_back_door(env)
    params = Rack::Utils.parse_query(env['QUERY_STRING'])
    user_param = params['as']

    return unless user_param.present?
    user = User.find(user_param)
    return unless user
    env['warden'].set_user(user)
  end
end

config/environments/test.rb

require_relative '../../spec/support/back_door'
config.middleware.use BackDoor

In specs:

user = # get_user_here
get '/url', as: user

@ehubbell
Copy link

ehubbell commented Aug 9, 2019

I got stuck on this for quite a bit. I'm using Rails 5.2.3 with a JWT Devise Strategy and came up against numerous issues with all the options above. My solution was the following:

test/support/authentication.rb

module Authentication
  def authenticate_user(user)
    token = JsonWebToken.encode(id: user.id)
    user.token = 'JWT ' + token
    user.save
  end
end

test/test_helper.rb

ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)
require 'rails/test_help'
require 'bcrypt'

class ActiveSupport::TestCase
  fixtures :all
  Dir[Rails.root.join("test/support/**/*.rb")].each { |f| require f }
end

test/fixtures/users.yml

frederick:
  type: 'AuthUser'
  name: 'Frederick'
  email: 'frederick@myhubapp.com'
  phone: '1234567890'
  encrypted_password: <%= BCrypt::Password.create("password", cost: 4) %>

test/controllers/v1/users_controller_test.rb

require 'test_helper'

class V1::UsersControllerTest < ActionDispatch::IntegrationTest
  include Authentication

  setup do
    @user = users(:frederick)
    authenticate_user(@user)
    @headers = { "Authorization": @user.token }
  end

  test "fetching users should succeed" do
  	get users_url, headers: @headers, xhr: true
    assert_response :success
  end

  test "fetching a user should succeed" do
    get "/v1/users/#{@user.id}", headers: @headers, xhr: true
    assert_response :success
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests