# `MailglassInbound.Fixtures`
[🔗](https://github.com/szTheory/mailglass/blob/v2.3.0/lib/mailglass_inbound/fixtures.ex#L1)

Code-built inbound payload fixtures for adopter tests.

`MailglassInbound.Fixtures` builds a canonical `%MailglassInbound.InboundMessage{}`
and raw provider payloads — Postmark JSON, SendGrid form-encoded MIME, Mailgun
multipart params, and a valid X.509-signed SES SNS notification — **entirely
from code**. Each provider payload is shaped so it round-trips through the real
provider `verify!`/`normalize` seam to a valid `%InboundMessage{}`, which makes
the fixtures faithful to production parsing rather than to a hand-written stub.

This module belongs to the **Testing** surface and ships in `lib/` so adopters
can build inbound messages from their own test suites with a single call. It
references only core/runtime modules (`Jason`, `:public_key`, the inbound
providers, and the SES `CertCache` / `S3Fetcher.Fake` seams) so it compiles
under `mix compile --no-optional-deps --warnings-as-errors` — never `Oban`,
`ExAws`, or `Plug.Test`.

## Locked posture

- **Code-built only.** No `.eml` file and no `.pem` key/cert is ever written to
  disk or committed (security V6/V7).
- **No real-PII sample data.** Defaults use `.test` / `example.com` addresses.
- **Defaulted `tenant_id`.** Every builder defaults a `tenant_id` so a test
  cannot accidentally assert across tenants (security V4, T-47-04).
- **Ephemeral SES keypair.** `build_ses_sns_payload/1` mints a fresh in-memory
  RSA-2048 keypair per call; it primes the real `CertCache` so the signed SNS
  notification verifies through the real `SES.verify!` with no network fetch.

## Examples

    # A canonical message, ready for `MailglassInbound.Test.Ingress`:
    message = MailglassInbound.Fixtures.build_inbound_message(subject: "Hello")

    # A raw Postmark body that the real Postmark provider normalizes:
    raw = MailglassInbound.Fixtures.build_postmark_payload(subject: "Hello")
    %{message: %MailglassInbound.InboundMessage{}} =
      MailglassInbound.Ingress.Providers.Postmark.normalize(raw, [])

# `build_inbound_message`
*since 0.2.0* 

```elixir
@spec build_inbound_message(keyword()) :: MailglassInbound.InboundMessage.t()
```

Builds a canonical `%MailglassInbound.InboundMessage{}` entirely from code.

Mirrors the canonical builder shape used across the inbound suite (address
shape `%{address: ...}`, defaulted list fields). A `tenant_id` is always
defaulted.

## Options

- `:tenant_id` (default `"fixture-tenant"`)
- `:provider` (default `:postmark`)
- `:provider_message_id` (default a generated id)
- `:message_id` (default mirrors `:provider_message_id`)
- `:from` / `:to` — bare address strings (default fixture addresses)
- `:subject`, `:text_body`, `:html_body`, `:envelope_recipient`

# `build_mailgun_payload`
*since 0.2.0* 

```elixir
@spec build_mailgun_payload(keyword()) :: %{
  params: map(),
  headers: [{String.t(), String.t()}],
  config: map()
}
```

Builds a Mailgun inbound payload (parsed mode) that passes the **real**
`Mailgun.verify!` HMAC seam out of the box.

The params carry a `timestamp`/`token`/`signature` triple HMAC-signed
(`sha256` over `timestamp <> token`) against the documented default signing key
returned by `mailgun_fixture_config/0` (mirroring how `build_ses_sns_payload/1`
self-signs against a primed cert), so
`MailglassInbound.Test.Ingress.receive_provider_payload(:mailgun, …)` works with
no extra config — the driver defaults its config to that same signing key. The
`timestamp` is the current Unix time so it lands inside `verify!`'s skew
tolerance; the `token` is a fresh per-call nonce (also the replay-cache key).
Pass `signing_key:` here to sign against your own key (then supply the matching
`config: %{signing_key: …}` to the driver).

Returns a map with `:params` (the flat form fields the Mailgun provider
normalizes, including the signature triple), `:headers` (the request header
list), and `:config` (`%{signing_key: …}` the driver defaults to).
`message-headers` carries the RFC `Message-Id` Mailgun has no flat field for.

## Options

- `:tenant_id`, `:subject`, `:from`, `:recipient`, `:text_body`
- `:signing_key` — the HMAC signing key to sign against (default the documented
  `mailgun_fixture_config/0` key)

# `build_postmark_payload`
*since 0.2.0* 

```elixir
@spec build_postmark_payload(keyword()) :: binary()
```

Builds a raw Postmark inbound JSON body (a binary, ready for
`Postmark.normalize/2`).

## Options

- `:tenant_id` (carried for API symmetry; Postmark normalize ignores it)
- `:provider_message_id` / `:subject` / `:from` / `:recipient` / `:text_body`

# `build_sendgrid_payload`
*since 0.2.0* 

```elixir
@spec build_sendgrid_payload(keyword()) :: %{
  raw_mime: binary(),
  headers: [{String.t(), String.t()}],
  params: map(),
  config: map()
}
```

Builds a SendGrid inbound payload that passes the **real** `Sendgrid.verify!`
basic-auth seam out of the box.

The header list carries an `authorization: Basic …` header self-signed against
the documented default credentials returned by `sendgrid_fixture_config/0`
(mirroring how `build_ses_sns_payload/1` self-signs against a primed cert), so
`MailglassInbound.Test.Ingress.receive_provider_payload(:sendgrid, …)` works
with no extra config — the driver defaults its config to those same credentials.
Pass `basic_auth: {user, pass}` here to sign against your own credentials (then
supply the matching `config: %{basic_auth: {user, pass}}` to the driver).

Returns a map with:

- `:raw_mime` — the raw MIME the SendGrid provider parses (also the dedupe key
  `md5(raw_mime)` when `provider_message_id` is nil; expose it via
  `evidence: %{raw_mime: ...}` so replays are detected — Pitfall 5).
- `:headers` — the request header list, including the self-signed
  `authorization` header the real `verify!` requires.
- `:params` — the form params (carries `"envelope"` so the provider resolves
  `envelope_recipient`).
- `:config` — `%{basic_auth: {user, pass}}` the SendGrid `verify!` config seam;
  the driver defaults to this when the caller passes no `:config`.

## Options

- `:tenant_id`, `:subject`, `:from`, `:recipient`, `:text_body`
- `:basic_auth` — `{user, pass}` to self-sign against (default the documented
  `sendgrid_fixture_config/0` credentials)

# `build_ses_sns_payload`
*since 0.2.0* 

```elixir
@spec build_ses_sns_payload(keyword()) :: %{
  raw_body: binary(),
  headers: [{String.t(), String.t()}],
  config: map()
}
```

Builds a valid X.509-signed SES SNS `Notification` payload entirely from code.

Mints a fresh in-memory RSA-2048 keypair, builds an SNS `Notification`
envelope carrying an `Action:S3` receipt, computes the byte-sorted canonical
string, signs it with `:public_key.sign(canonical, :sha, private_key)`, and
`Jason.encode!`s the result. The builder then primes:

- the **real** `Mailglass.Webhook.Providers.SES.CertCache` so the real
  `SES.verify!` is a cache hit (no `:httpc` fetch), and
- the `MailglassInbound.S3Fetcher.Fake` so the `Action:S3` body path resolves
  to the fixture's raw MIME.

The keypair is ephemeral, in-memory, and per call — nothing is written to
disk (security V6). The `SigningCertURL` carries a per-call unique
suffix so concurrent fixtures priming the shared `:public` cert cache never
collide (Pitfall 2); the host still satisfies the SNS cert-host TrustPolicy.

> #### Cross-test hygiene: reset the cert cache {: .warning}
>
> This builder primes the **process-global** ETS cert cache
> (`Mailglass.Webhook.Providers.SES.CertCache`, shared across concurrent async
> tests). Each call inserts one entry under a unique cert URL with a 24h TTL
> that is never evicted within a run. If you build SES fixtures from a plain
> `ExUnit.Case` (without `MailglassInbound.MailboxCase`), reset the cache
> between tests so entries do not accumulate or bleed across cases:
>
>     setup do: Mailglass.Webhook.Providers.SES.CertCache.reset()
>
> `MailglassInbound.MailboxCase` already does this in its `setup`, so suites
> that `use` it need no extra step.

Returns a map with:

- `:raw_body` — the JSON SNS envelope (feed to `SES.verify!/2` then `SES.normalize/1`).
- `:headers` — the request header list.
- `:config` — `%{s3_fetcher: S3Fetcher.Fake, cert_cache_ttl_seconds: 86_400}`
  (the SES config seam).

## Options

- `:tenant_id` (carried for API symmetry), `:subject`, `:from`, `:recipient`,
  `:text_body`, `:provider_message_id`

# `mailgun_fixture_config`
*since 0.2.0* 

```elixir
@spec mailgun_fixture_config() :: %{signing_key: String.t()}
```

The documented default Mailgun `verify!` config the fixture self-signs against:
`%{signing_key: key}`. `Test.Ingress.receive_provider_payload(:mailgun, …)`
defaults its config to this so the fixture verifies out of the box.

# `sendgrid_fixture_config`
*since 0.2.0* 

```elixir
@spec sendgrid_fixture_config() :: %{basic_auth: {String.t(), String.t()}}
```

The documented default SendGrid `verify!` config the fixture self-signs against:
`%{basic_auth: {user, pass}}`. `Test.Ingress.receive_provider_payload(:sendgrid,
…)` defaults its config to this so the fixture verifies out of the box.

---

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