Design choices around integrating phx_gen_auth with a mailer

What’s your thoughts on pros/cons of design choices as it relates to how you could follow up with using the phx_gen_auth and at what entry points you could integrating with say Swoosh or Bamboo?

Let’s take the user’s registration controller for example.

lib/my_app_web/controllers/user_registration_controller.ex

alias MyAppWeb.{Mailer, AccountEmail}

def create(conn, %{"user" => user_params}) do
    case Accounts.register_user(user_params) do
      {:ok, user} ->
        {:ok, _} =
          Accounts.deliver_user_confirmation_instructions(
            user,
            &Routes.user_confirmation_url(conn, :confirm, &1),
            &Mailer.deliver/1,
            &AccountEmail.confirmation_email/1
          )
      ...
    end
  end

You will notice that just as with the originally generated confirmation link helper, I’m also passing in the email and mailer’s deliver function to be used downstream.

Looking at how I choose to call said functions downstream in the context you will see

lib/my_app/accounts.ex

      UserNotifier.deliver_instructions(
        user,
        confirmation_url_fun.(encoded_token),
        deliver_fun,
        email_fun
      )

Using dependency injection I just pass the funs along to deliver_instructions

lib/my_app/accounts/user_notifier.ex
  def deliver_instructions(user, url, deliver_fun, email_fun) do
    Task.start(fn ->
      %{url: url, email: user.email}
      |> email_fun.()
      |> deliver_fun.()
    end)
  end

In this example, I’m using swoosh and a very basic async deliver call.

Where I’ve hit the wall is the expected result of deliver_instructions and trying to salvage many of the tests already generated by phx_gen_auth. The specific point of conflict looks to be the helper used for extracting the token.

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

This got me to question: if the result of deliver_instructions is really only to benefit the test is it really the right choice? It’s likely it is, so with that what does deliver_instructions need to return to satisfy the test?

I am on my phone but you can see how we did it on Bytepack: GitHub - dashbitco/bytepack_archive: Archive of bytepack.io

We send emails from the account context and we pick the token up in our test support helpers.

5 Likes

I can’t say for sure that I’ve done anything the “correct” way, but I’ve used phx_gen_auth with Swoosh, and this is how I did a “confirm user email” test:

test "Confirm a user account", %{conn: conn, user: user} do
  token = Accounts.deliver_user_confirmation_instructions(user, &add_confirm_param/1)
  {:ok, conn} = live(conn, "/?confirm=#{token}") |> follow_redirect(conn)

  assert html_response(conn, 200) =~ "Account confirmed successfully."
end

I don’t think I made any changes to Accounts.deliver_user_confirmation_instructions/2, but the final call of that function goes to UserNotifier.deliver_confirmation_instructions/2, which I altered to return the token after it’s done.

I also had to adjust the way the URL was generated, for other reasons. So I make use of these helpers:

def return_token(url) do
  [_, token] = String.split(url, "=")
  token
end
def add_confirm_param(token) do
  MyAppWeb.Endpoint.url() <> "?confirm=" <> token
end
1 Like

This is intresting.

I would have thought you would keep these templates/layouts in phoenix.

Could you speak to some of the rationalee behind this choice?

Ah, the issue is that templates depend on Phoenix.View, which would require a dependency on Phoenix from our domain that we wanted to avoid.

But this will be addressed on Phoenix v1.6 because Phoenix.View is finally a separate package.

8 Likes

Sasa Juric, in this article, presents a very interesting solution for avoiding coupling when the domain depends on the interface.

Using dependency injection still requires the web layer to exist though. This makes sense for the example of urls, which to be useful also need to be served by the web layer.

The extraction of phoenix_view however allows both the web layer and the domain layer to depend on a library for handling templates on their own. No need for the web layer to exists for the domain to be able to send html emails – which is a sane requirements given even systems which do not serve an http endpoint might want to send html emails.

2 Likes

So it was this that lead me to pass in the functions from in the phoenix app when calling into the context. Just like the use case of calling the link helper for the token url, is that not a good pattern to follow for injecting the out of scope dependency? I guess I don’t understand why that is good for the link helper but not for the mailer.

Well, it depends on how you draw the lines. For me, the mailer is part of the domain (for example, I want to send the email regardless if the sign up happens in an API, LiveView, Controller, etc). But you can slice it differently if you prefer.