Initial Commit
This commit is contained in:
130
packages/api-stack/api/api-secrets/src/testing/conformance.ts
Normal file
130
packages/api-stack/api/api-secrets/src/testing/conformance.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { SecretsLike } from "@suaveplan/api-types/secrets";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { SECRETS_CODES, type SecretsCode } from "../error-codes/error-codes.js";
|
||||
|
||||
/**
|
||||
* Factory-supplied test fixture for the secrets-source conformance harness.
|
||||
* Implementations populate `seed` (initial name → value map), then return
|
||||
* the `SecretsLike` instance to exercise.
|
||||
*/
|
||||
export interface SecretsConformanceFixture {
|
||||
/** The `SecretsLike` under test. */
|
||||
readonly secrets: SecretsLike;
|
||||
/** Optional teardown — called after every scenario. */
|
||||
readonly teardown?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construction options for the conformance harness. Sources declare
|
||||
* `caseSensitive` (most are; environment-variable sources may not be on
|
||||
* Windows) and `supportsListReadingAhead` (`list()` returns every seeded
|
||||
* name without resolving them first).
|
||||
*/
|
||||
export interface RunSecretsConformanceOptions {
|
||||
/** Human-readable label embedded in `describe` titles. */
|
||||
readonly label: string;
|
||||
/**
|
||||
* Asynchronously build a fresh fixture for each scenario. The factory
|
||||
* receives the seed map for the upcoming case and MUST honour it.
|
||||
*/
|
||||
readonly factory: (
|
||||
seed: Readonly<Record<string, string>>
|
||||
) => Promise<SecretsConformanceFixture> | SecretsConformanceFixture;
|
||||
/**
|
||||
* Predicate that recognises the source's representation of a
|
||||
* `SECRETS.SECRET_NOT_FOUND` failure thrown from `require`. Defaults to
|
||||
* matching `(err as { code?: string }).code === "SECRETS.SECRET_NOT_FOUND"`.
|
||||
*/
|
||||
readonly isNotFound?: (err: unknown) => boolean;
|
||||
}
|
||||
|
||||
function defaultIsNotFound(err: unknown): boolean {
|
||||
const code: SecretsCode = SECRETS_CODES.SECRET_NOT_FOUND;
|
||||
return (
|
||||
typeof err === "object" &&
|
||||
err !== null &&
|
||||
"code" in err &&
|
||||
(err as { code: unknown }).code === code
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable spec-conformance suite for every concrete `SecretsLike`
|
||||
* implementation. Covers the four documented behaviours: `get` returns a
|
||||
* present value, `get` returns undefined for missing names, `require`
|
||||
* resolves to the value, and `require` throws `SECRETS.SECRET_NOT_FOUND`
|
||||
* for missing names. Also exercises `list()` enumeration.
|
||||
*
|
||||
* Every Stage-4/Stage-8 source MUST call this harness from its own test
|
||||
* suite. Failing the harness means the source breaks the cross-package
|
||||
* contract.
|
||||
*/
|
||||
export function runSecretsConformance(
|
||||
options: RunSecretsConformanceOptions
|
||||
): void {
|
||||
const { label, factory } = options;
|
||||
const isNotFound = options.isNotFound ?? defaultIsNotFound;
|
||||
|
||||
describe(`${label} — SecretsLike conformance`, () => {
|
||||
it("get resolves to the seeded value", { timeout: 5000 }, async () => {
|
||||
const fx = await factory({ ALPHA: "one" });
|
||||
try {
|
||||
await expect(fx.secrets.get("ALPHA")).resolves.toBe("one");
|
||||
} finally {
|
||||
await fx.teardown?.();
|
||||
}
|
||||
});
|
||||
|
||||
it("get resolves to undefined for missing names", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fx = await factory({});
|
||||
try {
|
||||
await expect(
|
||||
fx.secrets.get("MISSING")
|
||||
).resolves.toBeUndefined();
|
||||
} finally {
|
||||
await fx.teardown?.();
|
||||
}
|
||||
});
|
||||
|
||||
it("require resolves to the seeded value", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fx = await factory({ BETA: "two" });
|
||||
try {
|
||||
await expect(fx.secrets.require("BETA")).resolves.toBe("two");
|
||||
} finally {
|
||||
await fx.teardown?.();
|
||||
}
|
||||
});
|
||||
|
||||
it("require throws SECRET_NOT_FOUND for missing names", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fx = await factory({});
|
||||
try {
|
||||
let caught: unknown;
|
||||
try {
|
||||
await fx.secrets.require("MISSING");
|
||||
} catch (err) {
|
||||
caught = err;
|
||||
}
|
||||
expect(caught).toBeDefined();
|
||||
expect(isNotFound(caught)).toBe(true);
|
||||
} finally {
|
||||
await fx.teardown?.();
|
||||
}
|
||||
});
|
||||
|
||||
it("list enumerates seeded names", { timeout: 5000 }, async () => {
|
||||
const fx = await factory({ A: "1", B: "2" });
|
||||
try {
|
||||
const names = [...fx.secrets.list()].sort();
|
||||
expect(names).toEqual(["A", "B"]);
|
||||
} finally {
|
||||
await fx.teardown?.();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user