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

Support custom has_secure_password attributes #1356

Merged
merged 1 commit into from Sep 14, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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