refactor: 프론트 템플릿 리펙토링
This commit is contained in:
@@ -1,61 +1,68 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createHttpClient } from "../../../src/adapters/http/client.ts";
|
||||
import { createDemoSessionAdapter } from "../../../src/adapters/auth/external-session-adapter.ts";
|
||||
import {
|
||||
createReferenceHttpGateway,
|
||||
} from "../../../src/features/reference-feature/adapters/reference-http-gateway.ts";
|
||||
import { createReferenceFeatureInput } from "../../../src/features/reference-feature/application/reference-feature-api.ts";
|
||||
import { REFERENCE_FEATURE_CONTRACT } from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
|
||||
import { mapReferenceOperation } from "../../../src/features/reference-feature/contracts/reference-mapper.ts";
|
||||
import {
|
||||
validateReferencePayload,
|
||||
validateReferenceRequest,
|
||||
} from "../../../src/features/reference-feature/contracts/reference-schemas.ts";
|
||||
import { createContractHttpExecutor } from "../../../src/adapters/http/index.ts";
|
||||
import { createHttpObservationProjector } from "../../../src/bootstrap/runtime-adapters.ts";
|
||||
import { createReferenceFeatureInstalledInput } from "../../../src/features/reference-feature/adapters/create-reference-feature-input.ts";
|
||||
import { REFERENCE_FEATURE_TEMPLATE_CONTRIBUTION } from "../../../src/features/reference-feature/contracts/reference-feature-contract-contribution.ts";
|
||||
|
||||
describe("reference feature diagnostics correlation", () => {
|
||||
it("preserves route, operation and request correlation through the vertical path", async () => {
|
||||
function scopeSnapshot() {
|
||||
return Object.freeze({
|
||||
generation: 1,
|
||||
fingerprint: "reference-scope",
|
||||
identities: Object.freeze({}) as never,
|
||||
signal: new AbortController().signal,
|
||||
isCurrent: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
describe("reference feature HTTP diagnostics", () => {
|
||||
it("preserves route and operation identity through the installed V3 path", async () => {
|
||||
const record = vi.fn();
|
||||
const operations =
|
||||
REFERENCE_FEATURE_CONTRACT.apiOperations as Readonly<
|
||||
Record<
|
||||
string,
|
||||
ReturnType<
|
||||
NonNullable<Parameters<typeof createHttpClient>[0]["getOperation"]>
|
||||
>
|
||||
>
|
||||
>;
|
||||
const client = createHttpClient({
|
||||
const contractHttp = createContractHttpExecutor({
|
||||
baseUrl: "https://api.test",
|
||||
authSession: createDemoSessionAdapter("authenticated"),
|
||||
fetcher: async () =>
|
||||
Response.json({
|
||||
success: true,
|
||||
data: [{ id: "reference-1", name: "Reference" }],
|
||||
meta: { requestId: "safe-request", traceId: "safe-trace" },
|
||||
}),
|
||||
getOperation(operationId) {
|
||||
const operation = operations[operationId];
|
||||
if (!operation) throw new Error("Unregistered reference operation");
|
||||
return operation;
|
||||
},
|
||||
validatePayload: validateReferencePayload,
|
||||
validateRequest: validateReferenceRequest,
|
||||
mapPayload: mapReferenceOperation,
|
||||
correlationIdFactory: () => "reference-correlation",
|
||||
diagnostics: { record },
|
||||
scheduler: {
|
||||
setTimeout: () => 1,
|
||||
clearTimeout: () => {},
|
||||
},
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY" as const,
|
||||
headers: { authorization: "Bearer diagnostics-test-token" },
|
||||
}),
|
||||
fetcher: (async () =>
|
||||
Response.json([
|
||||
{ id: "reference-1", name: "Reference" },
|
||||
])) as unknown as typeof fetch,
|
||||
observe: createHttpObservationProjector({
|
||||
diagnostics: { record },
|
||||
telemetry: { emit: vi.fn() },
|
||||
}),
|
||||
});
|
||||
const application = createReferenceFeatureInput(
|
||||
createReferenceHttpGateway(client),
|
||||
const operations = new Map(
|
||||
REFERENCE_FEATURE_TEMPLATE_CONTRIBUTION.http.map((operation) => [
|
||||
operation.contract.operationId,
|
||||
operation,
|
||||
]),
|
||||
);
|
||||
const installed = createReferenceFeatureInstalledInput({
|
||||
contractOperations: Object.freeze({
|
||||
async execute(operationId, input, context) {
|
||||
const operation = operations.get(operationId);
|
||||
if (!operation) throw new Error("Unregistered reference operation");
|
||||
return contractHttp.execute(operation, input, {
|
||||
routeId: context.routeId,
|
||||
scope: scopeSnapshot(),
|
||||
...(context.signal === undefined
|
||||
? {}
|
||||
: { signal: context.signal }),
|
||||
...(context.intent === undefined
|
||||
? {}
|
||||
: { intent: context.intent }),
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(
|
||||
application.listResources({ limit: 20 }),
|
||||
installed.input.listResources({ limit: 20 }),
|
||||
).resolves.toMatchObject({ ok: true });
|
||||
|
||||
expect(record).toHaveBeenCalledOnce();
|
||||
expect(record).toHaveBeenCalledWith({
|
||||
level: "info",
|
||||
@@ -63,8 +70,7 @@ describe("reference feature diagnostics correlation", () => {
|
||||
context: expect.objectContaining({
|
||||
route_id: "REFERENCE_RESOURCE_LIST",
|
||||
operation_id: "LIST_REFERENCE_RESOURCES",
|
||||
correlation_id: "reference-correlation",
|
||||
outcome: "success",
|
||||
outcome: "SUCCESS",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createFailure } from "../../../src/contracts/errors.ts";
|
||||
import type { Result } from "../../../src/contracts/result.ts";
|
||||
import { createFailure, type ApiFailure } from "../../../src/contracts/errors.ts";
|
||||
import {
|
||||
createReferenceHttpGateway,
|
||||
type RawReferenceHttpExecutor,
|
||||
type ReferenceHttpBinding,
|
||||
} from "../../../src/features/reference-feature/adapters/reference-http-gateway.ts";
|
||||
import type { ReferenceResource } from "../../../src/features/reference-feature/domain/reference-resource.ts";
|
||||
|
||||
@@ -20,14 +21,42 @@ const resources = Object.freeze({
|
||||
}),
|
||||
}) satisfies Readonly<Record<string, ReferenceResource>>;
|
||||
|
||||
type ScriptedResult = Result<
|
||||
ReferenceResource | readonly ReferenceResource[],
|
||||
ApiFailure
|
||||
>;
|
||||
|
||||
function scriptedBinding(results: ScriptedResult[]) {
|
||||
const calls: Array<
|
||||
readonly [operationId: string, input: unknown, context: unknown]
|
||||
> = [];
|
||||
let index = 0;
|
||||
const execute = (async (
|
||||
operationId: string,
|
||||
input: unknown,
|
||||
context?: unknown,
|
||||
) => {
|
||||
calls.push([operationId, input, context]);
|
||||
const result = results[index];
|
||||
index += 1;
|
||||
if (!result) throw new Error("Missing scripted result");
|
||||
return result;
|
||||
}) as ReferenceHttpBinding["execute"];
|
||||
|
||||
return {
|
||||
binding: Object.freeze({ execute }) satisfies ReferenceHttpBinding,
|
||||
calls,
|
||||
};
|
||||
}
|
||||
|
||||
describe("reference HTTP operation gateway", () => {
|
||||
it("builds the exact registered request for every gateway operation", async () => {
|
||||
const execute = vi
|
||||
.fn<RawReferenceHttpExecutor["execute"]>()
|
||||
.mockResolvedValueOnce({ ok: true, value: [resources.first] })
|
||||
.mockResolvedValueOnce({ ok: true, value: resources.created })
|
||||
.mockResolvedValueOnce({ ok: true, value: resources.first });
|
||||
const gateway = createReferenceHttpGateway({ execute });
|
||||
it("delegates exact typed feature inputs to the capability binding", async () => {
|
||||
const scripted = scriptedBinding([
|
||||
{ ok: true, value: [resources.first] },
|
||||
{ ok: true, value: resources.created },
|
||||
{ ok: true, value: resources.first },
|
||||
]);
|
||||
const gateway = createReferenceHttpGateway(scripted.binding);
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
await expect(
|
||||
@@ -40,67 +69,37 @@ describe("reference HTTP operation gateway", () => {
|
||||
gateway.get("reference-1", { signal }),
|
||||
).resolves.toEqual({ ok: true, value: resources.first });
|
||||
|
||||
expect(execute.mock.calls).toEqual([
|
||||
expect(scripted.calls).toEqual([
|
||||
[
|
||||
{
|
||||
operationId: "LIST_REFERENCE_RESOURCES",
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
searchParams: {
|
||||
cursor: "next",
|
||||
limit: 20,
|
||||
tags: ["active"],
|
||||
},
|
||||
signal,
|
||||
},
|
||||
"LIST_REFERENCE_RESOURCES",
|
||||
{ cursor: "next", limit: 20, tags: ["active"] },
|
||||
{ signal },
|
||||
],
|
||||
[
|
||||
{
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
body: { name: "Created", note: "safe note" },
|
||||
},
|
||||
"CREATE_REFERENCE_RESOURCE",
|
||||
{ name: "Created", note: "safe note" },
|
||||
undefined,
|
||||
],
|
||||
[
|
||||
{
|
||||
operationId: "GET_REFERENCE_RESOURCE",
|
||||
routeId: "REFERENCE_RESOURCE_DETAIL",
|
||||
pathParams: { resourceId: "reference-1" },
|
||||
signal,
|
||||
},
|
||||
"GET_REFERENCE_RESOURCE",
|
||||
{ resourceId: "reference-1" },
|
||||
{ signal },
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves a validated raw failure without casting it into success", async () => {
|
||||
it("preserves a capability-normalized failure across the feature gateway", async () => {
|
||||
const failure = createFailure(
|
||||
"SCHEMA_MISMATCH",
|
||||
"GET_REFERENCE_RESOURCE",
|
||||
0,
|
||||
);
|
||||
const execute = vi
|
||||
.fn<RawReferenceHttpExecutor["execute"]>()
|
||||
.mockResolvedValue({ ok: false, error: failure });
|
||||
const gateway = createReferenceHttpGateway({ execute });
|
||||
const scripted = scriptedBinding([{ ok: false, error: failure }]);
|
||||
const gateway = createReferenceHttpGateway(scripted.binding);
|
||||
|
||||
await expect(gateway.get("invalid")).resolves.toEqual({
|
||||
ok: false,
|
||||
error: failure,
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed when a raw success does not match its operation result", async () => {
|
||||
const execute = vi
|
||||
.fn<RawReferenceHttpExecutor["execute"]>()
|
||||
.mockResolvedValue({ ok: true, value: { id: "not-a-list" } });
|
||||
const gateway = createReferenceHttpGateway({ execute });
|
||||
|
||||
await expect(gateway.list({ limit: 20 })).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
kind: "MAPPING_CONTRACT_VIOLATION",
|
||||
code: "BOUND_RESULT_TYPE_MISMATCH",
|
||||
operationId: "LIST_REFERENCE_RESOURCES",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user