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("Bad Gateway", { status: 502 }), ); await assert.rejects(api.postRawJson("v1/test", {}), (error) => { assert.equal(error.status, 502); assert.equal(error.body, "Bad Gateway"); assert.match(error.message, /502/); assert.doesNotMatch(error.message, //); 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("login"), ); await assert.rejects(api.getData("v1/test"), { status: 200, body: "login", }); }); 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); });