Mailglass.Credo.NoRawSearchPathMutation (Mailglass v2.2.2)

Copy Markdown View Source

Basics

This check is disabled by default.

Learn how to enable it via .credo.exs.

This check has a base priority of high and works with any version of Elixir.

Explanation

Test code MUST NOT issue a raw search_path mutation as SQL. Route it through Mailglass.TestSupport.SandboxOwnership.with_search_path!/3, which pins ONE pooled connection for the whole block, restores the prior value on that same connection, and then RE-READS it to verify the restore actually landed.

The failure mode, by name

SET search_path TO public without LOCAL is a SESSION-level write: it persists on the physical Postgres connection for that connection's entire lifetime. Under Sandbox :auto mode every Repo.query checks a connection out of the 10-slot pool and returns it, so the poisoned connection goes straight back into the pool. config/test.exs + test/test_helper.exs give pool connections a startup search_path of "<schema>, public", and the whole rest of the suite relies on it to resolve unqualified relation names.

The result is pool poisoning: some later, wholly unrelated test draws the poisoned connection and raises (Postgrex.Error) ERROR 42P01 (undefined_table) relation "mailglass_deliveries" does not exist — a failure attributed to an innocent module hundreds of tests away from the one that broke it. This is not hypothetical: it is the confirmed root cause of the D-31 Class A cascade on the MAILGLASS_SCHEMA=mailglass axis (seven victim modules, two full misdiagnosis cycles). A throwaway probe confirmed it directly — after ONE unscoped SET search_path TO public, all 40 subsequent pool checkouts observed "public".

A trailing RESET search_path from on_exit does NOT undo it: that is a SEPARATE pool checkout that lands on whichever connection it lands on, which need not be the poisoned one.

What is banned, and why each form

Every form below is banned in SQL-statement position under test/:

  • SET search_path ... — session-scoped. The defect above.
  • SET SESSION search_path ... — an explicit spelling of the same session-scoped write.
  • SET LOCAL search_path ... — transaction-scoped, and therefore looks safe. It is not safe inside a migration: SET LOCAL persists for the remainder of the transaction, and Ecto.Migrator inserts its schema_migrations version row INSIDE that same transaction, AFTER the migration body. The pin redirects Ecto's own bookkeeping INSERT to a search_path holding no schema_migrations table, raising 42P01 ... relation "schema_migrations" does not exist. Observed live: MAILGLASS_SCHEMA=mailglass mix test test/mailglass/shipped_migration_divergence_test.exs failed 4 tests / 4 failures on exactly this.
  • RESET search_path — cannot poison (it restores the startup-packet value), but from :auto mode it is its own checkout on an arbitrary pooled connection, so it heals nothing observable while reading as a fix. A guard that reads as a guarantee without being one is the exact credibility failure this milestone exists to repair.
  • set_config('search_path', ...) — the function-call spelling of a session-level SET, banned for the same reason as the first form.

What is permitted

  • SHOW search_path — read-only.
  • search_path in the Postgrex :parameters connection option (test/test_helper.exs) — that is the connection's STARTUP value, set once at pool-connect time on every connection, which is precisely the invariant the bans above protect.
  • SET search_path = '' as a CREATE FUNCTION attribute clause — a different construct entirely (it hardens a function body against search-path injection, and is not a session write). It is never in statement-initial position inside a CREATE FUNCTION statement, so it does not match.
  • The same literals as ASSERTION MATCH TARGETS (body =~ "SET search_path = ''", String.contains?/2). A match target is compared, never executed, so it cannot poison anything. See :match_target_functions.
  • Anything inside Mailglass.TestSupport.SandboxOwnership — the one sanctioned seam, which owns the same-connection, verified restore — its own mechanism test, and this check's own fixture corpus. Those three modules are the whole allowlist; see .credo.exs for the per-entry justification.

Two-layer guard

This is the PREVENTION half. The detection half is Mailglass.TestSupport.SandboxOwnership.with_search_path!/3's post-restore re-read, which raises SearchPathError naming the offending module. The D-31 Class A cascade recurred precisely because detection shipped without prevention.

Check-Specific Parameters

Use the following parameters to configure this check:

:allowed_modules

Modules explicitly allowed to issue raw search_path mutations (the sanctioned seam and its own test).

This parameter defaults to [Mailglass.TestSupport.SandboxOwnership, Mailglass.TestSupport.SandboxOwnershipTest, Mailglass.Credo.NoRawSearchPathMutationTest].

:included_path_prefixes

Only files in these path prefixes are linted.

This parameter defaults to ["test/", "mailglass_inbound/test/"].

:match_target_functions

Function/operator names whose arguments are assertion MATCH TARGETS rather than executed SQL.

This parameter defaults to [:=~, :contains?].

General Parameters

Like with all checks, general params can be applied.

Parameters can be configured via the .credo.exs config file.