Testing Account Locking Mechanisms in Devise Authentication
This test suite validates the Lockable module functionality in Devise, focusing on account locking mechanisms and user authentication security. It thoroughly tests user account locking, unlocking strategies, and failed authentication attempt handling.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
heartcombo/devise
test/models/lockable_test.rb
# frozen_string_literal: true
require 'test_helper'
class LockableTest < ActiveSupport::TestCase
def setup
setup_mailer
end
test "should respect maximum attempts configuration" do
user = create_user
user.confirm
swap Devise, maximum_attempts: 2 do
2.times { user.valid_for_authentication?{ false } }
assert user.reload.access_locked?
end
end
test "should increment failed_attempts on successful validation if the user is already locked" do
user = create_user
user.confirm
swap Devise, maximum_attempts: 2 do
2.times { user.valid_for_authentication?{ false } }
assert user.reload.access_locked?
end
user.valid_for_authentication?{ true }
assert_equal 3, user.reload.failed_attempts
end
test "should not touch failed_attempts if lock_strategy is none" do
user = create_user
user.confirm
swap Devise, lock_strategy: :none, maximum_attempts: 2 do
3.times { user.valid_for_authentication?{ false } }
assert_not user.access_locked?
assert_equal 0, user.failed_attempts
end
end
test "should read failed_attempts from database when incrementing" do
user = create_user
initial_failed_attempts = user.failed_attempts
same_user = User.find(user.id)
user.increment_failed_attempts
same_user.increment_failed_attempts
assert_equal initial_failed_attempts + 2, user.reload.failed_attempts
end
test "reset_failed_attempts! updates the failed attempts counter back to 0" do
user = create_user(failed_attempts: 3)
assert_equal 3, user.failed_attempts
user.reset_failed_attempts!
assert_equal 0, user.failed_attempts
user.reset_failed_attempts!
assert_equal 0, user.failed_attempts
end
test "reset_failed_attempts! does not run model validations" do
user = create_user(failed_attempts: 1)
user.expects(:after_validation_callback).never
assert user.reset_failed_attempts!
assert_equal 0, user.failed_attempts
end
test "reset_failed_attempts! does not try to reset if not using failed attempts strategy" do
admin = create_admin
assert_not_respond_to admin, :failed_attempts
assert_not admin.reset_failed_attempts!
end
test 'should be valid for authentication with a unlocked user' do
user = create_user
user.lock_access!
user.unlock_access!
assert user.valid_for_authentication?{ true }
end
test "should verify whether a user is locked or not" do
user = create_user
assert_not user.access_locked?
user.lock_access!
assert user.access_locked?
end
test "active_for_authentication? should be the opposite of locked?" do
user = create_user
user.confirm
assert user.active_for_authentication?
user.lock_access!
assert_not user.active_for_authentication?
end
test "should unlock a user by cleaning locked_at, failed_attempts and unlock_token" do
user = create_user
user.lock_access!
assert_not_nil user.reload.locked_at
assert_not_nil user.reload.unlock_token
user.unlock_access!
assert_nil user.reload.locked_at
assert_nil user.reload.unlock_token
assert_equal 0, user.reload.failed_attempts
end
test "new user should not be locked and should have zero failed_attempts" do
assert_not new_user.access_locked?
assert_equal 0, create_user.failed_attempts
end
test "should unlock user after unlock_in period" do
swap Devise, unlock_in: 3.hours do
user = new_user
user.locked_at = 2.hours.ago
assert user.access_locked?
Devise.unlock_in = 1.hour
assert_not user.access_locked?
end
end
test "should not unlock in 'unlock_in' if :time unlock strategy is not set" do
swap Devise, unlock_strategy: :email do
user = new_user
user.locked_at = 2.hours.ago
assert user.access_locked?
end
end
test "should set unlock_token when locking" do
user = create_user
assert_nil user.unlock_token
user.lock_access!
assert_not_nil user.unlock_token
end
test "should never generate the same unlock token for different users" do
unlock_tokens = []
3.times do
user = create_user
user.lock_access!
token = user.unlock_token
assert_not_includes unlock_tokens, token
unlock_tokens << token
end
end
test "should not generate unlock_token when :email is not an unlock strategy" do
swap Devise, unlock_strategy: :time do
user = create_user
user.lock_access!
assert_nil user.unlock_token
end
end
test "should send email with unlock instructions when :email is an unlock strategy" do
swap Devise, unlock_strategy: :email do
user = create_user
assert_email_sent do
user.lock_access!
end
end
end
test "doesn't send email when you pass option send_instructions to false" do
swap Devise, unlock_strategy: :email do
user = create_user
assert_email_not_sent do
user.lock_access! send_instructions: false
end
end
end
test "sends email when you pass options other than send_instructions" do
swap Devise, unlock_strategy: :email do
user = create_user
assert_email_sent do
user.lock_access! foo: :bar, bar: :foo
end
end
end
test "should not send email with unlock instructions when :email is not an unlock strategy" do
swap Devise, unlock_strategy: :time do
user = create_user
assert_email_not_sent do
user.lock_access!
end
end
end
test 'should find and unlock a user automatically based on raw token' do
user = create_user
raw = user.send_unlock_instructions
locked_user = User.unlock_access_by_token(raw)
assert_equal user, locked_user
assert_not user.reload.access_locked?
end
test 'should return a new record with errors when a invalid token is given' do
locked_user = User.unlock_access_by_token('invalid_token')
assert_not locked_user.persisted?
assert_equal "is invalid", locked_user.errors[:unlock_token].join
end
test 'should return a new record with errors when a blank token is given' do
locked_user = User.unlock_access_by_token('')
assert_not locked_user.persisted?
assert locked_user.errors.added?(:unlock_token, :blank)
end
test 'should find a user to send unlock instructions' do
user = create_user
user.lock_access!
unlock_user = User.send_unlock_instructions(email: user.email)
assert_equal user, unlock_user
end
test 'should return a new user if no email was found' do
unlock_user = User.send_unlock_instructions(email: "[email protected]")
assert_not unlock_user.persisted?
end
test 'should add error to new user email if no email was found' do
unlock_user = User.send_unlock_instructions(email: "[email protected]")
assert_equal 'not found', unlock_user.errors[:email].join
end
test 'should find a user to send unlock instructions by authentication_keys' do
swap Devise, authentication_keys: [:username, :email] do
user = create_user
unlock_user = User.send_unlock_instructions(email: user.email, username: user.username)
assert_equal user, unlock_user
end
end
test 'should require all unlock_keys' do
swap Devise, unlock_keys: [:username, :email] do
user = create_user
unlock_user = User.send_unlock_instructions(email: user.email)
assert_not unlock_user.persisted?
assert unlock_user.errors.added?(:username, :blank)
end
end
test 'should not be able to send instructions if the user is not locked' do
user = create_user
assert_not user.resend_unlock_instructions
assert_not user.access_locked?
assert_equal 'was not locked', user.errors[:email].join
end
test 'should not be able to send instructions if the user if not locked and have username as unlock key' do
swap Devise, unlock_keys: [:username] do
user = create_user
assert_not user.resend_unlock_instructions
assert_not user.access_locked?
assert_equal 'was not locked', user.errors[:username].join
end
end
test 'should unlock account if lock has expired and increase attempts on failure' do
swap Devise, unlock_in: 1.minute do
user = create_user
user.confirm
user.failed_attempts = 2
user.locked_at = 2.minutes.ago
user.valid_for_authentication? { false }
assert_equal 1, user.failed_attempts
end
end
test 'should unlock account if lock has expired on success' do
swap Devise, unlock_in: 1.minute do
user = create_user
user.confirm
user.failed_attempts = 2
user.locked_at = 2.minutes.ago
user.valid_for_authentication? { true }
assert_equal 0, user.failed_attempts
assert_nil user.locked_at
end
end
test 'required_fields should contain the all the fields when all the strategies are enabled' do
swap Devise, unlock_strategy: :both do
swap Devise, lock_strategy: :failed_attempts do
assert_equal [
:failed_attempts,
:locked_at,
:unlock_token
], Devise::Models::Lockable.required_fields(User)
end
end
end
test 'required_fields should contain only failed_attempts and locked_at when the strategies are time and failed_attempts are enabled' do
swap Devise, unlock_strategy: :time do
swap Devise, lock_strategy: :failed_attempts do
assert_equal [
:failed_attempts,
:locked_at
], Devise::Models::Lockable.required_fields(User)
end
end
end
test 'required_fields should contain only failed_attempts and unlock_token when the strategies are token and failed_attempts are enabled' do
swap Devise, unlock_strategy: :email do
swap Devise, lock_strategy: :failed_attempts do
assert_equal [
:failed_attempts,
:unlock_token
], Devise::Models::Lockable.required_fields(User)
end
end
end
test 'should not return a locked unauthenticated message if in paranoid mode' do
swap Devise, paranoid: :true do
user = create_user
user.failed_attempts = Devise.maximum_attempts + 1
user.lock_access!
assert_equal :invalid, user.unauthenticated_message
end
end
test 'should return last attempt message if user made next-to-last attempt of password entering' do
swap Devise, last_attempt_warning: true, lock_strategy: :failed_attempts do
user = create_user
user.failed_attempts = Devise.maximum_attempts - 2
assert_equal :invalid, user.unauthenticated_message
user.failed_attempts = Devise.maximum_attempts - 1
assert_equal :last_attempt, user.unauthenticated_message
user.failed_attempts = Devise.maximum_attempts
assert_equal :locked, user.unauthenticated_message
end
end
test 'should not return last attempt message if last_attempt_warning is disabled' do
swap Devise, last_attempt_warning: false, lock_strategy: :failed_attempts do
user = create_user
user.failed_attempts = Devise.maximum_attempts - 1
assert_equal :invalid, user.unauthenticated_message
end
end
test 'should return locked message if user was programatically locked' do
user = create_user
user.lock_access!
assert_equal :locked, user.unauthenticated_message
end
test 'unlock_strategy_enabled? should return true for both, email, and time strategies if :both is used' do
swap Devise, unlock_strategy: :both do
user = create_user
assert_equal true, user.unlock_strategy_enabled?(:both)
assert_equal true, user.unlock_strategy_enabled?(:time)
assert_equal true, user.unlock_strategy_enabled?(:email)
assert_equal false, user.unlock_strategy_enabled?(:none)
assert_equal false, user.unlock_strategy_enabled?(:an_undefined_strategy)
end
end
test 'unlock_strategy_enabled? should return true only for the configured strategy' do
swap Devise, unlock_strategy: :email do
user = create_user
assert_equal false, user.unlock_strategy_enabled?(:both)
assert_equal false, user.unlock_strategy_enabled?(:time)
assert_equal true, user.unlock_strategy_enabled?(:email)
assert_equal false, user.unlock_strategy_enabled?(:none)
assert_equal false, user.unlock_strategy_enabled?(:an_undefined_strategy)
end
end
end