# Fixture API

The routes a product exposes so each test run can create its test data in any environment, production included, and delete it when the run ends.

A test run creates the data its tests need, such as a verified user with a role, in every
environment it tests, production included, and deletes all of it when it ends. The product does
this through its **Fixture API**: a few locked-down routes in the product, written in its own
stack. Clanker Ship defines the protocol and signs a **fixture token** for each run, the only
credential a Fixture API accepts. A deployment's `fixtureUrl` says where its Fixture API is. The
`set-up-testing` skill builds one from your `fixtures`
[clanker instructions](https://docs.clankership.com/get-started/concepts/planning/clanker-instructions.md).

## Routes

Paths are relative to the fixture URL. Every request sends `Authorization: Bearer <fixture token>`.

| Route                                  | What it does                                                                                                                   |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `GET /manifest`                        | Lists the fixtures and jobs the Fixture API offers                                                                             |
| `POST /scopes/{scope}/fixtures/{name}` | Creates one fixture in the scope from the JSON body and returns what it made: IDs, emails, passwords, session cookies and keys |
| `GET /scopes/{scope}/outbox`           | Returns the messages the product would have sent to the scope's test users                                                     |
| `POST /scopes/{scope}/jobs/{name}`     | Runs one scheduled job now, for the scope's tenants only                                                                       |
| `GET /scopes/{scope}`                  | Returns `{ scope, counts, expiresAt }`, with the scope's records counted by kind                                               |
| `DELETE /scopes/{scope}`               | Deletes the scope and returns `{ deleted, remaining }`, then deletes other scopes past their expiry                            |

The manifest names each fixture with a JSON Schema for its body (`params`) and a body that creates
it (`example`). `outbox` says whether the outbox route is there.

```json
{
  "protocol": 1,
  "fixtures": [
    {
      "name": "user",
      "description": "A verified user with a role in a new tenant.",
      "params": { "type": "object", "properties": { "role": { "type": "string" } } },
      "example": { "role": "admin" }
    }
  ],
  "jobs": [{ "name": "send-digests", "description": "Sends the daily digest emails." }],
  "outbox": true
}
```

- Create through the product's own services, never raw SQL. Take times in the past, such as an
  expired invite, as parameters.
- Give each root, a record that belongs to no other such as a user or a tenant, the scope and its
  expiry. Delete every other record with its root, so deleting the roots deletes the scope.
- A fixture call may send an `Idempotency-Key` header. Keep the first answer for each key and return
  it when the call comes again with that key, without making anything new.
- `deleted` counts records by kind, such as `{ "users": 4, "tenants": 1 }`, and `remaining` is how
  many records are left. Anything left fails the run.
- Delete expired scopes after answering, so the teardown stays fast.

## Fixture tokens

Starting a run, a shard or a retry returns `fixture: { scope, token, url, expiresAt }` when the
deployment has a fixture URL. [Create a fixture token](https://docs.clankership.com/api/fixture-tokens/create.md) returns the same
without a run, lasting from 5 seconds to six hours; anyone who can publish test runs can create
one. A scope is `scp_` and 12 lowercase letters or digits, such as `scp_7h2k9q2m4x1a`, so it fits in
an email address or a hostname. It expires with its token, six hours after it starts at most.

| Claim   | Value                                                              |
| ------- | ------------------------------------------------------------------ |
| `iss`   | The Clanker Ship API that signed it: `https://api.clankership.com` |
| `sub`   | The Clanker Ship workspace's ID                                    |
| `aud`   | The fixture URL                                                    |
| `env`   | The environment's slug                                             |
| `class` | `production` or `non-production`                                   |
| `kind`  | `fixture`                                                          |
| `scope` | The scope                                                          |
| `run`   | The run's number, or null for a token without a run                |
| `iat`   | When it was issued, in seconds                                     |
| `exp`   | When it expires, in seconds; the scope expires then too            |

A Fixture API accepts a token only when all of these hold:

1. Its signature checks out against `GET {iss}/.well-known/jwks.json`: Ed25519 keys, `alg: EdDSA`,
   matched by `kid`.
2. `iss`, `sub` and `env` are the issuer, workspace and environment the deployment trusts.
3. `aud` is the Fixture API's own URL, so a token for staging fails on production.
4. `exp` is in the future, and the scope in the path is the token's `scope`.

## Run context

`clanker run` and `clanker retry` hand Playwright the run context as `use.clanker`. Specs read it
through a fixture and never from environment variables.

| Field         | Value                                                                                                                              |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `environment` | `{ slug, class }` of the environment tested, or null without one                                                                   |
| `local`       | True when the site under test is on this machine                                                                                   |
| `deployments` | Each product's base URL in that environment, by product slug                                                                       |
| `fixture`     | The scope, token, Fixture API URL and expiry, or null without a Fixture API                                                        |
| `semaphores`  | Where the run takes [semaphore](https://docs.clankership.com/get-started/concepts/test-runs.md#semaphores) leases, and its run token; null for an unpublished run |

## Check a Fixture API

```bash
clanker fixtures check --environment staging
```

It asks for a ten-minute token, reads the manifest, creates every fixture from its example and
deletes the scope. It exits 1 when a step fails or anything is left. `--fixture-url` checks another
Fixture API, such as a local server. See [Test runs](https://docs.clankership.com/get-started/concepts/test-runs.md#test-data) for
how a run cleans up and reports it.
