feat: harden test and registry evidence
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
export function successEnvelope<Value>(
|
||||
data: Value,
|
||||
requestId = "fixture-request",
|
||||
) {
|
||||
return Object.freeze({
|
||||
success: true as const,
|
||||
data: structuredClone(data),
|
||||
meta: Object.freeze({
|
||||
requestId,
|
||||
traceId: "fixture-trace",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function failureEnvelope(
|
||||
code: string,
|
||||
details?: Readonly<Record<string, unknown>>,
|
||||
) {
|
||||
return Object.freeze({
|
||||
success: false as const,
|
||||
error: Object.freeze({
|
||||
code,
|
||||
...(details ? { details: structuredClone(details) } : {}),
|
||||
}),
|
||||
meta: Object.freeze({
|
||||
requestId: "fixture-request",
|
||||
traceId: "fixture-trace",
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { http, HttpResponse } from "msw";
|
||||
|
||||
export function createBootstrapHandlers(
|
||||
runtimeConfig: Readonly<Record<string, unknown>>,
|
||||
releaseManifest: Readonly<Record<string, unknown>>,
|
||||
baseUrl = "http://app.test",
|
||||
) {
|
||||
return [
|
||||
http.get(`${baseUrl}/config.json`, () =>
|
||||
HttpResponse.json(structuredClone(runtimeConfig)),
|
||||
),
|
||||
http.get(`${baseUrl}/release-manifest.json`, () =>
|
||||
HttpResponse.json(structuredClone(releaseManifest)),
|
||||
),
|
||||
] as const;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { delay, http, HttpResponse } from "msw";
|
||||
|
||||
import {
|
||||
failureEnvelope,
|
||||
successEnvelope,
|
||||
} from "../contracts/envelopes.js";
|
||||
import type { HttpScenarioId } from "../scenarios/catalog.js";
|
||||
import { assertOperationScenario } from "../scenarios/catalog.js";
|
||||
|
||||
export type ReferenceResourceFixture = Readonly<{
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt?: string;
|
||||
}>;
|
||||
|
||||
type ScenarioOptions = Readonly<{
|
||||
baseUrl?: string;
|
||||
scenarios?: Partial<
|
||||
Record<
|
||||
| "LIST_REFERENCE_RESOURCES"
|
||||
| "CREATE_REFERENCE_RESOURCE"
|
||||
| "GET_REFERENCE_RESOURCE",
|
||||
HttpScenarioId
|
||||
>
|
||||
>;
|
||||
resources?: ReferenceResourceFixture[];
|
||||
onList?(search: string): void;
|
||||
onCreate?(body: Readonly<Record<string, unknown>>): void;
|
||||
}>;
|
||||
|
||||
const DEFAULT_RESOURCE = Object.freeze({
|
||||
id: "reference-1",
|
||||
name: "Reference",
|
||||
createdAt: "2026-07-26T00:00:00.000Z",
|
||||
});
|
||||
|
||||
async function scenarioResponse(
|
||||
scenario: HttpScenarioId,
|
||||
payload: unknown,
|
||||
attempt: number,
|
||||
) {
|
||||
if (scenario === "slow") await delay(50);
|
||||
if (scenario === "timeout") await delay(30_000);
|
||||
if (scenario === "network-error") return HttpResponse.error();
|
||||
if (scenario === "content-type-mismatch") {
|
||||
return new HttpResponse("<html>not json</html>", {
|
||||
headers: { "Content-Type": "text/html" },
|
||||
});
|
||||
}
|
||||
if (scenario === "malformed-json") {
|
||||
return new HttpResponse("{invalid", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (scenario === "envelope-mismatch") {
|
||||
return HttpResponse.json({ data: payload });
|
||||
}
|
||||
if (scenario === "schema-mismatch") {
|
||||
return HttpResponse.json(successEnvelope({ unexpected: true }));
|
||||
}
|
||||
if (
|
||||
scenario === "auth-persistent-401" ||
|
||||
(scenario === "auth-recover-once" && attempt === 1)
|
||||
) {
|
||||
return HttpResponse.json(failureEnvelope("AUTH_REQUIRED"), {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
if (scenario === "forbidden-403") {
|
||||
return HttpResponse.json(failureEnvelope("FORBIDDEN"), { status: 403 });
|
||||
}
|
||||
if (scenario === "not-found-404") {
|
||||
return HttpResponse.json(failureEnvelope("NOT_FOUND"), { status: 404 });
|
||||
}
|
||||
if (scenario === "conflict-409") {
|
||||
return HttpResponse.json(failureEnvelope("CONFLICT"), { status: 409 });
|
||||
}
|
||||
if (scenario === "validation-422") {
|
||||
return HttpResponse.json(
|
||||
failureEnvelope("VALIDATION_REJECTED", {
|
||||
issues: [{ path: "name", code: "too_small" }],
|
||||
}),
|
||||
{ status: 422 },
|
||||
);
|
||||
}
|
||||
if (scenario === "rate-limited-429") {
|
||||
return HttpResponse.json(failureEnvelope("RATE_LIMITED"), {
|
||||
status: 429,
|
||||
headers: { "Retry-After": "1" },
|
||||
});
|
||||
}
|
||||
if (
|
||||
scenario === "server-terminal-500" ||
|
||||
(scenario === "server-retry-success" && attempt === 1)
|
||||
) {
|
||||
return HttpResponse.json(failureEnvelope("SERVER_FAILURE"), {
|
||||
status: 503,
|
||||
});
|
||||
}
|
||||
return HttpResponse.json(successEnvelope(payload));
|
||||
}
|
||||
|
||||
export function createReferenceScenarioHandlers(options: ScenarioOptions = {}) {
|
||||
const baseUrl = options.baseUrl ?? "https://api.test";
|
||||
const resources = options.resources ?? [{ ...DEFAULT_RESOURCE }];
|
||||
const attempts = new Map<string, number>();
|
||||
const scenarioFor = (
|
||||
operationId: keyof NonNullable<ScenarioOptions["scenarios"]>,
|
||||
) =>
|
||||
assertOperationScenario(
|
||||
operationId,
|
||||
options.scenarios?.[operationId] ?? "success",
|
||||
);
|
||||
const nextAttempt = (operationId: string) => {
|
||||
const next = (attempts.get(operationId) ?? 0) + 1;
|
||||
attempts.set(operationId, next);
|
||||
return next;
|
||||
};
|
||||
|
||||
return [
|
||||
http.get(`${baseUrl}/api/reference-resources`, ({ request }) => {
|
||||
options.onList?.(new URL(request.url).search);
|
||||
const scenario = scenarioFor("LIST_REFERENCE_RESOURCES");
|
||||
const payload = scenario === "empty" ? [] : resources;
|
||||
return scenarioResponse(
|
||||
scenario,
|
||||
payload,
|
||||
nextAttempt("LIST_REFERENCE_RESOURCES"),
|
||||
);
|
||||
}),
|
||||
http.post(
|
||||
`${baseUrl}/api/reference-resources`,
|
||||
async ({ request }) => {
|
||||
const rawBody = await request.json();
|
||||
const body =
|
||||
rawBody &&
|
||||
typeof rawBody === "object" &&
|
||||
!Array.isArray(rawBody)
|
||||
? (rawBody as Readonly<Record<string, unknown>>)
|
||||
: {};
|
||||
options.onCreate?.(body);
|
||||
const scenario = scenarioFor("CREATE_REFERENCE_RESOURCE");
|
||||
const created = {
|
||||
id: "reference-created",
|
||||
name: String(body.name ?? "Created"),
|
||||
createdAt: "2026-07-26T00:00:00.000Z",
|
||||
};
|
||||
if (scenario === "success") resources.push(created);
|
||||
return scenarioResponse(
|
||||
scenario,
|
||||
created,
|
||||
nextAttempt("CREATE_REFERENCE_RESOURCE"),
|
||||
);
|
||||
},
|
||||
),
|
||||
http.get(
|
||||
`${baseUrl}/api/reference-resources/:resourceId`,
|
||||
({ params }) => {
|
||||
const scenario = scenarioFor("GET_REFERENCE_RESOURCE");
|
||||
const resource =
|
||||
resources.find((entry) => entry.id === params.resourceId) ??
|
||||
DEFAULT_RESOURCE;
|
||||
return scenarioResponse(
|
||||
scenario,
|
||||
resource,
|
||||
nextAttempt("GET_REFERENCE_RESOURCE"),
|
||||
);
|
||||
},
|
||||
),
|
||||
] as const;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export const HTTP_SCENARIO_IDS = Object.freeze([
|
||||
"success",
|
||||
"empty",
|
||||
"slow",
|
||||
"network-error",
|
||||
"timeout",
|
||||
"aborted",
|
||||
"content-type-mismatch",
|
||||
"malformed-json",
|
||||
"envelope-mismatch",
|
||||
"schema-mismatch",
|
||||
"auth-recover-once",
|
||||
"auth-persistent-401",
|
||||
"forbidden-403",
|
||||
"not-found-404",
|
||||
"conflict-409",
|
||||
"validation-422",
|
||||
"rate-limited-429",
|
||||
"server-retry-success",
|
||||
"server-terminal-500",
|
||||
] as const);
|
||||
|
||||
export type HttpScenarioId = (typeof HTTP_SCENARIO_IDS)[number];
|
||||
|
||||
export const OPERATION_SCENARIO_CATALOG = Object.freeze({
|
||||
LIST_REFERENCE_RESOURCES: HTTP_SCENARIO_IDS,
|
||||
CREATE_REFERENCE_RESOURCE: Object.freeze(
|
||||
HTTP_SCENARIO_IDS.filter(
|
||||
(scenario) => !["empty", "aborted"].includes(scenario),
|
||||
),
|
||||
),
|
||||
GET_REFERENCE_RESOURCE: HTTP_SCENARIO_IDS,
|
||||
} satisfies Readonly<Record<string, readonly HttpScenarioId[]>>);
|
||||
|
||||
export function assertOperationScenario(
|
||||
operationId: keyof typeof OPERATION_SCENARIO_CATALOG,
|
||||
scenario: HttpScenarioId,
|
||||
) {
|
||||
if (!OPERATION_SCENARIO_CATALOG[operationId].includes(scenario)) {
|
||||
throw new Error(
|
||||
`Scenario ${scenario} is not declared for ${operationId}`,
|
||||
);
|
||||
}
|
||||
return scenario;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { setupServer } from "msw/node";
|
||||
|
||||
export function createStrictMockServer(
|
||||
...handlers: Parameters<typeof setupServer>
|
||||
) {
|
||||
const server = setupServer(...handlers);
|
||||
return Object.freeze({
|
||||
server,
|
||||
listen: () => server.listen({ onUnhandledRequest: "error" }),
|
||||
reset: () => server.resetHandlers(),
|
||||
close: () => server.close(),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user