Support custom has_secure_password attributes

Starting with version 6, Rails accepts custom attributes for
has_secure_password macro. Without one it defaults to `password`,
so everything should still work without breaking for older versions of
Rails as well.
This commit is contained in:
Kristopher Michalski 2020-09-14 16:56:55 +02:00 committed by Elliot Winkler
parent 4ab77e8b46
commit 4868da7503
3 changed files with 78 additions and 33 deletions

View File

@ -10,49 +10,49 @@ module Shoulda
# include ActiveModel::Model
# include ActiveModel::SecurePassword
# attr_accessor :password
# attr_accessor :reset_password
#
# has_secure_password
# has_secure_password :reset_password
# end
#
# # RSpec
# RSpec.describe User, type: :model do
# it { should have_secure_password }
# it { should have_secure_password(:reset_password) }
# end
#
# # Minitest (Shoulda)
# class UserTest < ActiveSupport::TestCase
# should have_secure_password
# should have_secure_password(:reset_password)
# end
#
# @return [HaveSecurePasswordMatcher]
#
def have_secure_password
HaveSecurePasswordMatcher.new
def have_secure_password(attr = :password)
HaveSecurePasswordMatcher.new(attr)
end
# @private
class HaveSecurePasswordMatcher
attr_reader :failure_message
CORRECT_PASSWORD = "aBcDe12345"
INCORRECT_PASSWORD = "password"
EXPECTED_METHODS = [
:authenticate,
:password=,
:password_confirmation=,
:password_digest,
:password_digest=,
]
CORRECT_PASSWORD = "aBcDe12345".freeze
INCORRECT_PASSWORD = "password".freeze
MESSAGES = {
authenticated_incorrect_password: "expected %{subject} to not authenticate an incorrect password",
did_not_authenticate_correct_password: "expected %{subject} to authenticate the correct password",
authenticated_incorrect_password: "expected %{subject} to not authenticate an incorrect %{attribute}",
did_not_authenticate_correct_password: "expected %{subject} to authenticate the correct %{attribute}",
method_not_found: "expected %{subject} to respond to %{methods}"
}
}.freeze
def initialize(attribute)
@attribute = attribute.to_sym
end
def description
"have a secure password"
"have a secure password, defined on #{@attribute} attribute"
end
def matches?(subject)
@ -71,21 +71,41 @@ module Shoulda
attr_reader :subject
def validate
missing_methods = EXPECTED_METHODS.select {|m| !subject.respond_to?(m) }
missing_methods = expected_methods.reject {|m| subject.respond_to?(m) }
if missing_methods.present?
[:method_not_found, { methods: missing_methods.to_sentence }]
else
subject.password = CORRECT_PASSWORD
subject.password_confirmation = CORRECT_PASSWORD
subject.send("#{@attribute}=", CORRECT_PASSWORD)
subject.send("#{@attribute}_confirmation=", CORRECT_PASSWORD)
if not subject.authenticate(CORRECT_PASSWORD)
[:did_not_authenticate_correct_password, {}]
elsif subject.authenticate(INCORRECT_PASSWORD)
[:authenticated_incorrect_password, {}]
if not subject.send(authenticate_method, CORRECT_PASSWORD)
[:did_not_authenticate_correct_password, { attribute: @attribute }]
elsif subject.send(authenticate_method, INCORRECT_PASSWORD)
[:authenticated_incorrect_password, { attribute: @attribute }]
end
end
end
private
def expected_methods
@expected_methods ||= %I[
#{authenticate_method}
#{@attribute}=
#{@attribute}_confirmation=
#{@attribute}_digest
#{@attribute}_digest=
]
end
def authenticate_method
if @attribute == :password
:authenticate
else
"authenticate_#{@attribute}".to_sym
end
end
end
end
end

View File

@ -32,5 +32,9 @@ module UnitTests
def active_model_supports_full_attributes_api?
active_model_version >= '5.2'
end
def active_model_supports_custom_has_secure_password_attribute?
active_model_version >= '6.0'
end
end
end

View File

@ -1,18 +1,39 @@
require 'unit_spec_helper'
describe Shoulda::Matchers::ActiveModel::HaveSecurePasswordMatcher, type: :model do
it 'matches when the subject configures has_secure_password with default options' do
working_model = define_model(:example, password_digest: :string) { has_secure_password }
expect(working_model.new).to have_secure_password
context "with no arguments passed to has_secure_password" do
it 'matches when the subject configures has_secure_password with default options' do
working_model = define_model(:example, password_digest: :string) { has_secure_password }
expect(working_model.new).to have_secure_password
end
it 'does not match when the subject does not authenticate a password' do
no_secure_password = define_model(:example)
expect(no_secure_password.new).not_to have_secure_password
end
it 'does not match when the subject is missing the password_digest attribute' do
no_digest_column = define_model(:example) { has_secure_password }
expect(no_digest_column.new).not_to have_secure_password
end
end
it 'does not match when the subject does not authenticate a password' do
no_secure_password = define_model(:example)
expect(no_secure_password.new).not_to have_secure_password
end
if active_model_supports_custom_has_secure_password_attribute?
context "when custom attribute is given to has_secure_password" do
it 'matches when the subject configures has_secure_password with correct options' do
working_model = define_model(:example, reset_password_digest: :string) { has_secure_password :reset_password }
expect(working_model.new).to have_secure_password :reset_password
end
it 'does not match when the subject is missing the password_digest attribute' do
no_digest_column = define_model(:example) { has_secure_password }
expect(no_digest_column.new).not_to have_secure_password
it 'does not match when the subject does not authenticate a password' do
no_secure_password = define_model(:example)
expect(no_secure_password.new).not_to have_secure_password :reset_password
end
it 'does not match when the subject is missing the custom digest attribute' do
no_digest_column = define_model(:example) { has_secure_password :reset_password }
expect(no_digest_column.new).not_to have_secure_password :reset_password
end
end
end
end