elixir-testing

Write or improve ExUnit tests for portal/ the emisar way — DataCase/ConnCase, the real fixtures in test/support, and the mandatory happy / denial / cross-account paths (§7). Use when adding tests for

SKILL.md

GitHub

Tests (the emisar way)

This project uses plain ExUnit + Ecto sandbox + hand-written fixtures. There is no Mox, no ExMachina, no StreamData in the dep stack — do not introduce them or write tests that assume them (/tooling-verify-api: test with what's here). Read a sibling test (test/emisar/runs_test.exs, policies_test.exs) and match it.

Setup

  • Domain: use Emisar.DataCase, async: true. Web: use EmisarWeb.ConnCase.
  • Fixtures: the per-domain Fixtures.* modules under test/support/fixtures/; Fixtures.Subjects.owner_subject/1 and subject_for/2 build a real %Subject{}, not a stub. Add a fixture there if the context needs one; don't inline ad-hoc setup that duplicates an existing fixture.

The three paths (non-negotiable — §7, IL-3)

Every context function covers:

  1. Happy — the authorized subject gets {:ok, …}.
  2. Denial — a role without the permission gets {:error, :unauthorized}. A write isn't done without this.
  3. Cross-account — account A's subject cannot see/touch account B's row: {:error, :not_found}.

Doubling third-party calls (no Mox)

Per IL-19, vendor APIs (Paddle, mailer) are wrapped behind a project module. Test by swapping/configuring that wrapper, or with a config flag — the codebase already uses the notify_approvers_async?-style flag to make async side effects synchronous in test. Follow that pattern; don't reach for a mocking library.

Concurrency

  • async: true is the default — keep it. Anything that spawns a DB-touching process must inherit $callers or be made synchronous in test (the config-flag pattern).
  • No Process.sleep for synchronization — assert_receive {…}, 500 across process boundaries.

Jobs / LiveView

  • Jobs: call JobModule.execute([]) or execute(keyword_config) directly. Cover the idempotency path (run twice → second is a no-op), plus pagination/batches for sweeps.
  • LiveView: Phoenix.LiveViewTest (live/2, render_click, render_submit); assert the authorized + denied event paths (IL-15).

Run

Use ./run test portal <path> (or <path>:<line>) from the repository root for focused feedback, including the denial/cross-account cases. A focused pass is not completion: the lead runs the final gate required by portal/AGENTS.md.