Devise Lockable not working in rspec controller tests - ruby-on-rails

We have a custom SessionsController that inherits from the standard Devise::SessionsController and have lockable enabled for the User model. This works when testing manually, but in our controller tests failed_attempts is not incrementing beyond 1. If I reduce maximum_attempts attempts to 1 it will successfully lock the account in testing, but it still will not increment failed_attempts beyond 1.
Below is my test example. Any ideas as to why failed_attempts is not incrementing beyond one 1 controller tests?
it{
bad_user = create(:user, password: 'passworD1')
3.times do
post :create, user: { email: bad_user.email, password: 'asdf' }
end
post :create, user: { email: bad_user.email, password: 'asdf' }
bad_user.reload
expect(bad_user.failed_attempts).to eq(4)
expect(bad_user.locked_at).not_to be_blank
}

I tried this method warden.clear_strategies_cache! after post and I was able to lock the account.
For your example it would look like this:
it{
bad_user = create(:user, password: 'passworD1')
3.times do
post :create, user: { email: bad_user.email, password: 'asdf' }
warden.clear_strategies_cache!
end
post :create, user: { email: bad_user.email, password: 'asdf' }
bad_user.reload
expect(bad_user.failed_attempts).to eq(4)
expect(bad_user.locked_at).not_to be_blank
}
Regards,
Ruslan

Per Devise lockable module
There is a method lock_access! which locks access. That's one way to test another - brute force. Enter right email and wrong password at new_user_session_path as many time as needed per devise initializer then test new_user_unlock_path.

Related

Railstutorial | Chapter 10 | Exercise 10.4.1.2 | Why does abritrary password setting work?

Description of the Exercise: Railstutorial Exercise 10.4.1.2
The exercise: 'FILL_IN' has to be replaced with the proper code, so that the test is working
test "should not allow the admin attribute to be edited via the web" do
log_in_as(#other_user)
assert_not #other_user.admin?
patch user_path(#other_user), params: {
user: { password: FILL_IN,
password_confirmation: FILL_IN,
admin: FILL_IN} }
assert_not #other_user.FILL_IN.admin?
end
My solution:
test "should not allow the admin attribute to be edited via the web" do
log_in_as(#other_user)
assert_not #other_user.admin?
patch user_path(#other_user), params: {
user: { password: 'password',
password_confirmation: 'password',
admin: true } }
assert_not #other_user.reload.admin?
end
My Question(s):
If you have set the ':admin' attribute to the list of permitted parameters in user_params (in the UsersController class) the test turns 'Red' as it is supposed to. What I don't understand though, is that you can set random passwords and the test is still working properly, like so:
test "should not allow the admin attribute to be edited via the web" do
log_in_as(#other_user)
assert_not #other_user.admin?
patch user_path(#other_user), params: {
user: { password: 'foobar',
password_confirmation: 'foobar',
admin: true } }
assert_not #other_user.reload.admin?
end
Shouldn't the only valid option be 'password' (and not 'foobar' or even '' (i.e. blank)), since the instance variable #other_user contains the values of the User 'archer' from the fixtures file 'users.yml', who has 'password' as a password_digest? Wouldn't it result in a mismatch between the two attributes password_digest(='password') and password(='foobar')? Or is the 'password_digest' attribute from 'archer' somehow updated as well? If so, how does it work?
And why does the test turn 'Green', if you type in an invalid password, like:
test "should not allow the admin attribute to be edited via the web" do
log_in_as(#other_user)
assert_not #other_user.admin?
patch user_path(#other_user), params: {
user: { password: 'foo',
password_confirmation: 'bar',
admin: true } }
assert_not #other_user.reload.admin?
end
Could it be that the 'patch' request aborts due to the incorrect password input and thus also fails to update the admin status? Is that the reason why the test is 'Green' (since admin is still 'nil')?
I already looked up the reference implementation of the sample application by Michael Hartl (https://bitbucket.org/railstutorial/sample_app_4th_ed), but unfortunately he does not provide any code relating to the exercises.
Thanks for help!
In Chapter 10.1, for the update tests, we allowed empty password for tests on the User Controller :
class User < ApplicationRecord
.
.
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
.
.
end
As mentionned in the same chapter, the password is secured with the has_secure_password helper for the online use.
Hope I helped

Rails test - checking if user is signed in with Devise

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.

Why does my test return a nil class error on an attribute it shouldn't?

I am trying to write a test for my InvitationsController#Create.
This is a POST http action.
Basically what should happen is, once the post#create is first executed, the first thing that needs to do is we need to check to see if a User exists in the system for the email passed in via params[:email] on the Post request.
I am having a hard time wrapping my head around how I do this.
I will refactor later, but first I want to get the test functionality working.
This is what I have:
describe 'POST #create' do
context 'when invited user IS an existing user' do
before :each do
#users = [
attributes_for(:user),
attributes_for(:user),
attributes_for(:user)
]
end
it 'correctly finds User record of invited user' do
post :create, { email: #users.first[:email] }
expect(response).to include(#users.first[:email])
end
end
end
This is the error I get:
1) Users::InvitationsController POST #create when invited user IS an existing user correctly finds User record of invited user
Failure/Error: post :create, { email: #users.first[:email] }
NoMethodError:
undefined method `name' for nil:NilClass
##myapp/gems/devise-3.2.4/app/controllers/devise_controller.rb:22:in 'resource_name'
# #myapp/gems/devise_invitable-1.3.6/lib/devise_invitable/controllers/helpers.rb:18:in 'authenticate_inviter!'
# #myapp/gems/devise_invitable-1.3.6/app/controllers/devise/invitations_controller.rb:67:in 'current_inviter'
# #myapp/gems/devise_invitable-1.3.6/app/controllers/devise/invitations_controller.rb:71:in 'has_invitations_left?'
I am using FactoryGirl and it works perfectly, in the sense that it returns valid data for all the data-types. The issue here is how do I get RSpec to actually test for the functionality I need.
Edit 1
Added my :user factory:
FactoryGirl.define do
factory :user do
association :family_tree
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
email { Faker::Internet.email }
password "password123"
password_confirmation "password123"
bio { Faker::Lorem.paragraph }
invitation_relation { Faker::Lorem.word }
# required if the Devise Confirmable module is used
confirmed_at Time.now
gender 1
end
end
It seems you're using Devise which require you to be logged in before going to the next step. On your error, Devise cannot get the same of your inviter because he's not logged.
Your test should be like this:
describe 'POST #create' do
context 'when invited user IS an existing user' do
before :each do
#users = [
attributes_for(:user),
attributes_for(:user),
attributes_for(:user)
]
#another_user = FactoryGirl.create(:user_for_login)
sign_in #another_user
end
it 'correctly finds User record of invited user' do
post :create, { email: #users.first[:email] }
expect(response).to include(#users.first[:email])
end
end
end
Example for FactoryGirl model for Devise
factory :user_for_login, class: User do |u|
u.email 'admin#myawesomeapp.com'
u.password 'password'
u.password_confirmation 'password'
u.name "MyName"
end
Of course, you need to add as much data as your validators want.. Basically for Devise you need email, password and password_confirmation. In you case, it seems you also need name.

Simulating a login attempt with incorrect password in an RSpec controller spec raises BCrypt::Errors::InvalidHash

The SessionController spec for an app I'm working on currently looks like this:
require 'rails_helper'
RSpec.describe SessionController do
let(:user) { create(:user, phone_verified: true, email_verified: true) }
describe "POST #create" do
context "with valid username and password" do
before do
post :create, user: { username: user.username, password: user.password }
end
specify { expect(response).to redirect_to(dashboard_path) }
specify { expect(session[:user_id]).to eq(user.id) }
end
context "with invalid username" do
before do
post :create, user: { username: "doesntexist", password: user.password }
end
specify { expect(response).to render_template(“login”) }
specify { expect(session[:user_id]).to be_nil }
end
context "with invalid password" do
before do
post :create, user: { username: user.username, password: "badpassword" }
end
specify { expect(response).to render_template(“login”) }
specify { expect(session[:user_id]).to be_nil }
end
end
end
The first two context blocks work exactly as expected. The examples in the third context block fail:
2) SessionController POST #create with invalid password
Failure/Error: post :create, user: { username: user.username, password: "badpassword" }
BCrypt::Errors::InvalidHash:
invalid hash
From what I've been able to find out on StackOverflow and by looking at the bcrypt-ruby source code, this means the password digest stored in the database is invalid. Since all three blocks are using the same let block to create the user, I'm at a loss as to why this would happen with an incorrect password and not a correct one. It also works normally when I start up rails s and try to log in through the view. Does anyone have any idea what is happening?
We're using the built-in has_secure_password method, and we're not using devise or any other authentication-related gems.
Resetting the database fixed this problem, although it's still not clear to me what caused it or why it only happened under such specific circumstances.

How do I mock models, instances and helpers in a controller spec?

require 'spec_helper'
describe UsersController do
let(:user){ double(User, id: 2, name: "Jimbo", email: 'jimbo#email.com', password: 'passwordhuzzah', password_confirmation: 'passwordhuzzah') }
before do
mock_model("User")
end
describe 'PATCH #update' do
User.should_receive(:find).with(user.id.to_s).and_return user
user.should_receive(:update_attributes).with({ "email" => user.email, "name" => user.name, "password" => user.password, "password_confirmation" => user.password_confirmation })#.and_return true
patch :update, id: user.id, user: { email: user.email, name: user.name, password: user.password, password_confirmation: user.password_confirmation }
flash[:error].should == "could not update user"
response.status.should == 200
end
end
codebase:
def update
#user = User.find(params[:id])
if #user.update_attributes(user_params)
redirect_to #user, flash: { success: 'succesfully updated user' }
else
flash.now[:error] = "could not update user"
render 'edit'
end
end
While the above spec passes (and passes in just 0.05 seconds!) am I doing it correctly? With the mocks above the request, and the 'normal' expectations below it? It seems a bit clumsy. Not only is it hard to read, but if one expectation fails, all of them will appear to fail.
What makes me think I'm doing it wrong is a weird error I'm getting. See the second line of the describe block, where I'm saying the instance of user (user) should have its update_attributes triggered with updated attributes? Note the and_return true method I've commented out. When it's chained on, running the above spec hits me with this stinker:
1) UsersController should_receive
Failure/Error: patch :update, id: user.id, user: { email: user.email, name: user.name, password: user.password, password_confirmation: user.password_confirmation }
NoMethodError:
undefined method `model_name' for RSpec::Mocks::Mock:Class
While this a over my head, I think it's because the user 'instance' isn't actually an instance, just a hash. I thought because I used mock_model and the double inherits from that mocked model, all of the active-record-y stuff such as the 'model_name' method would be set.
Anyway, how should I write this spec properly? I don't want to use FactoryGirl, I want to keep it all contained so my specs are fast and accurately report a fault.
Yes, in this case of controller testing, your basic flow is "normal", except that your innermost code needs to be within an it block (and perhaps is and is just not transcribed properly).
However, you're not making use of the mock_model call, since you're not doing anything with the result. This method doesn't make any fundamental changes to the class whose string name you pass it, it simply creates a mock object that simulates an ActiveRecord instance, which in your case you effectively discard. Like all RSpec doubles, the first parameter is just giving it a name that can be used for error messages and such.
So yes, the reason you're getting the error when you return true is that redirect_to expects #user to be an ActiveRecord instance and while it's not just a Hash as you suggest it might be, it does not have the model_name method it needs.
There are lots of ways to rewrite this, particularly if you want to support both the success and failure cases, but one way that minimizes the changes is (not fully tested):
describe UsersController do
let(:user){ mock_model(User, id: 2, name: "Jimbo", email: 'jimbo#email.com', password: 'passwordhuzzah', password_confirmation: 'passwordhuzzah') }
describe 'PATCH #update' do
it "should fail in this case" do
User.should_receive(:find).with(user.id.to_s).and_return user
user.should_receive(:update_attributes).with({ "email" => user.email, "name" => user.name, "password" => user.password, "password_confirmation" => user.password_confirmation })#.and_return true
patch :update, id: user.id, user: { email: user.email, name: user.name, password: user.password, password_confirmation: user.password_confirmation }
flash[:error].should == "could not update user"
response.status.should == 200
end
end
end

Resources