Before_save model callback rspec testing

hi all,

i’m learning rspec and i can’t figure out how to test if a callback is
executed in a model.

my model code is:

class User < ActiveRecord::Base
before_save :encrypt_password

def encrypt(password)
self.class.encrypt(password, salt)
end

thanks a lot,
cs.

user.should_receive(:encrypt_password).with(your_password)
user.save

Is this what you want?

It might be …

I’ve done this :

it “should encrypt password before save” do
user = mock(“User”)
user.should_receive(:encrypt_password).with(“password”)
user.save
end

but I’ve got :

Spec::Mocks::MockExpectationError in ‘User ActiveRecord Callbacks before
save encrypt password’
Mock ‘User’ received unexpected message :save with (no args)
./spec/models/user_spec.rb:84:

Hey,

This isn’t doing much to test the behavior of the object. Why do
you want to encrypt the password? Probably so you can authenticate,
right? I would probably start off with

describe “authenticate” do
it “finds the user with the given credentials” do
u = User.create!(:login => “pat”, :password => “password”)
User.authenticate(“pat”, “password”).should == u
end
end

That might be a bit much to chew at first though. So you can write
some interim tests that you then throw away. For example, you might
do

describe User, “when saved” do
it “should create a salt” do
u = User.create(:login => “pat”, :password => “password”)
u.salt.should_not be_blank
end

it “should create a hashed pass” do
u = User.create(:login => “pat”, :password => “password”)
u.hashed_pass.should_not be_blank
end
end

Once you have those passing, you can move to the User.authenticate
spec. Once that passes, you can throw away the salt/hashed_pass
specs, because they’re no longer useful. They’re testing
implementation at this point, and were just a tool to get you where
you wanted to go in small steps.

Pat

Thanks Pat,

I’ve tried this way but the test did not passed …
I’m trying to Rspec Authorization’s plugin User class
(writertopia)

Which looks like this:

== Schema Information

Schema version: 92

Table name: users

id :integer(11) not null, primary key

login :string(255)

email :string(255)

crypted_password :string(40)

salt :string(40)

created_at :datetime

updated_at :datetime

remember_token :string(255)

remember_token_expires_at :datetime

require ‘digest/sha1’

class User < ActiveRecord::Base

Relationships

-------------

has_many :roles, :dependent => :destroy

Authentication plugins

----------------------

acts_as_authorized_user
acts_as_authorizable

Callbacks

---------

for Authentication

before_save :encrypt_password

methods from the Authentication plugin

--------------------------------------

Hardwired roles

def has_role?( role, authorized_object = nil )
# - site admin
return true if self.login.downcase == ‘admin’ and (role == ‘admin’
or role == ‘site_admin’)
super
end

Authenticates a user by their login name and unencrypted password.

Returns the user or nil.
def self.authenticate(login, password)
u = find_by_login(login) # need to get the salt
u && u.authenticated?(password) ? u : nil
end

Encrypts some data with the salt.

def self.encrypt(password, salt)
Digest::SHA1.hexdigest(“–#{salt}–#{password}–”)
end

Encrypts the password with the user salt

def encrypt(password)
self.class.encrypt(password, salt)
end

def authenticated?(password)
crypted_password == encrypt(password)
end

def remember_token?
remember_token_expires_at && Time.now.utc <
remember_token_expires_at
end

These create and unset the fields required for remembering users

between browser closes
def remember_me
self.remember_token_expires_at = 2.weeks.from_now.utc
self.remember_token =
encrypt(“#{email}–#{remember_token_expires_at}”)
save(false)
end

def forget_me
self.remember_token_expires_at = nil
self.remember_token = nil
save(false)
end

protected

def encrypt_password
  return if password.blank?
  self.salt = 

Digest::SHA1.hexdigest(“–#{Time.now.to_s}–#{login}–”) if new_record?
self.crypted_password = encrypt(password)
end

def password_required?
  crypted_password.blank? || !password.blank?
end

end

I’ve managed to Rspec the following tests (copying from Rspec output):

User validations

  • :login must be present
  • :email must be present
  • :password must be present
  • :password_confirmation must be present
  • :login length must be 2…40
  • :email length must be 2…40
  • :password length must be 2…40
  • :login must be unique
  • :email must be unique
  • :login must not contain < > + : ; ! # $ % ^ & * /
  • :email must not contain < > + : ; ! # $ % ^ & * /
  • :password must not contain < > + : ; ! # $ % ^ & * /

and

User Acts As Authenticated Authentication

  • cannot login with empty username and empty password
  • cannot login with valid username and empty password
  • cannot login with empty username and valid password
  • cannot login with invalid username
  • cannot login with invalid password
  • can login with valid username and valid password
  • there can be two passwords with the same value

But when I’m trying to test if the password is encrypted I’m getting
errors.

I’m trying “your” way:

u = User.create(:login => “test”, :email => “[email protected]”, :password
=> “test123”, :password_confirmation => “test123”)
u.crypted_password.should_not be_nil

the error message is:
NoMethodError in ‘User ActiveRecord Callbacks before save encrypts
password’
You have a nil object when you didn’t expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.each
./spec/models/user_spec.rb:82:

[line 82 ia User.create(…)

and “my” way:

@user = User.new
@user.login = “test”
@user.email = “[email protected]
@user.password = “test123”
@user.password_confirmation = “test123”
@user.save
@user.crypted_password.should_not be_nil

the error message is:
‘User Acts As Authenticated encrypts password’ FAILED
expected not nil, got nil

On Jun 20, 2008, at 11:01 AM, Csongor Bartus wrote:

You might have expected an instance of Array.
@user.email = “[email protected]
@user.password = “test123”
@user.password_confirmation = “test123”
@user.save
@user.crypted_password.should_not be_nil

the error message is:
‘User Acts As Authenticated encrypts password’ FAILED
expected not nil, got nil

Try using create! or save! - I’ll bet the record is not being saved
correctly and you’re not seeing the error.

David C. wrote:

Try using create! or save! - I’ll bet the record is not being saved
correctly and you’re not seeing the error.

done, still the same errors (exactly)

[Offtopic question]

I might be paranoic testing if the password is salted/hashed correctly
since all my login tests passed?

In learning RSpec I find the most important to draw a line and don’t
look behind

… but what if, in this case, accidentally something happens with
‘digest/sha1’ and the logins will pass but the password will be not
crypted?

Yi Wen wrote:

fire up script/console and copy line 82 and try it out. and report the
result here.

u = User.create!(:login => “test”, :email => “[email protected]”, :password => “test123”, :password_confirmation => “test123”)
NoMethodError: You have a nil object when you didn’t expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.each
from
./script/…/config/…/config/…/vendor/rails/activerecord/lib/active_record/base.rb:1671:in
attributes=' from ./script/../config/../config/../vendor/rails/activerecord/lib/active_record/base.rb:1505:ininitialize_without_callbacks’
from
./script/…/config/…/config/…/vendor/rails/activerecord/lib/active_record/callbacks.rb:225:in
initialize' from ./script/../config/../config/../vendor/rails/activerecord/lib/active_record/validations.rb:726:innew’
from
./script/…/config/…/config/…/vendor/rails/activerecord/lib/active_record/validations.rb:726:in
`create!’
from (irb):1

fire up script/console and copy line 82 and try it out. and report the
result here.

You don’t have attr_accessor :password, :password_confirmation in
User, do you? You may want to add this and try again

Yi Wen wrote:

You don’t have attr_accessor :password, :password_confirmation in
User, do you? You may want to add this and try again

I had :password, I’ve added :password_confirmation but still the same:

u = User.create!(:login => “test”, :email => “[email protected]”, :password => “test123”, :password_confirmation => “test123”)
NoMethodError: You have a nil object when you didn’t expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.each
from
./script/…/config/…/config/…/vendor/rails/activerecord/lib/active_record/base.rb:1671:in
attributes=' from ./script/../config/../config/../vendor/rails/activerecord/lib/active_record/base.rb:1505:ininitialize_without_callbacks’
from
./script/…/config/…/config/…/vendor/rails/activerecord/lib/active_record/callbacks.rb:225:in
initialize' from ./script/../config/../config/../vendor/rails/activerecord/lib/active_record/validations.rb:726:innew’
from
./script/…/config/…/config/…/vendor/rails/activerecord/lib/active_record/validations.rb:726:in
`create!’
from (irb):1

Well, at least now we know this is nothing to do with RSpec. My
suggestion is that you comment out the code which will be executed
during a creation, bit by bit in User (such as before_save) and try to
create a user. In this way you can pinpoint where the problem is.

That’s because you executed the same creation code into the
script/console, which has nothing to do with RSpec, and still get the
same error message, right?

Yi

Yi Wen wrote:

Well, at least now we know this is nothing to do with RSpec. My
suggestion is that you comment out the code which will be executed
during a creation, bit by bit in User (such as before_save) and try to
create a user. In this way you can pinpoint where the problem is.

Thanks a lot Yi Wen,

Now I’ll find out easier where the bug lies …
But how did you realized the bug is not in RSpec but in the code?

Yi Wen wrote:

That’s because you executed the same creation code into the
script/console, which has nothing to do with RSpec, and still get the
same error message, right?

Yi

Gotcha :smiley:
It seems I’ve got tired … Thanks a lot!