Closed mshappe closed 6 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.
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.
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.
@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.
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 :)
@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.
user
auth_headers = user.create_new_auth_token
# merge the headers into the request.headers object
request.headers.merge!(auth_headers)
# make the request
xhr :delete, :destroy, format: :json
# pass the auth headers as the last argument of the request
get '/demo/members_only', {}, auth_headers
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!
@jrogozen - does this guide help you out at all?
@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'
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
@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
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.
@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.
Ok, I can dig into the session_serializer exception a little more.
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.
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.
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!
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 :)
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
Nope, I'm already doing that and it doesn't work unless you use it in combination with the sign_in
method.
I NEVER use the sign_in
method, and it works for me.
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
What the output value without sign_in
. Take a look at log/test.log
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
This thread has been invaluable. The content really deserves a place in the readme or wiki.
@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)?
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.
Thank you so much @dash-rai... until I saw your Feb 27 post I was most confused!
and thanks to @bwillis for your Jan 8 post. Got me out of a jam!
@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
Hi there,
Someone managed to sign_in in a features spec?
thanks
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
This post is very helpful when write test case in controllers. Thanks
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>'
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
Thanks, @TravisTam. work like a charm.
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.
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.
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
Extremely valuable thread. The solution described by dchersey worked for me like a charm. Thanks :)
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
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
@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?
These tests are a good example https://github.com/Hawatel/rails5-api-starter/blob/master/spec/acceptance/auth_spec.rb
@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 }
Implemented another helper for request specs: auth helper for requests
@blaze182 - that's an interesting approach. i'm going to try that out
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
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.