I have devise authentication and registration set up on my Rails app. I'm using after_sign_in_path_for() to customise the redirect when the user signs in based on various scenarios.
What I'm asking is how to test this method? It seems hard to isolate since it is called automatically by Devise when the user signes in. I want to do something like this:
describe ApplicationController do
describe "after_sign_in_path_for" do
before :each do
#user = Factory :user
#listing = Factory :listing
sign_in #user
end
describe "with listing_id on the session" do
before :each do
session[:listing_id] = #listing.id
end
describe "and a user in one team" do
it "should save the listing from the session" do
expect {
ApplicationController.new.after_sign_in_path_for(#user)
}.to change(ListingStore, :count).by(1)
end
it "should return the path to the users team page" do
ApplicationController.new.after_sign_in_path_for(#user).should eq team_path(#user.team)
end
end
end
end
end
but that's obviously not the way to do it because I just get an error:
Failure/Error: ApplicationController.new.after_sign_in_path_for(#user)
RuntimeError:
ActionController::Metal#session delegated to #_request.session, but #_request is nil: #<ApplicationController:0x00000104581c68 #_routes=nil, #_action_has_layout=true, #_view_context_class=nil, #_headers={"Content-Type"=>"text/html"}, #_status=200, #_request=nil, #_response=nil>
So, how can I test this method?
Oddly, I was wondering this very thing today. Here's what I came up with. I created an anonymous subclass of ApplicationController. In this anonymous subclass, I exposed the protected methods that I wanted to test as public methods. Then I tested them directly.
describe ApplicationController do
controller do
def after_sign_in_path_for(resource)
super resource
end
end
before (:each) do
#user = FactoryGirl.create(:user)
end
describe "After sigin-in" do
it "redirects to the /jobs page" do
controller.after_sign_in_path_for(#user).should == jobs_path
end
end
end
On a similar note - if you want to test the redirect after sign-up, you have two options.
First, you can follow a pattern similar to above and very directly test the method in RegistrationsController:
require 'spec_helper'
describe RegistrationsController do
controller(RegistrationsController) do
def after_sign_up_path_for(resource)
super resource
end
end
describe "After sign-up" do
it "redirects to the /organizations/new page" do
#user = FactoryGirl.build(:user)
controller.after_sign_up_path_for(#user).should == new_organization_path
end
end
end
Or, you can take a more integration-testing sort of approach and do the following:
require 'spec_helper'
describe RegistrationsController do
describe "After successfully completing the sign-up form" do
before do
#request.env["devise.mapping"] = Devise.mappings[:user]
end
it "redirects to the new organization page" do
post :create, :user => {"name" => "Test User", "email" => "test#example.com", "password" => "please"}
response.should redirect_to(new_organization_path)
end
end
end
For the newcomers, I would recommend doing this way:
RSpec.describe ApplicationController, type: :controller do
let(:user) { create :user }
describe "After sing-in" do
it "redirects to the /yourpath/ home page" do
expect(subject.after_sign_in_path_for(user)).to eq(yourpath_root_path)
end
end
end
I found this answer through Google recently and thought I would add my solution. I didn't like the accepted answer because it was testing the return value of a method on the application controller vs testing the desired behavior of the app.
I ended up just testing the call to create a new sessions as a request spec.
RSpec.describe "Sessions", type: :request do
it "redirects to the internal home page" do
user = FactoryBot.create(:user, password: 'password 123', password_confirmation: 'password 123')
post user_session_path, params: {user: {email: user.email, password: 'password 123'}}
expect(response).to redirect_to(internal_home_index_path)
end
end
(Rails 5, Devise 4, RSpec 3)
context "without previous page" do
before do
Factory.create(:user, email: "junior#example.com", password: "123456", password_confirmation: "123456")
request.env["devise.mapping"] = Devise.mappings[:user]
post :create, user: { email: "junior#example.com", password: "123456" }
end
end
it { response.should redirect_to(root_path) }
context "with previous page" do
before do
Factory.create(:user, email: "junior#example.com", password: "123456", password_confirmation: "123456")
request.env["devise.mapping"] = Devise.mappings[:user]
request.env['HTTP_REFERER'] = 'http://test.com/restaurants'
post :create, user: { email: "junior#example.com", password: "123456" }
end
it { response.should redirect_to("http://test.com/restaurants") }
end
Related
I'm facing the problem with logging in rspec with selected user. I've tried making controller module like that:
module ControllerMacros
def login(user)
before(:each) do
#request.env['devise.mapping'] = Devise.mappings[:user]
payload = { jti: SecureRandom.uuid, sub: user.id.to_s }
cookies['access_token'] = JWT.encode(payload, ENV['DEVISE_JWT_SECRET_KEY'], 'HS256')
sign_in user
end
end
end
The issue is that I'm either unbale to pass user in situation like that:
context 'as admin' do
let(:user) { create :user, :super_admin }
login user
before do
get :index
end
it { expect(response).to be_ok }
end
and i get:
Or if I try something like that:
context 'as admin' do
let(:user) { create :user, :super_admin }
before do
login user
get :index
end
it { expect(response).to be_ok }
end
I get:
How can I make it work?
you have created login helper with before(:each) block
In the first case, where you are calling login helper outside the it or before block and passing user instance created using let. here user is not available as scope of let variables is inside the it or before block.
In the second case, where you are calling login helper inside the before block, but login helper also adds before(:each) block. I suspect due to calling of before(:each) within before it raises the error. similar issue reported here
Possible solutions
create user inside the login helper and call it outside the it block
module ControllerMacros
def login
before(:each) do
user = create :user, :super_admin
#request.env['devise.mapping'] = Devise.mappings[:user]
payload = { jti: SecureRandom.uuid, sub: user.id.to_s }
cookies['access_token'] = JWT.encode(payload, ENV['DEVISE_JWT_SECRET_KEY'], 'HS256')
sign_in user
end
end
end
context 'as admin' do
login
before do
get :index
end
it { expect(response).to be_ok }
end
Remove before(:each) block from login helper
module ControllerMacros
def login(user)
#request.env['devise.mapping'] = Devise.mappings[:user]
payload = { jti: SecureRandom.uuid, sub: user.id.to_s }
cookies['access_token'] = JWT.encode(payload, ENV['DEVISE_JWT_SECRET_KEY'], 'HS256')
sign_in user
end
end
context 'as admin' do
let(:user) { create :user, :super_admin }
before do
login user
get :index
end
it { expect(response).to be_ok }
end
I've got custom member_action in my Active Admin panel which is responsible for resending devise reset password instructions.
admin/users.rb
ActiveAdmin.register User do
member_action :reset_password do
user = User.find(params[:id])
user.send_reset_password_instructions
redirect_to(admin_user_path(user),
notice: "Password reset email sent to #{user.email}")
end
end
How to write RSpec tests for such an action? The only thing I found is this one and I think it's not quite related to my problem.
I was trying to sth like below:
require 'rails_helper'
describe Admin::UsersController, type: :controller do
include Devise::TestHelpers
let!(:admin) { create(:admin_user) }
before(:each) do
sign_in admin
end
describe 'GET user' do
let(:user) { create(:user, :random_email) }
before(:each) do
User.should_receive(:find).at_least(:once).and_return(user)
get :show
end
it 'sends email' do
get :reset_password
expect(user).should_receive(:send_reset_password_instructions)
end
end
end
But I'm getting an error:
ActionController::UrlGenerationError:
No route matches {:action=>"reset_password", :controller=>"admin/users"}
Personally I prefer to use a feature test, since when using active admin, UI stuff handle by the framework:
RSpec.feature 'Reset Password', type: :feature do
let(:user) { create :user }
before do
login_as(user, scope: :user)
end
scenario 'can delete future episode' do
visit some_path
click_link 'Reset Password'
expect(page.current_path).to eq(admin_user_path(user))
expect(page).to have_content("Password reset email sent to #{user.email}")
end
end
Ok, it turns out small adjustments (pass the user.id in params) make the trick.
describe Admin::UsersController, type: :controller do
include Devise::Test::ControllerHelpers
before { sign_in admin }
let!(:admin) { create(:admin_user) }
describe 'GET user' do
let(:user) { create(:user, :random_email) }
before do
allow(User).to receive(:find).at_least(:once) { user }
get :show, params: { id: user.id }
end
it 'sends email' do
get :reset_password, params: { id: user.id }
expect(flash[:notice]).to match("Password reset email sent to #{user.email}")
end
end
end
I am currently using RSpec to test my Rails 4 application and when testing, I found this strange problem: subject.current_user is nil in the second method in a context. Code snippet:
describe 'GET #register_as_team:' do
context 'user logged in but not admin:' do
login_user
it 'should redirect_to user_path if user is not student' do
get :register_as_team, id: subject.current_user.id
expect(response).to redirect_to(user_path(subject.current_user))
expect(flash[:danger]).not_to be_nil
end
it 'should redirect_to student_path if user is a non-pending student' do
student = FactoryGirl.create(:student, user: subject.current_user, is_pending: false)
get :register_as_team, id: subject.current_user.id
expect(response).to redirect_to(student_path(student))
end
end
end
So when subject.current_user is used first time, it is OK and I can just get the logged user but in the second method it returns nil.
For background information, login_user is like this:
module ControllerMacros
def login_user(user = nil)
before(:each) do
# #request.env["devise.mapping"] = Devise.mappings[:user]
user ||= User.find_by(email: 'default_user#controller.spec')
user ||= FactoryGirl.create(:user, email: 'default_user#controller.spec', uid: 'default_user.controller.spec')
sign_in user
end
end
end
In an example, subject can only be resolved once.
When you did, get :register_as_team, id: subject.current_user.id, you essentially resolved subject already and subject.current_user is not resolved in next line.
Try this:
describe 'GET #register_as_team:' do
context 'user logged in but not admin:' do
login_user
it 'should redirect_to user_path if user is not student' do
user = subject.current_user
get :register_as_team, id: user.id
expect(response).to redirect_to(user_path(user))
expect(flash[:danger]).not_to be_nil
end
it 'should redirect_to student_path if user is a non-pending student' do
student = FactoryGirl.create(:student, user: subject.current_user, is_pending: false)
user = subject.current_user
get :register_as_team, id: user.id
expect(response).to redirect_to(student_path(student))
end
end
I am using Devise for my user logins and stuff and rspec for testing. I have looked at the Devise testing guide for rspec and mixined ControllerMicros to controller specs.
And actually things are all working fine if I have tests organized like this:
describe 'GET #index' do
context 'user logged in but not admin' do
login_user
it 'should redirect to root_path for non_user' do
get :index
// I have asserted that the current_user here is not nil
expect(response).to redirect_to(root_path)
end
end
end
However, if I have 2 tests in the context and I got current_user is nil for the non-first test.
describe 'GET #index' do
context 'user logged in but not admin' do
login_user
it 'should redirect to root_path for non_user' do
get :index
// I have asserted that the current_user here is not nil
expect(response).to redirect_to(root_path)
end
it 'should do some other thing' do
get :index
// the current_user method returns nil here
expect(response).to redirect_to(root_path)
end
end
end
And the worst part is that it seems this problem is not deterministic: happens somewhat randomly--cause after several failed runs the suite just passed on my computer(but still fails on Travis my build)
Some additional information:
the ControllerMacro.rb
module ControllerMacros
def login_admin
before(:each) do
# #request.env["devise.mapping"] = Devise.mappings[:user]
user = User.find_by(email: 'default_admin#controller.spec')
user ||= FactoryGirl.create(:user, email: 'default_admin#controller.spec', uid: 'default_admin.controller.spec')
admin = Admin.find_by(user_id: user.id)
FactoryGirl.create(:admin, user: user) if not admin
sign_in user
end
end
def login_user(user = nil)
before(:each) do
# #request.env["devise.mapping"] = Devise.mappings[:user]
user ||= User.find_by(email: 'default_user#controller.spec')
user ||= FactoryGirl.create(:user, email: 'default_user#controller.spec', uid: 'default_user.controller.spec')
sign_in user
end
end
end
the rails_helper.rb
RSpec.configure do |config|
# for loading devise in test
config.include Devise::TestHelpers, :type => :controller
config.extend ControllerMacros, :type => :controller
end
Your login_user method is run when the test suite load, you should put it in a before :each block to run it once for each test.
describe "GET index" do
before do
login_user
end
it 'blabla' do
get :index
expect(response).to redirect_to(root_path)
end
end
PS : Don't know what you do in your login_user method, but Devise have some nice helpers you can include as follow
#rails_helper.rb
RSpec.configure do |config|
config.include Devise::TestHelpers, type: :controller
end
#then in you test
before do
sign_in user_instance
end
UPDATE from comment
If you have multiple type of user / devise login entry, maybe try to specify the devise mapping you're trying to sign in the user to , as follow :
sign_in :user, user_instance
sign_in :admin, admin_user_instance
In the devise documentation they give tips on how you can have access to current_user when testing a controller:
https://github.com/plataformatec/devise/wiki/How-To:-Test-controllers-with-Rails-3-and-4-%28and-RSpec%29
However, what about when doing a feature test? I am trying to test a create method of one of my controllers, and in that controller is used the current_user variable.
The problem is that the macro suggested in devise uses the #request variable, and it is nil for a feature spec. What is a workaround?
EDIT:
This is what I have so far for my current spec:
feature 'As a user I manage the orders of the system' do
scenario 'User is logged in ad an admin' do
user = create(:user)
order = create(:order, user: user)
visit orders_path
#Expectations
end
end
The problem is that in my OrdersController I have a current_user.orders call, and since current_user is not defined, it will redirect me to /users/sign_in.
I have defined this under /spec/features/manage_orders.rb
from https://github.com/plataformatec/devise/wiki/How-To:-Test-controllers-with-Rails-3-and-4-%28and-RSpec%29
if i have understood you right, maybe you need to use
subject.current_user.email
#or
controller.current_user.email
for example :
describe OrdersController, :type => :controller do
login_user
describe "POST 'create'" do
it "with valid parametres" do
post 'create', title: 'example order', email: subject.current_user.email
end
end
end
controller_macros.rb :
module ControllerMacros
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryGirl.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
Don't forget to include this into your spec_helper.rb :
config.include Devise::TestHelpers, type: :controller
config.extend ControllerMacros, type: :controller
Here's what I think you are looking for:
require 'spec_helper'
include Warden::Test::Helpers
Warden.test_mode!
feature 'As a user I manage the orders of the system' do
scenario 'User is logged in ad an admin' do
user = create(:user)
login_as(user, scope: :user)
order = create(:order, user: user)
visit orders_path
#Expectations
end
end
you can define login_user as a method for the user to login as follows (put it in support folder):
def login_user
Warden.test_mode!
user = create(:user)
login_as user, :scope => :user
user.confirmed_at = Time.now
user.confirm!
user.save
user
end
Then in the scenario say:
user = login_user