first commit
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# `runTenantProviderConformance` — reusable conformance harness
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Every concrete `TenantProvider` — the in-memory reference impl
|
||||
(`api-tenant-provider-memory`, ), the host-DB-backed adapter,
|
||||
any directory-service adapter — MUST pass the same behavioural suite.
|
||||
Putting the suite here, importable as `runTenantProviderConformance`,
|
||||
guarantees that adding a new provider source is a one-line job in the
|
||||
adapter's test file and that every adapter agrees on the contract's
|
||||
edge cases (active-only default, includeSuspended, lookup-returns-null,
|
||||
cursor invalidation, dispose idempotency).
|
||||
|
||||
The harness uses `describe` / `it` / `expect` from
|
||||
`@suaveplan/testing/runner` so it works under both the Vitest default
|
||||
runner and the Bun-conditional runner (rule 11).
|
||||
|
||||
## Features
|
||||
|
||||
- Eight tests covering `list` default filter, `includeSuspended`, `count`, `lookup`-returns-record, `lookup`-returns-null, pagination round-trip, invalid-cursor, and dispose idempotency.
|
||||
- `factory(seed)` callback constructs a fresh provider per test — no shared singleton, no leaked state.
|
||||
- Compatible with both Vitest and Bun via `@suaveplan/testing/runner`.
|
||||
- Every `it(...)` carries `{ timeout: 5000 }` per CLAUDE.md.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
// In api-tenant-provider-memory/src/memory.test.ts (Stage 4):
|
||||
import { runTenantProviderConformance } from "@suaveplan/api-tenant-provider/testing";
|
||||
import { MemoryTenantProvider } from "./memory.js";
|
||||
|
||||
runTenantProviderConformance({
|
||||
label: "in-memory",
|
||||
factory: (seed) => new MemoryTenantProvider(seed),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { runTenantProviderConformance } from "@suaveplan/api-tenant-provider/testing";
|
||||
import { SurrealTenantProvider } from "./surreal.js";
|
||||
import { startSurrealdb } from "@suaveplan/testing-integration-surrealdb";
|
||||
|
||||
// Spin up a real SurrealDB container per provider instance — the
|
||||
// factory can be async, so heavyweight fixtures are first-class.
|
||||
runTenantProviderConformance({
|
||||
label: "surrealdb",
|
||||
factory: async (seed) => {
|
||||
const db = await startSurrealdb();
|
||||
const provider = new SurrealTenantProvider(db);
|
||||
for (const record of seed) await provider.put(record);
|
||||
// Wrap dispose so the container also stops when the test ends.
|
||||
const originalDispose = provider.dispose.bind(provider);
|
||||
provider.dispose = async () => {
|
||||
await originalDispose();
|
||||
await db.stop();
|
||||
};
|
||||
return provider;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface TenantProviderConformanceOptions { readonly label: string; readonly factory: (seed: readonly TenantRecord[]) => Promise<TenantProvider> | TenantProvider }`.
|
||||
- `function runTenantProviderConformance(options: TenantProviderConformanceOptions): void` — registers a `describe` block with the documented tests.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The harness is intentionally minimal: it does not test telemetry,
|
||||
metrics, or implementation-specific properties — those belong in the
|
||||
adapter's own test file. It only pins the cross-adapter contract that
|
||||
every consumer of `TenantProvider` relies on.
|
||||
|
||||
The `factory` callback receives a `readonly TenantRecord[]` seed and
|
||||
MUST return a provider in which `list({ includeSuspended: true })`
|
||||
yields exactly the active and suspended subset of `seed` (deterministic
|
||||
order is not required — assertions normalise via sort).
|
||||
|
||||
`dispose` idempotency is tested by calling `dispose()` twice and
|
||||
asserting the second call resolves. Adapters whose `dispose` reasonably
|
||||
fails on double-call must reset internal state on the first call to
|
||||
make the second a no-op.
|
||||
Reference in New Issue
Block a user