From 8a46b62dbe875689664e5a205dc6d19177af681b Mon Sep 17 00:00:00 2001 From: Kristopher Michalski Date: Mon, 14 Sep 2020 16:56:55 +0200 Subject: [PATCH] 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. --- .../have_secure_password_matcher.rb | 66 ++++++++++++------- .../unit/helpers/active_model_versions.rb | 4 ++ .../have_secure_password_matcher_spec.rb | 41 +++++++++--- 3 files changed, 78 insertions(+), 33 deletions(-) diff --git a/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb b/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb index e73a75496..47ac8f0d4 100644 --- a/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +++ b/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb @@ -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) @@ -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 diff --git a/spec/support/unit/helpers/active_model_versions.rb b/spec/support/unit/helpers/active_model_versions.rb index 15468cf20..b07633f6a 100644 --- a/spec/support/unit/helpers/active_model_versions.rb +++ b/spec/support/unit/helpers/active_model_versions.rb @@ -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 diff --git a/spec/unit/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb b/spec/unit/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb index d3add54f7..e01ad18bd 100644 --- a/spec/unit/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb @@ -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