131 lines
4.6 KiB
TypeScript
131 lines
4.6 KiB
TypeScript
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?.();
|
|
}
|
|
});
|
|
});
|
|
}
|