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
.emlfile and no.pemkey/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.comaddresses. - Defaulted
tenant_id. Every builder defaults atenant_idso a test cannot accidentally assert across tenants (security V4, T-47-04). - Ephemeral SES keypair.
build_ses_sns_payload/1mints a fresh in-memory RSA-2048 keypair per call; it primes the realCertCacheso the signed SNS notification verifies through the realSES.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
@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
@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 documentedmailgun_fixture_config/0key)
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
@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 keymd5(raw_mime)whenprovider_message_idis nil; expose it viaevidence: %{raw_mime: ...}so replays are detected — Pitfall 5).:headers— the request header list, including the self-signedauthorizationheader the realverify!requires.:params— the form params (carries"envelope"so the provider resolvesenvelope_recipient).:config—%{basic_auth: {user, pass}}the SendGridverify!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 documentedsendgrid_fixture_config/0credentials)
@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.CertCacheso the realSES.verify!is a cache hit (no:httpcfetch), and - the
MailglassInbound.S3Fetcher.Fakeso theAction:S3body 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 toSES.verify!/2thenSES.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
@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.
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.