feat: implement the TechLog Studio HTTP gateway
Adds createHttpStudioGateway, the adapter the Studio UI calls in production. It drives every mutation through mutationIntent() (defineMutationIntent/defineIdempotencyKey, not createBrowserMutationIntentFactory, so the caller-supplied idempotency key is preserved rather than regenerated) and leaves header construction to the executor/credential collaborator entirely - the gateway never sees or sets Idempotency-Key or x-csrf-token itself. Also moves stable-stringify.ts out of adapters/mock/ so the mock and http adapters share one pure function without production code depending on the mock directory, and fixes the resulting import in cursor.ts, mock-studio-gateway.ts, and mock-studio-gateway.test.ts. Adds MSW handlers (tests/mocks/handlers/tech-log-studio.ts) that wrap the reference mock-studio-gateway implementation, and a contract test suite that drives the gateway through a thin fetch-based executor built from the contract's own projectRequest, proving canonical path/body/header construction without assembling the full platform transport. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9313018ef5
commit
1d01a522de
@@ -0,0 +1,147 @@
|
||||
import { http, HttpResponse } from "msw";
|
||||
|
||||
import { createMockStudioGateway } from "../../../src/features/tech-log/adapters/mock/mock-studio-gateway.ts";
|
||||
import { isStudioGatewayError } from "../../../src/features/tech-log/application/ports/studio-gateway-error.ts";
|
||||
|
||||
const BASE = "http://api.test";
|
||||
|
||||
function problemResponse(error: unknown) {
|
||||
if (isStudioGatewayError(error)) {
|
||||
return HttpResponse.json(error.problem, {
|
||||
status: error.status,
|
||||
headers: { "content-type": "application/problem+json" },
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function json(work: () => Promise<unknown>, status = 200) {
|
||||
try {
|
||||
return HttpResponse.json((await work()) as never, { status });
|
||||
} catch (error) {
|
||||
return problemResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export function createTechLogStudioHandlers(
|
||||
gateway = createMockStudioGateway(),
|
||||
baseUrl = BASE,
|
||||
) {
|
||||
const key = (request: Request) =>
|
||||
request.headers.get("Idempotency-Key") ?? "missing-key";
|
||||
|
||||
return {
|
||||
gateway,
|
||||
handlers: [
|
||||
http.get(`${baseUrl}/api/v1/studio/session`, () =>
|
||||
HttpResponse.json({
|
||||
authenticated: true,
|
||||
displayName: "테스트 편집자",
|
||||
roles: ["STUDIO_EDITOR"],
|
||||
csrfToken: "csrf-test-token",
|
||||
csrfHeaderName: "X-CSRF-TOKEN",
|
||||
}),
|
||||
),
|
||||
http.get(`${baseUrl}/api/v1/studio/dashboard`, () =>
|
||||
json(() => gateway.getDashboard()),
|
||||
),
|
||||
http.get(`${baseUrl}/api/v1/studio/documents`, ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const limit = url.searchParams.get("limit");
|
||||
return json(() =>
|
||||
gateway.listDocuments({
|
||||
...(url.searchParams.get("q") ? { q: url.searchParams.get("q")! } : {}),
|
||||
...(limit ? { limit: Number(limit) } : {}),
|
||||
} as never),
|
||||
);
|
||||
}),
|
||||
http.post(`${baseUrl}/api/v1/studio/documents`, async ({ request }) =>
|
||||
json(
|
||||
async () =>
|
||||
gateway.createDocument((await request.json()) as never, {
|
||||
idempotencyKey: key(request),
|
||||
}),
|
||||
201,
|
||||
),
|
||||
),
|
||||
http.get(`${baseUrl}/api/v1/studio/documents/:documentId`, ({ params }) =>
|
||||
json(() => gateway.getDocument(String(params.documentId))),
|
||||
),
|
||||
http.put(
|
||||
`${baseUrl}/api/v1/studio/documents/:documentId`,
|
||||
async ({ request, params }) =>
|
||||
json(async () =>
|
||||
gateway.saveDocument(
|
||||
String(params.documentId),
|
||||
(await request.json()) as never,
|
||||
{ idempotencyKey: key(request) },
|
||||
),
|
||||
),
|
||||
),
|
||||
http.post(
|
||||
`${baseUrl}/api/v1/studio/documents/:documentId/validate`,
|
||||
async ({ request, params }) =>
|
||||
json(async () =>
|
||||
gateway.validateDocument(
|
||||
String(params.documentId),
|
||||
(await request.json()) as never,
|
||||
{ idempotencyKey: key(request) },
|
||||
),
|
||||
),
|
||||
),
|
||||
http.get(
|
||||
`${baseUrl}/api/v1/studio/documents/:documentId/preview`,
|
||||
({ params }) => json(() => gateway.getCurrentPreview(String(params.documentId))),
|
||||
),
|
||||
http.post(
|
||||
`${baseUrl}/api/v1/studio/documents/:documentId/preview`,
|
||||
async ({ request, params }) =>
|
||||
json(
|
||||
async () =>
|
||||
gateway.createPreview(
|
||||
String(params.documentId),
|
||||
(await request.json()) as never,
|
||||
{ idempotencyKey: key(request) },
|
||||
),
|
||||
201,
|
||||
),
|
||||
),
|
||||
http.post(
|
||||
`${baseUrl}/api/v1/studio/documents/:documentId/publish`,
|
||||
async ({ request, params }) =>
|
||||
json(async () =>
|
||||
gateway.publishDocument(
|
||||
String(params.documentId),
|
||||
(await request.json()) as never,
|
||||
{ idempotencyKey: key(request) },
|
||||
),
|
||||
),
|
||||
),
|
||||
http.get(`${baseUrl}/api/v1/studio/publications`, () =>
|
||||
json(() => gateway.listPublications({})),
|
||||
),
|
||||
http.post(
|
||||
`${baseUrl}/api/v1/studio/publications/:publicationId/unpublish`,
|
||||
async ({ request, params }) =>
|
||||
json(async () =>
|
||||
gateway.unpublishPublication(
|
||||
String(params.publicationId),
|
||||
(await request.json()) as never,
|
||||
{ idempotencyKey: key(request) },
|
||||
),
|
||||
),
|
||||
),
|
||||
http.get(
|
||||
`${baseUrl}/api/v1/studio/publications/:publicationEventId/preview`,
|
||||
({ params }) =>
|
||||
json(() => gateway.getPublicationSnapshot(String(params.publicationEventId))),
|
||||
),
|
||||
http.get(`${baseUrl}/api/v1/studio/catalog`, ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
return json(() =>
|
||||
gateway.getCatalog({ type: url.searchParams.get("type") as never }),
|
||||
);
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user