I'm using RSpec to test my implementation of the mixture of Devise and the simple_token_authentication gem.
I using devise_for to tell Devise where to look for my sessions controller (source).
devise_for :users, controllers: {
registrations: 'users/devise/registrations',
sessions: 'users/devise/sessions'
}
In my sessions controller, I invoke a custom method if the request has a specific header (source).
def create
req_from_coposition_app? ? respond_with_auth_token : super
end
def destroy
req_from_coposition_app? ? destroy_auth_token : super
end
I have a test that ensures the user can get an auth token.
The request works as expected (source).
it "should be able to sign in" do
request.headers["X-Secret-App-Key"] = "this-is-a-mobile-app"
request.env['devise.mapping'] = Devise.mappings[:user]
post :create,
user: {
email: user.email,
password: user.password
},
format: :json
expect(res_hash[:email]).to eq user.email
expect(res_hash[:authentication_token]).to eq user.authentication_token
end
Next, I'd like to test signing out (destroying the auth key).
I am trying to use the following code:
it "should be able to sign out" do
token_before = user.authentication_token
request.env['devise.mapping'] = Devise.mappings[:user]
request.headers["X-Secret-App-Key"] = "this-is-a-mobile-app"
request.headers["X-User-Token"] = token_before
delete :destroy, nil, format: :json
expect(user.reload.authentication_token).to_not eq token_before
end
However, the Users::Devise::SessionsController#destroy is never hit.
response.status is 302, and
response.body
#=> "<html><body>You are being redirected.</body></html>"
I have checked rake routes, formatted the request in different ways, and checked that the delete method wasn't being messed with anywhere. I am completely at loss as to why post :create works, but delete :destroy doesn't hit the action.
You're not fooling the before filter on line 4 here https://github.com/plataformatec/devise/blob/master/app/controllers/devise/sessions_controller.rb
Devise thinks no one is signed in and is bouncing you away from the destroy action. Skip it, stub it, trick it.
Related
I'm trying to get a test to pass. The controller action I'm testing finishes with redirect_to request.referrer and it works in development.
In my controller test however I'm getting:
ActionController::ActionControllerError: Cannot redirect to nil!
Here's the test:
it 'should update a group' do
put :update, id: group1.id, group: { name: 'foo' }
expect(response).to be_success
end
I have other tests for the same controller that...
expect(response).to be_success
...and they pass, provided the action they test finishes by rendering or redirecting to something that is not request.referer.
I'm authenticating my user in that controller:
before_action :authenticate_user!
I have each example preceded with:
before(:each) do
sign_in user1
end
I'm configuring RSpec like so:
RSpec.configure do |config|
config.include Devise::Test::ControllerHelpers, type: :controller
end
From what I understand if I pass a headers hash to my test request (for example with 'HTTP_REFERER') it will overwrite the one from Devise. Is the solution here to properly set that hash?
I'm using rspec-rails (3.5.2), devise (4.2.0), rails (5.0.0.1)
I am trying to test that someone is able to login to my site by making a POST request to my SessionsController. I've seen this way recommended in a few places:
it 'must be able to sign in a user' do
user = create(:user)
post :create, format: :js, user: {email: user.email, password: user.password, remember_me: 0}
assert_response :success
#controller.current_user.must_equal user
end
But this test is not correct. Calling #controller.current_user will attempt to authenticate the user using the posted parameters and will return user if the supplied email/password is correct. There is no guarantee that the create action is actually calling sign_in or current_user.
Even if I re-write the test to check that these methods are called, it's possible that other methods could be called e.g. sign_out.
Is there a more definitive way to ultimately check if a user is logged in, and if so, who the user is?
EDIT -
For example, the following test will pass
it 'must sign in a user' do
#controller.current_user.must_equal nil
post :create, format: :js, user: {email: #user.email, password: #user.password, remember_me: 0}
assert_response :success
#controller.current_user.must_equal #user
end
when the SessionsController#create action is:
def create
respond_to do |format|
format.js {
render nothing: true, status: 200
}
end
end
Solution with minimal changes to proposed code in the question:
You need to initialize the system before the test starts. Try prepending following code before your it 'must be able to sign in a user' do code:
before (:each) do
user = FactoryGirl.create(:user)
sign_out user
end
This should turn your test into a valid test for your post controller.
Explanation:
My assumption is, that your test above always succeeds, because the user is already signed in (by other tests run before this one). You could verify this by using byebug in the line after it and run current_user in bybug's console. If it is not nil, the user is already signed in, which is invalidating your test.
Note, that (different from what is discussed above in the comments), current_user does not change the status of the user; it is a read-only function.
Shorter/cleaner solution:
In my opinion, there is a a cleaner way to perform such a test like follows:
def sign_in_via_post(user)
post :create, format: :js, user: {email: user.email, password: user.password, remember_me: 0}
end
...
before (:each) do
user = FactoryGirl.create(:user)
sign_out user
end
it 'must be able to sign in a user' do
{ sign_in_via_post user }.should change { current_user }.from(nil).to(user)
end
With the should change from nil to user statement, you verify, that the user was logged out before the test begins and that the user is logged in, after the test has been performed.
Note, that the part
{ sign_in_via_post user }.should change { current_user }.from(nil).to(user)
is equivalent to the (maybe easier to understand) code
{ sign_in_via_post user }.should change { user_signed_in? }.from(false).to(true)
as discussed here.
I've spent far too long messing with this before asking for help. I can't seem to get RSpec and Sorcery to play together nicely. I've read through the docs on Integration testing with Sorcery and can post the login action properly, but my tests still doesn't think the user is logged in.
# spec/controllers/user_controller_spec
describe 'user access' do
let (:user) { create(:user) }
before :each do
login_user(user[:email], user[:password])
end
it "should log in the user" do
controller.should be_logged_in
end
end
And my login_user method
# spec/support/sorcery_login
module Sorcery
module TestHelpers
module Rails
def login_user email, password
page.driver.post(sessions_path, { email: email , password: password, remember_me: false })
end
end
end
end
The sessions controller handles the pages properly when I use them on the generated pages just fine. I tried outputting the results of the login_user method and it appears to properly post the data. How do I persist this logged in user through the tests? Does a before :each block not work for this? I'm just not sure where it could be running wrong and I'm pretty new to testing/RSpec so I may be missing something obvious. I'd appreciate any help.
Here's the output of the failed tests:
1) UsersController user access should log in the user
Failure/Error: controller.should be_logged_in
expected logged_in? to return true, got false
I just went through this yesterday. Here's what I did, if it helps.
Sorcery provides a test helper login_user that relies on a #controller object being available. This works great in controller specs, but doesn't work in integration tests. So the workaround in integration tests is to write another method (like the one you have above) to simulate actually logging in via an HTTP request (essentially simulating submitting a form).
So my first thought is that you should try renaming your method to login_user_post or something else that doesn't collide with the built-in test helper.
Another potential gotcha is that it looks to me like the Sorcery helper assumes that your user's password is 'secret'.
Here's a link to the built-in helper so you can see what I'm talking about:
https://github.com/NoamB/sorcery/blob/master/lib/sorcery/test_helpers/rails.rb
Good luck - I really like this gem except for this part. It is really only fully explained by patching together SO posts. Here's the code I use:
Integration Helper
module Sorcery
module TestHelpers
module Rails
def login_user_post(user, password)
page.driver.post(sessions_url, { username: user, password: password})
end
def logout_user_get
page.driver.get(logout_url)
end
end
end
end
Integration Spec (where user needs to be logged in to do stuff)
before(:each) do
#user = create(:user)
login_user_post(#user.username, 'secret')
end
Controller Spec (where the regular login_user helper works fine)
before(:each) do
#user = create(:user)
login_user
end
Note that login_user doesn't need any arguments if you have an #user object with the password 'secret'.
Did you try adding to spec/spec_helpers.
RSpec.configure do |config|
# ...
config.include Sorcery::TestHelpers::Rails::Controller
end
Nota that you need to include Sorcery::TestHelpers::Rails::Controller, not just Sorcery::TestHelpers::Rails.
Then you will be able to login_user from any controller specs like:
describe CategoriesController do
before do
#user = FactoryGirl::create(:user)
end
describe "GET 'index'" do
it "returns http success" do
login_user
get 'index'
expect(response).to be_success
end
end
end
The way you pass a password is probably wrong. It may be encrypted at this point. In provided example I will try to do this at first:
describe 'user access' do
let (:user) { create(:user, password: 'secret') }
before :each do
login_user(user[:email], 'secret')
end
it "should log in the user" do
controller.should be_logged_in
end
end
This seems to be very poorly documented. The above solutions did not work for me. Here's how I got it to work:
Check your sessions_url. Make sure it is correct. Also, check what params are necessary to log in. It may be email, username, etc.
module Sorcery
module TestHelpers
module Rails
def login_user_post(email, password)
page.driver.post(sessions_url, { email:email, password: password })
end
end
end
end
RSpec config:
config.include Sorcery::TestHelpers::Rails
Spec helper:
def app
Capybara.app
end
spec/controllers/protected_resource_spec.rb:
describe UsersController do
before do
# Create user
# Login
response = login_user_post( user.email, :admin_password )
expect( response.headers[ 'location' ]).to eq 'http://test.host/'
# I test for login success here. Failure redirects to /sign_in.
#cookie = response.headers[ 'Set-Cookie' ]
end
specify 'Gets protected resource' do
get protected_resource, {}, { cookie:#cookie }
expect( last_response.status ).to eq 200
end
Following the Railscast on Devise and OmniAuth I have implemented an OmniauthCallbacksController < Devise::OmniauthCallbacksController which contains a single method to handle an OmniAuth callback:
def all
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted?
sign_in_and_redirect user
else
session["devise.user_attributes"] = user.attributes
redirect_to new_user_registration_url
end
end
alias_method :facebook, :all
routes.rb:
devise_for :users, controllers: {omniauth_callbacks: "omniauth_callbacks", :sessions => "sessions" }
I would like to customise this, so I'm trying to test it using RSpec. The question is how do I test this method and the redirects?
If in the spec I put user_omniauth_callback_path(:facebook) it doesn't complain about the route not existing, but doesn't seem to actually call the method.
According to this answer "controller tests use the four HTTP verbs (GET, POST, PUT, DELETE), regardless of whether your controller is RESTful." I tried get user_... etc. but here it does complain that the route doesn't exist. And indeed if I do rake routes it shows there is no HTTP verb for this route:
user_omniauth_callback [BLANK] /users/auth/:action/callback(.:format) omniauth_callbacks#(?-mix:facebook)
Can you see what I'm missing?
EDIT
So following this question one way of calling the method is:
controller.send(:all)
However I then run into the same error that the questioner ran into:
ActionController::RackDelegation#content_type delegated to #_response.content_type, but #_response is nil
You will need to do three things to get this accomplished.
enter OmniAuth test environment
create an OmniAuth test mock
stub out your from_omniauth method to return a user
Here is a possible solution, entered in the spec itself
(spec/feature/login_spec.rb for example) . . .
let(:current_user) { FactoryGirl.create(:user) }
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new({
provider: :facebook,
uid:'12345',
info: {
name: "Joe"
}
})
User.stub(:from_omniauth).and_return(current_user)
end
I adapted this from a google authentication, so facebook may require more fields, but those are the only ones required by omniauth docs. You should be able to find the correct fields by looking at your database schema and finding fields that match the documentation.
In my case, the minimum was enough to pass the request phase and move onto the stubbed out method returning my user.
This example also uses FactoryGirl.
It may not be perfect, but I hope it helps. Good luck!
-Dan
If you hit this and you are running rspec 3.4 this example should work for you:
describe Users::OmniauthCallbacksController, type: :controller do
let(:current_user) { FactoryGirl.create(:user) }
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:your_oauth_provider_here] = OmniAuth::AuthHash.new(
provider: :your_oauth_provider_here,
uid: rand(5**10),
credentials: { token: ENV['CLIENT_ID'], secret: ENV['CLIENT_SECRET'] }
)
request.env['devise.mapping'] = Devise.mappings[:user]
allow(#controller).to receive(:env) { { 'omniauth.auth' => OmniAuth.config.mock_auth[:your_oauth_provider_here] } }
allow(User).to receive(:from_omniauth) { current_user }
end
describe '#your_oauth_provider_here' do
context 'new user' do
before { get :your_oauth_provider_here }
it 'authenticate user' do
expect(warden.authenticated?(:user)).to be_truthy
end
it 'set current_user' do
expect(current_user).not_to be_nil
end
it 'redirect to root_path' do
expect(response).to redirect_to(root_path)
end
end
end
end
I am experiencing problem for writhing RSpec for OmniauthCallbacksController, do some research on this and it working for me. Here is my codes, if anyone found necessary. Tests are for happy path and it should work for news version of RSpec eg. 3.x
require 'spec_helper'
describe OmniauthCallbacksController, type: :controller do
describe "#linkedin" do
let(:current_user) { Fabricate(:user) }
before(:each) do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:linkedin] = OmniAuth::AuthHash.new({provider: :linkedin, uid: '12345', credentials: {token: 'linkedin-token', secret: 'linkedin-secret'}})
request.env["devise.mapping"] = Devise.mappings[:user]
#controller.stub!(:env).and_return({"omniauth.auth" => OmniAuth.config.mock_auth[:linkedin]})
User.stub(:from_auth).and_return(current_user)
end
describe "#linkedin" do
context "with a new linkedin user" do
before { get :linkedin }
it "authenticate user" do
expect(warden.authenticated?(:user)).to be_truthy
end
it "set current_user" do
expect(subject.current_user).not_to be_nil
end
it "redirect to root_path" do
expect(response).to redirect_to(root_path)
end
end
end
end
end
I'm trying to get RSpec working for a simple scaffolded app, starting with the rspec scaffold tests.
Per the devise wiki, I have added the various devise config entries, a factory for a user and an admin, and the first things I do in my spec controller is login_admin.
Weirdest thing, though... all my specs fail UNLESS I add the following statement right after the it ... do line:
dummy=subject.current_user.inspect
(With the line, as shown below, the specs pass. Without that line, all tests fail with the assigns being nil instead of the expected value. I only happened to discover that when I was putting some puts statements to see if the current_user was being set correctly.)
So what it acts like is that dummy statement somehow 'forces' the current_user to be loaded or refreshed or recognized.
Can anyone explain what's going on, and what I should be doing differently so I don't need the dummy statement?
#specs/controllers/brokers_controller_spec.rb
describe BrokersController do
login_admin
def valid_attributes
{:name => "Bill", :email => "rspec_broker#example.com", :company => "Example Inc", :community_id => 1}
end
def valid_session
{}
end
describe "GET index" do
it "assigns all brokers as #brokers" do
dummy=subject.current_user.inspect # ALL SPECS FAIL WITHOUT THIS LINE!
broker = Broker.create! valid_attributes
get :index, {}, valid_session
assigns(:brokers).should eq([broker])
end
end
describe "GET show" do
it "assigns the requested broker as #broker" do
dummy=subject.current_user.inspect # ALL SPECS FAIL WITHOUT THIS LINE!
broker = Broker.create! valid_attributes
get :show, {:id => broker.to_param}, valid_session
assigns(:broker).should eq(broker)
end
end
and per the devise wiki here is how I login a :user or :admin
#spec/support/controller_macros.rb
module ControllerMacros
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
sign_in Factory.create(:admin) # Using factory girl as an example
end
end
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = Factory.create(:user)
user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the confirmable module
sign_in user
end
end
end
What a struggle! Thank you Robin, I've been googling on this for hours and finally saw your post; now my controller tests are working :)
To add to your answer, I figured out how to get the devise session into the valid_session hash, which allows the controller tests to run properly as generated by rails.
def valid_session
{"warden.user.user.key" => session["warden.user.user.key"]}
end
In your tests, there is the following code:
def valid_session
{}
end
...
get :index, {}, valid_session
Because of this 'session' variable, the "log_in" that you did is essentially not being used during the 'get'.
The way that I solved it was to remove all of the "valid_session" arguments to the get, post, put, delete calls in that controller's spec. The example above becomes:
get :index, {}
I suspect that there's a way to add the devise's session to the "valid_session" hash, but I don't know what it is.
Thanks for this solution.
If you are using a different Devise model, the session id also changes.
For a model Administrator use the following:
def valid_session
{'warden.user.administrator.key' => session['warden.user.administrator.key']}
end