Files
clean-architecture-frontend…/tests/integration/http-scenario-catalog.test.ts
T
DongHyeonkaandClaude Opus 5 4e87bacdf3 fix: enforce installed HTTP auth profiles
Install the REST auth profile registry once at composition and make it the
single transport authority for V3. Contract composition now rejects an
unregistered authProfileId, so the executor never resolves a profile at
runtime.

The credential collaborator contributes proof headers only: Fetch credentials
come from the resolved profile, transport-owned and forbidden headers are
rejected, headers outside the profile's allowed set are rejected, and a missing
required header fails closed as AUTH_INTEGRATION_FAILURE with zero fetch calls.
The final invariant re-proves credentials mode and the exact header sets.

Demo mode satisfies the strict bearer profile with a fixed non-secret marker
instead of weakening REFERENCE_EXTERNAL_BEARER. Credential owners now receive
the operation lifetime through AuthOperationContext.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:56:44 +09:00

420 lines
14 KiB
TypeScript

import { mkdir, rm } from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { readBoundedBytes } from "../../src/adapters/http/bounded-body-reader.ts";
import {
createContractHttpExecutor,
type HttpExecutionObservation,
} from "../../src/adapters/http/http-execution-v3.ts";
import {
type InstalledHttpContract,
} from "../../src/contracts/external-contract-runtime.ts";
import type { CacheScopeSnapshot } from "../../src/contracts/server-state-scope.ts";
import { REFERENCE_FEATURE_TEMPLATE_CONTRIBUTION } from "../../src/features/reference-feature/contracts/reference-feature-contract-contribution.ts";
import {
computeHttpScenarioCatalogDigest,
httpScenarioReceiptSchema,
type AttemptStatus,
type BodyDisposition,
type HttpScenarioAssertionGroups,
type RetryReason,
} from "../../scripts/lib/http-scenario-evidence.ts";
import { writeValidatedJsonArtifact } from "../../scripts/lib/validated-json-artifact.ts";
import { createReferenceScenarioHandlers } from "../mocks/handlers/reference-resources.ts";
import { createStrictMockServer } from "../mocks/server.ts";
import {
HTTP_SCENARIO_EXECUTION_IDS,
HTTP_SCENARIO_EXPECTATIONS,
HTTP_SCENARIO_RECEIPT_SCHEMA_VERSION,
OPERATION_SCENARIO_CATALOG,
type HttpScenarioExpectation,
type HttpScenarioOperationId,
} from "../mocks/scenarios/catalog.ts";
/** The reference gateway owns these low-cardinality route identities. */
function routeIdFor(operationId: HttpScenarioOperationId): string {
return operationId === "GET_REFERENCE_RESOURCE"
? "REFERENCE_RESOURCE_DETAIL"
: "REFERENCE_RESOURCE_LIST";
}
const RECEIPT_PATH = path.resolve(
"artifacts/tests/http-scenario-executions.json",
);
const mockApi = createStrictMockServer();
beforeAll(mockApi.listen);
afterAll(mockApi.close);
type AttemptTrace = {
status: AttemptStatus;
media: string | null;
pulledBytes: number;
appliedCeiling: number | null;
completed: boolean;
cancelled: boolean;
};
function operationFor(operationId: HttpScenarioOperationId) {
const operation = REFERENCE_FEATURE_TEMPLATE_CONTRIBUTION.http.find(
(candidate) => candidate.contract.operationId === operationId,
);
if (!operation) throw new Error(`Missing operation fixture: ${operationId}`);
return operation as InstalledHttpContract<unknown, unknown, unknown>;
}
function inputFor(operationId: HttpScenarioOperationId): unknown {
if (operationId === "LIST_REFERENCE_RESOURCES") return { limit: 20 };
if (operationId === "GET_REFERENCE_RESOURCE") {
return { resourceId: "reference-1" };
}
return { name: "Created" };
}
function normalizeMedia(response: Response): string | null {
const value = response.headers.get("content-type");
return value?.split(";", 1)[0]?.trim().toLowerCase() || null;
}
function tracingFetcher(attempts: AttemptTrace[]): typeof fetch {
return async (input, init) => {
const trace: AttemptTrace = {
status: "PENDING_ABORT",
media: null,
pulledBytes: 0,
appliedCeiling: null,
completed: false,
cancelled: false,
};
attempts.push(trace);
let response: Response;
try {
response = await fetch(input, init);
} catch (error) {
trace.status = init?.signal?.aborted
? "PENDING_ABORT"
: "NETWORK_REJECTION";
throw error;
}
trace.status = response.status;
trace.media = normalizeMedia(response);
if (!response.body) return response;
const reader = response.body.getReader();
const proxy = new ReadableStream<Uint8Array>(
{
async pull(controller) {
try {
const chunk = await reader.read();
if (chunk.done) {
trace.completed = true;
controller.close();
return;
}
trace.pulledBytes += chunk.value.byteLength;
controller.enqueue(chunk.value);
} catch (error) {
controller.error(error);
}
},
cancel(reason) {
trace.cancelled = true;
// MSW-transferred oversized bodies leave the original cancel promise
// permanently pending. Record and request cancellation without making
// the proxy response less responsive than the real executor contract.
void reader.cancel(reason).catch(() => {});
},
},
{ highWaterMark: 0 },
);
return new Response(proxy, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
};
}
function outcomeDetail(outcome: Readonly<Record<string, unknown>>): string | null {
if (outcome.kind === "CONTRACT_VIOLATION") {
return String((outcome.violation as Readonly<{ kind: string }>).kind);
}
if (outcome.kind === "TRANSPORT_FAILURE") {
return String((outcome.failure as Readonly<{ kind: string }>).kind);
}
if (outcome.kind === "PROBLEM") {
return String((outcome.metadata as Readonly<{ status: number }>).status);
}
return null;
}
function retryReason(status: AttemptStatus): RetryReason {
if (status === "NETWORK_REJECTION") return "NETWORK_FAILURE";
if (status === 429) return "HTTP_429";
if (status === 503) return "HTTP_503";
throw new Error(`Sleep followed a non-retryable attempt: ${String(status)}`);
}
function bodyDisposition(
trace: AttemptTrace,
outcome: Readonly<Record<string, unknown>>,
): BodyDisposition {
if (trace.status === "NETWORK_REJECTION" || trace.status === "PENDING_ABORT") {
return "NO_RESPONSE";
}
if (
outcome.kind === "CONTRACT_VIOLATION" &&
outcomeDetail(outcome) === "RESPONSE_TOO_LARGE" &&
trace.cancelled
) {
return "REJECTED_LIMIT";
}
if (trace.completed) return "FULLY_READ_WITHIN_BOUND";
if (trace.cancelled) return "CANCELLED_WITHOUT_READ";
throw new Error(`Response body was neither consumed nor cancelled: ${trace.status}`);
}
async function waitForHandler(
ready: Promise<void>,
executionId: string,
): Promise<void> {
let watchdog: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
ready,
new Promise<never>((_resolve, reject) => {
watchdog = setTimeout(
() => reject(new Error(`Handler readiness timed out: ${executionId}`)),
2_000,
);
}),
]);
} finally {
if (watchdog !== undefined) clearTimeout(watchdog);
}
}
async function executeScenario(
entry: HttpScenarioExpectation,
): Promise<HttpScenarioAssertionGroups> {
const physicalAttempts: AttemptTrace[] = [];
const sleeps: RetryReason[] = [];
const observations: HttpExecutionObservation[] = [];
const caller = new AbortController();
const scopeLifetime = new AbortController();
let scopeCurrent = true;
let handlerReady!: () => void;
const ready = new Promise<void>((resolve) => {
handlerReady = resolve;
});
const scope: CacheScopeSnapshot = Object.freeze({
generation: 1,
fingerprint: "scenario-scope-1",
identities: Object.freeze({}) as CacheScopeSnapshot["identities"],
signal: scopeLifetime.signal,
isCurrent: () => scopeCurrent,
});
const baseOperation = operationFor(entry.operationId);
const operation =
entry.testDeadlineOverrideMs === null
? baseOperation
: Object.freeze({
...baseOperation,
frontend: Object.freeze({
...baseOperation.frontend,
totalDeadlineMs: entry.testDeadlineOverrideMs,
}),
});
const fetcher = tracingFetcher(physicalAttempts);
const executor = createContractHttpExecutor({
baseUrl: "https://api.test",
maxRetryAttempts: 2,
// The reference contribution declares REFERENCE_EXTERNAL_BEARER, so the
// credential owner must supply the required proof header before dispatch.
attachCredentials: () => ({
kind: "READY",
headers: { authorization: "Bearer scenario-token" },
}),
fetcher,
random: () => 0,
readBoundedResponseBytes: async (response, maximumBytes) => {
const trace = physicalAttempts.at(-1);
if (!trace) throw new Error("Body reader ran before a physical attempt");
if (trace.appliedCeiling !== null) {
throw new Error("Body reader ran more than once for one attempt");
}
trace.appliedCeiling = maximumBytes;
return readBoundedBytes(response, maximumBytes);
},
sleep: async () => {
const prior = physicalAttempts.at(-1);
if (!prior) throw new Error("Retry sleep occurred before an attempt");
sleeps.push(retryReason(prior.status));
},
observe: (observation) => observations.push(observation),
});
mockApi.server.use(
...createReferenceScenarioHandlers({
scenarios: { [entry.operationId]: entry.scenarioId },
onAttempt(attempt) {
if (
attempt.operationId === entry.operationId &&
attempt.scenarioId === entry.scenarioId
) {
handlerReady();
}
},
}),
);
try {
const execution = executor.execute(operation, inputFor(entry.operationId), {
routeId: routeIdFor(entry.operationId),
scope,
signal: caller.signal,
...(entry.operationId === "CREATE_REFERENCE_RESOURCE"
? {
intent: Object.freeze({
intentId: `intent-${entry.scenarioId}`,
operationId: entry.operationId,
canonicalInputIdentity: "scenario-input",
idempotencyKey: `scenario-${entry.scenarioId}`,
createdAtMonotonicMs: 1,
}),
}
: {}),
});
if (entry.scenarioId === "timeout" || entry.scenarioId === "aborted") {
await waitForHandler(ready, entry.executionId);
if (entry.scenarioId === "aborted") {
if (entry.operationId === "LIST_REFERENCE_RESOURCES") {
caller.abort();
} else {
scopeCurrent = false;
scopeLifetime.abort();
}
}
}
const outcome = (await execution) as unknown as Readonly<Record<string, unknown>>;
expect(observations, `${entry.executionId} observer count`).toHaveLength(1);
const observation = observations[0]!;
const observedSignal = scopeLifetime.signal.aborted ? "ABORTED" : "ACTIVE";
const cancellationOwner =
observation.terminalReason === "TIMEOUT"
? "DEADLINE"
: caller.signal.aborted
? "CALLER"
: scopeLifetime.signal.aborted
? "SCOPE_FENCE"
: "NONE";
const observed: HttpScenarioAssertionGroups = Object.freeze({
status: Object.freeze({
attempts: Object.freeze(physicalAttempts.map((attempt) => attempt.status)),
final: physicalAttempts.at(-1)!.status,
}),
outcome: Object.freeze({
kind: String(outcome.kind),
detail: outcomeDetail(outcome),
}),
effect: Object.freeze({
outcome: String(outcome.effect),
observer: observation.terminalReason,
}),
retry: Object.freeze({ count: sleeps.length, reasons: Object.freeze(sleeps) }),
fetch: Object.freeze({
count: physicalAttempts.length,
observerAttempts: observation.attemptCount,
agrees: physicalAttempts.length === observation.attemptCount,
}),
media: Object.freeze({
attempts: Object.freeze(physicalAttempts.map((attempt) => attempt.media)),
final: physicalAttempts.at(-1)?.media ?? null,
}),
body: Object.freeze({
attempts: Object.freeze(
physicalAttempts.map((attempt) =>
Object.freeze({
disposition: bodyDisposition(attempt, outcome),
pulledBytes: attempt.pulledBytes,
ceiling: attempt.appliedCeiling ?? 0,
}),
),
),
}),
scope: Object.freeze({
start: "CURRENT",
end: scopeCurrent ? "CURRENT" : "STALE",
signal: observedSignal,
cancellationOwner,
}),
});
return observed;
} finally {
caller.abort();
scopeLifetime.abort();
mockApi.reset();
}
}
describe("HTTP scenario catalog execution evidence", () => {
it("declares exactly the 54 behaviorally distinct operation scenarios", () => {
const executionIds = Object.entries(OPERATION_SCENARIO_CATALOG).flatMap(
([operationId, scenarioIds]) =>
scenarioIds.map((scenarioId) => `${operationId}::${scenarioId}`),
);
expect(OPERATION_SCENARIO_CATALOG.LIST_REFERENCE_RESOURCES).toHaveLength(19);
expect(OPERATION_SCENARIO_CATALOG.GET_REFERENCE_RESOURCE).toHaveLength(18);
expect(OPERATION_SCENARIO_CATALOG.CREATE_REFERENCE_RESOURCE).toHaveLength(17);
expect(executionIds).toHaveLength(54);
expect(new Set(executionIds)).toHaveLength(54);
expect(executionIds).toEqual(HTTP_SCENARIO_EXECUTION_IDS);
});
it("executes every declared scenario and publishes one complete receipt", async () => {
await rm(RECEIPT_PATH, { force: true });
const rows: Array<Readonly<{
executionId: string;
expected: HttpScenarioAssertionGroups;
observed: HttpScenarioAssertionGroups;
testDeadlineOverrideMs: number | null;
}>> = [];
for (const entry of HTTP_SCENARIO_EXPECTATIONS) {
const observed = await executeScenario(entry);
expect(observed, entry.executionId).toEqual(entry.expected);
rows.push(
Object.freeze({
executionId: entry.executionId,
expected: entry.expected,
observed,
testDeadlineOverrideMs: entry.testDeadlineOverrideMs,
}),
);
}
const sortedRows = [...rows].sort((left, right) =>
left.executionId.localeCompare(right.executionId),
);
await mkdir(path.dirname(RECEIPT_PATH), { recursive: true });
await writeValidatedJsonArtifact({
path: RECEIPT_PATH,
schema: httpScenarioReceiptSchema,
value: {
schemaVersion: HTTP_SCENARIO_RECEIPT_SCHEMA_VERSION,
catalogDigest: computeHttpScenarioCatalogDigest(
HTTP_SCENARIO_RECEIPT_SCHEMA_VERSION,
HTTP_SCENARIO_EXPECTATIONS,
),
catalogTotal: 54,
executedIds: sortedRows.map((row) => row.executionId),
rows: sortedRows,
},
});
}, 30_000);
});