lynndylanhurley / devise_token_auth

Token based authentication for Rails JSON APIs. Designed to work with jToker and ng-token-auth.
Do What The F*ck You Want To Public License
3.55k stars 1.14k forks source link

Testing with rspec #75

Closed mshappe closed 6 years ago

mshappe commented 9 years ago

Most tutorials out there for devise_token_auth talk about using feature specs to test authentication integration. This is great, of course, but it doesn't speak to controller specs.

E-mailing with @lynndylanhurley, it's clear that it's not really that hard and it could be described easily in the README, but it could be made even easier with a handful of TestHelpers along the same lines Devise and Warden provide.

I already have one simple helper written in my own project that I will contribute against this issue when I get a chance, but I'd be interested to hear what sort of things other people have seen/done for testing and what they'd want to see here.

lynndylanhurley commented 9 years ago

Thanks for posting this @mshappe!

I'd like to add a README section on testing, but first I'd like to create a few simple helpers to make testing against this gem as straightforward as possible.

Some of the following testing helpers are used by this gem internally, but I don't think they're available from outside this gem:

We can start by consolidating these features into a helper module, and then documenting its use.

But let's leave this list open for a few days to see if anyone else has any ideas before we get started.

mshappe commented 9 years ago

Yeah, I saw the token aging helper for example when looking through your test code and realized it wasn't a generally available helper. Almost just copied it and then realized I didn't immediately need it for my local purposes yet.

mshappe commented 9 years ago

I also note that you're mainly using Test::Unit and its paradigms, while I'm very much in the RSpec universe :smile: There are some definite differences -- most notably that I'm using Capybara for "feature specs" instead of Test::Unit's "integration tests". Still, I think we should be able to come up with helpers for each of them.

ghost commented 9 years ago

@mshappe What are you asking about? I'm testing my API with RSpec controller tests, but I'm planning to use Protractor to my feature testing. I don't see the benefit of feature testing outside my UI.

dillonwelch commented 9 years ago

Any suggestions on how to mock authenticate_user! in an RSpec controller spec?

More generally, any rough drafts or demos of code/explanation for testing would be much appreciated. I'm working on an API demo at work and am struggling to get my tests to be both functional and not atrocious :)

lynndylanhurley commented 9 years ago

@oniofchaos - just make sure that the headers are included in the mock request.

You can get a user's auth headers using the create_new_auth_token method of your User model instance.

Example: create auth headers for user
auth_headers = user.create_new_auth_token
Example: include request headers in a unit test
# merge the headers into the request.headers object
request.headers.merge!(auth_headers)

# make the request
xhr :delete, :destroy, format: :json
Example: include request headers in an integration test
# pass the auth headers as the last argument of the request
get '/demo/members_only', {}, auth_headers
jrogozen commented 9 years ago

Hi,

I'm really confused on how to get access to something like "current_user" in an RSpec controller test

In my integration test, I do something like this:

post :create, auth_headers, post: { id: 1, name: name, category_id: 1 }

let(:name) { "Super Cool Post" }
let(:user) { FactoryGirl.create(:confirmed_admin_user) } 
let(:auth_headers) { user.create_new_auth_token }

When I put a binding.pry in the controller, and run a test to see what the status code of the response is, It looks like the token + info is being sent and the User correctly mocked. However, current_user still returns false.

I'm just looking for a way to mock out an actually signed_in user that responds to current_user method within a RSpec controller test.

Thanks!

lynndylanhurley commented 9 years ago

@jrogozen - does this guide help you out at all?

dillonwelch commented 9 years ago

@lynndylanhurley I tried your suggestion (the unit test portion) but get the following error:

 NoMethodError:
   undefined method `session_serializer' for nil:NilClass
 # /Users/dillonwelch/.rvm/gems/ruby-2.1.5/gems/devise-3.4.1/lib/devise/controllers/sign_in_out.rb:38:in `sign_in'
 # /Users/dillonwelch/.rvm/gems/ruby-2.1.5/gems/devise_token_auth-0.1.31.beta1/app/controllers/devise_token_auth/concerns/set_user_by_token.rb:41:in `set_user_by_token'
bwillis commented 9 years ago

I had the same problem @oniofchaos. I combined the auth_headers with the devise_helper sign_in method and it worked for me. My helper module looks like this:

module AuthHelper
  def auth_request(user)
    sign_in user
    request.headers.merge!(user.create_new_auth_token)
  end
end
jrogozen commented 9 years ago

@lynndylanhurley Thanks for the example. I got it working by doing what you/others pointed out and using request.headers.merge! for controller tests.

for feature tests I'm mocking a user using omniauth.config.add_mock

dillonwelch commented 9 years ago

Strangely, I seem to have fixed my previous issue simply by adding include Devise::TestHelpers to my spec. I didn't have to add the sign_in call like you suggested @bwillis.

lynndylanhurley commented 9 years ago

@bwillis - I'm worried that using the sign_in method might cause trouble. The user should be signed in using only the auth headers, and if there's something preventing that from happening, then the test should probably fail.

bwillis commented 9 years ago

Ok, I can dig into the session_serializer exception a little more.

lynndylanhurley commented 9 years ago

This could be an issue with devise 3.4.

I think I'm still testing against 3.3. I'll update tonight and see if I can reproduce the issue.

bwillis commented 9 years ago

Sorry to confuse the conversation, I am able to test controller by setting the headers just as in the example @lynndylanhurley provided. I realized that my issue was caused by a leftover devise configuration (initialize/devise.rb) that I forgot to remove. This gem works fine with devise 3.4.1.

lynndylanhurley commented 9 years ago

Sorry to confuse the conversation

No problem! I'm sure someone else will have the same issue, and this thread may help them out. Thanks @bwillis!

dash-rai commented 9 years ago

I'm having the same issue that @bwillis had. Unless I use Devise's sign_in method along with create_new_auth_token the test fails with

undefined method `session_serializer' for nil:NilClass
# ~/.rvm/gems/ruby-2.1.2/gems/devise-3.4.1/lib/devise/controllers/sign_in_out.rb:38:in `sign_in'

I made sure that there was no devise initializer. I'm using the rails-api gem and this thread (which is quite old) does mention that rails-api used to leave out some dependencies, but it seems fixed.

What do you think is causing the issue?

Thanks :)

nicolas-besnard commented 9 years ago

I don't know if it can help you, but I use this to sign-in my user :

module Request
  module RequestsHelpers
    def set_authentication_headers_for(user)
      user_headers = user.create_new_auth_token
      @request.headers.merge!(user_headers)
    end
  end
end

Then in rspec

before do
    set_authentication_headers_for(user)
    post :create, user_id: user.id
end
dash-rai commented 9 years ago

Nope, I'm already doing that and it doesn't work unless you use it in combination with the sign_in method.

nicolas-besnard commented 9 years ago

I NEVER use the sign_in method, and it works for me.

dash-rai commented 9 years ago

I'm using 'rails', '~> 4.2.0', 'rails-api', '~> 0.4.0' and the current master of devise_token_auth.

Since I was starting out, the code is actually pretty basic:

# spec/controllers/merchants_controller_spec.rb

require 'rails_helper'

RSpec.describe MerchantsController, type: :controller do
  describe "#GET #create" do
    it "assigns the requested merchant as @merchant" do
      m = create(:merchant)
      @request.env["devise.mapping"] = Devise.mappings[:merchant]

      @request.headers.merge! m.create_new_auth_token
      sign_in m # does not work without this.

      expect {
        get :create, format: :json
      }.to change(Merchant, :count).by(1)
    end
  end
end
# app/controllers/merchants_controller.rb

require 'factory_girl_rails'

class MerchantsController < ApplicationController
  before_action :authenticate_merchant!

  def create
    FactoryGirl.create(:merchant)
  end
end
# spec/spec_helper.rb

require 'factory_girl_rails'
require 'devise'

RSpec.configure do |config|
  config.include FactoryGirl::Syntax::Methods
  config.include Devise::TestHelpers, type: :controller
end
nicolas-besnard commented 9 years ago

What the output value without sign_in. Take a look at log/test.log

dash-rai commented 9 years ago

It throws the error I posted earlier and the log/test.log reports Completed 500 Internal Server Error. Debugging, I found out that the warden() method (which returns request.env['warden']) is nil at ~/.rvm/gems/ruby-2.1.2/gems/devise-3.4.1/lib/devise/controllers/sign_in_out.rb:38

christophermlne commented 9 years ago

This thread has been invaluable. The content really deserves a place in the readme or wiki.

theotherdon commented 9 years ago

@christophermlne Agreed. I was having a really hard time getting my tests to work until I found this.

@lynndylanhurley What would you think about a PR with some sample feature and controller tests (RSpec / Capybara)?

jasonswett commented 9 years ago

Hmm, getting controller specs working has never really been a huge issue for me, as far as I can remember. This post of mine may be somewhat helpful: https://www.airpair.com/ruby-on-rails/posts/authentication-with-angularjs-and-ruby-on-rails

In general, I don't think I've had to treat my controller specs any differently than non-DTA apps. The post I linked doesn't specifically cover controller specs (just integration specs) but hopefully I can encourage a little bit of a Roger Bannister Effect just by commenting that it CAN successfully be done. I just follow the normal Devise docs for controller specs + Devise.

Marviel commented 9 years ago

Thank you so much @dash-rai... until I saw your Feb 27 post I was most confused!

albatrocity commented 9 years ago

and thanks to @bwillis for your Jan 8 post. Got me out of a jam!

betoharres commented 9 years ago

@lynndylanhurley first suggestion didn't work to me, but using the oficial devise wiki post did work to me. Just include the controller_macros.rb file in spec/support/ and require it in rails_helper.rb with it's configuration lines and you're ok to use in your controller_spec ^^ ps: just delete de ! in user.confirm! line to remove the warning ;] ps[2]: I was using a blank app to test the gem along with versionist and rails-api, if anyone is interested, here's my Gemfile.lock

My gems version

charlesdg commented 8 years ago

Hi there,

Someone managed to sign_in in a features spec?

thanks

theotherdon commented 8 years ago

Hey @charlesdg,

The devise sign_in helper didn't work for me last time I was using this (albeit a little while ago). Here's the helper method that I used in my specs: https://gist.github.com/donald-s/2034d290b6344d89adba

SunnyTam commented 8 years ago

This post is very helpful when write test case in controllers. Thanks

SunnyTam commented 8 years ago

I am suffering some issue while I try using

user_headers = user.create_new_auth_token
      @request.headers.merge!(user_headers)

anyone know what is the problem?

NoMethodError:
       undefined method `user' for nil:NilClass
     # /Users/XX/.rvm/gems/ruby-2.2.3@global/gems/devise_token_auth-0.1.36/lib/devise_token_auth/controllers/helpers.rb:115:in `current_user'
     # /Users/XX/.rvm/gems/ruby-2.2.3@global/gems/devise_token_auth-0.1.36/lib/devise_token_auth/controllers/helpers.rb:103:in `authenticate_user!'
     # ./spec/XX/v1/user_details_controller_spec.rb:13:in `block (3 levels) in <module:V1>'
SunnyTam commented 8 years ago

for the problem above, it is caused by warden is not included in rspec. To fix it use below

config.include Devise::TestHelpers, :type => :controller

Please refer to this for details https://github.com/plataformatec/devise/issues/3475

stephannv commented 8 years ago

Thanks, @TravisTam. work like a charm.

DoubtedGoat commented 8 years ago

I'm just starting to encounter this issue now. Using the snippet provided by @donald-s , I am still getting nil for current_user inside the controller. Does anyone have any further things to try, or explanations of what they did? Devise::TestHelpers is included in my rails_helper.rb.

ryaz commented 8 years ago

Very nice thread! Is there any way to have user logged in for feature tests with capybara, log in everytima is so slow... I was thinking about to set change_headers_on_each_request to false for test env and add token headers to each request.

dchersey commented 8 years ago

dash-rai's solution worked best for me. Very simple. I created a file spec/support/authentication_helper.rb containing

module AuthenticationHelper

  def authenticate_user user
    @request.env["devise.mapping"] = Devise.mappings[:user]
    @request.headers.merge! user.create_new_auth_token
    sign_in user
  end
end

and included it in my rails_helper.rb

  config.include Devise::TestHelpers, type: :controller
  config.include AuthenticationHelper, :type => :controller

make sure you

require 'devise'
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}

then you can

      let! (:user) {create(:user)}
      before do
        authenticate_user user
        post :create, {<etc>}}
      end

and your controller's

def create
  current_user != nil ? "YAY"
end
rahulnyk commented 8 years ago

Extremely valuable thread. The solution described by dchersey worked for me like a charm. Thanks :)

jotolo commented 8 years ago

Guys, you can use request specs.This is a simple solution to test the complete process:

    require 'rails_helper'

    RSpec.describe 'Authentication', type: :request do
      describe 'POST /auth (Sign Up process)' do
        it 'Should respond with status 200(OK)' do
          post user_registration_path(:email => 'email@email.com', :password => 'qwertyuiop')
          expect(response).to have_http_status(200)
        end

        it 'Should respond with status 200(OK)' do
          expect{
            post user_registration_path(:email => 'email@email.com', :password => 'qwertyuiop')
          }.to change(User, :count).by(1)
        end
      end

      describe 'POST /auth/confirmation (Confirmation process)' do
        it 'Should respond with status 302(URL redirection)' do
          # Sign Up
          post user_registration_path(:email => 'email@email.com', :password => 'qwertyuiop')
          # Email Confirmation
          user = User.last
          get user_confirmation_path(:config => 'default', :confirmation_token => user.confirmation_token, :redirect_url => '/')
          expect(response).to be_redirect
          #expect(response).to have_http_status(302)
        end
      end

      describe 'POST /auth/sign_in (Sign In process)' do
        it 'Should respond with status 200(OK)' do
          # Sign Up
          post user_registration_path(:email => 'email@email.com', :password => 'qwertyuiop')
          # Email Confirmation
          user = User.last
          get user_confirmation_path(:config => 'default', :confirmation_token => user.confirmation_token, :redirect_url => '/')
          #Sign In
          post user_session_path(:email => 'email@email.com', :password => 'qwertyuiop')
          expect(response).to be_success
          #expect(response).to have_http_status(200)
        end
      end
    end
CJYate commented 7 years ago

To add to @jotolo 's excellent post, here's a method of providing the necessary headers that will work well for general request testing without the need for the extra sign up / confirmation / login requests:

RSpec.describe 'User access', type: :request, focus: :true do
    context 'user not signed in' do
        describe 'GET #current' do
            it 'returns unauthorized status' do
                get current_users_path
                expect(response).to have_http_status(:unauthorized)
            end
        end
    end

    context 'user signed in' do
        describe 'GET #current' do
            it 'Should respond with status 200(OK)' do

                @user = FactoryGirl.create :user
                @user.confirm

                @auth_headers = @user.create_new_auth_token
                get current_users_path, params: {}, headers: @auth_headers
                expect(response).to have_http_status(:success)
            end
        end
    end
end
chansuke commented 7 years ago

@lynndylanhurley I prepared the macro for API authentication

module AuthenticationMacros
  def login_auth
    @user = create(:user)
    post '/api/v1/auth/sign_in', email: @user.email, password: @user.password, format: :json
    return {
            'Uid' => response.headers['Uid'],
            'Access-Token' => response.headers['Access-Token'],
            'Client' => response.headers['Client'],
    }
  end
end

and call it in request spec, but it doesn't works.

# coding: utf-8

require 'rails_helper'

RSpec.describe 'HogeApi', type: :request, autodoc: true do
  context "with auth" do
    describe "GET /api/v1/hoge" do
      it "should respond with 200(OK)" do
        get "/api/v1/event", {}, login_auth
        expect(response).to have_http_status(200)
      end
    end
  end
end

how can I make this work in request spec?

stephanebruckert commented 7 years ago

These tests are a good example https://github.com/Hawatel/rails5-api-starter/blob/master/spec/acceptance/auth_spec.rb

wasifhossain commented 7 years ago

@chansuke in order to make login_auth work in the request spec, first you need to place the module AuthenticationMacros inside spec/support/authentication_macros.rb and include it in rails_helper.rb like this:

RSpec.configure do |config|
  ...
  config.include AuthenticationMacros, type: :request
  ...
end

Lastly dont forget to require the files under spec/support by uncommenting the line:

Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
niinyarko commented 7 years ago

This is what worked for me. gist

blaze182 commented 7 years ago

Implemented another helper for request specs: auth helper for requests

lynndylanhurley commented 7 years ago

@blaze182 - that's an interesting approach. i'm going to try that out

alvarofernandoms commented 6 years ago

Thank you, @dchersey. Using the authentication_helper.rb and updating the rails_helper.rb and the block below (without the post method) inside my test describe, works just fine! Thanks!!!

let(:user_test) { create(:user) }
before(:each) do
  authenticate_user user_test
end