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:
co-authored by
Claude Opus 4.8
parent
353a6af7c3
commit
edc933be73
@@ -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 值', () => {
|
test('loadConfig 在提供 token 時保留 token 值', () => {
|
||||||
const cfg = loadConfig({ ...base, GITEA_TOKEN: 'secret' })
|
const cfg = loadConfig({ ...base, GITEA_TOKEN: 'secret' })
|
||||||
assert.equal(cfg.token, 'secret')
|
assert.equal(cfg.token, 'secret')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
@@ -108,14 +108,14 @@ test('cleanupReleases 在 deleteResource 回傳非 204 時仍繼續處理其餘
|
|||||||
assert.deepEqual(client.deleted, ['http://x/releases/2', 'http://x/releases/1'])
|
assert.deepEqual(client.deleted, ['http://x/releases/2', 'http://x/releases/1'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('cleanupReleases 對 id 進行 URL 編碼(防路徑穿越)', async () => {
|
test('cleanupReleases 略過非整數(惡意)id,不發出刪除請求', async () => {
|
||||||
const client = fakeClient([
|
const client = fakeClient([
|
||||||
{ id: 1, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
|
{ 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' },
|
{ id: '../evil', tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
|
||||||
])
|
])
|
||||||
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
|
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
|
||||||
// 保留最新(id=1),刪除較舊者(惡意 id);id 中的 ../ 應被編碼
|
// 保留最新(id=1);待刪除者 id 非整數 → 被略過,不會送出任何刪除
|
||||||
assert.deepEqual(client.deleted, ['http://x/releases/..%2Fevil'])
|
assert.deepEqual(client.deleted, [])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('cleanupReleases 在刪除回傳非 204 時記錄錯誤至 stderr', async () => {
|
test('cleanupReleases 在刪除回傳非 204 時記錄錯誤至 stderr', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user