Files
release-cleanup/app/test/releases.test.js
T
JefferyandClaude Opus 4.8 1f615fee90 test(release-cleanup): 補上一般網路錯誤與空陣列邊界測試
新增 fetchAllPages 在 fetch 拋一般網路錯誤時向外拋出的測試,以及
selectReleasesToDelete 對空陣列回傳空陣列的測試。測試共 67 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:14:18 +08:00

172 lines
6.4 KiB
JavaScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { selectReleasesToDelete, cleanupReleases } from '../releases.js'
const releases = [
{ id: 1, tag_name: 'v1.0.0', created_at: '2024-01-01T00:00:00Z' },
{ id: 3, tag_name: 'v3.0.0', created_at: '2024-03-01T00:00:00Z' },
{ id: 2, tag_name: 'v2.0.0', created_at: '2024-02-01T00:00:00Z' },
]
test('selectReleasesToDelete 保留最新的 keepCount 筆,刪除其餘', () => {
const toDelete = selectReleasesToDelete(releases, 2)
assert.equal(toDelete.length, 1)
assert.equal(toDelete[0].tag_name, 'v1.0.0')
})
test('selectReleasesToDelete 在數量不超過保留數時回傳空陣列', () => {
assert.deepEqual(selectReleasesToDelete(releases, 3), [])
assert.deepEqual(selectReleasesToDelete(releases, 5), [])
})
test('selectReleasesToDelete 對空陣列回傳空陣列', () => {
assert.deepEqual(selectReleasesToDelete([], 1), [])
assert.deepEqual(selectReleasesToDelete([], 0), [])
})
test('selectReleasesToDelete 在 keepCount 為 0 時刪除全部(由新到舊)', () => {
const toDelete = selectReleasesToDelete(releases, 0)
assert.deepEqual(
toDelete.map((r) => r.tag_name),
['v3.0.0', 'v2.0.0', 'v1.0.0'],
)
})
test('selectReleasesToDelete 在 created_at 相同時以 id 決定順序(具決定性)', () => {
const same = '2024-01-01T00:00:00Z'
const list = [
{ id: 1, created_at: same },
{ id: 3, created_at: same },
{ id: 2, created_at: same },
]
const toDelete = selectReleasesToDelete(list, 1)
// 保留 id 最大者(3),刪除 id 2 與 1(順序固定)
assert.deepEqual(
toDelete.map((r) => r.id),
[2, 1],
)
})
test('selectReleasesToDelete 不改動原陣列', () => {
const original = [...releases]
selectReleasesToDelete(releases, 1)
assert.deepEqual(releases, original)
})
test('selectReleasesToDelete 對無效 created_at 視為最舊,不產生 NaN 排序', () => {
const list = [
{ id: 1, tag_name: 'good', created_at: '2024-05-01T00:00:00Z' },
{ id: 2, tag_name: 'bad', created_at: 'not-a-date' },
{ id: 3, tag_name: 'empty' },
]
const toDelete = selectReleasesToDelete(list, 1)
// 最新的 good 被保留;兩筆無效日期視為最舊,落入待刪除
assert.equal(toDelete.length, 2)
assert.ok(!toDelete.some((r) => r.tag_name === 'good'))
})
// 建立可記錄刪除呼叫的假 client
function fakeClient(releases, statusFor = () => 204) {
const deleted = []
return {
deleted,
async fetchAllPages() {
return releases
},
async deleteResource(url) {
deleted.push(url)
return statusFor(url)
},
}
}
test('cleanupReleases 刪除超出保留數的舊 release', async () => {
const client = fakeClient([
{ id: 1, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
{ id: 2, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
{ id: 3, tag_name: 'v3', name: 'n3', created_at: '2024-03-01T00:00:00Z' },
])
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
// 保留最新 v3,刪除 v2 與 v1
assert.deepEqual(client.deleted, ['http://x/releases/2', 'http://x/releases/1'])
})
test('cleanupReleases 在未超過保留數時不刪除', async () => {
const client = fakeClient([
{ id: 1, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
])
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 2 })
assert.equal(client.deleted.length, 0)
})
test('cleanupReleases 在 deleteResource 回傳非 204 時仍繼續處理其餘項目', async () => {
const client = fakeClient(
[
{ id: 1, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
{ id: 2, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
{ id: 3, tag_name: 'v3', name: 'n3', created_at: '2024-03-01T00:00:00Z' },
],
(url) => (url.endsWith('/2') ? 500 : 204), // v2 刪除失敗
)
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
// 即使 v2 失敗(500),v1 仍被嘗試刪除
assert.deepEqual(client.deleted, ['http://x/releases/2', 'http://x/releases/1'])
})
test('cleanupReleases 略過非整數(惡意)id,不發出刪除請求', async () => {
const client = fakeClient([
{ id: 1, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
{ id: '../evil', tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
])
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
// 保留最新(id=1);待刪除者 id 非整數 → 被略過,不會送出任何刪除
assert.deepEqual(client.deleted, [])
})
test('cleanupReleases 在刪除回傳非 204 時記錄錯誤至 stderr', async () => {
const client = fakeClient(
[
{ id: 1, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
{ id: 2, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
],
() => 500,
)
const realWrite = process.stderr.write
const chunks = []
process.stderr.write = (chunk) => {
chunks.push(String(chunk))
return true
}
try {
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
} finally {
process.stderr.write = realWrite
}
assert.match(chunks.join(''), /\[ERR\]/)
})
test('cleanupReleases 對含 null/異常結構的 release 不拋例外且照常處理正常項目', async () => {
const client = fakeClient([
{ id: 3, tag_name: 'v3', name: 'n3', created_at: '2024-03-01T00:00:00Z' },
null,
{ created_at: '2024-02-01T00:00:00Z' }, // 缺 id/tag_name
{ id: 1, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
])
await assert.doesNotReject(() =>
cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 }),
)
// 保留最新(id=3);待刪除者中僅 id=1 為有效正整數 id,其餘(null、缺 id)被略過
assert.deepEqual(client.deleted, ['http://x/releases/1'])
})
test('cleanupReleases 略過沒有 id 的 release', async () => {
const client = fakeClient([
{ id: 1, tag_name: 'v3', name: 'n3', created_at: '2024-03-01T00:00:00Z' },
{ id: null, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
{ id: 2, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
])
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
// 保留最新 v3;待刪除為 v2 與 (無 id 的) v1 → 無 id 者被略過
assert.deepEqual(client.deleted, ['http://x/releases/2'])
})