feat(ai-character): 리소스 관리 기반 정비

This commit is contained in:
Yu Sung
2026-08-01 01:30:24 +09:00
parent 55ba0df77a
commit a1cae336d9
73 changed files with 5162 additions and 816 deletions

View File

@@ -1,7 +1,7 @@
import { http, HttpResponse } from "msw";
import { afterEach, describe, expect, test, vi } from "vitest";
import { AccessDeniedError } from "../api-error";
import { AccessDeniedError, ApiError, UNKNOWN_API_ERROR_MESSAGE } from "../api-error";
import { createApiClient } from "../client";
import { server } from "../../test/server";
import { apiBaseUrl, createTestClient, valueSchema } from "./client-test-helpers";
@@ -147,6 +147,42 @@ describe("authenticated API requests", () => {
expect(onAuthExpired).toHaveBeenCalledTimes(2);
});
test.each([
["malformed JSON", () => new HttpResponse("not-json", { status: 401 })],
["empty body", () => new HttpResponse(null, { status: 401 })],
["schema mismatch", () => HttpResponse.json({ success: false, data: null, errorProperty: null }, { status: 401 })],
])("clears the session once for concurrent protected 401 responses with %s", async (_label, responseFactory) => {
// Given
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
const { client, clearSession, onAuthExpired } = createTestClient();
server.use(http.get(`${apiBaseUrl}/protected`, responseFactory));
// When
const results = await Promise.allSettled([
client.request({
path: "/protected",
responseSchema: valueSchema,
authentication: "required",
}),
client.request({
path: "/protected",
responseSchema: valueSchema,
authentication: "required",
}),
]);
// Then
for (const result of results) {
expect(result.status).toBe("rejected");
if (result.status === "rejected") {
expect(result.reason).toBeInstanceOf(ApiError);
expect(result.reason).toMatchObject({ status: 401, message: UNKNOWN_API_ERROR_MESSAGE, errorProperty: null });
}
}
expect(clearSession).toHaveBeenCalledTimes(1);
expect(onAuthExpired).toHaveBeenCalledTimes(1);
});
test("surfaces access denied without clearing the session", async () => {
// Given
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);

View File

@@ -1,7 +1,7 @@
import { http, HttpResponse } from "msw";
import { afterEach, describe, expect, test, vi } from "vitest";
import { ApiError } from "../api-error";
import { ApiError, UNKNOWN_API_ERROR_MESSAGE } from "../api-error";
import { server } from "../../test/server";
import { apiBaseUrl, createTestClient, valueSchema } from "./client-test-helpers";
@@ -115,7 +115,7 @@ describe("API client", () => {
await expect(request).rejects.toBeInstanceOf(ApiError);
});
test("surfaces a network failure without a mock response", async () => {
test("normalizes a network failure to the shared unknown API error", async () => {
// Given
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
const { client } = createTestClient();
@@ -129,6 +129,27 @@ describe("API client", () => {
});
// Then
await expect(request).rejects.toBeInstanceOf(TypeError);
await expect(request).rejects.toMatchObject({ status: 0, message: UNKNOWN_API_ERROR_MESSAGE, errorProperty: null });
});
test.each([
["malformed JSON", "not-json"],
["malformed envelope", JSON.stringify({ success: false, data: null, errorProperty: null })],
["empty message", JSON.stringify({ success: false, message: "", data: null, errorProperty: null })],
])("normalizes %s error responses to the shared unknown API error", async (_label, body) => {
// Given
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
const { client } = createTestClient();
server.use(http.get(`${apiBaseUrl}/unknown-error`, () => new HttpResponse(body, { status: 500 })));
// When
const request = client.request({
path: "/unknown-error",
responseSchema: valueSchema,
authentication: "none",
});
// Then
await expect(request).rejects.toMatchObject({ status: 500, message: UNKNOWN_API_ERROR_MESSAGE, errorProperty: null });
});
});

View File

@@ -1,52 +1,8 @@
import { describe, expect, test } from "vitest";
import { createPageParams, type PageData } from "../pagination";
describe("createPageParams", () => {
test("uses documented default page and size", () => {
// Given
const request = {};
// When
const pageParams = createPageParams(request);
// Then
expect(pageParams).toEqual({ page: 0, size: 20 });
});
test("clamps size to the documented lower bound", () => {
// Given
const request = { size: 1 };
// When
const pageParams = createPageParams(request);
// Then
expect(pageParams.size).toBe(20);
});
test("clamps size to the documented upper bound", () => {
// Given
const request = { size: 51 };
// When
const pageParams = createPageParams(request);
// Then
expect(pageParams.size).toBe(50);
});
test("keeps a provided page unchanged", () => {
// Given
const request = { page: 3, size: 20 };
// When
const pageParams = createPageParams(request);
// Then
expect(pageParams.page).toBe(3);
});
import type { PageData } from "../pagination";
describe("PageData", () => {
test("defines the documented page response shape", () => {
// Given
const page: PageData<string> = {