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 undertest/support/fixtures/;Fixtures.Subjects.owner_subject/1andsubject_for/3build 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:
- Happy — the authorized subject gets
{:ok, …}. - Denial — a role without the permission gets
{:error, :unauthorized}. A write isn't done without this. - 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: trueis the default — keep it. Anything that spawns a DB-touching process must inherit$callersor be made synchronous in test (the config-flag pattern).- No
Process.sleepfor synchronization —assert_receive {…}, 500across process boundaries.
Jobs / LiveView
- Jobs: call
JobModule.execute([])orexecute(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.