Files
skills/docs/api-stack/api/api-tenant-provider/src/testing/conformance.md
2026-05-29 08:20:56 +01:00

3.6 KiB

runTenantProviderConformance — reusable conformance harness

(no submodules)

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

// 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

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.