Testing reset password flow

I’m working on adding tests for the password reset flow in an Ash application, similar to the one used in Phoenix authentication. Specifically, I need help with extracting the token that is sent to the user via email during the password reset process. I’m trying to replicate the approach used in Phoenix authentication tests. Does anyone have insights on how to extract and use this token for testing in Ash?

defmodule AuthSampleWeb.UserResetPasswordLiveTest do
use AuthSampleWeb.ConnCase, async: true

import Phoenix.LiveViewTest
import AuthSample.AccountsFixtures

alias AuthSample.Accounts

setup do
user = user_fixture()

token =
extract_user_token(fn url ->
Accounts.deliver_user_reset_password_instructions(user, url)
end)

%{token: token, user: user}
end

describe "Reset password page" do
test "renders reset password with valid token", %{conn: conn, token: token} do
{:ok, _lv, html} = live(conn, ~p"/users/reset_password/#{token}")

assert html =~ "Reset Password"
end

test "does not render reset password with invalid token", %{conn: conn} do
{:error, {:redirect, to}} = live(conn, ~p"/users/reset_password/invalid")

assert to == %{
        flash: %{"error" => "Reset password link is invalid or it has expired."},
        to: ~p"/"
      }
end

test "renders errors for invalid data", %{conn: conn, token: token} do
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")

result =
lv
|> element("#reset_password_form")
|> render_change(
  user: %{"password" => "secret12", "password_confirmation" => "secret123456"}
)

assert result =~ "should be at least 12 character"
assert result =~ "does not match password"
end
end

describe "Reset Password" do
test "resets password once", %{conn: conn, token: token, user: user} do
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")

{:ok, conn} =
lv
|> form("#reset_password_form",
  user: %{
    "password" => "new valid password",
    "password_confirmation" => "new valid password"
  }
)
|> render_submit()
|> follow_redirect(conn, ~p"/users/log_in")

refute get_session(conn, :user_token)
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password reset successfully"
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
end

test "does not reset password on invalid data", %{conn: conn, token: token} do
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")

result =
lv
|> form("#reset_password_form",
  user: %{
    "password" => "too short",
    "password_confirmation" => "does not match"
  }
)
|> render_submit()

assert result =~ "Reset Password"
assert result =~ "should be at least 12 character(s)"
assert result =~ "does not match password"
end
end

describe "Reset password navigation" do
test "redirects to login page when the Log in button is clicked", %{conn: conn, token: token} do
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")

{:ok, conn} =
lv
|> element(~s|main a:fl-contains("Log in")|)
|> render_click()
|> follow_redirect(conn, ~p"/users/log_in")

assert conn.resp_body =~ "Log in"
end

test "redirects to registration page when the Register button is clicked", %{
conn: conn,
token: token
} do
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")

{:ok, conn} =
lv
|> element(~s|main a:fl-contains("Register")|)
|> render_click()
|> follow_redirect(conn, ~p"/users/register")

assert conn.resp_body =~ "Register"
end
end
end

 def extract_user_token(fun) do
    {:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
    [_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
    token
  end

We don’t currently support passing a URL down to configure the route that is generated the way that phx.gen.auth does. To me its a bit of a conflation of two separate things, i.e using the “how to generate a URL” as a tool for “telling a test what the generated token was”, so I 'm not sure I’d want to implement it that way anyway?

I think you might be able to use Swoosh.Adapters.Test — Swoosh v1.17.6 and to parse the token out of the email, but I think the LiveView testing code there starts a separate process I believe, which would confuse the swoosh test adapter. Worth trying that approach to start with.

@jimsynz will probably have some thoughts here, or perhaps there is a way of testing this e2e that I’m not aware of.

Firstly, a disclaimer: I don’t really think these are high value tests - it’s not like phx.gen.auth in that it’s code that you now own and maintain as soon as it’s generated. I feel like AA has adequate test coverage to be comfortable that confirmation works as expected.

With all that said however, I can see why you might want to acceptance test the happy path and can think of a couple of ways to do it:

Probably the easiest way is to use a registration action to create your user (rather than a seed or factory that bypasses action logic). You will find a :confirmation_token key in the user’s metadata which you can retrieve with Ash.Resource.get_metadata/2. If you want to validate that the email is correctly generated then you should be able to match it’s text contents against the token you retrieved from the metadata.

The second way to do it is to bypass the registration step and pass your fixture user into AshAuthentiation.Jwt.token_for_user/3 with %{"act" => "confirm"} (or whatever your confirmation action is called) to generate a confirmation token which you can then use to generate your email and validate it’s content.

3 Likes

Via Swoosh you can do it. IIRC the integrated helpers don’t give access to the email, but you can call assert_received directly (as the helpers do):

    assert_received {:email, %Swoosh.Email{} = email}
    assert email.to == [{member1.full_name.string, member1.email.string}]
    [_, html_body_body] = Regex.run(~r|<body[^>]*>(.*)</body>|s, email.html_body)
    [_, reset_link] = Regex.run(~r|\bhref="([^"]*)"|, html_body_body)
    assert String.length(reset_link) >= 240, "password reset link/token too short"
    [_, token] = Regex.run(~r|/password-reset/([\w-]+\.[\w-]+\.[\w-]+)|, reset_link)

(The 240 is pretty much arbitrary.)