# `Swoosh.Adapters.Sandbox`
[🔗](https://github.com/swoosh/swoosh/blob/v1.25.0/lib/swoosh/adapters/sandbox.ex#L1)

A sandbox adapter for email delivery in tests, analogous to
[`Ecto.Adapters.SQL.Sandbox`](https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html).

Each test registers as an owner via `checkout/0`.  All emails delivered from
that test process or its `$callers` chain are routed to the test's private
inbox.  This makes email assertions safe with `async: true`.

Compatible with `Swoosh.TestAssertions` — the owner process receives
`{:email, email}` messages so `assert_email_sent/1` continues to work.

## Setup

In `config/test.exs`:

    config :my_app, MyApp.Mailer, adapter: Swoosh.Adapters.Sandbox

    # Disable the HTTP API client — it is not needed for test adapters.
    config :swoosh, :api_client, false

In `test/test_helper.exs`, start the storage process before `ExUnit.start()`:

    {:ok, _} = Swoosh.Adapters.Sandbox.Storage.start_link([])

## Unit and integration tests

Call `checkout/0` in setup and `checkin/0` on exit.  Tests can be
`async: true` — each test process has its own isolated inbox.

    setup do
      :ok = Swoosh.Adapters.Sandbox.checkout()
      on_exit(&Swoosh.Adapters.Sandbox.checkin/0)
    end

## Phoenix integration (async-safe browser tests)

In browser/E2E tests the web server spawns request-handling processes
independently, so they have no `$callers` ancestry back to the test process.
A plug and a LiveView hook solve this: each process is explicitly allowed into
the test's sandbox via a token embedded in the `user-agent` header.

### With PhoenixTest.Playwright

[PhoenixTest.Playwright](https://hexdocs.pm/phoenix_test_playwright/) 0.13+
unconditionally sets the browser user-agent to
`BeamMetadata (...)` Ecto metadata.  Derive the Swoosh sandbox owner from
that metadata rather than embedding a separate token.

**1. Add a plug to your endpoint** (e.g. `lib/my_app_web/plug/swoosh_sandbox.ex`):

    defmodule MyAppWeb.Plug.SwooshSandbox do
      import Plug.Conn

      def init(opts), do: opts

      def call(conn, _opts) do
        conn
        |> get_req_header("user-agent")
        |> List.first()
        |> allow()

        conn
      end

      defp allow(nil), do: :ok

      defp allow(user_agent) do
        case Phoenix.Ecto.SQL.Sandbox.decode_metadata(user_agent) do
          %{owner: owner_pid} -> Swoosh.Adapters.Sandbox.allow(owner_pid, self())
          _ -> :ok
        end
      end
    end

Register it in `endpoint.ex`, guarded by the test environment:

    if Application.compile_env(:my_app, :swoosh_sandbox, false) do
      plug MyAppWeb.Plug.SwooshSandbox
    end

**2. Add a LiveView hook** (e.g. `lib/my_app_web/live/live_allow_swoosh_sandbox.ex`)
and mount it on any live session that delivers email:

    defmodule MyAppWeb.LiveAllowSwooshSandbox do
      import Phoenix.LiveView

      def on_mount(:default, _params, _session, socket) do
        if connected?(socket) do
          case Phoenix.Ecto.SQL.Sandbox.decode_metadata(
                 get_connect_info(socket, :user_agent)
               ) do
            %{owner: owner_pid} -> Swoosh.Adapters.Sandbox.allow(owner_pid, self())
            _ -> :ok
          end
        end

        {:cont, socket}
      end
    end

Then mount it:

    live_session :default,
      on_mount: [MyAppWeb.LiveAllowSwooshSandbox] do
      ...
    end

**3. Configure the endpoint** to pass the user-agent to LiveView connect_info:

    socket "/live", Phoenix.LiveView.Socket,
      websocket: [connect_info: [:user_agent, ...]]

**4. Enable the plug** in `config/test.exs`:

    config :my_app, :swoosh_sandbox, true

**5. In your test setup**, check out the sandbox.  No `encode_owner()` or
custom `user_agent` key is needed — PTP supplies the Ecto metadata automatically:

    setup do
      :ok = Swoosh.Adapters.Sandbox.checkout()
      on_exit(&Swoosh.Adapters.Sandbox.checkin/0)
    end

### With Wallaby

[Wallaby](https://hexdocs.pm/wallaby/) lets you set the user-agent freely.
Use a plug that decodes the `SwooshSandbox (...)` token instead of Ecto metadata.

**1. Add a plug to your endpoint** (e.g. `lib/my_app_web/plug/swoosh_sandbox.ex`):

    defmodule MyAppWeb.Plug.SwooshSandbox do
      import Plug.Conn

      def init(opts), do: opts

      def call(conn, _opts) do
        conn
        |> get_req_header("user-agent")
        |> List.first()
        |> allow()

        conn
      end

      defp allow(nil), do: :ok

      defp allow(user_agent) do
        with [_, encoded] <- Regex.run(~r/SwooshSandbox \((\S+)\)/, user_agent),
             {:ok, binary} <- Base.url_decode64(encoded),
             pid when is_pid(pid) <- :erlang.binary_to_term(binary, [:safe]) do
          Swoosh.Adapters.Sandbox.allow(pid, self())
        end

        :ok
      end
    end

Register it in `endpoint.ex` with the same guard as above.

**2. Add a LiveView hook** if any live sessions deliver email, following the
same pattern as the PhoenixTest.Playwright section but decoding with the
`SwooshSandbox` regex instead of `Phoenix.Ecto.SQL.Sandbox.decode_metadata/1`.

**3-4.** Same `connect_info` and `config/test.exs` steps as above.

**5. In your test setup**, pass `encode_owner()` as the `--user-agent` flag:

    setup do
      :ok = Swoosh.Adapters.Sandbox.checkout()
      token = Swoosh.Adapters.Sandbox.encode_owner()
      on_exit(&Swoosh.Adapters.Sandbox.checkin/0)

      {:ok, session} =
        Wallaby.start_session(
          capabilities: %{"goog:chromeOptions": %{args: ["--user-agent=#{token}"]}}
        )

      on_exit(fn -> Wallaby.end_session(session) end)
      {:ok, session: session}
    end

## Shared mode (simple alternative for non-async E2E tests)

If adding the plug is not practical, shared mode routes all unregistered
deliveries to a single owner.  Note that shared mode requires `async: false`
since there is only one global shared owner at a time.

    setup do
      :ok = Swoosh.Adapters.Sandbox.set_shared(self())
      on_exit(fn -> Swoosh.Adapters.Sandbox.set_shared(nil) end)
    end

## Allowing other processes

If email is sent from a process not in the `$callers` chain of the test
(e.g. a background worker), allow it explicitly:

    Swoosh.Adapters.Sandbox.allow(self(), worker_pid)

## Configuration options

- `:on_unregistered`
  what to do when email is delivered from a process with no registered owner.
  Defaults to `:raise`.  Set to `:ignore` to silently succeed.

      config :my_app, MyApp.Mailer,
        adapter: Swoosh.Adapters.Sandbox,
        on_unregistered: :ignore

# `all`

Returns all emails captured for the given owner.

# `allow`

Allows `allowed_pid` to deliver emails into `owner_pid`'s sandbox inbox.

This is useful when a process that is not in the test process' `$callers`
chain sends email, such as a background worker or a browser-driven request.

# `checkin`

Unregisters the current process as a sandbox owner and clears its inbox.

# `checkout`

Registers the current process as a sandbox owner.

Emails delivered by this process, its `$callers`, or processes explicitly
allowed with `allow/2` are stored in this owner's inbox.

# `encode_owner`

Encodes the given owner pid into a `user-agent` token for browser tests.

# `flush`

Returns and removes all emails captured for the given owner.

# `set_shared`

Sets or clears the shared sandbox owner for non-async tests.

When set, unregistered deliveries are routed to the shared owner instead of
raising.

# `validate_config`

# `validate_dependency`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
