Phoenix.Ecto.SQL.Sandbox (Phoenix/Ecto v4.3.0) View Source
A plug to allow concurrent, transactional acceptance tests with [Ecto.Adapters.SQL.Sandbox]
(https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html).
Example
This plug should only be used during tests. First, set a flag to
enable it in config/test.exs:
config :your_app, sql_sandbox: trueAnd use the flag to conditionally add the plug to lib/your_app/endpoint.ex:
if Application.get_env(:your_app, :sql_sandbox) do
plug Phoenix.Ecto.SQL.Sandbox
endIt's important that this is at the top of endpoint.ex, before any other plugs.
Then, within an acceptance test, checkout a sandboxed connection as before.
Use metadata_for/2 helper to get the session metadata to that will allow access
to the test's connection.
Here's an example using Hound:
use Hound.Helpers
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(YourApp.Repo)
metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(YourApp.Repo, self())
Hound.start_session(metadata: metadata)
:ok
endSupporting socket connections
To support socket connections the spawned processes need access to the header
used for transporting the metadata. By default this is the user agent header,
but you can also use custom X--headers.
socket "/path", Socket,
websocket: [connect_info: [:user_agent, …]]
socket "/path", Socket,
websocket: [connect_info: [:x_headers, …]]To fetch the value you either use connect_info[:user_agent] or for a custom header:
Enum.find_value(connect_info.x_headers, fn
{"x-my-custom-header", val} -> val
_ -> false
end)Channels
For channels :connect_info data is available to any of your Sockets'
c:Phoenix.Socket.connect/3 callbacks:
# user_socket.ex
def connect(_params, socket, connect_info) do
{:ok, assign(socket, :phoenix_ecto_sandbox, connect_info[:user_agent])}
endThis stores the value on the socket, so it can be available to all of your channels for allowing the sandbox.
# room_channel.ex
def join("room:lobby", _payload, socket) do
allow_ecto_sandbox(socket)
{:ok, socket}
end
# This is a great function to extract to a helper module
defp allow_ecto_sandbox(socket) do
Phoenix.Ecto.SQL.Sandbox.allow(
socket.assigns.phoenix_ecto_sandbox,
Ecto.Adapters.SQL.Sandbox
)
endallow/2 needs to be manually called once for each channel, at best directly
at the start of c:Phoenix.Channel.join/3.
Live View
LiveViews can be supported in a similar fashion than channels, but using the
c:Phoenix.LiveView.mount/3 callback.
def mount(_, _, socket) do
allow_ecto_sandbox(socket)
…
end
# This is a great function to extract to a helper module
defp allow_ecto_sandbox(socket) do
%{assigns: %{phoenix_ecto_sandbox: metadata}} =
assign_new(socket, :phoenix_ecto_sandbox, fn ->
if connected?(socket), do: get_connect_info(socket)[:user_agent]
end)
Phoenix.Ecto.SQL.Sandbox.allow(metadata, Ecto.Adapters.SQL.Sandbox)
endThis is a bit more complex than the channel code, because LiveViews not only
are their own processes when spawned via a socket connection, but also when
doing the static render as part of the plug pipeline. Given get_connect_info/1
is only available for socket connections, this uses the :phoenix_ecto_sandbox
assign of the rendering conn for the static render.
Concurrent end-to-end tests with external clients
Concurrent and transactional tests for external HTTP clients is supported,
allowing for complete end-to-end tests. This is useful for cases such as
JavaScript test suites for single page applications that exercise the
Phoenix endpoint for end-to-end test setup and teardown. To enable this,
you can expose a sandbox route on the Phoenix.Ecto.SQL.Sandbox plug by
providing the :at, and :repo options. For example:
plug Phoenix.Ecto.SQL.Sandbox,
at: "/sandbox",
repo: MyApp.Repo,
timeout: 15_000 # the defaultThis would expose a route at "/sandbox" for the given repo where
external clients send POST requests to spawn a new sandbox session,
and DELETE requests to stop an active sandbox session. By default,
the external client is expected to pass up the "user-agent" header
containing serialized sandbox metadata returned from the POST request,
but this value may customized with the :header option.
Link to this section Summary
Functions
Decodes encoded metadata back into map generated from metadata_for/2.
Encodes metadata generated by metadata_for/2 for client response.
Returns metadata to associate with the session to allow the endpoint to access the database connection checked out by the test process.
Spawns a sandbox session to checkout a connection for a remote client.
Stops a sandbox session holding a connection for a remote client.
Link to this section Functions
Decodes encoded metadata back into map generated from metadata_for/2.
Encodes metadata generated by metadata_for/2 for client response.
Specs
metadata_for(Ecto.Repo.t() | [Ecto.Repo.t()], pid()) :: map()
Returns metadata to associate with the session to allow the endpoint to access the database connection checked out by the test process.
Spawns a sandbox session to checkout a connection for a remote client.
Examples
iex> {:ok, _owner_pid, metadata} = start_child(MyApp.Repo)
Stops a sandbox session holding a connection for a remote client.
Examples
iex> {:ok, owner_pid, metadata} = start_child(MyApp.Repo)
iex> :ok = stop(owner_pid)