Search code examples
ruby-on-railstestingcapybaraclearance

How do I set the port correctly in email links during a system test?


I'm writing a System test to confirm the entire sign up flow is working in a Rails 7 app (with the Clearance gem and an email confirmation SignInGuard).

The test is working fine right up until I "click" the confirm link in the email (after parsing it with Nokogiri). For some reason the URL in the email points to my dev server (port 3000) instead of pointing to the test server (port 49736, 49757, 49991, whatever).

I could look up the current port the test server is using (it changes every run) and replace the port portion of the URL but that seems quite hacky. Am I missing something obvious or doing something wrong?

URL in mailer: confirm_email_url(@user.email_confirmation_token)

Route from rails routes:

Prefix          Verb   URI Pattern                       Controller#Action
confirm_email   GET    /confirm_email/:token(.:format)   email_confirmations#update

The system test so far:

require "application_system_test_case"
require "test_helper"
require "action_mailer/test_helper"

class UserSignUpFlowTest < ApplicationSystemTestCase
  include ActionMailer::TestHelper

  test "Sign up for a user account" do
    time = Time.now
    email = "test_user_#{time.to_s(:number)}@example.com"
    password = "Password#{time.to_s(:number)}!"

    # Sign up (sends confirmation email)
    visit sign_up_url
    fill_in "Email", with: email
    fill_in "Password", with: password
    assert_emails 1 do
      click_on "Sign up"
      sleep 1 # Not sure why this is required... Hotwire/Turbo glitch?
    end
    assert_selector "span", text: I18n.t("flashes.confirmation_pending")

    # Confirm
    last_email = ActionMailer::Base.deliveries.last
    parsed_email = Nokogiri::HTML(last_email.body.decoded)
    target_link = parsed_email.at("a:contains('#{I18n.t("clearance_mailer.confirm_email.link_text")}')")
    visit target_link["href"]
  # ^^^^^^^^^^^^^^^^^^^^^^^^^
  # This is the bit that fails... The link in the email points to my dev server (port 3000) rather than
  # the test server (port 49736, 49757, 49991, etc). I only figured this out when my dev server crashed
  # and Selenium started choking on a "net::ERR_CONNECTION_REFUSED" error
    assert_selector "span", text: I18n.t("flashes.email_confirmed")
  end
end

Edit

For the time being I've worked around it by replacing visit target_link["href"] with visit hacky_way_to_fix_incorrect_port(target_link["href"]):

  private

  def hacky_way_to_fix_incorrect_port(url)
    uri = URI(url)
    return "#{root_url}#{uri.path}"
  end

Solution

  • The URL used in mailers is specified by:
    Rails.application.configure.action_mailer.default_url_options

    In config/environments/test.rb I had set mine to port 3000 when I first installed Clearance:
    config.action_mailer.default_url_options = {host: "localhost:3000"}

    To fix it, I first tried specifying the port dynamically but the suggested method didn't actually work and it seems it isn't necessary. Removing the port number was enough to get my system test passing:
    config.action_mailer.default_url_options = {host: "localhost"}

    As mentioned by Thomas Walpole, the reason this works is that Capybara.always_include_port is set to true. With this setting, attempts to access http://localhost/confirm_email/<token> (with no port specified) are automatically rerouted to http://localhost:<port>/confirm_email/<token>.

    The always_include_port setting defaults to false in the Capybara gem but it turns out Rails sets it to true when it starts up the System Test server.