'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'); }, ); });