Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored validations rules for has_secure_password #13772

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions activemodel/lib/active_model/secure_password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,15 @@ def has_secure_password(options = {})
include InstanceMethodsOnActivation

if options.fetch(:validations, true)
validates_confirmation_of :password, if: :password_confirmation_required?
validates_presence_of :password, on: :create
validates_presence_of :password_confirmation, if: :password_confirmation_required?
# This ensures the model has a password by checking whether the password_digest
# is present, so that this works with both new and existing records. However,
# when there is an error, the message is added to the password attribute instead
# so that the error message will make sense to the end-user.
validate do |record|
record.errors.add(:password, :blank) unless record.password_digest.present?
end

before_create { raise "Password digest missing on new record" if password_digest.blank? }
validates_confirmation_of :password, if: ->{ password.present? }
end

if respond_to?(:attributes_protected_by_default)
Expand Down Expand Up @@ -100,7 +104,9 @@ def authenticate(unencrypted_password)
# user.password = 'mUc3m00RsqyRe'
# user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
def password=(unencrypted_password)
unless unencrypted_password.blank?
if unencrypted_password.nil?
self.password_digest = nil
elsif unencrypted_password.present?
@password = unencrypted_password
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
Expand All @@ -110,12 +116,6 @@ def password=(unencrypted_password)
def password_confirmation=(unencrypted_password)
@password_confirmation = unencrypted_password
end

private

def password_confirmation_required?
password_confirmation && password.present?
end
end
end
end
178 changes: 120 additions & 58 deletions activemodel/test/cases/secure_password_test.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
require 'cases/helper'
require 'models/user'
require 'models/oauthed_user'
require 'models/visitor'

class SecurePasswordTest < ActiveModel::TestCase
Expand All @@ -9,69 +8,152 @@ class SecurePasswordTest < ActiveModel::TestCase

@user = User.new
@visitor = Visitor.new
@oauthed_user = OauthedUser.new

# Simulate loading an existing user from the DB
@existing_user = User.new
@existing_user.password_digest = BCrypt::Password.create('password', cost: BCrypt::Engine::MIN_COST)
end

teardown do
ActiveModel::SecurePassword.min_cost = false
end

test "blank password" do
@user.password = @visitor.password = ''
assert !@user.valid?(:create), 'user should be invalid'
test "create and updating without validations" do
assert @visitor.valid?(:create), 'visitor should be valid'
end
assert @visitor.valid?(:update), 'visitor should be valid'

@visitor.password = '123'
@visitor.password_confirmation = '456'

test "nil password" do
@user.password = @visitor.password = nil
assert !@user.valid?(:create), 'user should be invalid'
assert @visitor.valid?(:create), 'visitor should be valid'
assert @visitor.valid?(:update), 'visitor should be valid'
end

test "blank password doesn't override previous password" do
@user.password = 'test'
test "create a new user with validation and a blank password" do
@user.password = ''
assert_equal @user.password, 'test'
assert !@user.valid?(:create), 'user should be invalid'
assert_equal 1, @user.errors.count
assert_equal ["can't be blank"], @user.errors[:password]
end

test "create a new user with validation and a nil password" do
@user.password = nil
assert !@user.valid?(:create), 'user should be invalid'
assert_equal 1, @user.errors.count
assert_equal ["can't be blank"], @user.errors[:password]
end

test "create a new user with validation and a blank password confirmation" do
@user.password = 'password'
@user.password_confirmation = ''
assert !@user.valid?(:create), 'user should be invalid'
assert_equal 1, @user.errors.count
assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
end

test "password must be present" do
assert !@user.valid?(:create)
assert_equal 1, @user.errors.size
test "create a new user with validation and a nil password confirmation" do
@user.password = 'password'
@user.password_confirmation = nil
assert @user.valid?(:create), 'user should be valid'
end

test "match confirmation" do
@user.password = @visitor.password = "thiswillberight"
@user.password_confirmation = @visitor.password_confirmation = "wrong"
test "create a new user with validation and an incorrect password confirmation" do
@user.password = 'password'
@user.password_confirmation = 'something else'
assert !@user.valid?(:create), 'user should be invalid'
assert_equal 1, @user.errors.count
assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
end

assert !@user.valid?
assert @visitor.valid?
test "create a new user with validation and a correct password confirmation" do
@user.password = 'password'
@user.password_confirmation = 'something else'
assert !@user.valid?(:create), 'user should be invalid'
assert_equal 1, @user.errors.count
assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
end

@user.password_confirmation = "thiswillberight"
test "update an existing user with validation and no change in password" do
assert @existing_user.valid?(:update), 'user should be valid'
end

assert @user.valid?
test "updating an existing user with validation and a blank password" do
@existing_user.password = ''
assert @existing_user.valid?(:update), 'user should be valid'
end

test "authenticate" do
@user.password = "secret"
test "updating an existing user with validation and a blank password and password_confirmation" do
@existing_user.password = ''
@existing_user.password_confirmation = ''
assert @existing_user.valid?(:update), 'user should be valid'
end

assert !@user.authenticate("wrong")
assert @user.authenticate("secret")
test "updating an existing user with validation and a nil password" do
@existing_user.password = nil
assert !@existing_user.valid?(:update), 'user should be invalid'
assert_equal 1, @existing_user.errors.count
assert_equal ["can't be blank"], @existing_user.errors[:password]
end

test "updating an existing user with validation and a blank password confirmation" do
@existing_user.password = 'password'
@existing_user.password_confirmation = ''
assert !@existing_user.valid?(:update), 'user should be invalid'
assert_equal 1, @existing_user.errors.count
assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
end

test "User should not be created with blank digest" do
assert_raise RuntimeError do
@user.run_callbacks :create
end
@user.password = "supersecretpassword"
assert_nothing_raised do
@user.run_callbacks :create
end
test "updating an existing user with validation and a nil password confirmation" do
@existing_user.password = 'password'
@existing_user.password_confirmation = nil
assert @existing_user.valid?(:update), 'user should be valid'
end

test "Oauthed user can be created with blank digest" do
assert_nothing_raised do
@oauthed_user.run_callbacks :create
end
test "updating an existing user with validation and an incorrect password confirmation" do
@existing_user.password = 'password'
@existing_user.password_confirmation = 'something else'
assert !@existing_user.valid?(:update), 'user should be invalid'
assert_equal 1, @existing_user.errors.count
assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
end

test "updating an existing user with validation and a correct password confirmation" do
@existing_user.password = 'password'
@existing_user.password_confirmation = 'something else'
assert !@existing_user.valid?(:update), 'user should be invalid'
assert_equal 1, @existing_user.errors.count
assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
end

test "updating an existing user with validation and a blank password digest" do
@existing_user.password_digest = ''
assert !@existing_user.valid?(:update), 'user should be invalid'
assert_equal 1, @existing_user.errors.count
assert_equal ["can't be blank"], @existing_user.errors[:password]
end

test "updating an existing user with validation and a nil password digest" do
@existing_user.password_digest = nil
assert !@existing_user.valid?(:update), 'user should be invalid'
assert_equal 1, @existing_user.errors.count
assert_equal ["can't be blank"], @existing_user.errors[:password]
end

test "setting a blank password should not change an existing password" do
@existing_user.password = ''
assert @existing_user.password_digest == 'password'
end

test "setting a nil password should clear an existing password" do
@existing_user.password = nil
assert_equal nil, @existing_user.password_digest
end

test "authenticate" do
@user.password = "secret"

assert !@user.authenticate("wrong")
assert @user.authenticate("secret")
end

test "Password digest cost defaults to bcrypt default cost when min_cost is false" do
Expand All @@ -95,24 +177,4 @@ class SecurePasswordTest < ActiveModel::TestCase
@user.password = "secret"
assert_equal BCrypt::Engine::MIN_COST, @user.password_digest.cost
end

test "blank password_confirmation does not result in a confirmation error" do
@user.password = ""
@user.password_confirmation = ""
assert @user.valid?(:update), "user should be valid"
end

test "password_confirmation validations will not be triggered if password_confirmation is not sent" do
@user.password = "password"
assert @user.valid?(:create)
end

test "will not save if confirmation is blank but password is not" do
@user.password = "password"
@user.password_confirmation = ""
assert_not @user.valid?(:create)

@user.password_confirmation = "password"
assert @user.valid?(:create)
end
end
11 changes: 0 additions & 11 deletions activemodel/test/models/oauthed_user.rb

This file was deleted.

2 changes: 1 addition & 1 deletion activemodel/test/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ class User

has_secure_password

attr_accessor :password_digest, :password_salt
attr_accessor :password_digest
end