Migrating Away from Devise Part 6: Trackable Module and Tests

Published: January 24, 2025
Part 6 of a multi-part series for moving away from devise to Rails' authentication generator
This is the sixth part of a multi-part series about moving from devise to the Rails generated authentication system

In the user model, this method will update the old trackable columns. This is based heavily off of devise's implementation.

def update_trackables(request)
  self.last_sign_in_at = current_sign_in_at || Time.now.utc
  self.current_sign_in_at = Time.now.utc
  self.last_sign_in_ip = current_sign_in_ip || request.remote_ip
  self.current_sign_in_ip = request.remote_ip
  self.sign_in_count ||= 0
  self.sign_in_count += 1
  save(validate: false)
end

When we create a new session for the user in the Authorization module, the new method will be called.

def start_new_session_for(user)
  user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
    Current.session = session
    user.update_trackables(request) # Here's the new code
    cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
  end
end

In regards to testing, devise provided a set of helper methods along with warden to sign in users. It also provided a helper method for fixtures.

test_helper.rb contains a TEST_PASSWORD constant with a password string that can be used by fixtures and test (such as "testing123"). It can be applied to fixtures for users like the following.

<% password_digest = BCrypt::Password.create(TEST_PASSWORD) %>

one:
  email: one@internet.com
  username: one
  password_digest: <%= password_digest %>
  confirmed_at: <%= Time.now %>

test_helper.rb includes this helper method to sign in a given user to generate a session for unit, controller, and integration tests.

TEST_PASSWORD = "testing123"

class ActiveSupport::TestCase
  # Run tests in parallel with specified workers
  parallelize(workers: :number_of_processors)

  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all

  # Add more helper methods to be used by all tests here...
  def sign_in(user)
    post session_path, params: { user: { username: user.username, password: TEST_PASSWORD } }
  end
end

For system tests, a similar helper method works in application_system_test_case.rb.

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]

  def sign_in(user)
    visit new_session_path
    fill_in "Username", with: user.username
    fill_in "Password", with: TEST_PASSWORD
    click_on "Sign in"

    assert_text "Successfully signed in."
  end
end

And... that's it! This should complete the process of removing devise from your app. Remove the gem from your Gemfile if you haven't done so already as well as the initializer. Commit the branch, open a PR, review it, merge, and deploy. Depending on how long you've taken to do this process, the sessions table should have enough active sessions from part 1 to keep existing users signed in.

Possibly in the future - Bonus part: Implement omniauth.