Limen.Test (limen v0.2.1)

Copy Markdown View Source

Helpers for testing an application that uses Limen.

An application's tests usually share one instance, the one its supervision tree starts, configured for tests in config/test.exs. Most tests should not care about Limen at all, so run that instance in dry-run mode: every request is evaluated and recorded, and none is refused. The tests that are about Limen then enforce it request by request, and give each test a client address of its own, so they can run concurrently without seeing each other's bans:

test "a client without a user agent is challenged", %{conn: conn} do
  conn =
    conn
    |> Limen.Test.put_client_ip(Limen.Test.unique_ip())
    |> Limen.Test.put_mode(:enforce)
    |> get(~p"/")

  assert html_response(conn, 403) =~ "Checking your browser"
  assert %Limen.Decision{action: :challenge} = Limen.decision(conn)
end

Changing the instance itself, with Limen.set_mode/2 or Limen.update_config/3, affects every test using it: keep those to tests that run on their own (async: false).

LiveView

Phoenix.LiveViewTest connects its sockets with the test's request, but reports the test adapter's address as the socket's peer, not the remote_ip the request was given; put_client_ip/2 sets both, so the socket check sees the client the HTTP gate saw. put_socket_token/2 then gives the socket the token the page would embed, and put_mode/2 applies to the socket check and to Limen.LiveView.check_form/3 too:

conn =
  conn
  |> Limen.Test.put_client_ip(Limen.Test.unique_ip())
  |> Limen.Test.put_mode(:enforce)
  |> put_req_header("user-agent", "Mozilla/5.0 ...")
  |> Limen.Test.put_socket_token(otp_app: :my_app)

{:ok, view, _html} = live(conn, ~p"/signup")

Decision log sinks

A Limen.DecisionLog.Sink normally runs in a background process, which a test's Ecto sandbox doesn't allow. Configure the test instance with decision_log: [delivery: :inline] to hand each sampled decision to the sink in the process that made it, so what it writes is in the test's transaction as soon as the request returns. See the testing guide.

Isolated instances

To test a policy on its own, start an instance of your own for the test, which shares nothing with any other:

setup do
  start_supervised!({Limen, name: :policy_test, config: [mode: :enforce]})
  :ok
end

test "curl is challenged" do
  opts = Limen.Plug.init(instance: :policy_test, policy: MyApp.BotPolicy)
  conn = conn(:get, "/") |> put_req_header("user-agent", "curl/8.5.0")

  assert %Limen.Decision{action: :challenge} = Limen.decision(Limen.Plug.call(conn, opts))
end

Summary

Functions

Makes ip the address of the client sending conn, for Limen.Plug and for Phoenix.LiveViewTest sockets, which read it from the connection's peer data.

Sets the mode for this request only, overriding the instance's, the plug's, the route's and the policy's: Limen.Plug, form checks with the connection, and in LiveView tests the socket check and Limen.LiveView.check_form/3 follow it.

Adds the socket token for the client of conn to the connect parameters Phoenix.LiveViewTest.live/2 connects with, keeping any already set. Options are those of socket_params/2.

The connect parameters carrying the socket token for the client of conn, as app.js sends them. Options are those of Limen.Socket.token/2: pass the instance unless conn already went through Limen.Plug.

An IPv6 address no other call returns, in the documentation range 2001:db8::/32: a client of its own for a test.

Functions

put_client_ip(conn, ip)

@spec put_client_ip(Plug.Conn.t(), :inet.ip_address()) :: Plug.Conn.t()

Makes ip the address of the client sending conn, for Limen.Plug and for Phoenix.LiveViewTest sockets, which read it from the connection's peer data.

put_mode(conn, mode)

@spec put_mode(Plug.Conn.t(), :dry_run | :enforce) :: Plug.Conn.t()

Sets the mode for this request only, overriding the instance's, the plug's, the route's and the policy's: Limen.Plug, form checks with the connection, and in LiveView tests the socket check and Limen.LiveView.check_form/3 follow it.

put_socket_token(conn, opts \\ [])

@spec put_socket_token(Plug.Conn.t(), keyword()) :: Plug.Conn.t()

Adds the socket token for the client of conn to the connect parameters Phoenix.LiveViewTest.live/2 connects with, keeping any already set. Options are those of socket_params/2.

socket_params(conn, opts \\ [])

@spec socket_params(Plug.Conn.t(), keyword()) :: %{required(String.t()) => String.t()}

The connect parameters carrying the socket token for the client of conn, as app.js sends them. Options are those of Limen.Socket.token/2: pass the instance unless conn already went through Limen.Plug.

unique_ip()

@spec unique_ip() :: :inet.ip6_address()

An IPv6 address no other call returns, in the documentation range 2001:db8::/32: a client of its own for a test.

Addresses differ in their /48 (for the first 65,536 calls) and in their /64, so they are different clients whatever the instance's :ipv6_prefix.