diff --git a/app/test/delete-utils.test.js b/app/test/delete-utils.test.js new file mode 100644 index 0000000..e2814cd --- /dev/null +++ b/app/test/delete-utils.test.js @@ -0,0 +1,97 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { + isRetryableStatus, + deleteWithRetry, + runWithConcurrency, +} from '../delete-utils.js' + +const noSleep = () => Promise.resolve() + +test('isRetryableStatus 僅對暫時性狀態碼回傳 true', () => { + for (const s of [429, 502, 503, 504]) assert.equal(isRetryableStatus(s), true) + for (const s of [200, 204, 400, 401, 403, 404, 500]) { + assert.equal(isRetryableStatus(s), false) + } +}) + +test('deleteWithRetry 成功(204)時不重試', async () => { + let calls = 0 + const client = { + async deleteResource() { + calls += 1 + return 204 + }, + } + const result = await deleteWithRetry(client, 'u', { sleep: noSleep }) + assert.equal(result.status, 204) + assert.equal(calls, 1) +}) + +test('deleteWithRetry 對暫時性錯誤重試後成功', async () => { + const statuses = [503, 503, 204] + let i = 0 + const client = { + async deleteResource() { + return statuses[i++] + }, + } + const result = await deleteWithRetry(client, 'u', { sleep: noSleep }) + assert.equal(result.status, 204) + assert.equal(result.attempts, 3) +}) + +test('deleteWithRetry 對永久性錯誤(403)立即停止重試', async () => { + let calls = 0 + const client = { + async deleteResource() { + calls += 1 + return 403 + }, + } + const result = await deleteWithRetry(client, 'u', { sleep: noSleep }) + assert.equal(result.status, 403) + assert.equal(calls, 1) +}) + +test('deleteWithRetry 對網路例外重試至次數用盡並回傳 error', async () => { + let calls = 0 + const client = { + async deleteResource() { + calls += 1 + throw new Error('network down') + }, + } + const result = await deleteWithRetry(client, 'u', { attempts: 3, sleep: noSleep }) + assert.equal(result.status, 0) + assert.match(result.error.message, /network down/) + assert.equal(calls, 3) +}) + +test('runWithConcurrency 回傳對應輸入順序的結果', async () => { + const items = [1, 2, 3, 4, 5] + const out = await runWithConcurrency(items, async (n) => n * 2, 2) + assert.deepEqual(out, [2, 4, 6, 8, 10]) +}) + +test('runWithConcurrency 不超過併發上限', async () => { + let active = 0 + let maxActive = 0 + const items = Array.from({ length: 10 }, (_, i) => i) + await runWithConcurrency( + items, + async () => { + active += 1 + maxActive = Math.max(maxActive, active) + await Promise.resolve() + active -= 1 + }, + 3, + ) + assert.ok(maxActive <= 3, `maxActive=${maxActive} 應 <= 3`) +}) + +test('runWithConcurrency 對空陣列回傳空陣列', async () => { + const out = await runWithConcurrency([], async () => 1, 4) + assert.deepEqual(out, []) +})