# Conformance blocks

> Prove a custom executor, cache adapter, sink, auth resolver, framework adapter, generator or plugin meets its contract.

Source: https://bettersupabase.com/docs/extending/conformance

`better-supabase/testing` has a block per extension interface. A block runs every
check and resolves with a report, or throws a `ConformanceError` that lists
all failed checks. Blocks don't depend on a test runner, so they work in vitest,
`node:test` and bun.

```ts title="kysely-executor.test.ts"
import { testExecutor } from "better-supabase/testing";
import { it } from "vitest";

it("conforms", () =>
  testExecutor(kyselyExecutor(db), {
    betterSupabase: defineSupabase(schema),
    table: "tags",
    create: { organizationId: ORG, name: "conformance" },
  }));
```

| Block                                                                   | Checks                                                                                                                                                                                                                                                                                                                                        |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `testExecutor(executor, { betterSupabase, table?, create? })`           | rows keyed by selection aliases, limits, counts, `aborted` errors, failures returned instead of thrown, rpc failures, and with `create` a create, read and delete round trip plus a `deleteMany` past `maxAffected` that fails with `max_affected` and deletes nothing                                                                        |
| `testCacheAdapter(adapter, { table? })`                                 | table and row targets, tenants, repeated and concurrent calls, unknown tables, no mutation of the target                                                                                                                                                                                                                                      |
| `testEventSink(sink, { received? })`                                    | empty and full batches, no mutation of events, and with `received` delivery of every event                                                                                                                                                                                                                                                    |
| `testQueueBackend(backend, { queue, dedupe? })`                         | API v1, payload round trip, claimed messages hidden for their lease, attempt counts, retry then dead letter at `max_attempts`, and with `leases` stale attempts rejected and lease extension; dedupe keys return the first id; with `stats` and `listDead`, the dead letter is counted and listed, and with `retryDead` it is claimable again |
| `testSupportSessionStore(store, { admin, targets })`                    | API v1, a started session matches its input and only its admin sees it, a second start ends the first, ending works once and the ended session is listed                                                                                                                                                                                      |
| `testNotificationChannel(channel, { email?, received? })`               | API v1, a name, a frozen message sent without mutating it, a valid result, and with `received` delivery of the message                                                                                                                                                                                                                        |
| `testStreamStore(store, { id?, timeoutMs? })`                           | API v1, a name, one `open` per stream, idempotent appends that refuse a gap, reads from any index that end once the stream closes, a live read that sees later appends, a cancel the writer sees, appends refused after close, a missing stream read as empty, and `purge` with a count                                                       |
| `testChatState(state, { message, id?, ttlMs? })`                        | subscriptions, one lock holder at a time with token-checked release and extension, lock and cache TTLs, `setIfNotExists`, list trimming, and a per-thread queue that keeps the newest entries, reports its depth and drops expired ones                                                                                                       |
| `testCredentialProvider(provider, { ref, seed, userRef?, inbound? })`   | API v1, `invalid_input` for a ref of another provider, a stored token with headers, a new value seen after storing it, separate credentials per user, `revoke` then `not_found`, and with `inbound` a signed request accepted and a tampered one refused                                                                                      |
| `testWebhookSigner(signer, { verify? })`                                | API v1, a name, string headers that are stable for the same input and change with the body, and with `verify` a signature the receiver accepts                                                                                                                                                                                                |
| `testWebhookTransport(transport, { url, received? })`                   | API v1, a name, an HTTP status and string body for a POST to `url`, and with `received` delivery of the body                                                                                                                                                                                                                                  |
| `testWebhookSecretStore(store, { endpointId })`                         | API v1, secrets as strings, and with `rotate` a new secret listed first while the previous one keeps signing                                                                                                                                                                                                                                  |
| `testAuthResolver(resolver, { invalid, valid?, unrelated? })`           | `undefined` for requests without its credentials, `invalid` (never anon) for bad ones, the expected user for good ones, never throws                                                                                                                                                                                                          |
| `testAdapter(name, { betterSupabase, serve, errorsInBody?, cookies? })` | a 401 without running the handler for a refused caller, data for an allowed one, a `DbError` answered with its status, no leaked message for other errors, `bs-primary-until` after a write, pending event sends handed to `waitUntil`                                                                                                        |
| `testGenerator(generator, { meta, config?, model? })`                   | `apiVersion` 1 or unset, relative, unique paths inside the project, deterministic output, no mutation of the input                                                                                                                                                                                                                            |
| `testBlockTransportMiddleware(middleware)`                              | API v1, a name, `next` called at most once, an unchanged request, and a rejection that keeps the database error's code and hint                                                                                                                                                                                                               |
| `testAuthorizationProvider(provider)`                                   | API v1, plain JSON, valid and unique scopes with a known `tenantScope`, templates that use only their placeholders, `requires` lists every function the templates call, permissions listed once at known scopes                                                                                                                               |
| `testAuthorizer(authorizer, { permissions, granted?, unknown? })`       | API v1, a name, stable non-empty `key()` strings, a known outcome for every request without mutating it, `evaluations` that agree with `evaluate` and keep the order, decisions from the subject and never from a forged input, an unknown permission never granted, and with `granted` a grant through the same check the server runs        |
| `testPlugin(plugin, { betterSupabase, table?, context?, create? })`     | API v1, installs alone and next to the first-party plugins in either order, a known `enforce`, `repository` methods that never replace base ones, pure and deterministic `context`, `transformQuery` and `beforeMutation`, `wrapExecutor` keeps results intact, `mapError` returns a `DbError` or `undefined`                                 |

Inputs that should stay untouched are deep-frozen, so an implementation that
mutates them fails with the check that caught it.

The first-party executors, adapters, generators and plugins run these blocks in
the package's own test suite.