Files
tech-log-frontend/tests/mocks/handlers/tech-log-studio.ts
T
DongHyeonkaandClaude Opus 5 7424ed4594 fix: forward every documented Studio query filter and pin canonicalInputIdentity
Two review Minor findings against the brief itself, both closed:

- tests/mocks/handlers/tech-log-studio.ts silently dropped documented
  query filters instead of mirroring the mock gateway it wraps:
  listStudioDocuments forwarded only q/limit (dropping kind,
  publicationStatus, nextAction, projectId, sort, cursor),
  listStudioCatalog forwarded only type (dropping q/cursor/limit), and
  listStudioPublications ignored all four of its parameters outright.
  Added a shared queryParams() helper and forward every field each
  operation's projectRequest actually emits, plus a regression test
  that narrows the fixture set by kind through the real HTTP gateway
  (confirmed it fails without the fix).

- canonicalInputIdentity (on the idempotency-safety path, reused by
  Task 6) had no test. Added three tests against mutationIntent(): a
  large Korean payload stays within the byte bound, the same input
  retried twice yields an identical identity, and a mid-codepoint
  truncation cut leaves no replacement character (confirmed the third
  fails without the strip). The known collision limitation of any
  bounded-length identity scheme is documented in the test file rather
  than solved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:43:51 +09:00

181 lines
5.8 KiB
TypeScript

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);
}
}
/**
* Forwards every query parameter a `projectRequest` puts on the wire, not
* just the ones a first pass happened to reach for. A handler that silently
* drops a documented filter lets a later filter test pass for the wrong
* reason — it would observe the *unfiltered* mock result and never learn the
* parameter never left the URL.
*/
function queryParams(
url: URL,
stringNames: readonly string[],
numberNames: readonly string[] = [],
): Record<string, string | number> {
const result: Record<string, string | number> = {};
for (const name of stringNames) {
const value = url.searchParams.get(name);
if (value !== null) result[name] = value;
}
for (const name of numberNames) {
const value = url.searchParams.get(name);
if (value !== null) result[name] = Number(value);
}
return result;
}
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);
return json(() =>
gateway.listDocuments(
queryParams(
url,
["q", "kind", "publicationStatus", "nextAction", "projectId", "sort", "cursor"],
["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`, ({ request }) => {
const url = new URL(request.url);
return json(() =>
gateway.listPublications(
queryParams(url, ["q", "type", "cursor"], ["limit"]) as never,
),
);
}),
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(
queryParams(url, ["type", "q", "cursor"], ["limit"]) as never,
),
);
}),
],
};
}