Mutare.Phoenix.Swoosh.RenderBody (mutare_phoenix_swoosh v0.2.1)

Copy Markdown View Source

:render_body — mutates calls that control which email body parts are rendered: Phoenix.Swoosh.render_body/2,3, the call that renders an email's templates onto its html_body/text_body fields, and the format map of Phoenix.Swoosh.put_new_formats/2, which maps template extensions to body fields. Three kinds, variant-labelled:

Removal (label remove) — the call collapses to the email it received, so the email is built, addressed, and delivered with no rendered body at all:

render_body(email, :welcome, %{name: name})  ->  email
email |> render_body(:welcome)               ->  email

A survivor means no test asserts the rendered body content — the mail pipeline is exercised, but nothing checks that html_body/text_body ever got set.

Atom-template narrowing (labels html_only / text_only) — an atom template renders both the .html and the .text template; the string form renders exactly one. Narrowing the literal atom to each string form drops the other body part:

render_body(email, :welcome, assigns)  ->  render_body(email, "welcome.html", assigns)
                                       ->  render_body(email, "welcome.text", assigns)

A survivor means no test asserts the other body part. Use Swoosh.TestAssertions.assert_email_sent/1 on both bodies, or a direct assertion such as email.text_body =~ ..., to kill it. Narrowing applies only to a literal atom template; a string or computed template gets the removal mutant alone.

Format dropping (label format) — a literal put_new_formats/2 map with more than one entry loses one entry per mutant, so that extension's template stops rendering:

put_new_formats(email, %{"html" => :html_body, "amp" => :html_body})
  ->  put_new_formats(email, %{"amp" => :html_body})
  ->  put_new_formats(email, %{"html" => :html_body})

This is the custom-formats form of the same gap the narrowings probe — and the only form of it once a mailer configures :formats, since the narrowings' .html/.text extensions may not exist there (a narrowed mutant then causes a missing-template crash: an uninformative kill, never a wrong survivor). A single-entry map produces nothing: dropping the only format renders no body at all, duplicating the remove mutation.

Suppress one kind with # mutare:ignore[render_body:remove] / [render_body:html_only] / [render_body:text_only] / [render_body:format], or the family with # mutare:ignore[render_body].

Mutations apply only to the supported arities — render_body/2,3 (/2 is the use-injected wrapper's default-assigns form) and put_new_formats/2 — so a name-matched call of any other arity is left alone, keeping every metamutant compiling. A piped call is the same call with the email piped in, and its removal collapses the stage to that email.

How the three kinds relate

At an atom-template site the removal mutant is the weakest of the three: any assertion that kills a narrowing also kills the removal, so a surviving remove where both narrowings also survive is one gap reported three times, not three gaps. It is kept because it is the only mutant at a string or computed template site, and the only one that also drops the assigns merge (a test that reads email.assigns rather than the bodies kills it alone).

The template and format positions are pinned

The template name (argument 1 — atom or string) is a structural identifier, not a computed value: a perturbed name is a missing-template crash at render time (an uninformative crash-kill), never a signal. put_new_formats/2's extension→field map is the same kind of value — a perturbed extension key is a missing template, a perturbed field value is a crash-free but meaningless field swap. This family registers all three calls in the call-routing registry with those positions :raw, so no family — core value literals included — mutates them or anything inside them, at bare, qualified, and aliased call sites alike. The registry route (rather than an argument mark) is deliberate: it covers the position's whole subtree, and registration is also what makes the bare forms resolvable and witness-safe at all (see below). The :raw route excludes the argument subtree from mutation, while this family can still rewrite the argument as part of a call-level mutation.

Resolution: why this family is half of a matched pair

use Phoenix.Swoosh injects import Phoenix.Swoosh, except: [render_body: 3] plus a local def render_body/2,3 wrapper — so a mailer's bare render_body calls resolve to a local definition that Mutare does not resolve. Mutare.Phoenix.Swoosh (listed under :extensions) overrides that use expansion to return a whole import Phoenix.Swoosh in its place. This family's registry entries serve two purposes:

  • the /2 wrapper arity is not a real Phoenix.Swoosh export, so import reflection can never resolve a bare render_body(email, :welcome) — the registry's whole-import fallback is what resolves it;
  • a bare imported call normally carries a compile-time witness that re-imports the resolved provider next to the mutant — which, next to the real injected local def, is an import/local conflict that would fail the single metamutant compile. Registered calls have that witness dropped.

Without the extension, bare render_body sites are neither mutated nor pinned (the injected except: import excludes exactly this one name); qualified and aliased sites work regardless.

Deliberately left alone

  • The assigns argument is ordinary runtime data — core's families mutate the values inside it as they would anywhere (a wrong interpolated value in a body only a body-content assertion catches). Its keys are likewise subject to core's map-literal mutation rules, not pinned here. Mutare.Phoenix.Swoosh.Layout mutates the :layout assign, which configures phoenix_swoosh rather than supplying template data.
  • Dropping an assigns entry was considered and rejected: a template referencing the dropped assign raises KeyError/ArgumentError at render — a crash-kill, not a test of body-content assertions.
  • Swapping a format's body field ("html" => :html_body → :text_body) is not generated: the rendered html would land in text_body alongside whatever the text template put there, and which one survives depends on map ordering — a mutant whose behaviour a reader cannot predict from its diff.