# `Mailglass.TestAssertions`
[🔗](https://github.com/szTheory/mailglass/blob/v1.0.0/lib/mailglass/test_assertions.ex#L1)

Test assertions extending Swoosh.TestAssertions (TEST-01, D-05).

Lives in `lib/` (not `test/support/`) because it's exported for
adopter consumption. Adopters `import Mailglass.TestAssertions` in
their test helpers or use `Mailglass.MailerCase` which imports it.

## Matcher styles

    # 1. Presence (bare call)
    assert_mail_sent()

    # 2. Keyword match (Swoosh familiarity)
    assert_mail_sent(subject: "Welcome", to: "user@example.com")

    # 3. Struct pattern (macro — no explicit quoting)
    assert_mail_sent(%{mailable: MyApp.UserMailer})

    # 4. Predicate fn
    assert_mail_sent(fn msg -> msg.stream == :transactional end)

## Supported keyword matcher keys

`:subject`, `:to`, `:mailable`, `:stream`, `:tenant` — extensible in
future versions. Any other key raises `ExUnit.AssertionError`.

## Process-local + PubSub-backed assertions

- `last_mail/0` reads Fake-backed delivery storage without consuming the
  process mailbox.
- `wait_for_mail/1`, `assert_no_mail_sent/0`, and `assert_mail_sent/0,1`
  read the current process mailbox (`Mailglass.Adapters.Fake.Storage` sends
  `{:mail, %Message{}}` to the owner via `send/2` on every delivery).
- `assert_mail_delivered/2`, `assert_mail_bounced/2` — consume
  PubSub broadcasts from
  `Mailglass.Outbound.Projector.broadcast_delivery_updated/3`.
  Use when asserting webhook-received events (Phase 4) or
  Fake-triggered events (`Mailglass.Adapters.Fake.trigger_event/3`).

## Async-safe

Process mailbox + ETS-per-owner (Fake) + PubSub subscription (all
cleaned up on process exit) means `async: true` tests see only their
own mail. Use `Mailglass.MailerCase` for the canonical setup.

## PII policy (T-3-06-01)

Failure messages embed caller-supplied values (e.g. `subject` or `to`
address) because adopter test failures need that context. These values
appear only in the adopter's own test output — not in telemetry,
log streams, or cross-tenant surfaces.

# `assert_mail_bounced`
*since 0.1.0* 

```elixir
@spec assert_mail_bounced(Mailglass.Outbound.Delivery.t() | binary(), timeout()) ::
  :ok
```

Blocks until a `{:delivery_updated, _, :bounced, _}` broadcast arrives.
See `assert_mail_delivered/2` for usage notes.

# `assert_mail_delivered`
*since 0.1.0* 

```elixir
@spec assert_mail_delivered(Mailglass.Outbound.Delivery.t() | binary(), timeout()) ::
  :ok
```

Blocks until a `{:delivery_updated, _, :delivered, _}` broadcast
arrives for the given delivery id (or `%Delivery{}`). Flunks on timeout.

The current test process MUST be subscribed to either
`Mailglass.PubSub.Topics.events(tenant_id)` or
`Mailglass.PubSub.Topics.events(tenant_id, delivery_id)` before
this assertion runs. `Mailglass.MailerCase` handles the tenant-wide
subscription in setup.

## Accepts

- `delivery_id :: binary()` — the UUID string
- `delivery :: %Mailglass.Outbound.Delivery{}` — the struct (`.id` extracted)

# `assert_mail_html_matches`
*since 0.4.0* 

```elixir
@spec assert_mail_html_matches(Mailglass.Message.t(), String.t() | Regex.t()) :: :ok
```

Asserts that the HTML body of a message matches a binary or regex pattern.

# `assert_mail_sent`
*since 0.1.0* *macro* 

Asserts that at least one mail was sent in the current test process.

Four matcher styles supported; see module doc.

## Style 1: presence (bare call)

    assert_mail_sent()

## Style 2: keyword list

    assert_mail_sent(subject: "Welcome", to: "user@example.com")
    assert_mail_sent(mailable: MyApp.UserMailer, stream: :transactional)

Supported keys: `:subject`, `:to`, `:mailable`, `:stream`, `:tenant`

## Style 3: struct pattern (no quoting needed)

    assert_mail_sent(%{mailable: MyApp.UserMailer})

## Style 4: predicate function

    assert_mail_sent(fn msg -> msg.stream == :transactional end)

# `assert_mail_sent`
*since 0.1.0* *macro* 

# `assert_mail_text_matches`
*since 0.4.0* 

```elixir
@spec assert_mail_text_matches(Mailglass.Message.t(), String.t() | Regex.t()) :: :ok
```

Asserts that the text body of a message matches a binary or regex pattern.

# `assert_no_mail_sent`
*since 0.1.0* *macro* 

Asserts that no mail was sent in the current test process.

Reads the current process mailbox. Flunks if any `{:mail, _}` message
is present.

# `last_mail`
*since 0.1.0* 

```elixir
@spec last_mail() :: Mailglass.Message.t() | nil
```

Returns the most recent `%Mailglass.Message{}` sent from the current
owner's Fake ETS bucket; or `nil` if none.

Reads directly from the Fake ETS table — no process mailbox consumed.
The returned message is still present in the mailbox for subsequent
`assert_mail_sent/0,1` calls.

# `wait_for_mail`
*since 0.1.0* 

```elixir
@spec wait_for_mail(timeout()) :: Mailglass.Message.t()
```

Blocks until a `{:mail, %Message{}}` arrives or `timeout` elapses. Returns the message.
Flunks on timeout with a descriptive failure message.

Unlike `assert_mail_sent/0` (which uses `assert_received` and checks
the mailbox synchronously), `wait_for_mail/1` blocks the test for up
to `timeout` milliseconds — useful when the delivery may arrive
slightly after the assertion site.

---

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