feat: harden test and registry evidence

This commit is contained in:
donghyeon-ka
2026-07-26 17:15:26 +09:00
parent 3f634eb655
commit 98d4fd4960
68 changed files with 6167 additions and 250 deletions
+30
View File
@@ -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",
}),
});
}
+16
View File
@@ -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;
}
+171
View File
@@ -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;
}
+45
View File
@@ -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;
}
+13
View File
@@ -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(),
});
}