From edc933be73516ee93649936ec9756bdee7b93f39 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 26 Jun 2026 13:55:48 +0800 Subject: [PATCH] =?UTF-8?q?test(release-cleanup):=20=E8=A3=9C=E4=B8=8A?= =?UTF-8?q?=E6=95=B4=E5=90=88=E6=B8=AC=E8=A9=A6=E3=80=81server=20URL=20?= =?UTF-8?q?=E6=AD=A3=E8=A6=8F=E5=8C=96=E8=88=87=20id=20=E9=A9=97=E8=AD=89?= =?UTF-8?q?=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增端對端整合測試(列出→刪除舊 release→重列→列 tag→刪孤立 tag 的呼叫順序)、 loadConfig 去尾斜線測試,並將 id 編碼測試改為驗證非整數 id 被略過。測試共 62 項全數通過。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/test/config.test.js | 9 +++++ app/test/integration.test.js | 70 ++++++++++++++++++++++++++++++++++++ app/test/releases.test.js | 6 ++-- 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 app/test/integration.test.js diff --git a/app/test/config.test.js b/app/test/config.test.js index 4cea9e7..2444b2b 100644 --- a/app/test/config.test.js +++ b/app/test/config.test.js @@ -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') diff --git a/app/test/integration.test.js b/app/test/integration.test.js new file mode 100644 index 0000000..d3941bb --- /dev/null +++ b/app/test/integration.test.js @@ -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) +}) diff --git a/app/test/releases.test.js b/app/test/releases.test.js index a55964b..0091e8c 100644 --- a/app/test/releases.test.js +++ b/app/test/releases.test.js @@ -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 () => {