feat: add the TechLog Studio asset gateway port and JSON adapter
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
|
||||
import { createHttpStudioAssetGateway } from "../../../src/features/tech-log/adapters/http/http-studio-asset-gateway.ts";
|
||||
import { createCsrfTokenProvider } from "../../../src/features/tech-log/adapters/http/studio-session-csrf.ts";
|
||||
import { isStudioGatewayError } from "../../../src/features/tech-log/application/ports/studio-gateway-error.ts";
|
||||
|
||||
const READY_ASSET = {
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
assetKey: "fetch-strategy-boundary",
|
||||
kind: "DIAGRAM",
|
||||
mediaType: "image/svg+xml",
|
||||
originalFilename: "boundary.svg",
|
||||
byteSize: 4096,
|
||||
width: 1080,
|
||||
height: 420,
|
||||
altText: "Fetch Join과 Batch Fetch 비교",
|
||||
decorative: false,
|
||||
managementStatus: "READY",
|
||||
publicPath: "/media/fetch-strategy-boundary.svg",
|
||||
usageCount: 1,
|
||||
version: 1,
|
||||
createdAt: "2026-08-14T01:00:00.000Z",
|
||||
updatedAt: "2026-08-14T01:00:00.000Z",
|
||||
};
|
||||
|
||||
function deps(outcomes: Record<string, unknown>) {
|
||||
const calls: { operationId: string; input: unknown }[] = [];
|
||||
return {
|
||||
calls,
|
||||
dependencies: {
|
||||
operations: {
|
||||
async execute(operationId: string, input: unknown) {
|
||||
calls.push({ operationId, input });
|
||||
const outcome = outcomes[operationId];
|
||||
if (!outcome) throw new Error(`no outcome for ${operationId}`);
|
||||
return outcome as never;
|
||||
},
|
||||
},
|
||||
csrf: createCsrfTokenProvider({
|
||||
async execute() {
|
||||
return { csrfToken: "csrf", csrfHeaderName: "X-CSRF-TOKEN" };
|
||||
},
|
||||
}),
|
||||
upload: {
|
||||
async upload() {
|
||||
return READY_ASSET as never;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("lists assets through the canonical operation", async () => {
|
||||
const { calls, dependencies } = deps({
|
||||
listStudioAssets: {
|
||||
kind: "SUCCESS",
|
||||
value: { items: [READY_ASSET], nextCursor: null },
|
||||
effect: "APPLIED_CONFIRMED",
|
||||
},
|
||||
});
|
||||
const gateway = createHttpStudioAssetGateway(dependencies as never);
|
||||
|
||||
const page = await gateway.listAssets({ kind: "DIAGRAM", limit: 20 });
|
||||
|
||||
assert.equal(page.items.length, 1);
|
||||
assert.equal(calls[0]!.operationId, "listStudioAssets");
|
||||
});
|
||||
|
||||
test("delegates upload to the transport with CSRF and idempotency headers", async () => {
|
||||
let received: Record<string, string> = {};
|
||||
const { dependencies } = deps({});
|
||||
const gateway = createHttpStudioAssetGateway({
|
||||
...dependencies,
|
||||
upload: {
|
||||
async upload(_form: unknown, headers: Record<string, string>) {
|
||||
received = headers;
|
||||
return READY_ASSET as never;
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
const asset = await gateway.uploadAsset(
|
||||
{ file: new File(["<svg/>"], "boundary.svg", { type: "image/svg+xml" }), kind: "DIAGRAM" },
|
||||
{ idempotencyKey: "upload-1" },
|
||||
);
|
||||
|
||||
assert.equal(asset.managementStatus, "READY");
|
||||
assert.equal(received["X-CSRF-TOKEN"], "csrf");
|
||||
assert.equal(received["Idempotency-Key"], "upload-1");
|
||||
});
|
||||
|
||||
test("surfaces ASSET_IN_USE from a rejected delete", async () => {
|
||||
const { dependencies } = deps({
|
||||
deleteStudioAsset: {
|
||||
kind: "PROBLEM",
|
||||
problem: {
|
||||
type: "https://techlog.local/problems/asset-in-use",
|
||||
title: "ASSET_IN_USE",
|
||||
status: 409,
|
||||
detail: "사용 중인 Asset은 삭제할 수 없습니다.",
|
||||
code: "ASSET_IN_USE",
|
||||
},
|
||||
metadata: { status: 409 },
|
||||
effect: "NOT_APPLIED",
|
||||
},
|
||||
});
|
||||
const gateway = createHttpStudioAssetGateway(dependencies as never);
|
||||
|
||||
await assert.rejects(
|
||||
gateway.deleteAsset(READY_ASSET.id, { idempotencyKey: "delete-1" }),
|
||||
(error: unknown) => {
|
||||
assert.ok(isStudioGatewayError(error));
|
||||
assert.equal(error.code, "ASSET_IN_USE");
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("sends expectedVersion when updating metadata", async () => {
|
||||
const { calls, dependencies } = deps({
|
||||
updateStudioAsset: {
|
||||
kind: "SUCCESS",
|
||||
value: { ...READY_ASSET, version: 2, decorative: true, altText: null },
|
||||
effect: "APPLIED_CONFIRMED",
|
||||
},
|
||||
});
|
||||
const gateway = createHttpStudioAssetGateway(dependencies as never);
|
||||
|
||||
const updated = await gateway.updateAssetMetadata(
|
||||
READY_ASSET.id,
|
||||
{ expectedVersion: 1, decorative: true, altText: null },
|
||||
{ idempotencyKey: "update-1" },
|
||||
);
|
||||
|
||||
assert.equal(updated.version, 2);
|
||||
const input = calls[0]!.input as Record<string, unknown>;
|
||||
assert.equal(input["expectedVersion"], 1);
|
||||
assert.equal(input["assetId"], READY_ASSET.id);
|
||||
});
|
||||
Reference in New Issue
Block a user