MailglassInbound.Fixtures (MailglassInbound v0.2.0)

Copy Markdown View Source

Code-built inbound payload fixtures for adopter tests (ITEST-07).

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 (D-47-10, D-47-11; 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, [])

Summary

Functions

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

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

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

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

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

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.

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.

Functions

build_inbound_message(opts \\ [])

@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(opts \\ [])

@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 (D-46-10).

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(opts \\ [])

@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(opts \\ [])

@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(opts \\ [])

@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 (D-47-10, 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

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()

@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()

@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.