Back to Repositories

Testing Email Confirmation Implementation in Devise

This test suite validates the email confirmation functionality in Devise, covering user registration confirmation flows and email change verification processes. It ensures robust handling of confirmation tokens, expiration periods, and various edge cases in authentication workflows.

Test Coverage Overview

Comprehensive testing of Devise’s confirmable module covering:
  • User email confirmation flows
  • Token validation and expiration
  • Custom mailer integration
  • Reconfirmation for email changes
  • Paranoid mode behavior
  • JSON format responses

Implementation Analysis

The test suite employs Devise’s IntegrationTest framework with a focus on user interactions and system responses. It utilizes helper methods for common operations and implements swap functionality to test different Devise configurations.

Notable patterns include token manipulation, time-based testing, and custom routing verification.

Technical Details

Key technical components:
  • Minitest integration framework
  • ActionMailer for email verification
  • Custom helper methods for token handling
  • Devise configuration swapping
  • JSON response testing
  • Mock objects and stubbing

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through comprehensive edge case coverage, isolation of test scenarios, and proper setup/teardown management. It demonstrates effective use of helper methods, clear test naming, and thorough validation of both success and failure paths.

heartcombo/devise

test/integration/confirmable_test.rb

            
# frozen_string_literal: true

require 'test_helper'

class ConfirmationTest < Devise::IntegrationTest

  def visit_user_confirmation_with_token(confirmation_token)
    visit user_confirmation_path(confirmation_token: confirmation_token)
  end

  def resend_confirmation
    user = create_user(confirm: false)
    ActionMailer::Base.deliveries.clear

    visit new_user_session_path
    click_link "Didn't receive confirmation instructions?"

    fill_in 'email', with: user.email
    click_button 'Resend confirmation instructions'
  end

  test 'user should be able to request a new confirmation' do
    resend_confirmation

    assert_current_url '/users/sign_in'
    assert_contain 'You will receive an email with instructions for how to confirm your email address in a few minutes'
    assert_equal 1, ActionMailer::Base.deliveries.size
    assert_equal ['[email protected]'], ActionMailer::Base.deliveries.first.from
  end

  test 'user should receive a confirmation from a custom mailer' do
    User.any_instance.stubs(:devise_mailer).returns(Users::Mailer)
    resend_confirmation
    assert_equal ['[email protected]'], ActionMailer::Base.deliveries.first.from
  end

  test 'user with invalid confirmation token should not be able to confirm an account' do
    visit_user_confirmation_with_token('invalid_confirmation')
    assert_have_selector '#error_explanation'
    assert_contain %r{Confirmation token(.*)invalid}
  end

  test 'user with valid confirmation token should not be able to confirm an account after the token has expired' do
    swap Devise, confirm_within: 3.days do
      user = create_user(confirm: false, confirmation_sent_at: 4.days.ago)
      assert_not user.confirmed?
      visit_user_confirmation_with_token(user.raw_confirmation_token)

      assert_have_selector '#error_explanation'
      assert_contain %r{needs to be confirmed within 3 days}
      assert_not user.reload.confirmed?
      assert_current_url "/users/confirmation?confirmation_token=#{user.raw_confirmation_token}"
    end
  end

  test 'user with valid confirmation token where the token has expired and with application router_name set to a different engine it should raise an error' do
    user = create_user(confirm: false, confirmation_sent_at: 4.days.ago)

    swap Devise, confirm_within: 3.days, router_name: :fake_engine do
      assert_raise ActionView::Template::Error do
        visit_user_confirmation_with_token(user.raw_confirmation_token)
      end
    end
  end

  test 'user with valid confirmation token where the token has expired and with application router_name set to a different engine and route overrides back to main it shows the path' do
    user = create_user(confirm: false, confirmation_sent_at: 4.days.ago)

    swap Devise, confirm_within: 3.days, router_name: :fake_engine do
      visit user_on_main_app_confirmation_path(confirmation_token: user.raw_confirmation_token)

      assert_current_url "/user_on_main_apps/confirmation?confirmation_token=#{user.raw_confirmation_token}"
    end
  end

  test 'user with valid confirmation token where the token has expired with router overrides different engine it shows the path' do
    user = create_user(confirm: false, confirmation_sent_at: 4.days.ago)

    swap Devise, confirm_within: 3.days do
      visit user_on_engine_confirmation_path(confirmation_token: user.raw_confirmation_token)

      assert_current_url "/user_on_engines/confirmation?confirmation_token=#{user.raw_confirmation_token}"
    end
  end

  test 'user with valid confirmation token should be able to confirm an account before the token has expired' do
    swap Devise, confirm_within: 3.days do
      user = create_user(confirm: false, confirmation_sent_at: 2.days.ago)
      assert_not user.confirmed?
      visit_user_confirmation_with_token(user.raw_confirmation_token)

      assert_contain 'Your email address has been successfully confirmed.'
      assert_current_url '/users/sign_in'
      assert user.reload.confirmed?
    end
  end

  test 'user should be redirected to a custom path after confirmation' do
    Devise::ConfirmationsController.any_instance.stubs(:after_confirmation_path_for).returns("/?custom=1")

    user = create_user(confirm: false)
    visit_user_confirmation_with_token(user.raw_confirmation_token)

    assert_current_url "/?custom=1"
  end

  test 'already confirmed user should not be able to confirm the account again' do
    user = create_user(confirm: false)
    user.confirmed_at = Time.now
    user.save
    visit_user_confirmation_with_token(user.raw_confirmation_token)

    assert_have_selector '#error_explanation'
    assert_contain 'already confirmed'
  end

  test 'already confirmed user should not be able to confirm the account again neither request confirmation' do
    user = create_user(confirm: false)
    user.confirmed_at = Time.now
    user.save

    visit_user_confirmation_with_token(user.raw_confirmation_token)
    assert_contain 'already confirmed'

    fill_in 'email', with: user.email
    click_button 'Resend confirmation instructions'
    assert_contain 'already confirmed'
  end

  test 'not confirmed user with setup to block without confirmation should not be able to sign in' do
    swap Devise, allow_unconfirmed_access_for: 0.days do
      sign_in_as_user(confirm: false)

      assert_contain 'You have to confirm your email address before continuing'
      assert_not warden.authenticated?(:user)
    end
  end

  test 'not confirmed user should not see confirmation message if invalid credentials are given' do
    swap Devise, allow_unconfirmed_access_for: 0.days do
      sign_in_as_user(confirm: false) do
        fill_in 'password', with: 'invalid'
      end

      assert_contain 'Invalid Email or password'
      assert_not warden.authenticated?(:user)
    end
  end

  test 'not confirmed user but configured with some days to confirm should be able to sign in' do
    swap Devise, allow_unconfirmed_access_for: 1.day do
      sign_in_as_user(confirm: false)

      assert_response :success
      assert warden.authenticated?(:user)
    end
  end

  test 'unconfirmed but signed in user should be redirected to their root path' do
    swap Devise, allow_unconfirmed_access_for: 1.day do
      user = sign_in_as_user(confirm: false)

      visit_user_confirmation_with_token(user.raw_confirmation_token)
      assert_contain 'Your email address has been successfully confirmed.'
      assert_current_url '/'
    end
  end

  test 'user should be redirected to sign in page whenever signed in as another resource at same session already' do
    sign_in_as_admin

    user = create_user(confirm: false)
    visit_user_confirmation_with_token(user.raw_confirmation_token)

    assert_current_url '/users/sign_in'
  end

  test "should not be able to confirm an email with a blank confirmation token" do
    visit_user_confirmation_with_token("")

    assert_contain %r{Confirmation token can['’]t be blank}
  end

  test "should not be able to confirm an email with a nil confirmation token" do
    visit_user_confirmation_with_token(nil)

    assert_contain %r{Confirmation token can['’]t be blank}
  end

  test "should not be able to confirm user with blank confirmation token" do
    user = create_user(confirm: false)
    user.update_attribute(:confirmation_token, "")

    visit_user_confirmation_with_token("")

    assert_contain %r{Confirmation token can['’]t be blank}
  end

  test "should not be able to confirm user with nil confirmation token" do
    user = create_user(confirm: false)
    user.update_attribute(:confirmation_token, nil)

    visit_user_confirmation_with_token(nil)

    assert_contain %r{Confirmation token can['’]t be blank}
  end

  test 'error message is configurable by resource name' do
    store_translations :en, devise: {
      failure: { user: { unconfirmed: "Not confirmed user" } }
    } do
      sign_in_as_user(confirm: false)
      assert_contain 'Not confirmed user'
    end
  end

  test 'resent confirmation token with valid e-mail in JSON format should return empty and valid response' do
    user = create_user(confirm: false)
    post user_confirmation_path(format: 'json'), params: { user: { email: user.email } }
    assert_response :success
    assert_equal({}.to_json, response.body)
  end

  test 'resent confirmation token with invalid e-mail in JSON format should return invalid response' do
    create_user(confirm: false)
    post user_confirmation_path(format: 'json'), params: { user: { email: '[email protected]' } }
    assert_response :unprocessable_entity
    assert_includes response.body, '{"errors":{'
  end

  test 'confirm account with valid confirmation token in JSON format should return valid response' do
    user = create_user(confirm: false)
    get user_confirmation_path(confirmation_token: user.raw_confirmation_token, format: 'json')
    assert_response :success
    assert_includes response.body, '{"user":{'
  end

  test 'confirm account with invalid confirmation token in JSON format should return invalid response' do
    create_user(confirm: false)
    get user_confirmation_path(confirmation_token: 'invalid_confirmation', format: 'json')
    assert_response :unprocessable_entity
    assert_includes response.body, '{"confirmation_token":['
  end

  test "when in paranoid mode and with a valid e-mail, should not say that the e-mail is valid" do
    swap Devise, paranoid: true do
      user = create_user(confirm: false)
      visit new_user_session_path

      click_link "Didn't receive confirmation instructions?"
      fill_in 'email', with: user.email
      click_button 'Resend confirmation instructions'

      assert_contain "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
      assert_current_url "/users/sign_in"
    end
  end

  test "when in paranoid mode and with a invalid e-mail, should not say that the e-mail is invalid" do
    swap Devise, paranoid: true do
      visit new_user_session_path

      click_link "Didn't receive confirmation instructions?"
      fill_in 'email', with: "[email protected]"
      click_button 'Resend confirmation instructions'

      assert_not_contain "1 error prohibited this user from being saved:"
      assert_not_contain "Email not found"

      assert_contain "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
      assert_current_url "/users/sign_in"
    end
  end
end

class ConfirmationOnChangeTest < Devise::IntegrationTest
  def create_second_admin(options = {})
    @admin = nil
    create_admin(options)
  end

  def visit_admin_confirmation_with_token(confirmation_token)
    visit admin_confirmation_path(confirmation_token: confirmation_token)
  end

  test 'admin should be able to request a new confirmation after email changed' do
    admin = create_admin
    admin.update(email: '[email protected]')

    visit new_admin_session_path
    click_link "Didn't receive confirmation instructions?"

    fill_in 'email', with: admin.unconfirmed_email
    assert_difference "ActionMailer::Base.deliveries.size" do
      click_button 'Resend confirmation instructions'
    end

    assert_current_url '/admin_area/sign_in'
    assert_contain 'You will receive an email with instructions for how to confirm your email address in a few minutes'
  end

  test 'admin with valid confirmation token should be able to confirm email after email changed' do
    admin = create_admin
    admin.update(email: '[email protected]')
    assert_equal '[email protected]', admin.unconfirmed_email
    visit_admin_confirmation_with_token(admin.raw_confirmation_token)

    assert_contain 'Your email address has been successfully confirmed.'
    assert_current_url '/admin_area/sign_in'
    assert admin.reload.confirmed?
    assert_not admin.reload.pending_reconfirmation?
  end

  test 'admin with previously valid confirmation token should not be able to confirm email after email changed again' do
    admin = create_admin
    admin.update(email: '[email protected]')
    assert_equal '[email protected]', admin.unconfirmed_email

    raw_confirmation_token = admin.raw_confirmation_token
    admin = Admin.find(admin.id)

    admin.update(email: '[email protected]')
    assert_equal '[email protected]', admin.unconfirmed_email

    visit_admin_confirmation_with_token(raw_confirmation_token)
    assert_have_selector '#error_explanation'
    assert_contain(/Confirmation token(.*)invalid/)

    visit_admin_confirmation_with_token(admin.raw_confirmation_token)
    assert_contain 'Your email address has been successfully confirmed.'
    assert_current_url '/admin_area/sign_in'
    assert admin.reload.confirmed?
    assert_not admin.reload.pending_reconfirmation?
  end

  test 'admin email should be unique also within unconfirmed_email' do
    admin = create_admin
    admin.update(email: '[email protected]')
    assert_equal '[email protected]', admin.unconfirmed_email

    create_second_admin(email: "[email protected]")

    visit_admin_confirmation_with_token(admin.raw_confirmation_token)
    assert_have_selector '#error_explanation'
    assert_contain(/Email.*already.*taken/)
    assert admin.reload.pending_reconfirmation?
  end
end