The shipped ExUnit.CaseTemplate adopters use to test inbound mailboxes
the inbound analog of outbound's Mailglass.MailerCase, and the
keystone that ties the inbound Testing helpers together.
use MailglassInbound.MailboxCase injects import MailglassInbound.TestAssertions
and the Fixtures/Test aliases, checks out an Ecto sandbox on the adopter's
configured repo, sets tenancy, resets the only process-global state the inbound
fixtures touch, and tears the sandbox down on exit.
Ships in lib/ (not test/support/)
Like the other inbound Testing helpers, this case template ships in the
mailglass_inbound Hex package via the files: ~w(lib …) manifest so adopters
can use it from their own suites. Because it ships in lib/ it references
ONLY core/runtime modules — never Oban, ExAws, or Plug.Test (Pitfall 6),
so it compiles cleanly under mix compile --no-optional-deps --warnings-as-errors.
The adopter's repo is resolved from app-env
The sandbox is checked out on the repo resolved from
Application.get_env(:mailglass_inbound, :repo), exactly like the
MailglassInbound.Repo facade. The case template NEVER references the
package's own test repo literal — the package does not own a repo (Pitfall 1).
Adopters configure one in config/test.exs:
config :mailglass_inbound, :repo, MyApp.RepoIf unset, setup raises with that exact instruction. (The package's own
self-tests work because config/test.exs points :repo at the support repo
under test/support.)
It snapshots NO app-env
Unlike Mailglass.MailerCase (which snapshots/restores its async-mode
application-env keys so deliver_later/2 runs inline), this case template
writes and restores no application-env key. Inbound achieves synchronous
execution structurally: MailglassInbound.Test.Ingress drives
MailglassInbound.Execution.execute/2 (SYNC) directly — there is no
async-mode app-env key to flip and therefore no leak surface across tests
The only teardown is Sandbox.stop_owner/1; the only
shared-state hygiene is the per-setup ETS / process-dict reset below.
Default setup
Ecto.Adapters.SQL.Sandbox.start_owner!(repo, shared: not tags[:async])Mailglass.Tenancy.put_current("test-tenant")(unless@tag tenant: :unset)- resets
Mailglass.Webhook.Providers.SES.CertCache(process-global ETS) - resets
MailglassInbound.S3Fetcher.Fake(process-dict) - best-effort
Phoenix.PubSub.subscribe/2on the coreMailglass.PubSubserver (the inbound plug broadcasts there); the send-based capture fromMailglassInbound.Test.Ingressis the primary assertion path on_exit→Sandbox.stop_owner/1
Supported tags
@tag tenant: "acme"— override the default"test-tenant"@tag tenant: :unset— disable tenancy stamping@tag async: false— disable async (sandbox checks out in shared mode)
Example
defmodule MyApp.WelcomeMailboxTest do
use MailglassInbound.MailboxCase, async: false
test "accepts a welcome message" do
message = Fixtures.build_inbound_message(subject: "Welcome")
{:ok, %{outcome: %{outcome: :accept}, route: %{mailbox: MyApp.WelcomeMailbox}}} =
Test.Ingress.receive_inbound(message, routes: my_routes())
# ONE assertion per drive: each `assert_inbound_*` reads the captured
# tuple with `assert_received`, which CONSUMES it from the process
# mailbox. To run a second assertion, drive a second message (with a
# distinct `provider_message_id` so it is a fresh receive).
assert_inbound_received(subject: "Welcome")
end
end