feat: 기능 추가 과정중

This commit is contained in:
donghyeon-ka
2026-07-30 15:58:20 +09:00
parent d3ef801fe6
commit 6c52cdb916
648 changed files with 126325 additions and 6680 deletions
@@ -2,12 +2,11 @@ import { HttpResponse, http } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createExternalAuthSessionAdapter } from "../../src/adapters/auth/external-session-adapter.js";
import { createHttpClient } from "../../src/adapters/http/client.js";
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.js";
import { createExternalAuthSessionAdapter } from "../../src/adapters/auth/external-session-adapter.ts";
import { createHttpClient } from "../../src/adapters/http/client.ts";
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.ts";
/** @type {number[]} */
let responseStatuses = [];
let responseStatuses: number[] = [];
const server = setupServer(
http.get("https://api.test/api/entities", () => {
const status = responseStatuses.shift() ?? 200;
@@ -38,22 +37,25 @@ afterAll(() => server.close());
const clock = { now: () => 0, sleep: async () => {} };
/** @param {Parameters<typeof createHttpClient>[0]} options */
function testClient(options) {
type HttpDependencies = Parameters<typeof createHttpClient>[0];
function testClient(options: HttpDependencies) {
return createHttpClient({ ...TEST_HTTP_CONTRACT, ...options });
}
/**
* @param {Partial<Parameters<typeof createExternalAuthSessionAdapter>[0]>} overrides
* @returns {Parameters<typeof createExternalAuthSessionAdapter>[0]}
*/
function createOwner(overrides = {}) {
type ExternalSessionOwner = Parameters<
typeof createExternalAuthSessionAdapter
>[0];
function createOwner(
overrides: Partial<ExternalSessionOwner> = {},
): ExternalSessionOwner {
return {
readState: () => "authenticated",
subscribe: () => () => {},
beginSignIn: async () => {},
signOut: async () => {},
attachCredential: async (request) => request,
attachCredential: async () => ({ headers: {} }),
recoverSession: async () => "restored",
notifyUnauthenticated: () => {},
...overrides,
@@ -63,9 +65,9 @@ function createOwner(overrides = {}) {
describe("bounded 401 session recovery", () => {
it("calls recovery once and replays a safe request once", async () => {
responseStatuses = [401, 200];
const recoverSession = vi.fn(async () => /** @type {const} */ ("restored"));
const recoverSession = vi.fn(async () => "restored" as const);
const authSession = createExternalAuthSessionAdapter(createOwner({
attachCredential: async (request) => request,
attachCredential: async () => ({ headers: {} }),
recoverSession,
notifyUnauthenticated: vi.fn(),
}));
@@ -85,7 +87,7 @@ describe("bounded 401 session recovery", () => {
responseStatuses = [401, 401];
const notifyUnauthenticated = vi.fn();
const authSession = createExternalAuthSessionAdapter(createOwner({
attachCredential: async (request) => request,
attachCredential: async () => ({ headers: {} }),
recoverSession: async () => "restored",
notifyUnauthenticated,
}));
@@ -2,8 +2,8 @@ import { HttpResponse, http } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { createHttpClient } from "../../src/adapters/http/client.js";
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.js";
import { createHttpClient } from "../../src/adapters/http/client.ts";
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.ts";
let attempts = 0;
const server = setupServer(
@@ -35,8 +35,9 @@ const clock = {
sleep: async () => {},
};
/** @param {Parameters<typeof createHttpClient>[0]} options */
function testClient(options) {
type HttpDependencies = Parameters<typeof createHttpClient>[0];
function testClient(options: HttpDependencies) {
return createHttpClient({ ...TEST_HTTP_CONTRACT, ...options });
}
@@ -145,7 +146,7 @@ describe("shared HTTP client", () => {
expect(JSON.stringify(result)).not.toContain("raw field copy");
});
it("guards mapper exceptions as UNKNOWN_FAILURE", async () => {
it("guards mapper exceptions as a mapping contract violation", async () => {
server.use(
http.get("https://api.test/api/entities", () =>
HttpResponse.json({
@@ -166,7 +167,7 @@ describe("shared HTTP client", () => {
const result = await client.execute("LIST_ENTITIES");
expect(result).toMatchObject({
ok: false,
error: { kind: "UNKNOWN_FAILURE" },
error: { kind: "MAPPING_CONTRACT_VIOLATION" },
});
expect(JSON.stringify(result)).not.toContain("raw mapper detail");
});
+4 -4
View File
@@ -1,8 +1,8 @@
import { describe, expect, it, vi } from "vitest";
import { createHttpClient } from "../../src/adapters/http/client.js";
import type { DiagnosticRecordInput } from "../../src/contracts/diagnostics.js";
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.js";
import { createHttpClient } from "../../src/adapters/http/client.ts";
import type { DiagnosticRecordInput } from "../../src/contracts/diagnostics.ts";
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.ts";
function successResponse(data: unknown) {
return Response.json({
@@ -88,7 +88,7 @@ describe("HTTP diagnostics and terminal telemetry", () => {
correlation_id: "correlation-fixed",
outcome: "success",
error_kind: "NONE",
http_status_group: "none",
http_status_group: "2xx",
attempt_count_bucket: "1",
duration_bucket: "lt100ms",
},
@@ -1,13 +1,12 @@
import { describe, expect, it, vi } from "vitest";
import { createHttpClient } from "../../src/adapters/http/client.js";
import { createHttpClient } from "../../src/adapters/http/client.ts";
import {
entityQueryKeys,
TEST_HTTP_CONTRACT,
} from "../helpers/http-contract-fixture.js";
} from "../helpers/http-contract-fixture.ts";
/** @param {unknown} data */
function successResponse(data) {
function successResponse(data: unknown) {
return Response.json({
success: true,
data,
@@ -15,8 +14,7 @@ function successResponse(data) {
});
}
/** @param {number} status */
function failureResponse(status) {
function failureResponse(status: number) {
return Response.json(
{
success: false,
@@ -32,10 +30,10 @@ function immediateClock() {
}
function recordingScheduler() {
const callbacks = /** @type {Array<() => void>} */ ([]);
const callbacks: Array<() => void> = [];
return {
callbacks,
setTimeout: vi.fn((callback) => {
setTimeout: vi.fn((callback: () => void) => {
callbacks.push(callback);
return callbacks.length - 1;
}),
@@ -43,18 +41,62 @@ function recordingScheduler() {
};
}
/** @param {Parameters<typeof createHttpClient>[0]} options */
function testClient(options) {
type HttpDependencies = Parameters<typeof createHttpClient>[0];
function testClient(options: HttpDependencies) {
return createHttpClient({ ...TEST_HTTP_CONTRACT, ...options });
}
describe("HTTP operation execution contract", () => {
it("fails auth-required execution before fetch when the session integration is unavailable", async () => {
const fetcher = vi.fn();
const client = testClient({
baseUrl: "https://api.test",
fetcher,
authSession: undefined,
});
await expect(
client.execute({ operationId: "LIST_ENTITIES", routeId: "TEST_ROUTE" }),
).resolves.toMatchObject({
ok: false,
error: { kind: "AUTH_INTEGRATION_FAILURE" },
});
expect(fetcher).not.toHaveBeenCalled();
});
it("rejects an oversized response without materializing its JSON", async () => {
const largeOperation = {
...TEST_HTTP_CONTRACT.getOperation("LIST_ENTITIES"),
maxResponseBytes: 32,
responseMediaTypes: ["application/json"],
successStatuses: [200],
};
const client = testClient({
baseUrl: "https://api.test",
getOperation: () => largeOperation,
fetcher: vi.fn(async () =>
Response.json({
success: true,
data: [{ id: "one", name: "a".repeat(100) }],
meta: { requestId: "request-1", traceId: "trace-1" },
}),
),
});
await expect(client.execute("LIST_ENTITIES")).resolves.toMatchObject({
ok: false,
error: { kind: "RESPONSE_BODY_LIMIT" },
});
});
it("sends parsed search/body values and aligns canonical query identity", async () => {
const requests = /** @type {Request[]} */ ([]);
const requests: Request[] = [];
const scheduler = recordingScheduler();
const fetcher = vi.fn(async (request) => {
requests.push(/** @type {Request} */ (request));
if (/** @type {Request} */ (request).method === "POST") {
const fetcher = vi.fn(async (input: RequestInfo | URL) => {
const request = input as Request;
requests.push(request);
if (request.method === "POST") {
return successResponse({ id: "created", name: "Trimmed" });
}
return successResponse([]);
@@ -85,6 +127,10 @@ describe("HTTP operation execution contract", () => {
expect(entityQueryKeys.list(filters).at(-1)).toEqual(filters);
await expect(requests[1].json()).resolves.toEqual({ name: "Trimmed" });
expect(requests[1].headers.get("Idempotency-Key")).toBe("logical-command");
expect(requests[0].headers.get("X-Correlation-ID")).toBeTruthy();
expect(requests[0].credentials).toBe("same-origin");
expect(requests[0].cache).toBe("no-store");
expect(requests[0].redirect).toBe("error");
expect(scheduler.setTimeout).toHaveBeenCalledTimes(2);
expect(scheduler.clearTimeout).toHaveBeenCalledTimes(2);
});
@@ -148,9 +194,9 @@ describe("HTTP operation execution contract", () => {
it("distinguishes a runtime timeout from caller navigation abort and cleans listeners", async () => {
const scheduler = recordingScheduler();
const fetcher = vi.fn(
(request) =>
new Promise((_resolve, reject) => {
/** @type {Request} */ (request).signal.addEventListener(
(input: RequestInfo | URL) =>
new Promise<Response>((_resolve, reject) => {
(input as Request).signal.addEventListener(
"abort",
() => reject(new DOMException("aborted", "AbortError")),
{ once: true },
@@ -194,7 +240,7 @@ describe("HTTP operation execution contract", () => {
});
it("never retries unsafe, non-retryable status, or schema failures", async () => {
const unsafeOperation = /** @type {const} */ ({
const unsafeOperation = {
method: "POST",
path: "/api/unsafe",
operationId: "UNSAFE",
@@ -206,7 +252,7 @@ describe("HTTP operation execution contract", () => {
requestSchema: "unused",
responseSchema: "EntityPayload",
owner: "test",
});
} as const;
const unsafeFetch = vi.fn(async () => failureResponse(503));
const unsafeClient = testClient({
baseUrl: "https://api.test",