test(release-cleanup): 補上整合測試、server URL 正規化與 id 驗證測試

新增端對端整合測試(列出→刪除舊 release→重列→列 tag→刪孤立 tag 的呼叫順序)、
loadConfig 去尾斜線測試,並將 id 編碼測試改為驗證非整數 id 被略過。測試共 62 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-06-26 13:55:48 +08:00
co-authored by Claude Opus 4.8
parent 353a6af7c3
commit edc933be73
3 changed files with 82 additions and 3 deletions
+9
View File
@@ -52,6 +52,15 @@ test('loadConfig 在有效輸入時回傳完整設定物件', () => {
)
})
test('loadConfig 去除 GITEA_SERVER_URL 結尾斜線,避免 API 路徑出現雙斜線', () => {
const cfg = loadConfig({ ...base, GITEA_SERVER_URL: 'https://gitea.example.com/' })
assert.equal(cfg.serverUrl, 'https://gitea.example.com')
assert.equal(
cfg.releaseApiUrl,
'https://gitea.example.com/api/v1/repos/owner/repo/releases',
)
})
test('loadConfig 在提供 token 時保留 token 值', () => {
const cfg = loadConfig({ ...base, GITEA_TOKEN: 'secret' })
assert.equal(cfg.token, 'secret')
+70
View File
@@ -0,0 +1,70 @@
import { test, afterEach } from 'node:test'
import assert from 'node:assert/strict'
import { main } from '../index.js'
const realFetch = globalThis.fetch
const savedEnv = { ...process.env }
afterEach(() => {
globalThis.fetch = realFetch
process.env = { ...savedEnv }
})
function jsonResponse(data) {
return {
ok: true,
status: 200,
headers: { get: () => 'application/json' },
json: async () => data,
text: async () => JSON.stringify(data),
}
}
// 端對端驗證:列出 release → 刪除舊版本 → 重新列出 release → 列出 tag → 刪除孤立 tag。
test('main 完整清理流程:刪除舊 release 與孤立 tag,呼叫順序符合預期', async () => {
for (const k of ['GITEA_SERVER_URL', 'GITEA_REPOSITORY', 'KEEP_COUNT', 'GITEA_TOKEN']) {
delete process.env[k]
}
Object.assign(process.env, {
GITEA_SERVER_URL: 'https://gitea.example.com',
GITEA_REPOSITORY: 'owner/repo',
KEEP_COUNT: '2',
})
const releases = [
{ id: 3, tag_name: 'v3', name: 'r3', created_at: '2024-03-01T00:00:00Z' },
{ id: 2, tag_name: 'v2', name: 'r2', created_at: '2024-02-01T00:00:00Z' },
{ id: 1, tag_name: 'v1', name: 'r1', created_at: '2024-01-01T00:00:00Z' },
]
const tags = [{ name: 'v3' }, { name: 'v2' }, { name: 'v1' }, { name: 'orphan' }]
const calls = { get: [], delete: [] }
globalThis.fetch = async (url, opts) => {
const u = new URL(url)
const method = (opts && opts.method) || 'GET'
if (method === 'DELETE') {
calls.delete.push(u.pathname)
return { status: 204 }
}
const page = u.searchParams.get('page')
calls.get.push(`${u.pathname}?page=${page}`)
if (u.pathname.endsWith('/releases')) {
return jsonResponse(page === '1' ? releases : [])
}
if (u.pathname.endsWith('/tags')) {
return jsonResponse(page === '1' ? tags : [])
}
return jsonResponse([])
}
await main()
// 保留最新 2 筆(v3、v2),刪除最舊 v1(id=1);孤立 tag 'orphan' 被刪除
assert.deepEqual(calls.delete, [
'/api/v1/repos/owner/repo/releases/1',
'/api/v1/repos/owner/repo/tags/orphan',
])
// release 清單被讀取兩次(清理舊版本一次、清理 tag 前重新讀取一次)
const releaseListGets = calls.get.filter((c) => c.includes('/releases?page=1'))
assert.equal(releaseListGets.length, 2)
})
+3 -3
View File
@@ -108,14 +108,14 @@ test('cleanupReleases 在 deleteResource 回傳非 204 時仍繼續處理其餘
assert.deepEqual(client.deleted, ['http://x/releases/2', 'http://x/releases/1'])
})
test('cleanupReleases 對 id 進行 URL 編碼(防路徑穿越)', async () => {
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);id 中的 ../ 應被編碼
assert.deepEqual(client.deleted, ['http://x/releases/..%2Fevil'])
// 保留最新(id=1);待刪除者 id 非整數 → 被略過,不會送出任何刪除
assert.deepEqual(client.deleted, [])
})
test('cleanupReleases 在刪除回傳非 204 時記錄錯誤至 stderr', async () => {