217 lines
6.5 KiB
TypeScript
217 lines
6.5 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
createFeatureHttpBinding,
|
|
defineFeatureHttpOperation,
|
|
type HttpExecutionOutcome,
|
|
type InstalledHttpOperationExecutor,
|
|
} from "../../../src/adapters/http/index.ts";
|
|
import {
|
|
mappingFailure,
|
|
mappingSuccess,
|
|
} from "../../../src/contracts/boundary-mapper.ts";
|
|
import type {
|
|
InstalledHttpContract,
|
|
RuntimeValidator,
|
|
} from "../../../src/contracts/external-contract-runtime.ts";
|
|
|
|
type ResourceInput = Readonly<{ resourceId: string }>;
|
|
type ResourceWire = Readonly<{ id: string; title: string }>;
|
|
type ResourceProblem = Readonly<{ code?: string }>;
|
|
type Resource = Readonly<{ id: string; title: string }>;
|
|
|
|
function validator<T>(schemaId: string): RuntimeValidator<T> {
|
|
return Object.freeze({
|
|
schemaId,
|
|
safeParse(value: unknown) {
|
|
return Object.freeze({
|
|
success: true as const,
|
|
data: value as T,
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
const LOAD_RESOURCE_CONTRACT = Object.freeze({
|
|
contract: Object.freeze({
|
|
operationId: "LOAD_RESOURCE",
|
|
method: "GET" as const,
|
|
pathTemplate: "/resources/{resourceId}",
|
|
inputValidator: validator<ResourceInput>("ResourceInput"),
|
|
outputValidator: validator<ResourceWire>("ResourceWire"),
|
|
problemValidator: validator<ResourceProblem>("ResourceProblem"),
|
|
acceptedStatuses: Object.freeze([200]),
|
|
emptyBodyStatuses: Object.freeze([]),
|
|
retrySemantics: "SAFE" as const,
|
|
requestBody: "NONE" as const,
|
|
responseBody: "REQUIRED_JSON" as const,
|
|
commandRecovery: null,
|
|
commandEffect: null,
|
|
projectRequest(input: ResourceInput) {
|
|
return Object.freeze({
|
|
pathValues: Object.freeze({ resourceId: input.resourceId }),
|
|
queryEntries: Object.freeze([]),
|
|
body: null,
|
|
});
|
|
},
|
|
}),
|
|
frontend: Object.freeze({
|
|
policyId: "TEST_LOAD_RESOURCE",
|
|
requestByteLimit: 0,
|
|
responseByteLimit: 8_192,
|
|
totalDeadlineMs: 1_000,
|
|
retryBudget: 0 as const,
|
|
authProfileId: "TEST",
|
|
diagnosticsOperation: "test.load-resource",
|
|
}),
|
|
}) satisfies InstalledHttpContract<ResourceInput, ResourceWire, ResourceProblem>;
|
|
|
|
const OPERATIONS = Object.freeze({
|
|
LOAD_RESOURCE: defineFeatureHttpOperation({
|
|
contract: LOAD_RESOURCE_CONTRACT,
|
|
routeId: "RESOURCE_DETAIL",
|
|
mapSuccess(value) {
|
|
if (value.title.length === 0) {
|
|
return mappingFailure("MAPPING_INVARIANT_REJECTED");
|
|
}
|
|
return mappingSuccess(
|
|
Object.freeze({ id: value.id, title: value.title }),
|
|
);
|
|
},
|
|
mapProblem(problem, metadata) {
|
|
if (metadata.status !== 409) return undefined;
|
|
return Object.freeze({
|
|
kind: "CONFLICT" as const,
|
|
code: problem.code ?? "RESOURCE_CONFLICT",
|
|
});
|
|
},
|
|
}),
|
|
} as const);
|
|
|
|
function scriptedExecutor<WireOutput, Problem>(
|
|
outcome: HttpExecutionOutcome<WireOutput, Problem>,
|
|
) {
|
|
const calls: Array<
|
|
Readonly<{
|
|
contract: unknown;
|
|
input: unknown;
|
|
context: unknown;
|
|
}>
|
|
> = [];
|
|
const implementation = async (
|
|
contract: unknown,
|
|
input: unknown,
|
|
context: unknown,
|
|
) => {
|
|
calls.push(Object.freeze({ contract, input, context }));
|
|
return outcome;
|
|
};
|
|
const execute = vi.fn(implementation) as unknown as
|
|
InstalledHttpOperationExecutor["execute"];
|
|
return {
|
|
executor: Object.freeze({ execute }) satisfies InstalledHttpOperationExecutor,
|
|
calls,
|
|
execute,
|
|
};
|
|
}
|
|
|
|
describe("feature HTTP binding", () => {
|
|
it("keeps contract-derived input while platform owns route/context execution", async () => {
|
|
const scripted = scriptedExecutor<ResourceWire, ResourceProblem>(
|
|
Object.freeze({
|
|
kind: "SUCCESS" as const,
|
|
value: Object.freeze({ id: "resource-1", title: "Reference" }),
|
|
metadata: Object.freeze({ status: 200 }),
|
|
effect: "NOT_APPLICABLE" as const,
|
|
}),
|
|
);
|
|
const binding = createFeatureHttpBinding(scripted.executor, OPERATIONS);
|
|
|
|
const result = await binding.execute("LOAD_RESOURCE", {
|
|
resourceId: "resource-1",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
value: { id: "resource-1", title: "Reference" },
|
|
});
|
|
expect(scripted.calls).toEqual([
|
|
{
|
|
contract: LOAD_RESOURCE_CONTRACT,
|
|
input: { resourceId: "resource-1" },
|
|
context: { routeId: "RESOURCE_DETAIL" },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("normalizes transport failure before it crosses the feature gateway", async () => {
|
|
const scripted = scriptedExecutor<ResourceWire, ResourceProblem>(
|
|
Object.freeze({
|
|
kind: "TRANSPORT_FAILURE" as const,
|
|
failure: Object.freeze({
|
|
kind: "TIMEOUT" as const,
|
|
retryable: true,
|
|
}),
|
|
effect: "NOT_STARTED" as const,
|
|
}),
|
|
);
|
|
const binding = createFeatureHttpBinding(scripted.executor, OPERATIONS);
|
|
|
|
const result = await binding.execute("LOAD_RESOURCE", {
|
|
resourceId: "resource-1",
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error.kind).toBe("REQUEST_TIMEOUT");
|
|
expect(result.error.operationId).toBe("LOAD_RESOURCE");
|
|
expect(result.error.effect).toBe("NOT_STARTED");
|
|
});
|
|
|
|
it("turns a domain mapper rejection into the shared mapping failure", async () => {
|
|
const scripted = scriptedExecutor<ResourceWire, ResourceProblem>(
|
|
Object.freeze({
|
|
kind: "SUCCESS" as const,
|
|
value: Object.freeze({ id: "resource-1", title: "" }),
|
|
metadata: Object.freeze({ status: 200 }),
|
|
effect: "NOT_APPLICABLE" as const,
|
|
}),
|
|
);
|
|
const binding = createFeatureHttpBinding(scripted.executor, OPERATIONS);
|
|
|
|
const result = await binding.execute("LOAD_RESOURCE", {
|
|
resourceId: "resource-1",
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error.kind).toBe("MAPPING_CONTRACT_VIOLATION");
|
|
expect(result.error.code).toBe("MAPPING_INVARIANT_REJECTED");
|
|
});
|
|
|
|
it("lets the feature interpret a typed business problem", async () => {
|
|
const scripted = scriptedExecutor<ResourceWire, ResourceProblem>(
|
|
Object.freeze({
|
|
kind: "PROBLEM" as const,
|
|
problem: Object.freeze({ code: "RESOURCE_NAME_EXISTS" }),
|
|
metadata: Object.freeze({ status: 409 }),
|
|
effect: "NOT_APPLIED" as const,
|
|
}),
|
|
);
|
|
const binding = createFeatureHttpBinding(scripted.executor, OPERATIONS);
|
|
|
|
const result = await binding.execute("LOAD_RESOURCE", {
|
|
resourceId: "resource-1",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
kind: "CONFLICT",
|
|
code: "RESOURCE_NAME_EXISTS",
|
|
httpStatus: 409,
|
|
},
|
|
});
|
|
});
|
|
});
|