From 923c94d19813cf2e0f6cb1dee29f285b21bb9054 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 26 Jun 2026 10:41:34 +0800 Subject: [PATCH] =?UTF-8?q?test(calculate-version):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20version/config/releases=20=E5=96=AE=E5=85=83=E6=B8=AC?= =?UTF-8?q?=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以 node:test 為 app/ 模組補上 24 個單元測試,涵蓋版本計算、設定載入與 release 分頁抓取。 Co-Authored-By: Claude Opus 4.8 (1M context) --- test/config.test.js | 61 ++++++++++++++++++++++++++++ test/releases.test.js | 93 +++++++++++++++++++++++++++++++++++++++++++ test/version.test.js | 85 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 test/config.test.js create mode 100644 test/releases.test.js create mode 100644 test/version.test.js diff --git a/test/config.test.js b/test/config.test.js new file mode 100644 index 0000000..e72d1de --- /dev/null +++ b/test/config.test.js @@ -0,0 +1,61 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + isUnset, + requireEnv, + normalizeBetaFlag, + loadConfig, +} = require('../app/config'); + +test('isUnset 將 undefined/空字串/null 字面值視為未設定', () => { + assert.equal(isUnset(undefined), true); + assert.equal(isUnset(''), true); + assert.equal(isUnset('null'), true); + assert.equal(isUnset('value'), false); +}); + +test('requireEnv 未設定時拋錯,有值時回傳原值', () => { + assert.throws(() => requireEnv('FOO', ''), /FOO 未設定/); + assert.equal(requireEnv('FOO', 'bar'), 'bar'); +}); + +test('normalizeBetaFlag 僅 "true" 視為啟用', () => { + assert.equal(normalizeBetaFlag('true'), true); + assert.equal(normalizeBetaFlag('false'), false); + assert.equal(normalizeBetaFlag(undefined), false); + assert.equal(normalizeBetaFlag('TRUE'), false); +}); + +test('loadConfig 解析完整設定', () => { + const config = loadConfig({ + GITEA_SERVER_URL: 'https://gitea.example.com', + GITEA_REPOSITORY: 'owner/repo', + GITEA_TOKEN: 'secret', + IS_BETA: 'true', + }); + + assert.deepEqual(config, { + serverUrl: 'https://gitea.example.com', + repository: 'owner/repo', + token: 'secret', + isBeta: true, + }); +}); + +test('loadConfig 缺少 token 視為匿名(null)', () => { + const config = loadConfig({ + GITEA_SERVER_URL: 'https://gitea.example.com', + GITEA_REPOSITORY: 'owner/repo', + GITEA_TOKEN: 'null', + }); + + assert.equal(config.token, null); + assert.equal(config.isBeta, false); +}); + +test('loadConfig 缺少必填項目時拋錯', () => { + assert.throws(() => loadConfig({ GITEA_REPOSITORY: 'owner/repo' }), /GITEA_SERVER_URL 未設定/); +}); diff --git a/test/releases.test.js b/test/releases.test.js new file mode 100644 index 0000000..560957f --- /dev/null +++ b/test/releases.test.js @@ -0,0 +1,93 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { fetchReleases } = require('../app/releases'); + +// 以可控的假回應替換全域 fetch,並於結束後還原 +function withFetch(handler, run) { + const original = globalThis.fetch; + globalThis.fetch = handler; + return Promise.resolve() + .then(run) + .finally(() => { + globalThis.fetch = original; + }); +} + +// 建立模擬 Response 物件 +const jsonResponse = (body, ok = true) => ({ + ok, + text: async () => (typeof body === 'string' ? body : JSON.stringify(body)), +}); + +test('fetchReleases 單頁(不足一頁即停止)', async () => { + await withFetch( + async () => jsonResponse([{ tag_name: 'v1.0.0' }]), + async () => { + const releases = await fetchReleases('https://gitea.example.com/api'); + assert.deepEqual(releases, [{ tag_name: 'v1.0.0' }]); + }, + ); +}); + +test('fetchReleases 跨頁合併直到不足一頁', async () => { + const pages = { + 1: Array.from({ length: 10 }, (_, i) => ({ tag_name: `v1.0.${i}` })), + 2: [{ tag_name: 'v1.1.0' }], + }; + const seen = []; + + await withFetch( + async (url) => { + const page = Number(new URL(url).searchParams.get('page')); + seen.push(page); + return jsonResponse(pages[page]); + }, + async () => { + const releases = await fetchReleases('https://gitea.example.com/api'); + assert.equal(releases.length, 11); + assert.deepEqual(seen, [1, 2]); + }, + ); +}); + +test('fetchReleases 對非 2xx 回應拋錯', async () => { + await withFetch( + async () => jsonResponse('Not Found', false), + async () => { + await assert.rejects( + () => fetchReleases('https://gitea.example.com/api'), + /release API 請求失敗/, + ); + }, + ); +}); + +test('fetchReleases 對非陣列回應拋錯', async () => { + await withFetch( + async () => jsonResponse({ message: 'oops' }), + async () => { + await assert.rejects( + () => fetchReleases('https://gitea.example.com/api'), + /回傳非陣列資料/, + ); + }, + ); +}); + +test('fetchReleases 有 token 時帶入授權標頭', async () => { + let captured; + + await withFetch( + async (url, options) => { + captured = options; + return jsonResponse([]); + }, + async () => { + await fetchReleases('https://gitea.example.com/api', { token: 'secret' }); + assert.equal(captured.headers.Authorization, 'token secret'); + }, + ); +}); diff --git a/test/version.test.js b/test/version.test.js new file mode 100644 index 0000000..bfdccba --- /dev/null +++ b/test/version.test.js @@ -0,0 +1,85 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + compareVersionArrays, + parseStableVersions, + latestStableVersion, + nextReleaseVersion, + nextBetaNumber, + calculateVersion, +} = require('../app/version'); + +// 便於建構測試資料的小工具 +const rel = (tag) => ({ tag_name: tag }); + +test('compareVersionArrays 逐區段比較且較短者較小', () => { + assert.ok(compareVersionArrays([1, 10], [1, 2, 3]) > 0); + assert.ok(compareVersionArrays([1, 2], [1, 2, 0]) < 0); + assert.equal(compareVersionArrays([1, 2, 3], [1, 2, 3]), 0); +}); + +test('parseStableVersions 排除 beta 與非法 tag 並去除 v 前綴', () => { + const releases = [ + rel('v1.2.3'), + rel('2.0.0'), + rel('v1.0.0-beta.1'), + rel('latest'), + { name: '沒有 tag_name' }, + ]; + + assert.deepEqual(parseStableVersions(releases), [[1, 2, 3], [2, 0, 0]]); +}); + +test('latestStableVersion 無資料時回傳 0.0.0', () => { + assert.equal(latestStableVersion([]), '0.0.0'); + assert.equal(latestStableVersion(null), '0.0.0'); +}); + +test('latestStableVersion 取出最大穩定版', () => { + const releases = [rel('v1.2.3'), rel('v1.9.9'), rel('v1.2.9'), rel('v0.5.0-beta.2')]; + assert.equal(latestStableVersion(releases), '1.9.9'); +}); + +test('nextReleaseVersion patch 進位', () => { + assert.equal(nextReleaseVersion('0.0.0'), '0.0.1'); + assert.equal(nextReleaseVersion('1.2.3'), '1.2.4'); +}); + +test('nextReleaseVersion patch 達 10 進位至 minor', () => { + assert.equal(nextReleaseVersion('1.2.9'), '1.3.0'); +}); + +test('nextReleaseVersion minor 達 10 進位至 major', () => { + assert.equal(nextReleaseVersion('1.9.9'), '2.0.0'); +}); + +test('nextBetaNumber 無對應 beta 時回傳 1', () => { + assert.equal(nextBetaNumber([], '0.0.1'), 1); + assert.equal(nextBetaNumber([rel('v0.0.1')], '0.0.1'), 1); +}); + +test('nextBetaNumber 取最大流水號加一', () => { + const releases = [rel('v1.3.0-beta.1'), rel('v1.3.0-beta.3'), rel('v1.3.0-beta.2')]; + assert.equal(nextBetaNumber(releases, '1.3.0'), 4); +}); + +test('calculateVersion 無 release 的穩定版', () => { + assert.deepEqual(calculateVersion([], false), { latest: '0.0.0', version: '0.0.1' }); +}); + +test('calculateVersion 無 release 的 beta 版', () => { + assert.deepEqual(calculateVersion([], true), { latest: '0.0.0', version: '0.0.1-beta.1' }); +}); + +test('calculateVersion 既有穩定版推下一個穩定版', () => { + const releases = [rel('v1.2.3'), rel('v1.2.2')]; + assert.deepEqual(calculateVersion(releases, false), { latest: '1.2.3', version: '1.2.4' }); +}); + +test('calculateVersion 既有穩定版推下一個 beta 版', () => { + const releases = [rel('v1.2.3'), rel('v1.2.4-beta.1')]; + assert.deepEqual(calculateVersion(releases, true), { latest: '1.2.3', version: '1.2.4-beta.2' }); +});