2

I've put together a basic application with user authentication using bcrypt-ruby and has_secure_password. The result is essentially a barebones version of the application from the Rails Tutorial. In other words, I have a RESTful user model as well as sign-in and sign-out functionality.

As part of the tests for editing a user's information, I've written a test for changing a password. Whereas changing the password works just fine in the browser, my test below is not passing.

subject { page }

describe "successful password change"
  let(:new_password) { "foobaz" }
  before do
    fill_in "Password",               with: new_password
    fill_in "Password Confirmation",  with: new_password
    click_button "Save changes"
  end

  specify { user.reload.password.should == new_password }
end

Clearly, I'm misunderstanding some basic detail here.

In short:

1) Why exactly is the code above not working? The change-password functionality works in the browser. Meanwhile, rspec continues to reload the old password in the last line above. And then the test fails.

2) What is the better way to test the password change?

Edit:

With the initial password set to foobar, the error message is:

Failure/Error: specify { user.reload.password.should == new_password }
   expected: "foobaz"
        got: "foobar" (using ==)

Basically, it looks like the before block is not actually saving the new password.

For reference, the related controller action is as follows:

def update
  @user = User.find(params[:id])
  if @user.update_attributes(params[:user])
    flash[:success] = "Profile Updated"
    sign_in @user
    redirect_to root_path
  else
    render 'edit'
  end
end
enocom
  • 1,496
  • 1
  • 15
  • 22
  • I notice that `user` in the spec is a local variable - where have you defined it? Did you use a `let` for that too? – ian Oct 17 '12 at 03:14
  • @lain Yes, just a few lines above I've used `let` to create a user with `FactoryGirl`. – enocom Oct 17 '12 at 03:52
  • @veritas1 I've added the error message for reference. – enocom Oct 17 '12 at 03:53
  • For anyone interested, there is an answer to a similar and older question [here](http://stackoverflow.com/questions/10626831/rspec-doesnt-reload-changed-password-attribute-why), which doesn't seem to work in my case. – enocom Oct 17 '12 at 09:10
  • And yet another similar question [here](http://stackoverflow.com/questions/8647491/app-works-but-in-rspec-test-fails). Maybe this is an rspec bug?? – enocom Oct 19 '12 at 04:53
  • Have you tried `let!` rather than `let` to make sure that the `new_password` is available in the before block. – nmott Oct 19 '12 at 11:18
  • @nmott Yes, I've tried that as well. Still the same result. – enocom Oct 19 '12 at 12:42
  • Have you tried putting all the steps into an it block, rather than the before block to see works there? Have you tried save_and_open_page before the click button to see if the password field is being filled in with the expected password? – nmott Oct 20 '12 at 09:58
  • @nmott Those are good ideas. Unfortunately, the test still fails with each suggestion. I'm thinking this has to be a funny little quirk in using ```rspec``` with ```has_secure_password```. – enocom Oct 21 '12 at 04:12

3 Answers3

9

For Devise users, use #valid_password? instead:

expect(user.valid_password?('correct_password')).to be(true)

Credit: Ryan Bigg

Community
  • 1
  • 1
Franklin Yu
  • 8,920
  • 6
  • 43
  • 57
  • If you are rendering after the password change, you will, as in the other examples, need to do ```user.reload``` before calling and testing ```valid_password?```. If you are redirecting, you'll obviously need to test ```assigns[:user]``` or your equivalent in the redirected page. – armchairdj Aug 10 '17 at 20:13
2

One not so satisfying solution here is to write a test using the #authenticate method provided by bcrypt-ruby.

specify { user.reload.authenticate(new_password).should be_true }

Granted this isn't a proper integration test, but it will get us to green.

enocom
  • 1,496
  • 1
  • 15
  • 22
  • Docs for has_secure_password's authenticate: http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password – Pascal Jun 21 '18 at 09:45
2

Your answer (using authenticate) is the right approach; you should be satisfied with it. You want to compare the hashed versions of the passwords not the @password (via attr_accessor) in the model. Remember that you're saving a hash and not the actual password.

Your user in your test is an copy of that user in memory. When you run the tests the update method loads a different copy of that user in memory and updates its password hash which is saved to the db. Your copy is unchanged; which is why you thought to reload to get the updated data from the database.

The password field isn't stored in the db, it's stored as a hash instead, so the new hash gets reloaded from the db, but you were comparing the ephemeral state of @password in your user instance instead of the the encrypted_password.

Shawn Balestracci
  • 7,380
  • 1
  • 34
  • 52