Files
frontend/apps/sn/tests/api-transport.test.cjs
T
2026-09-15 10:00:42 +09:00

190 lines
6.1 KiB
JavaScript

const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
// Legacy facade를 실제로 실행하고 인증 외부 의존성만 고정한다.
const transport = loadTypeScript("src/shared/api/http-transport.ts");
const api = loadTypeScript("app/components/lib/api/index.ts", {
"@/shared/api/http-transport": transport,
"@/shared/config": { SITE_ID: "SN-TEST", CLAIM_NAME: "Authorization" },
"@/entities/auth/lib/token-manager": { tokenManager: {} },
"@common/auth": { getRemainingMs: () => 0 },
});
for (const method of [
"getData",
"postData",
"putData",
"deleteData",
"getRawJson",
"postRawJson",
]) {
test(`${method}: HTTP 500 JSON 오류를 status/body와 함께 reject`, async (t) => {
const body = { message: "저장 실패", data: null };
t.mock.method(globalThis, "fetch", async () =>
Response.json(body, { status: 500 }),
);
await assert.rejects(api[method]("v1/test", {}), (error) => {
assert.equal(error.status, 500);
assert.equal(error.message, "저장 실패");
assert.deepEqual(error.body, body);
return true;
});
});
}
test("HTML 502 오류가 JSON 파싱 오류로 바뀌지 않는다", async (t) => {
t.mock.method(
globalThis,
"fetch",
async () => new Response("<html>Bad Gateway</html>", { status: 502 }),
);
await assert.rejects(api.postRawJson("v1/test", {}), (error) => {
assert.equal(error.status, 502);
assert.equal(error.body, "<html>Bad Gateway</html>");
assert.match(error.message, /502/);
assert.doesNotMatch(error.message, /<html>/);
return true;
});
});
test("nested 오류 메시지와 비어 있는 오류 응답", async (t) => {
const fetchMock = t.mock.method(globalThis, "fetch", async () =>
Response.json({ error: { message: "권한 없음" } }, { status: 403 }),
);
await assert.rejects(api.deleteData("v1/test"), {
message: "권한 없음",
status: 403,
});
fetchMock.mock.mockImplementation(
async () => new Response(null, { status: 503 }),
);
await assert.rejects(api.postData("v1/test", {}), {
status: 503,
body: null,
});
});
test("POST 성공 래퍼를 해제하고 URL·사이트 헤더·JSON body 유지", async (t) => {
const calls = [];
t.mock.method(globalThis, "fetch", async (url, init) => {
calls.push({ url, init });
return Response.json({ status: 201, data: { id: 9 } }, { status: 201 });
});
assert.deepEqual(await api.postData("v1/test", { fcltMngNo: "P-1" }), {
id: 9,
});
assert.equal(calls[0].url, "/sewerage-service/api/v1/test");
assert.equal(calls[0].init.headers.get("X-Site-Id"), "SN-TEST");
assert.equal(calls[0].init.headers.get("Content-Type"), "application/json");
assert.deepEqual(JSON.parse(calls[0].init.body), { fcltMngNo: "P-1" });
await api.postData("/attach-service/custom", {});
assert.equal(calls[1].url, "/attach-service/custom");
});
test("204 정상: JSON 함수 null, DELETE true", async (t) => {
t.mock.method(
globalThis,
"fetch",
async () => new Response(null, { status: 204 }),
);
for (const method of [
"getData",
"postData",
"putData",
"getRawJson",
"postRawJson",
]) {
assert.equal(await api[method]("v1/test", {}), null);
}
assert.equal(await api.deleteData("v1/test"), true);
});
test("DELETE 200 성공 Response는 본문을 읽지 않은 채 반환", async (t) => {
t.mock.method(globalThis, "fetch", async () =>
Response.json({ deleted: true }),
);
const result = await api.deleteData("v1/test");
assert.ok(result instanceof Response);
assert.equal(result.status, 200);
assert.equal(result.bodyUsed, false);
assert.deepEqual(await result.json(), { deleted: true });
});
test("실패한 삭제 이후 성공 알림·두 번째 삭제를 실행하지 않는다", async (t) => {
let calls = 0;
let success = false;
t.mock.method(globalThis, "fetch", async () => {
calls++;
return new Response(null, { status: 500 });
});
await assert.rejects(async () => {
await api.deleteData("v1/first");
await api.deleteData("v1/second");
success = true;
});
assert.equal(calls, 1);
assert.equal(success, false);
});
test("raw GeoJSON·필드 이름 및 PUT raw/wrapper 계약 유지", async (t) => {
const geojson = {
type: "FeatureCollection",
features: [],
fclt_mng_no: "P",
fcltMngNo: "Q",
};
const fetchMock = t.mock.method(globalThis, "fetch", async () =>
Response.json(geojson),
);
assert.deepEqual(await api.getRawJson("/geoserver/test"), geojson);
assert.deepEqual(await api.putData("v1/test", {}), geojson);
fetchMock.mock.mockImplementation(async () => Response.json({ data: null }));
assert.equal(await api.putData("v1/test", {}), null);
});
test("정상 HTTP에서 HTML 본문은 성공 데이터로 취급하지 않는다", async (t) => {
t.mock.method(
globalThis,
"fetch",
async () => new Response("<html>login</html>"),
);
await assert.rejects(api.getData("v1/test"), {
status: 200,
body: "<html>login</html>",
});
});
test("Headers 객체, 사이트 헤더 및 AbortSignal을 보존하고 취소 예외를 유지", async (t) => {
const controller = new AbortController();
const abort = new DOMException("cancelled", "AbortError");
t.mock.method(globalThis, "fetch", async (_url, init) => {
assert.equal(init.signal, controller.signal);
assert.equal(init.headers.get("X-Custom"), "retained");
assert.equal(init.headers.get("X-Site-Id"), "SN-TEST");
throw abort;
});
await assert.rejects(
api.getRawJson("v1/test", {
signal: controller.signal,
headers: new Headers({ "X-Custom": "retained" }),
}),
(error) => error === abort,
);
});
test("401에서 주입한 인증 callback을 한 번 실행한 뒤 reject", async (t) => {
let count = 0;
const client = transport.createHttpTransport({
baseUrl: "/api",
onUnauthorized: () => count++,
});
t.mock.method(
globalThis,
"fetch",
async () => new Response(null, { status: 401 }),
);
await assert.rejects(client.postData("test", {}), { status: 401 });
assert.equal(count, 1);
});