Skip to content

Commit

Permalink
Support custom has_secure_password attributes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
krismichalski authored and mcmire committed Sep 14, 2020
1 parent 4ab77e8 commit 4868da7
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 33 deletions.
66 changes: 43 additions & 23 deletions lib/shoulda/matchers/active_model/have_secure_password_matcher.rb
Expand Up @@ -10,49 +10,49 @@ module ActiveModel
# 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)
Expand All @@ -71,21 +71,41 @@ def matches?(subject)
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
Expand Down
4 changes: 4 additions & 0 deletions spec/support/unit/helpers/active_model_versions.rb
Expand Up @@ -32,5 +32,9 @@ def active_model_supports_strict?
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
@@ -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
end
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 does not authenticate a password' do
no_secure_password = define_model(:example)
expect(no_secure_password.new).not_to have_secure_password
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 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
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 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

0 comments on commit 4868da7

Please sign in to comment.