Mutare Plug

Custom Mutare mutators for the Plug request surface — the Plug.Conn calls a plug, router, or controller action performs on the conn.

A plug returns a transformed conn, so its whole contract is which conn-transforming call ran — the status/header/session/cookie it set, the body it sent, whether it halted. These are exactly the calls a suite tends to under-assert: a test that checks "something happened" but not which transformation leaves a gap. mutare_plug turns each such gap into a located Mutare survivor.

The families

Mutare.Plug.all/0 returns six mutator families:

FamilyNameMutationThe gap a survivor exposes
Mutare.Plug.Halt:plug_haltremoves Plug.Conn.halt/1no test depends on this plug halting — the classic authorization-bypass
Mutare.Plug.Status:http_statusswaps the atom status of Plug.Conn.put_status/2, send_resp/3, resp/3, send_chunked/2, and send_file/3,4,5 for a same-family sibling (:ok → :created, :unauthorized → :forbidden)no test pins the exact status
Mutare.Plug.Session:plug_sessionremoves Plug.Conn.put_session/3, delete_session/2, clear_session/1, and configure_session/2no test depends on the session mutation — removing configure_session(conn, renew: true) on login is the classic session-fixation bypass
Mutare.Plug.Header:resp_headerremoves Plug.Conn.put_resp_header/3, delete_resp_header/2, and put_resp_content_type/2,3no test depends on the response header — or pins the content type
Mutare.Plug.Cookie:resp_cookieremoves Plug.Conn.put_resp_cookie/3,4 and delete_resp_cookie/2,3, flips explicit string same_site: values, and drops the max_age: option (persistent cookie → session cookie)no test depends on the response cookie, its SameSite policy, or its lifetime
Mutare.Plug.Body:resp_bodyblanks the body argument of Plug.Conn.send_resp/3 and resp/3 to ""no test reads the response body

Each family matches its call written directly (Plug.Conn.halt(conn)), aliased, or bare-imported (halt(conn), the form use Plug.Builder / use Plug.Router — or Phoenix's use MyAppWeb, :controller — produces). Integer statuses (put_status(conn, 200)) are left to Mutare's built-in literal family; :http_status only swaps atom statuses.

Usage

mutare_plug rides on the Mutare engine, so add both as :dev/:test dependencies:

# mix.exs
defp deps do
[
{:mutare, "~> 0.1", only: [:dev, :test], runtime: false},
{:mutare_plug, "~> 0.1", only: [:dev, :test], runtime: false}
]
end

Then list the families in .mutare.exs. Setting :mutatorsreplaces Mutare's default set, so include the :builtins family to keep the built-ins on:

# .mutare.exs
[mutators: [:builtins] ++ Mutare.Plug.all()]

Run it the usual way:

mix mutare

A Phoenix app adds the companion mutare_phoenix on top for the Phoenix.Controller surface (see "Scope").

Configuring a family

:http_status's swap table is tunable. Give the family {module, opts} with a :swaps map — a status you list replaces its built-in siblings, an empty list disables it, and any status you omit keeps its built-in siblings. Configuring a family means listing it yourself, so expand Mutare.Plug.all/0 into its members and replace that one entry:

# .mutare.exs — narrow :ok to one sibling, stop mutating :no_content, add a teapot
[
mutators: [
:builtins,
Mutare.Plug.Halt,
{Mutare.Plug.Status, swaps: %{ok: [:created], no_content: [], im_a_teapot: [:bad_request]}},
Mutare.Plug.Session,
Mutare.Plug.Header,
Mutare.Plug.Cookie,
Mutare.Plug.Body
]
]

Configured siblings must be valid Plug.Conn.Status reason atoms — trusted, not checked: a sibling that isn't a real status produces a crashing mutant rather than the built-in table's valid-but-wrong swap. See Mutare.Plug.Status for the full table.

Why not the built-in atom mutators?

In a status position, Mutare's built-in atom swaps (:ok → :error / :mutare) produce a value that crashes — an uninformative kill that tells you nothing about test quality. :http_status swaps to valid siblings, so a survivor means a genuine missing assertion rather than a crash, and Mutare's overlap pruning drops the redundant crashing leaves at the same range. :resp_body works the same way for a literal body: its whole-call blank covers the string node, so the built-in string family's sentinel leaves there are pruned automatically and one clean "is the body read?" mutant remains.

Example

examples/demo is a standalone mini-project — an auth plug and a few request handlers over a tiny Plug.Conn stand-in — with deliberate test gaps that surface survivors in the halt/status/body families. From the repo root:

mix compile
mix mutare examples/demo

Scope

This package owns the Plug.Conn surface only. The Phoenix.Controller surface (redirect/2's status, and the defensive :skip routing for Phoenix's compile-time macros) is the companion mutare_phoenix, which depends on this package the way phoenix depends on plug; LiveView is mutare_phoenix_live_view on top of that.

License

MIT — see LICENSE.