How to test a live_session on_mount handler that sets assigns

I’m running into an edge case with phoenix testing. I’ve got an on_mount handler behaving like a plug which creates a db session for a given client:

defmodule SgWeb.InitSessionId do
  @moduledoc """
  Adds the DB session ID to the assigns
  """
  import Phoenix.Component
  alias Sg.Sessions

  def on_mount(:default, _params, %{"client_id" => client_id}, socket) do
    session =
      case Sessions.get_session_by_client_id(client_id) do
        nil ->
          {:ok, session} = Sessions.create_session(%{client_id: client_id})
          session

        existing_session ->
          existing_session
      end

    {:cont, assign(socket, session_id: session.id)}
  end
end

However, whenever I try to test this as one would a plug, the assign function blows up and tells me I need to use render_component. I understand the logic, generally, but there has to be a way around this - I’m literally following a use case described in the docs in this instance (Phoenix.LiveView — Phoenix LiveView v0.19.5). Is there an easy way to mock a socket/assigns without triggering the exceptions?

Maybe you’re confusing the render functions from Phoenix.{LiveView, LiveComponent, Component} with the render_component function from Phoenix.LiveViewTest?

render_component/3 is meant to be used in tests to simulate rendering a component and accepts arbitrary assigns.

import Phoenix.LiveViewTest

test "greets" do
  assert render_component(&MyComponents.greet/1, name: "Mary") ==
           "<div>Hello, Mary!</div>"
end

source: render_component/3 | Phoenix.LiveViewTest docs

No I understand, but this is an on_mount callback, not a LV/LC etc. I just want to be able to unit test it.

 ** (ArgumentError) assign/3 expects a socket from Phoenix.LiveView/Phoenix.LiveComponent  or an assigns map from Phoenix.Component as first argument, got: %{}You passed an assigns map that does not have the relevant change tracking information. This typically means you are calling a function component by hand instead of using the HEEx template syntax.

my use of assign/2 is causing the above error in my test.

Can you show the test code you are trying to make work?

Sure, super simple

defmodule SgWeb.InitSessionIdTest do
  use Sg.DataCase

  alias Sg.Sessions

  test "creates a session db record if one not present" do
    session = %{"client_id" => "test123"}
    SgWeb.InitSessionId.on_mount(:default, nil, session,  %{})
    assert Sessions.get_session_by_client_id("test123").client_id == "test123" 
  end
end

the empty map where the socket should be is just a plaeholder of one of many and varied attempts to mock a socket

I’m very happy to leave this untested, as it’s pretty uncontroversial what it does, but I’m in it now and want to solve this for future reference as no doubt going to need to set assigns in live_session hooks again.

defmodule SgWeb.InitSessionIdTest do
  use Sg.DataCase

  alias Sg.Sessions
  import Phoenix.ChannelTest
  @endpoint SgWeb.Endpoint

  test "creates a session db record if one not present" do
    socket = socket(Phoenix.LiveView.Socket)
    session = %{"client_id" => "test123"}
    SgWeb.InitSessionId.on_mount(:default, nil, session, socket)
    assert Sessions.get_session_by_client_id("test123").client_id == "test123" 
  end
end

A little bit closer, but assigns still complains that it’s not a LV socket…

** (ArgumentError) assign/3 expects a socket from Phoenix.LiveView/Phoenix.LiveComponent  or an assigns map from Phoenix.Component as first argument, got: %Phoenix.Socket{assigns: %{}, channel: nil, channel_pid: nil, endpoint: SgWeb.Endpoint, handler: Phoenix.LiveView.Socket, id: nil, joined: false, join_ref: nil, private: %{}, pubsub_server: Sg.PubSub, ref: nil, serializer: Phoenix.ChannelTest.NoopSerializer, topic: nil, transport: {Phoenix.ChannelTest, #PID<0.507.0>}, transport_pid: #PID<0.504.0>}
     code: SgWeb.InitSessionId.on_mount(:default, nil, session, socket)
     stacktrace:
       (phoenix_live_view 0.19.5) lib/phoenix_component.ex:1198: Phoenix.Component.raise_bad_socket_or_assign!/2
       (elixir 1.15.2) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
       (sg 0.1.0) lib/sg_web/live/init_session_id.ex:19: SgWeb.InitSessionId.on_mount/4
       test/sg_web/live/init_session_id_test.exs:11: (test)

Got it! Used this as a ‘socket’ socket = %{session_id: 2, __changed__: %{client_id: true}}

I feel like unit testing these with a synthetic socket isn’t likely to maintain well over time. Maybe Phoenix offers a way to instantiate a socket?

Have you tried this?

socket = %Phoenix.LiveView.Socket{}

It seemed to do the trick for me.

Instead you should integration test this using the live/2 primitives. You can test both good and bad auth (redirects), and do the same assert Sessions.get_session_by_client_id("test123").client_id == "test123" at the end.

I swore I tried that before…anyway yes that actually worked. I think I was trying to build a socket but seems a raw struct will also do.

I should be clear that I was just testing out your example and think Chris’ suggestion is the way to go.

Personally, I forgo unit testing these hooks and just test their effects in each LiveView they’re used in. I make a helper for this, kind of like how phx_gen_auth does with its generated register_and_log_in_user test helper. But obviously if you want a unit test that’s absolutely fine, I’m just sharing!

Speaking of phx_gen_auth, it actually does generate unit tests for its hooks that look like your tests! For example:

test "authenticates current_user based on a valid user_token ", %{conn: conn, user: user} do
  user_token = Accounts.generate_user_session_token(user)
  session = conn |> put_session(:user_token, user_token) |> get_session()

  {:cont, updated_socket} =
    UserAuth.on_mount(:ensure_authenticated, %{}, session, %LiveView.Socket{})

  assert updated_socket.assigns.current_user.id == user.id
end
1 Like

Thanks for responding. I think I much prefer the unit style for middleware personally.