Set socket assigns before on_mount in testing

My app has a helper function (say ensure_authenicated) that is run by the live session in the router “on_mount” that fetches the current user and sets both the user and their account on the socket assigns before on_mount is called in the live view.

  scope "/", AppWeb do
    pipe_through [:browser, :require_authenticated_user]

    live_session :require_authenticated_user,
      on_mount: [{AppWeb.UserAuth, :ensure_authenticated}] do
      live "/users/settings", UserSettingsLive, :edit
      ...
    end
  end

I need to test this live view, so the precondition is thus that the user and account are on the socket assigns somehow before I call the code under test. The code under test reads only from the socket, not the session.

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     stream(
       socket,
       :thing,
       Thing.function(socket.assigns.current_account)
     )}
  end

Is is possible to do this? The test helpers seem to only let you pass in session data

It looks like you’re using phx_gen_auth in which case it adds a register_and_login_user helper to your ConnCase. You can use it like so:

setup :register_and_login_user

You can take a look at your ConnCase file to see how it’s done.

1 Like

Thanks! This only sets data on the conn and not the socket as I need though, correct?

  def register_and_log_in_user(%{conn: conn}) do
    user = App.UsersFixtures.user_fixture()
    %{conn: log_in_user(conn, user), user: user}
  end

  @doc """
  Logs the given `user` into the `conn`.

  It returns an updated `conn`.
  """
  def log_in_user(conn, user) do
    token = App.Users.generate_user_session_token(user)

    conn
    |> Phoenix.ConnTest.init_test_session(%{})
    |> Plug.Conn.put_session(:user_token, token)
  end

It’ll run through the app code which will transfer it into the socket. That all happens in AppWeb.UserAuth.

1 Like

Thanks again. Weird, I am not observing this but that could be a separate issue.

It’ll run through the app code which will transfer it into the socket

It might be helpful if you or anyone else could explain exactly how that happens since it’s not obvious to me. Looking at the contents of my user_auth.ex it seems like the socket only gets touched here:

  def on_mount(:ensure_authenticated, params, session, socket) do
    socket = mount_current_user(socket, session, params)

    if socket.assigns.current_user do
      {:cont, socket}
    else
      socket =
        socket
        |> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
        |> Phoenix.LiveView.redirect(to: ~p"/users/log_in")

      {:halt, socket}
    end
  end

...

  defp mount_current_user(socket, session, params) do
    case session do
      %{"user_token" => user_token} ->
        assign_new_current_user_and_account(socket, session, params, user_token)
      %{} ->
        Phoenix.Component.assign_new(socket, :current_user, fn -> nil end)
    end
  end

  defp assign_new_current_user_and_account(socket, session, params, user_token) do
    socket =
      socket
      |> Phoenix.Component.assign_new(:current_user, fn ->
        Users.get_user_by_session_token(user_token)
      end)

    socket
    |> Phoenix.Component.assign_new(:current_account, fn ->
      if user = socket.assigns.current_user do
        get_current_account(user, params, session)
      end
    end)
  end

I don’t follow how the :register_and_log_in_user setup helper is supposed to trigger this. To be clear, my test looks like this

    setup [:register_and_log_in_user, ...]

    test "lists things", %{conn: conn} do
      {:ok, _index_live, html} = live(conn, ~p"/route")

      assert html =~ "..."
    end

From register_and_log_in_user (which puts a user in the db), ConnCase.login_user generates a session token and puts it in the %Conn{}. This effectively authenticates the user in. The ensure_authenticated hook—triggered from the :on_mount option passed to live_session—is defined in UserAuth which calls mount_current_user, also in that module which looks like this:

  defp mount_current_user(socket, session) do
    Phoenix.Component.assign_new(socket, :current_user, fn ->
      if user_token = session["user_token"] do
        Accounts.get_user_by_session_token(user_token)
      end
    end)
  end
1 Like

Doh, it was a separate issue. Was not calling :ensure_authenticated in the scope my actual new routes were in.

I assumed I should be setting up the socket preconditions like this in my tests so I wasn’t looking out for this but makes sense and is better that it happens in the app code.

The general answer to the question in the title seems to be to setup preconditions on the conn and then design your app code to move it to the socket. Don’t see a reason to be intializing data on the socket in tests.

Darn router code snippet output by the 3rd party boilerplate generator left this out and the first thing I was trying to do was get the tests passing. In their defense it was a suggestion to “add routes like these to get started”, livesaaskit.com has actually been fairly nice.