feat: 기능 추가 과정중
This commit is contained in:
@@ -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");
|
||||
});
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
+65
-19
@@ -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",
|
||||
Reference in New Issue
Block a user