Wires the whole Asset capability into the running application: the multipart upload transport (the contract runtime can only express JSON bodies), a single composition-root-owned CSRF provider shared between the platform's credential collaborator (18 JSON operations) and the upload transport (1 multipart operation), and Studio/StudioShell exposure of the Asset gateway alongside the existing document gateway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
127 lines
4.6 KiB
TypeScript
127 lines
4.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "vitest";
|
|
|
|
import type { ApplicationFeatureInputs } from "../../../src/application/ports/in/application-api.ts";
|
|
import { createInstalledFeatureInputs } from "../../../src/features/installed-feature-adapters.ts";
|
|
import { FIXTURE_IDS } from "../../../src/features/tech-log/adapters/mock/fixtures.ts";
|
|
import { createCsrfTokenProvider } from "../../../src/features/tech-log/adapters/http/studio-session-csrf.ts";
|
|
import type { TechLogFeatureInput } from "../../../src/features/tech-log/application/tech-log-feature-input.ts";
|
|
import type { WorkingCopy } from "../../../src/features/tech-log/contracts/studio/contract.ts";
|
|
|
|
type Equal<Left, Right> =
|
|
(<Value>() => Value extends Left ? 1 : 2) extends <Value>() =>
|
|
Value extends Right ? 1 : 2
|
|
? true
|
|
: false;
|
|
type Expect<Value extends true> = Value;
|
|
|
|
type InstalledInputs = ReturnType<typeof createInstalledFeatureInputs>;
|
|
type _InstalledIdsAreExact = Expect<
|
|
Equal<keyof InstalledInputs, "reference-feature" | "tech-log">
|
|
>;
|
|
type _TechLogRegistryValueIsExact = Expect<
|
|
Equal<ApplicationFeatureInputs["tech-log"], TechLogFeatureInput>
|
|
>;
|
|
void (0 as unknown as _InstalledIdsAreExact);
|
|
void (0 as unknown as _TechLogRegistryValueIsExact);
|
|
|
|
function inputOf(document: WorkingCopy) {
|
|
const { id, version, updatedAt, ...input } = document;
|
|
void id;
|
|
void version;
|
|
void updatedAt;
|
|
return input;
|
|
}
|
|
|
|
function installedInputs(
|
|
studioSource: "MOCK" | "HTTP" = "MOCK",
|
|
): InstalledInputs {
|
|
return createInstalledFeatureInputs({
|
|
studioSource,
|
|
contractOperations: {
|
|
async execute() {
|
|
throw new Error("reference executor is not used by composition tests");
|
|
},
|
|
},
|
|
apiBaseUrl: "http://composition.test/",
|
|
requestTimeoutMs: 10_000,
|
|
csrf: createCsrfTokenProvider({
|
|
async execute() {
|
|
throw new Error("CSRF provider is not used by composition tests");
|
|
},
|
|
}),
|
|
});
|
|
}
|
|
|
|
test("installs TechLog beside the retained reference feature through application-facing inputs", () => {
|
|
const installed = installedInputs();
|
|
|
|
assert.deepEqual(Object.keys(installed), ["reference-feature", "tech-log"]);
|
|
assert.deepEqual(Object.keys(installed["tech-log"]).sort(), [
|
|
"createStudioAssetGateway",
|
|
"createStudioGateway",
|
|
"publicContent",
|
|
]);
|
|
assert.equal(Object.isFrozen(installed), true);
|
|
assert.equal(Object.isFrozen(installed["tech-log"]), true);
|
|
assert.equal(Object.isFrozen(installed["tech-log"].publicContent), true);
|
|
assert.equal(
|
|
installed["tech-log"].publicContent.getRelease("0.1.0")?.title,
|
|
"TechLog Public·Studio 경계를 확정했습니다",
|
|
);
|
|
});
|
|
|
|
test("selects the mock gateway by default and the HTTP gateway when switched", async () => {
|
|
const mockGateway = installedInputs("MOCK")["tech-log"].createStudioGateway();
|
|
const httpGateway = installedInputs("HTTP")["tech-log"].createStudioGateway();
|
|
|
|
// The mock reads an in-memory fixture immediately. HTTP defers to the
|
|
// (here, throwing) contract executor, so it rejects instead.
|
|
await assert.doesNotReject(mockGateway.getDashboard());
|
|
await assert.rejects(httpGateway.getDashboard());
|
|
});
|
|
|
|
test("each createStudioGateway call owns an isolated mutable Studio session", async () => {
|
|
const installed = installedInputs();
|
|
const first = installed["tech-log"].createStudioGateway();
|
|
const second = installed["tech-log"].createStudioGateway();
|
|
|
|
assert.notEqual(first, second);
|
|
const firstBefore = await first.getDocument(FIXTURE_IDS.fetchJoinCase);
|
|
const secondBefore = await second.getDocument(FIXTURE_IDS.fetchJoinCase);
|
|
assert.equal(firstBefore.document.title, secondBefore.document.title);
|
|
|
|
const saved = await first.saveDocument(
|
|
firstBefore.document.id,
|
|
{
|
|
expectedVersion: firstBefore.document.version,
|
|
document: {
|
|
...inputOf(firstBefore.document),
|
|
title: "첫 번째 세션에서만 수정",
|
|
},
|
|
},
|
|
{ idempotencyKey: "session-isolation" },
|
|
);
|
|
|
|
assert.equal(saved.document.title, "첫 번째 세션에서만 수정");
|
|
assert.equal(
|
|
(await second.getDocument(FIXTURE_IDS.fetchJoinCase)).document.title,
|
|
secondBefore.document.title,
|
|
);
|
|
assert.equal(
|
|
installed["tech-log"].publicContent.getRecord(
|
|
"CASE",
|
|
"collection-fetch-join-pagination",
|
|
)?.title,
|
|
"컬렉션 Fetch Join과 페이징은 왜 충돌하는가",
|
|
);
|
|
});
|
|
|
|
test("each Studio session gets its own asset gateway instance", () => {
|
|
const installed = installedInputs("HTTP");
|
|
assert.notEqual(
|
|
installed["tech-log"].createStudioAssetGateway(),
|
|
installed["tech-log"].createStudioAssetGateway(),
|
|
);
|
|
});
|