Back to Repositories

Testing Database Authentication Features in Devise Framework

This test suite validates the DatabaseAuthenticatable module in Devise, focusing on password handling, email validation, and user authentication mechanisms. It thoroughly tests user credential management and security features essential for Rails authentication.

Test Coverage Overview

The test suite provides comprehensive coverage of Devise’s database authentication features:
  • Password encryption and validation
  • Case-sensitive and insensitive email handling
  • User credential updates and verification
  • Password change notifications
  • Email change notifications

Implementation Analysis

The testing approach employs ActiveSupport::TestCase framework with focused unit tests for authentication workflows. It uses setup helpers and custom assertions to validate password hashing, email normalization, and parameter filtering patterns specific to Devise’s authentication implementation.

Key patterns include swappable configurations for notification testing and isolated user model testing.

Technical Details

Testing infrastructure includes:
  • Minitest framework integration
  • Custom test helpers and models
  • Mock mailer configuration
  • SHA1 digest library for password hashing
  • Parameter filtering system

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Isolated test cases with proper setup/teardown
  • Comprehensive edge case coverage
  • Security-focused validation testing
  • Clear test naming conventions
  • Effective use of assertions and error checking

heartcombo/devise

test/models/database_authenticatable_test.rb

            
# frozen_string_literal: true

require 'test_helper'
require 'test_models'
require 'digest/sha1'

class DatabaseAuthenticatableTest < ActiveSupport::TestCase
  def setup
    setup_mailer
  end

  test 'should downcase case insensitive keys when saving' do
    # case_insensitive_keys is set to :email by default.
    email = '[email protected]'
    user = new_user(email: email)

    assert_equal email, user.email
    user.save!
    assert_equal email.downcase, user.email
  end

  test 'should downcase case insensitive keys that refer to virtual attributes when saving' do
    email        = '[email protected]'
    confirmation = '[email protected]'
    attributes   = valid_attributes(email: email, email_confirmation: confirmation)
    user = UserWithVirtualAttributes.new(attributes)

    assert_equal confirmation, user.email_confirmation
    user.save!
    assert_equal confirmation.downcase, user.email_confirmation
  end

  test 'should not mutate value assigned to case insensitive key' do
    email          = '[email protected]'
    original_email = email.dup
    user           = new_user(email: email)

    user.save!
    assert_equal original_email, email
  end

  test 'should remove whitespace from strip whitespace keys when saving' do
    # strip_whitespace_keys is set to :email by default.
    email = ' [email protected] '
    user = new_user(email: email)

    assert_equal email, user.email
    user.save!
    assert_equal email.strip, user.email
  end

  test 'should not mutate value assigned to string whitespace key' do
    email          = ' [email protected] '
    original_email = email.dup
    user           = new_user(email: email)

    user.save!
    assert_equal original_email, email
  end

  test "doesn't throw exception when globally configured strip_whitespace_keys are not present on a model" do
    swap Devise, strip_whitespace_keys: [:fake_key] do
      assert_nothing_raised { create_user }
    end
  end

  test "doesn't throw exception when globally configured case_insensitive_keys are not present on a model" do
    swap Devise, case_insensitive_keys: [:fake_key] do
      assert_nothing_raised { create_user }
    end
  end

  test "param filter should not convert booleans and integer to strings" do
    conditions = { "login" => "[email protected]", "bool1" => true, "bool2" => false, "fixnum" => 123, "will_be_converted" => (1..10) }
    conditions = Devise::ParameterFilter.new([], []).filter(conditions)
    assert_equal( { "login" => "[email protected]", "bool1" => "true", "bool2" => "false", "fixnum" => "123", "will_be_converted" => "1..10" }, conditions)
  end

  test 'param filter should filter case_insensitive_keys as insensitive' do
    conditions = {'insensitive' => 'insensitive_VAL', 'sensitive' => 'sensitive_VAL'}
    conditions = Devise::ParameterFilter.new(['insensitive'], []).filter(conditions)
    assert_equal( {'insensitive' => 'insensitive_val', 'sensitive' => 'sensitive_VAL'}, conditions )
  end

  test 'param filter should filter strip_whitespace_keys stripping whitespaces' do
    conditions = {'strip_whitespace' => ' strip_whitespace_val ', 'do_not_strip_whitespace' => ' do_not_strip_whitespace_val '}
    conditions = Devise::ParameterFilter.new([], ['strip_whitespace']).filter(conditions)
    assert_equal( {'strip_whitespace' => 'strip_whitespace_val', 'do_not_strip_whitespace' => ' do_not_strip_whitespace_val '}, conditions )
  end

  test 'param filter should not add keys to filtered hash' do
    conditions = { 'present' => 'present_val' }
    conditions.default = ''
    conditions = Devise::ParameterFilter.new(['not_present'], []).filter(conditions)
    assert_equal({ 'present' => 'present_val' }, conditions)
  end

  test 'should respond to password and password confirmation' do
    user = new_user
    assert_respond_to user, :password
    assert_respond_to user, :password_confirmation
  end

  test 'should generate a hashed password while setting password' do
    user = new_user
    assert_present user.encrypted_password
  end

  test 'should support custom hashing methods' do
    user = UserWithCustomHashing.new(password: '654321')
    assert_equal '123456', user.encrypted_password
  end

  test 'allow authenticatable_salt to work even with nil hashed password' do
    user = User.new
    user.encrypted_password = nil
    assert_nil user.authenticatable_salt
  end

  test 'should not generate a hashed password if password is blank' do
    assert_blank new_user(password: nil).encrypted_password
    assert_blank new_user(password: '').encrypted_password
  end

  test 'should hash password again if password has changed' do
    user = create_user
    encrypted_password = user.encrypted_password
    user.password = user.password_confirmation = 'new_password'
    user.save!
    assert_not_equal encrypted_password, user.encrypted_password
  end

  test 'should test for a valid password' do
    user = create_user
    assert user.valid_password?('12345678')
    assert_not user.valid_password?('654321')
  end

  test 'should not raise error with an empty password' do
    user = create_user
    user.encrypted_password = ''
    assert_nothing_raised { user.valid_password?('12345678') }
  end

  test 'should be an invalid password if the user has an empty password' do
    user = create_user
    user.encrypted_password = ''
    assert_not user.valid_password?('654321')
  end

  test 'should respond to current password' do
    assert_respond_to new_user, :current_password
  end

  test 'should update password with valid current password' do
    user = create_user
    assert user.update_with_password(current_password: '12345678',
      password: 'pass4321', password_confirmation: 'pass4321')
    assert user.reload.valid_password?('pass4321')
  end

  test 'should add an error to current password when it is invalid' do
    user = create_user
    assert_not user.update_with_password(current_password: 'other',
      password: 'pass4321', password_confirmation: 'pass4321')
    assert user.reload.valid_password?('12345678')
    assert_match "is invalid", user.errors[:current_password].join
  end

  test 'should add an error to current password when it is blank' do
    user = create_user
    assert_not user.update_with_password(password: 'pass4321',
      password_confirmation: 'pass4321')
    assert user.reload.valid_password?('12345678')
    assert user.errors.added?(:current_password, :blank)
  end

  test 'should run validations even when current password is invalid or blank' do
    user = UserWithValidation.create!(valid_attributes)
    user.save
    assert user.persisted?
    assert_not user.update_with_password(username: "")
    assert_match "usertest", user.reload.username
    assert user.errors.added?(:username, :blank)
  end

  test 'should ignore password and its confirmation if they are blank' do
    user = create_user
    assert user.update_with_password(current_password: '12345678', email: "[email protected]")
    assert_equal "[email protected]", user.email
  end

  test 'should not update password with invalid confirmation' do
    user = create_user
    assert_not user.update_with_password(current_password: '12345678',
      password: 'pass4321', password_confirmation: 'other')
    assert user.reload.valid_password?('12345678')
  end

  test 'should clean up password fields on failure' do
    user = create_user
    assert_not user.update_with_password(current_password: '12345678',
      password: 'pass4321', password_confirmation: 'other')
    assert user.password.blank?
    assert user.password_confirmation.blank?
  end

  test 'should update the user without password' do
    user = create_user
    user.update_without_password(email: '[email protected]')
    assert_equal '[email protected]', user.email
  end

  test 'should not update password without password' do
    user = create_user
    user.update_without_password(password: 'pass4321', password_confirmation: 'pass4321')
    assert_not user.reload.valid_password?('pass4321')
    assert user.valid_password?('12345678')
  end

  test 'should destroy user if current password is valid' do
    user = create_user
    assert user.destroy_with_password('12345678')
    assert_not user.persisted?
  end

  test 'should not destroy user with invalid password' do
    user = create_user
    assert_not user.destroy_with_password('other')
    assert user.persisted?
    assert_match "is invalid", user.errors[:current_password].join
  end

  test 'should not destroy user with blank password' do
    user = create_user
    assert_not user.destroy_with_password(nil)
    assert user.persisted?
    assert user.errors.added?(:current_password, :blank)
  end

  test 'should not email on password change' do
    user = create_user
    assert_email_not_sent do
      assert user.update(password: 'newpass', password_confirmation: 'newpass')
    end
  end

  test 'should notify previous email on email change when configured' do
    swap Devise, send_email_changed_notification: true do
      user = create_user
      original_email = user.email
      assert_email_sent original_email do
        assert user.update(email: '[email protected]')
      end
      assert_match original_email, ActionMailer::Base.deliveries.last.body.encoded
    end
  end

  test 'should notify email on password change when configured' do
    swap Devise, send_password_change_notification: true do
      user = create_user
      assert_email_sent user.email do
        assert user.update(password: 'newpass', password_confirmation: 'newpass')
      end
      assert_match user.email, ActionMailer::Base.deliveries.last.body.encoded
    end
  end

  test 'should not notify email on password change even when configured if skip_password_change_notification! is invoked' do
    swap Devise, send_password_change_notification: true do
      user = create_user
      user.skip_password_change_notification!
      assert_email_not_sent do
        assert user.update(password: 'newpass', password_confirmation: 'newpass')
      end
    end
  end

  test 'should not notify email on email change even when configured if skip_email_changed_notification! is invoked' do
    swap Devise, send_email_changed_notification: true do
      user = create_user
      user.skip_email_changed_notification!
      assert_email_not_sent do
        assert user.update(email: '[email protected]')
      end
    end
  end

  test 'downcase_keys with validation' do
    User.create(email: "[email protected]", password: "123456")
    user = User.create(email: "[email protected]", password: "123456")
    assert_not user.valid?
  end

  test 'required_fields should be encryptable_password and the email field by default' do
    assert_equal [
      :encrypted_password,
      :email
    ], Devise::Models::DatabaseAuthenticatable.required_fields(User)
  end

  test 'required_fields should be encryptable_password and the login when the login is on authentication_keys' do
    swap Devise, authentication_keys: [:login] do
      assert_equal [
        :encrypted_password,
        :login
      ], Devise::Models::DatabaseAuthenticatable.required_fields(User)
    end
  end
end