Files
clean-architecture-frontend…/tests/contract/reusable-capability/feature-http-binding.test.ts
T

118 lines
3.6 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
createFeatureHttpBinding,
defineFeatureHttpOperation,
type InstalledHttpOperationExecutor,
} from "../../../src/adapters/http/index.ts";
import {
mappingFailure,
mappingSuccess,
} from "../../../src/contracts/boundary-mapper.ts";
type Resource = Readonly<{ id: string; title: string }>;
const OPERATIONS = Object.freeze({
LOAD_RESOURCE: defineFeatureHttpOperation<
Readonly<{ resourceId: string }>,
Resource
>({
operationId: "LOAD_RESOURCE",
routeId: "RESOURCE_DETAIL",
mapSuccess(value) {
if (
!value ||
typeof value !== "object" ||
typeof (value as Record<string, unknown>).id !== "string" ||
typeof (value as Record<string, unknown>).title !== "string"
) {
return mappingFailure("MAPPING_INVARIANT_REJECTED");
}
const candidate = value as Readonly<{ id: string; title: string }>;
return mappingSuccess(
Object.freeze({ id: candidate.id, title: candidate.title }),
);
},
}),
} as const);
describe("feature HTTP binding", () => {
it("keeps typed feature input while platform owns route/context execution", async () => {
const execute = vi.fn<InstalledHttpOperationExecutor["execute"]>(
async (_operationId, input, context) => {
expect(input).toEqual({ resourceId: "resource-1" });
expect(context.routeId).toBe("RESOURCE_DETAIL");
return 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(
Object.freeze({ execute }),
OPERATIONS,
);
const result = await binding.execute("LOAD_RESOURCE", {
resourceId: "resource-1",
});
expect(result).toEqual({
ok: true,
value: { id: "resource-1", title: "Reference" },
});
expect(execute).toHaveBeenCalledTimes(1);
});
it("normalizes transport failure before it crosses the feature gateway", async () => {
const executor: InstalledHttpOperationExecutor = Object.freeze({
async execute() {
return Object.freeze({
kind: "TRANSPORT_FAILURE" as const,
failure: Object.freeze({
kind: "TIMEOUT" as const,
retryable: true,
}),
effect: "NOT_STARTED" as const,
});
},
});
const binding = createFeatureHttpBinding(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 feature mapper rejection into the shared mapping failure", async () => {
const executor: InstalledHttpOperationExecutor = Object.freeze({
async execute() {
return Object.freeze({
kind: "SUCCESS" as const,
value: Object.freeze({ unexpected: true }),
metadata: Object.freeze({ status: 200 }),
effect: "NOT_APPLICABLE" as const,
});
},
});
const binding = createFeatureHttpBinding(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");
});
});