After calling `Phoenix.ConnTest.get/3` on a `conn` which got annotated with `put_connect_params` the param is not accessible anymore although it's present

{:phoenix, “~> 1.7.1”},
{:phoenix_live_view, “~> 1.0”},

After handling client timezone information via a connect param sent during mounting I ran into problems testing it.
My solution is inspired by this and this

Running live view tests that calls get_connect_params raise error already pointed to the right direction how to handle the timezone parameter in tests.

The context

// app.js
const liveSocket = new LiveSocket("/live", Socket, {
  params: {
    _csrf_token: csrfToken,
    // Sending the timezone from the users browser to the server to render datetime properly
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
  },
# lib/my_app_web/router.ex

# One of the on_mount hooks in the router which will call the UserAuth.on_mount function
  scope "/", MyApp do
    pipe_through [:browser]
    live_session :current_user,
      on_mount: [
        ...
        {MyApp.UserAuth, :put_timezone}
        ...
      ],
      layout: {MyApp.Layouts, :empty} do
      ...
    end
  end
# lib/my_app_web/user_auth.ex
  def on_mount(:put_timezone, _params, _session, socket) do
    socket =
      # Only handling the connected mount cycle
      if Phoenix.LiveView.connected?(socket) do
        timezone = Phoenix.LiveView.get_connect_params(socket)["timezone"]
        timezone =
          # validating the timezone coming from the client
          with false <- is_nil(timezone),
               {:ok, _} <- DateTime.from_naive(~N[0001-01-01 01:01:01], timezone) do
            timezone
          else
            # In case no timezone is sent by the client, raise an error.
            # This case indicates something is wrong with the client JS.
            true ->
              raise ArgumentError, "Client did not provide a timezone."
            # In case the timezone is invalid, raise an error.
            {:error, :time_zone_not_found} ->
              raise ArgumentError, "Invalid timezone: #{inspect(timezone)}"
          end
        Phoenix.Component.assign(
          socket,
          :user_timezone,
          timezone
        )
      else
        socket
      end
    {:cont, socket}
  end
# test/my_app_web/user_auth_test.exs
  describe "on_mount: :put_timezone" do
    test "mounts live_view with valid timezone present", %{conn: conn} do
      assert(
        {:ok, _view, _html} =
          conn
          |> put_connect_params(%{"timezone" => "America/Chicago"})
          |> live(~p"/users/register")
      )
    end
    test "crashes when client does not provide timezone", %{conn: conn} do
      assert {{%ArgumentError{message: ~s/Client did not provide a timezone./}, _}, _} =
               catch_exit(live(conn, ~p"/users/register"))
    end
    test "crashes with empty timezone present", %{conn: conn} do
      conn = put_connect_params(conn, %{"timezone" => ""})
      assert {{%ArgumentError{message: ~s/Invalid timezone: ""/}, _}, _} =
               catch_exit(live(conn, ~p"/users/register"))
    end
    test "crashes with invalid timezone present", %{conn: conn} do
      conn = put_connect_params(conn, %{"timezone" => "Imaginarynation/Capitol"})
      assert {{%ArgumentError{message: ~s/Invalid timezone: "Imaginarynation\/Capitol"/}, _}, _} =
               catch_exit(live(conn, ~p"/users/register"))
    end
  end

So far everything works like a charm but the restrictive behaviour of the UserAuth.on_mount(:put_timezone, ...) let’s fail most test. Because no timezone is given - no surprise.

I thought I just have to assign the Phoenix.LiveViewTest.put_connect_params(%{"timezone" => "America/Chicago"}) in the MyApp.ConnCase setup. Which I did and it solved most of the tests

# test/support/conn_case.ex
  setup tags do
    MyApp.DataCase.setup_sandbox(tags)

    {
      :ok,
      conn:
        Phoenix.ConnTest.build_conn()
        |> Phoenix.LiveViewTest.put_connect_params(%{"timezone" => "America/Chicago"})
    }
  end

The problem

But I have tests which use Phoenix.ConnTest.get(conn, ~p"/") to test redirect behaviour and I can’t make it work, so that after the call of Phoenix.ConnTest.get(conn, ~p"/") the timezone is still available in the on_mount.

defmodule MyApp.SomePageLiveTest do
  use MyApp.ConnCase, async: false



  test "some test", %{conn: conn} do

    # Setup fixtures and stuff...

    dbg(conn)
    # > conn #=> %{
    # >   ...
    # >   private: %{
    # >     ...
    # >     :live_view_connect_params => %{"timezone" => "America/Chicago"},
    # >     ...
    # >     },
    # >     ...
    # > }

    # works fine
    assert {:error, {:redirect, _}} = live(conn, ~p"/a_path_which_will_redirect")
    assert {:ok, _, _} = live(conn, ~p"/a_path_which_will_not_redirect")
    conn = get(conn, ~p"/a_path_which_will_not_redirect")
    assert {:ok, _, _} = live(conn)

    # Although the timezone is still in the `conn`...
    dbg(conn)
    # > conn #=> %{
    # >   ...
    # >   private: %{
    # >     ...
    # >     :live_view_connect_params => %{"timezone" => "America/Chicago"},
    # >     ...
    # >     },
    # >     ...
    # > }

    # ... it fails because the timezone is not available in the on_mount
    assert {:ok, _, _} = live(conn, ~p"/users/register")
    # > ...
    # > ** (ArgumentError) Client did not provide a timezone.
  end

I feel I don’t understand the Lifecycle of the conn and the socket in the Phoenix.ConnTest and Phoenix.LiveViewTest not good enough to solve this problem.

The questions

Is this intended behaviour?
Is it a problem of how the tests are constructed or how I handle the timezone in the on_mount?
What’s a good way to handle this problem?

2 Likes