test(calculate-version): 新增 version/config/releases 單元測試
以 node:test 為 app/ 模組補上 24 個單元測試,涵蓋版本計算、設定載入與 release 分頁抓取。 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
bd6b6017ab
commit
923c94d198
@@ -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 未設定/);
|
||||
});
|
||||
@@ -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');
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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' });
|
||||
});
|
||||
Reference in New Issue
Block a user