Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Bamboo.TestAdapter.forward/2 #620

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion lib/bamboo/adapters/test_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,71 @@ defmodule Bamboo.TestAdapter do

@behaviour Bamboo.Adapter

use GenServer

@doc false
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end

@doc """
Forward messages sent in `from_pid` to `to_pid`. This provides a way to
write tests that send emails in other processes without resorting to shared
mode (which cannot be used with async tests).

To enable this feature, the `Bamboo.TestAdapter` GenServer must be started in
your `test/test_helper.exs`:

{:ok, _} = Supervisor.start_link([Bamboo.TestAdapter], strategy: :one_for_one)

You must then have a way to find out the pid of the process that is sending
the email and forward it to the process that is running your test.

For example, when running browser tests with Pheonix, you can configure
[Phoenix.Ecto.SQL.Sandbox](https://hexdocs.pm/phoenix_ecto/4.3.0/Phoenix.Ecto.SQL.Sandbox.html#content)
to achieve this.

In `config/test.exs`:

config :your_app, :sandbox, Ecto.Adapters.SQL.Sandbox

In `lib/your_app_web/endpoint.ex`:

if sandbox = Application.get_env(:your_app, :sandbox) do
plug Phoenix.Ecto.SQL.Sandbox, sandbox: sandbox
end

Now add `test/support/sandbox.ex`:

defmodule YourApp.Sandbox do
def allow(repo, owner_pid, child_pid) do
# Delegate to the Ecto sandbox
Ecto.Adapters.SQL.Sandbox.allow(repo, owner_pid, child_pid)

# Forward emails back to the test process
Bamboo.TestAdapter.forward(child_pid, owner_pid)
end
end
"""
def forward(from_pid, to_pid) do
:ok = GenServer.call(__MODULE__, {:put_forward, from_pid, to_pid})
end

@doc false
def init(:ok) do
{:ok, %{forwards: %{}}}
end

@doc false
def handle_call({:put_forward, from_pid, to_pid}, _from, state) do
{:reply, :ok, put_in(state.forwards[from_pid], to_pid)}
end

@doc false
def handle_call({:get_forward, from_pid}, _from, state) do
{:reply, state.forwards[from_pid], state}
end

@doc false
def deliver(email, _config) do
email = clean_assigns(email)
Expand All @@ -27,7 +92,13 @@ defmodule Bamboo.TestAdapter do
end

defp test_process do
Application.get_env(:bamboo, :shared_test_process) || self()
Application.get_env(:bamboo, :shared_test_process) || forward_pid() || self()
end

defp forward_pid do
if GenServer.whereis(__MODULE__) do
GenServer.call(__MODULE__, {:get_forward, self()})
end
end

def handle_config(config) do
Expand Down
47 changes: 30 additions & 17 deletions lib/bamboo/test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@ defmodule Bamboo.Test do
you'll want to **unit test emails first**. Then, in integration tests, use
helpers from this module to test whether that email was delivered.

## Note on sending from other processes

If you are sending emails from another process (for example, from inside a
Task or GenServer) you may need to use shared mode when using
`Bamboo.Test`. See the docs `__using__/1` for an example.

For most scenarios you will not need shared mode.

## In your config

# Typically in config/test.exs
Expand Down Expand Up @@ -70,6 +62,29 @@ defmodule Bamboo.Test do
assert_delivered_email email
end
end

## Sending email from other processes

Most of the time, the process that is sending your email will be the one that
is running your test. So the email will arrive in your test process' mailbox
and assertions will work.

If you are sending emails from another process, you won't be able to receive
them in your test without some extra configuration. This will be relevant if
you send email from inside of a Task, GenServer, or are running acceptance
tests with a browser testing library like
[Wallaby](https://github.com/elixir-wallaby/wallaby).

There are two options to ensure that the email is delivered to your test
process:

1. Use shared mode. This prevents you from running the test asynchronously,
so that all tests run in the same process, which is where your emails are
delivered. See `__using__/1` for how to activate shared mode.

2. Use `Bamboo.TestAdapter.forward/2` to explicitly specify which process the
email should be sent to. This is a bit harder to set up but allows
asynchronous tests. See the docs for that function for details.
"""

@doc """
Expand All @@ -79,22 +94,20 @@ defmodule Bamboo.Test do
current process when an email is delivered. The process mailbox is then
checked when using the assertion helpers like `assert_delivered_email/1`.

Sometimes emails don't show up when asserting because you may deliver an email
from a _different_ process than the test process. When that happens, turn on
shared mode. This will tell `Bamboo.TestAdapter` to always send to the test process.
This means that you cannot use shared mode with async tests.
Sometimes emails don't show up when asserting because you may deliver an
email from a _different_ process than the test process. See [Sending email
from other processes](#module-sending-email-from-other-processes) to
understand your options for this situation.

## Try to use this version first
## Normal usage

use Bamboo.Test

## And if you are delivering from another process, set `shared: true`
## Shared mode usage (see above)

use Bamboo.Test, shared: true

Common scenarios for delivering mail from a different process are when you
send from inside of a Task, GenServer, or are running acceptance tests with a
headless browser like phantomjs.
Shared mode cannot be used with asynchronous tests.
"""
defmacro __using__(shared: true) do
quote do
Expand Down
24 changes: 24 additions & 0 deletions test/lib/bamboo/adapters/test_adapter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ defmodule Bamboo.TestAdapterTest do
assert_received {:delivered_email, ^email}
end

describe "forward/2" do
test "forward emails from another process" do
{:ok, test_adapter_pid} = Bamboo.TestAdapter.start_link([])

email = new_email()
config = %{}

other_process =
spawn(fn ->
receive do
:continue -> email |> TestAdapter.deliver(config)
end
end)

TestAdapter.forward(other_process, self())
send(other_process, :continue)

email = TestAdapter.clean_assigns(email)
assert_receive {:delivered_email, ^email}

Process.exit(test_adapter_pid, :kill)
end
end

describe "handle_config/1" do
test "handle_config makes sure that the ImmediateDeliveryStrategy is used" do
new_config = TestAdapter.handle_config(%{})
Expand Down