新增以 emoji(代理對)驗證截斷不拆分多位元組字元、不殘留落單代理碼元的測試。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
133 lines
4.2 KiB
JavaScript
133 lines
4.2 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const { fetchReleases, truncateString } = require('../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 對無法解析的 JSON 回應拋錯', async () => {
|
|
await withFetch(
|
|
async () => jsonResponse('{ this is not valid json', true),
|
|
async () => {
|
|
await assert.rejects(
|
|
() => fetchReleases('https://gitea.example.com/api'),
|
|
/回傳資料無法解析/,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('truncateString 以字元截斷且不拆分多位元組字元(emoji 代理對)', () => {
|
|
const emojis = '😀'.repeat(25);
|
|
const result = truncateString(emojis, 10);
|
|
// 應得到完整 10 個 emoji(以 code point 計),不殘留落單的代理碼元
|
|
assert.equal(Array.from(result).length, 10);
|
|
assert.equal(result, '😀'.repeat(10));
|
|
assert.ok(!/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(result), '不應殘留落單的高代理碼元');
|
|
});
|
|
|
|
test('fetchReleases 解析失敗時截斷過長的回應片段', async () => {
|
|
const huge = `{${'x'.repeat(5000)}`;
|
|
await withFetch(
|
|
async () => jsonResponse(huge, true),
|
|
async () => {
|
|
await assert.rejects(
|
|
() => fetchReleases('https://gitea.example.com/api'),
|
|
(err) => {
|
|
const match = err.message.match(/片段:「([\s\S]*)」$/);
|
|
assert.ok(match, '錯誤訊息應包含回應內容片段');
|
|
assert.ok(Array.from(match[1]).length <= 200, '片段應截斷至 200 字元內');
|
|
return true;
|
|
},
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
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');
|
|
},
|
|
);
|
|
});
|