const assert = require("node:assert/strict"); const { test } = require("node:test"); const { loadTypeScript } = require("./load-typescript.cjs"); const { uploadPhotosWithReferences } = loadTypeScript( "src/features/dredging/lib/photo-upload-queue.ts", ); const photos = () => ["one", "two", "three"].map((key) => ({ key, file: { name: `${key}.jpg` } })); test("부분 업로드 성공 참조는 실패 후에도 원본 Map에 남고 재시도는 실패 사진부터 진행", async () => { const references = new Map(); const calls = []; let failSecond = true; const upload = async (file) => { calls.push(file.name); if (file.name === "two.jpg" && failSecond) { failSecond = false; throw new Error("upload failed"); } return { atflCd: file.name, atflId: `id-${file.name}` }; }; await assert.rejects( uploadPhotosWithReferences(photos(), references, upload), ); assert.deepEqual([...references.keys()], ["one"]); assert.equal( await uploadPhotosWithReferences(photos(), references, upload), references, ); assert.deepEqual(calls, ["one.jpg", "two.jpg", "two.jpg", "three.jpg"]); assert.deepEqual([...references.keys()], ["one", "two", "three"]); }); test("기존 성공·빈 목록·같은 사진 키의 중복은 추가 업로드를 발생시키지 않는다", async () => { let calls = 0; const references = new Map([["one", { atflCd: "A", atflId: "B" }]]); const upload = async () => { calls++; return { atflCd: "C", atflId: "D" }; }; assert.equal( await uploadPhotosWithReferences([], references, upload), references, ); await uploadPhotosWithReferences( [photos()[0], photos()[1], photos()[1]], references, upload, ); assert.equal(calls, 1); assert.deepEqual(references.get("one"), { atflCd: "A", atflId: "B" }); }); /** 실제 브라우저 API 경계만 모의해 TUS callback과 타이머 정리를 검증한다. */ function browserStubs(t) { const originalWindow = globalThis.window; const originalTransfer = globalThis.DataTransfer; const timers = new Map(); let timerId = 0; globalThis.window = { setTimeout(callback, delay) { assert.equal(delay, 90_000); timers.set(++timerId, callback); return timerId; }, clearTimeout(id) { timers.delete(id); }, }; globalThis.DataTransfer = class { constructor() { this.files = []; this.items = { add: (file) => this.files.push(file) }; } }; t.after(() => { if (originalWindow === undefined) delete globalThis.window; else globalThis.window = originalWindow; if (originalTransfer === undefined) delete globalThis.DataTransfer; else globalThis.DataTransfer = originalTransfer; }); t.mock.method(console, "warn", () => {}); return timers; } test("실제 photo adapter의 TUS 비동기 오류는 즉시 reject하고 성공 참조·재시도를 보존", async (t) => { const timers = browserStubs(t); const calls = []; let failSecond = true; const { uploadDredgingPhotoDrafts } = loadTypeScript( "src/features/dredging/api/photo-upload.ts", { "@/components/lib/utils": { handleUpload(files, options) { const name = files[0].name; calls.push(name); queueMicrotask(() => { if (name === "two.jpg" && failSecond) { failSecond = false; options.handleError(new Error("tus failure")); return; } options.handleResponse({ getHeader: () => JSON.stringify({ atflCd: name, atflId: `id-${name}` }), }); }); }, }, }, ); const references = new Map(); await assert.rejects( uploadDredgingPhotoDrafts(photos(), references), /준설사진을 업로드하지 못했습니다/, ); assert.deepEqual([...references.keys()], ["one"]); assert.equal(timers.size, 0); assert.equal( await uploadDredgingPhotoDrafts(photos(), references), references, ); assert.deepEqual(calls, ["one.jpg", "two.jpg", "two.jpg", "three.jpg"]); assert.equal(timers.size, 0); }); test("잘못된 X-File-Info는 성공 참조를 만들지 않고 타이머를 해제", async (t) => { const timers = browserStubs(t); const { uploadDredgingPhotoDrafts } = loadTypeScript( "src/features/dredging/api/photo-upload.ts", { "@/components/lib/utils": { handleUpload(_files, options) { options.handleResponse({ getHeader: () => '{"atflCd":"A"}' }); }, }, }, ); const references = new Map(); await assert.rejects(uploadDredgingPhotoDrafts([photos()[0]], references)); assert.equal(references.size, 0); assert.equal(timers.size, 0); }); test("FE 선행 모의 모드에서는 TUS를 호출하거나 참조를 추가하지 않는다", async () => { const previous = process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED; process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED = "false"; try { const { uploadDredgingPhotoDrafts } = loadTypeScript( "src/features/dredging/api/photo-upload.ts", { "@/components/lib/utils": { handleUpload() { assert.fail("mock mode must not upload"); }, }, }, ); const references = new Map(); assert.equal( await uploadDredgingPhotoDrafts(photos(), references), references, ); assert.equal(references.size, 0); } finally { if (previous === undefined) delete process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED; else process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED = previous; } });