Back to Repositories

Testing Remember Me Token Management in Devise Authentication

This test suite validates the Rememberable module functionality in Devise, focusing on user session persistence and remember me token management. It ensures proper cookie serialization, token generation, and expiration handling for the remember me feature.

Test Coverage Overview

The test suite provides comprehensive coverage of Devise’s Rememberable module functionality.

Key areas tested include:
  • Token generation and management
  • Cookie serialization and deserialization
  • Session persistence timing
  • Remember me expiration handling
Integration points cover user authentication state management and cookie-based session handling.

Implementation Analysis

The testing approach utilizes ActiveSupport::TestCase with focused unit tests for the Rememberable module. Tests employ mocking and stubbing through expects() calls to isolate functionality.

Key patterns include:
  • Resource creation helpers
  • State verification
  • Time-based session management
  • Configuration swapping

Technical Details

Testing tools and configuration:
  • Minitest framework
  • ActiveSupport::TestCase base class
  • Devise configuration swapping
  • Mock expectations via expects()
  • Time manipulation helpers
  • Custom resource creation methods

Best Practices Demonstrated

The test suite exemplifies strong testing practices through isolation of concerns and comprehensive edge case coverage.

Notable practices include:
  • Systematic state verification
  • Configuration-dependent behavior testing
  • Error case handling
  • Time-sensitive operations testing
  • Clear test naming conventions

heartcombo/devise

test/models/rememberable_test.rb

            
# frozen_string_literal: true

require 'test_helper'

class RememberableTest < ActiveSupport::TestCase
  def resource_class
    User
  end

  def create_resource
    create_user
  end

  test 'remember_me should not generate a new token if using salt' do
    user = create_user
    user.expects(:valid?).never
    user.remember_me!
    assert user.remember_created_at
  end

  test 'remember_me should not generate a new token if valid token exists' do
    user = create_user
    user.singleton_class.send(:attr_accessor, :remember_token)
    User.to_adapter.expects(:find_first).returns(nil)

    user.remember_me!
    existing_token = user.remember_token

    user.remember_me!
    assert_equal existing_token, user.remember_token
  end

  test 'forget_me should not clear remember token if using salt' do
    user = create_user
    user.remember_me!
    user.expects(:valid?).never
    user.forget_me!
  end

  test 'can generate remember token' do
    user = create_user
    user.singleton_class.send(:attr_accessor, :remember_token)
    User.to_adapter.expects(:find_first).returns(nil)
    user.remember_me!
    assert user.remember_token
  end

  test 'serialize into cookie' do
    user = create_user
    user.remember_me!
    id, token, date = User.serialize_into_cookie(user)
    assert_equal id, user.to_key
    assert_equal token, user.authenticatable_salt
    assert date.is_a?(String)
  end

  test 'serialize from cookie' do
    user = create_user
    user.remember_me!
    assert_equal user, User.serialize_from_cookie(user.to_key, user.authenticatable_salt, Time.now.utc)
  end

  test 'serialize from cookie should accept a String with the datetime seconds and microseconds' do
    user = create_user
    user.remember_me!
    assert_equal user, User.serialize_from_cookie(user.to_key, user.authenticatable_salt, Time.now.utc.to_f.to_json)
  end

  test 'serialize from cookie should return nil with invalid datetime' do
    user = create_user
    user.remember_me!
    assert_nil User.serialize_from_cookie(user.to_key, user.authenticatable_salt, "2013")
  end

  test 'serialize from cookie should return nil if no resource is found' do
    assert_nil resource_class.serialize_from_cookie([0], "123", Time.now.utc)
  end

  test 'serialize from cookie should return nil if no timestamp' do
    user = create_user
    user.remember_me!
    assert_nil User.serialize_from_cookie(user.to_key, user.authenticatable_salt)
  end

  test 'serialize from cookie should return nil if timestamp is earlier than token creation' do
    user = create_user
    user.remember_me!
    assert_nil User.serialize_from_cookie(user.to_key, user.authenticatable_salt, 1.day.ago)
  end

  test 'serialize from cookie should return nil if timestamp is older than remember_for' do
    user = create_user
    user.remember_created_at = 1.month.ago
    user.remember_me!
    assert_nil User.serialize_from_cookie(user.to_key, user.authenticatable_salt, 3.weeks.ago)
  end

  test 'serialize from cookie me return nil if is a valid resource with invalid token' do
    user = create_user
    user.remember_me!
    assert_nil User.serialize_from_cookie(user.to_key, "123", Time.now.utc)
  end

  test 'raises a RuntimeError if the user does not implements a rememberable value' do
    user = User.new
    assert_raise(RuntimeError) { user.rememberable_value }

    user_with_remember_token = User.new
    def user_with_remember_token.remember_token; '123-token'; end
    assert_equal '123-token', user_with_remember_token.rememberable_value

    user_with_salt = User.new
    def user_with_salt.authenticatable_salt; '123-salt'; end
    assert_equal '123-salt', user_with_salt.rememberable_value
  end

  test 'raises a RuntimeError if authenticatable_salt is nil or empty' do
    user = User.new
    def user.authenticatable_salt; nil; end
    assert_raise RuntimeError do
      user.rememberable_value
    end

    user = User.new
    def user.authenticatable_salt; ""; end
    assert_raise RuntimeError do
      user.rememberable_value
    end
  end

  test 'should respond to remember_me attribute' do
    assert_respond_to resource_class.new, :remember_me
    assert_respond_to resource_class.new, :remember_me=
  end

  test 'forget_me should clear remember_created_at if expire_all_remember_me_on_sign_out is true' do
    swap Devise, expire_all_remember_me_on_sign_out: true do
      resource = create_resource
      resource.remember_me!
      assert_not_nil resource.remember_created_at

      resource.forget_me!
      assert_nil resource.remember_created_at
    end
  end

  test 'forget_me should not clear remember_created_at if expire_all_remember_me_on_sign_out is false' do
    swap Devise, expire_all_remember_me_on_sign_out: false do
      resource = create_resource
      resource.remember_me!

      assert_not_nil resource.remember_created_at

      resource.forget_me!
      assert_not_nil resource.remember_created_at
    end
  end

  test 'forget_me should not try to update resource if it has been destroyed' do
    resource = create_resource
    resource.expects(:remember_created_at).never
    resource.expects(:save).never

    resource.destroy
    resource.forget_me!
  end

  test 'remember expires at uses remember for configuration' do
    swap Devise, remember_for: 3.days do
      resource = create_resource
      resource.remember_me!
      assert_equal 3.days.from_now.to_date, resource.remember_expires_at.to_date

      Devise.remember_for = 5.days
      assert_equal 5.days.from_now.to_date, resource.remember_expires_at.to_date
    end
  end

  test 'should have the required_fields array' do
    assert_equal [
      :remember_created_at
    ], Devise::Models::Rememberable.required_fields(User)
  end
end