# `Statifier.Testing.HandlerCase`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.1.1/lib/statifier/testing/handler_case.ex#L1)

A reusable conformance case for `Statifier.Invoke.Handler` implementations
- the second member of the `Statifier.Testing` family ADR-0053 opened, and
the mechanical pin for the behaviour contract ADR-0051 decision 4 and
`docs/extending.md` state in prose. A host application `use`s this module
in a test alongside `use ExUnit.Case` and gets the contract checks as
generated tests against its own handler:

    defmodule MyApp.EnrichHandlerConformanceTest do
      use ExUnit.Case, async: false
      use Statifier.Testing.HandlerCase,
        handler: MyApp.EnrichHandler,
        type: "myapp:enrich"
    end

What the generated tests verify, each a clause of the documented contract:

1. `start/2`, `cancel/2`, and `forward/3` are pure planning callbacks -
   deterministic for fixed inputs, observably effect-free, returning the
   documented shapes (`Statifier.Invoke.Handler`'s moduledoc, point 1).
2. `perform/2` is idempotent on `invoke_id`: performing the handler's own
   planned instructions twice must not duplicate observable effects
   (point 2 there, and `docs/extending.md`'s "At-least-once" section).
   Idempotency needs an observation point only the implementor can name -
   see "The observation point" below.
3. `cancel/2` after `start/2` plans without raising, and `cancel/2` for an
   invoke_id the handler never saw is a no-op, never a raise - the library
   plans cancels off the live invocation table, but a crash-recovering host
   may replay a cancel whose start it never durably recorded.
4. `{:error, _}` from `start/2` surfaces as `error.execution` in the
   driving chart (ADR-0051 decision 1's classification table), pinned by
   driving a minimal chart fixture with a case-internal always-failing
   probe handler registered under the implementor's own type string.
5. Handler exceptions propagate - the library never rescues them into an
   event (`docs/extending.md`, "Where the library will not help") - pinned
   with a case-internal raising probe the same way.

Checks 4 and 5 pin the *library's* half of the contract with probe
handlers rather than the implementation under test, on purpose: a
conforming handler has no reason to expose a start that fails or raises on
demand, and the point of running the pins downstream is that the contract
an implementor builds against stays enforced by tests where the
implementations live, not by this repository's suite alone. A handler
whose `start/2` has a real failure path asserts its own half with
`assert_erroring_start/3`.

Every check is also a public function on this module, so a suite that
wants different fixtures - or only one of the checks - calls them directly
instead of `use`-ing the whole case.

## Options

- `:handler` (required) - the `Statifier.Invoke.Handler` implementation
  module under test.
- `:type` (required) - the `<invoke type>` string the handler serves.

## The fixtures, and overriding them

The generated tests plan against a synthetic `%Statifier.Effect.Invoke{}`,
a plan context, and an autoforwarded event, built by `build_invoke/2`,
`build_ctx/3`, and `build_event/1`. Each is reachable through an
overridable function in the `use`-ing module, so a handler that reads
`params`, `src`, or `content` overrides the one fixture it cares about:

- `conformance_invoke/0` - the `%Statifier.Effect.Invoke{}` handed to
  `start/2`.
- `conformance_ctx/0` - the plan context handed to every callback.
- `conformance_event/0` - the `%Statifier.Event{}` handed to `forward/3`.
- `observed_effects/1` - the observation point, below.

## The observation point

The case cannot see your job queue, your database, or your test inbox, so
idempotency (and planning purity) are judged against a function you
declare: override `observed_effects/1` to return the observable effects
attributable to the given `invoke_id` at the moment of the call - a list
of enqueued jobs, a set of rows, whatever your `perform/2` writes. The
default returns `:unobserved`, which skips the purity comparisons; the
idempotency test **flunks** under that default whenever the handler
actually routes instructions to `perform/2`, naming the override to
write - a handler with real performed effects never gets a silent pass
(ADR-0053's fail-not-skip discipline). A handler that routes nothing to
`perform/2` needs no observation point, and its idempotency test asserts
exactly that instead.

# `observe`

```elixir
@type observe() :: (invoke_id :: String.t() -&gt; term())
```

The observation point: returns the observable effects attributable to an
`invoke_id`, or `:unobserved` when the `use`-ing module has not declared
one - see the moduledoc's "The observation point" section.

# `__using__`
*macro* 

```elixir
@spec __using__(opts :: Macro.t()) :: Macro.t()
```

Generates the conformance tests into the `use`-ing module, which must
`use ExUnit.Case` first (the generated tests are ordinary `test` blocks).
Options and the overridable fixture functions are described in the
moduledoc.

# `assert_cancel_contract`

```elixir
@spec assert_cancel_contract(
  handler :: module(),
  invoke :: Statifier.Effect.Invoke.t(),
  ctx :: Statifier.Invoke.Handler.ctx(),
  observe :: observe()
) :: :ok
```

Contract check 3 for `cancel/2`: after `start/2` has planned, `cancel/2`
for the same `invoke_id` deterministically plans `{:ok, instructions}`;
for an `invoke_id` the handler never saw it is a no-op that returns
`{:ok, instructions}` rather than raising; and neither call moves
`observe`'s reading.

# `assert_error_start_raises_error_execution`

```elixir
@spec assert_error_start_raises_error_execution(type :: String.t()) :: :ok
```

Contract check 4, the library's half: an `{:error, _}` from `start/2` is
planned as `error.execution` (ADR-0051 decision 1 - a pure planning
failure, no communication ever attempted). Drives a minimal chart whose
initial state invokes `type`, registered to a case-internal probe handler
whose `start/2` always returns `{:error, _}`, and asserts the chart
transitions on `error.execution` - the observation a chart author
actually has.

# `assert_erroring_start`

```elixir
@spec assert_erroring_start(
  handler :: module(),
  invoke :: Statifier.Effect.Invoke.t(),
  ctx :: Statifier.Invoke.Handler.ctx()
) :: :ok
```

The implementor's half of contract check 4, for a handler whose `start/2`
has a real failure path: asserts `start/2` returns `{:error, _}` for
`invoke`, deterministically. Not generated by `use` - a conforming handler
need not expose a failing start at all - so a suite whose handler has one
calls this itself with the failing fixture.

# `assert_forward_contract`

```elixir
@spec assert_forward_contract(
  handler :: module(),
  invoke :: Statifier.Effect.Invoke.t(),
  ctx :: Statifier.Invoke.Handler.ctx(),
  event :: Statifier.Event.t(),
  observe :: observe()
) :: :ok
```

Contract check 1 for `forward/3`: called twice with fixed inputs it
returns the same `{:ok, instructions}`, and leaves `observe`'s reading
unchanged.

# `assert_handler_exceptions_propagate`

```elixir
@spec assert_handler_exceptions_propagate(type :: String.t()) :: :ok
```

Contract check 5: an exception raised from a handler callback propagates
and crashes the session - the library never rescues it into an event
(`docs/extending.md`, "Where the library will not help"). Drives the same
minimal chart with a case-internal probe whose `start/2` raises, and
asserts the session process exits with that very exception.

# `assert_perform_idempotent`

```elixir
@spec assert_perform_idempotent(
  handler :: module(),
  invoke :: Statifier.Effect.Invoke.t(),
  ctx :: Statifier.Invoke.Handler.ctx(),
  event :: Statifier.Event.t(),
  observe :: observe()
) :: :ok
```

Contract check 2: performing the handler's own planned `{:handler, module,
payload}` instructions twice - the crash-and-replay shape
`docs/extending.md`'s "At-least-once" section describes - must not
duplicate the effects `observe` reads for the invoke's id, and each
`perform/2` call must return `:ok` or `{:error, term}`.

When the handler routes instructions to `perform/2` and `observe` still
answers `:unobserved`, this flunks naming the `observed_effects/1`
override to write - never a silent pass. When the handler routes nothing
to `perform/2` (the built-in `scxml` handler's own shape), it asserts
exactly that and no observation point is needed.

# `assert_start_contract`

```elixir
@spec assert_start_contract(
  handler :: module(),
  invoke :: Statifier.Effect.Invoke.t(),
  ctx :: Statifier.Invoke.Handler.ctx(),
  observe :: observe()
) :: :ok
```

Contract check 1 for `start/2`: called twice with fixed inputs it returns
the same value, of the documented shape (`{:ok, instructions}` or
`{:error, term}`), and leaves `observe`'s reading for the invoke's own id
unchanged - planning is deciding, never doing.

# `build_ctx`

```elixir
@spec build_ctx(type :: String.t(), handler :: module(), overrides :: keyword()) ::
  Statifier.Invoke.Handler.ctx()
```

A plan context (`Statifier.Invoke.Handler.t:ctx/0`) declaring `type`
registered and dispatching it to `handler` - the same three keys
`Statifier.Session.Effects.plan/2` hands every planning callback.
`overrides` merges over the defaults, since `ctx` is a plain map by
contract.

# `build_event`

```elixir
@spec build_event(overrides :: keyword()) :: Statifier.Event.t()
```

A synthetic external event for `forward/3` (spec 6.4.2's `autoforward`):
name `"conformance.ping"`, no data. `overrides` replaces any field.

# `build_invoke`

```elixir
@spec build_invoke(type :: String.t(), overrides :: keyword()) ::
  Statifier.Effect.Invoke.t()
```

A synthetic `%Statifier.Effect.Invoke{}` for planning against: `type` as
given, `invoke_id` `"inv_1"` (the shape a generated platformid takes,
ADR-0008 as amended), empty `params`, and zeroed counters. `overrides`
replaces any field.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
