import { test, afterEach } from 'node:test' import assert from 'node:assert/strict' import { GiteaClient } from '../gitea-client.js' const realFetch = globalThis.fetch afterEach(() => { globalThis.fetch = realFetch }) // 建立模擬的 JSON 回應(含 content-type 標頭) function jsonResponse(data) { return { ok: true, status: 200, headers: { get: () => 'application/json' }, json: async () => data, text: async () => JSON.stringify(data), } } test('建構子在有 token 時帶上 Authorization 標頭', () => { const client = new GiteaClient({ token: 'abc' }) assert.equal(client.headers.Authorization, 'token abc') }) test('建構子在無 token 時不帶 Authorization 標頭', () => { const client = new GiteaClient({}) assert.equal(client.headers.Authorization, undefined) }) test('fetchAllPages 逐頁讀取直到空陣列', async () => { const pages = { 1: [{ id: 1 }, { id: 2 }], 2: [{ id: 3 }], 3: [], } const requested = [] globalThis.fetch = async (url) => { const page = new URL(url).searchParams.get('page') requested.push(page) return jsonResponse(pages[page]) } const client = new GiteaClient({}) const items = await client.fetchAllPages('https://example.com/api') assert.deepEqual( items.map((i) => i.id), [1, 2, 3], ) assert.deepEqual(requested, ['1', '2', '3']) }) test('fetchAllPages 帶上逾時 signal', async () => { let sawSignal = false globalThis.fetch = async (_url, opts) => { sawSignal = opts && typeof opts.signal === 'object' && opts.signal !== null return jsonResponse([]) } const client = new GiteaClient({}) await client.fetchAllPages('https://example.com/api') assert.equal(sawSignal, true) }) test('fetchAllPages 在 HTTP 錯誤時丟出例外', async () => { globalThis.fetch = async () => ({ ok: false, status: 500, text: async () => 'boom', }) const client = new GiteaClient({}) await assert.rejects( () => client.fetchAllPages('https://example.com/api'), /HTTP 500/, ) }) test('fetchAllPages 在回應非 JSON 時丟出例外', async () => { globalThis.fetch = async () => ({ ok: true, status: 200, headers: { get: () => 'text/html' }, text: async () => 'error', }) const client = new GiteaClient({}) await assert.rejects( () => client.fetchAllPages('https://example.com/api'), /non-JSON content-type/, ) }) test('fetchAllPages 在回應為非陣列 JSON 時丟出例外', async () => { globalThis.fetch = async () => jsonResponse({ message: 'error object' }) const client = new GiteaClient({}) await assert.rejects( () => client.fetchAllPages('https://example.com/api'), /non-array JSON payload/, ) }) test('fetchAllPages 在 fetch 因逾時拋出 AbortError 時向外拋出', async () => { globalThis.fetch = async () => { const err = new Error('The operation was aborted due to timeout') err.name = 'AbortError' throw err } const client = new GiteaClient({}) await assert.rejects( () => client.fetchAllPages('https://example.com/api'), (err) => err.name === 'AbortError', ) }) test('fetchAllPages 在頁數超過 MAX_PAGES 時中止以避免無限迴圈', async () => { // 永遠回傳非空陣列,模擬 API 不以空陣列結尾的異常情形 globalThis.fetch = async () => jsonResponse([{ id: 1 }]) const client = new GiteaClient({}) await assert.rejects( () => client.fetchAllPages('https://example.com/api'), /exceeded MAX_PAGES/, ) }) test('fetchAllPages 的錯誤訊息會清理控制字元並限制長度(sanitizeBody)', async () => { const nasty = 'line1\nline2\r\tinjected' + 'x'.repeat(500) globalThis.fetch = async () => ({ ok: false, status: 500, text: async () => nasty, }) const client = new GiteaClient({}) let message = '' try { await client.fetchAllPages('https://example.com/api') } catch (error) { message = error.message } // 控制字元(換行、Tab 等)已被移除 assert.doesNotMatch(message, /[\u0000-\u001F]/) // 回應內容片段受 200 字元上限約束(原始有 500 個 x) assert.ok((message.match(/x/g) || []).length <= 200) }) test('fetchAllPages 在第 MAX_PAGES 頁回傳空陣列時正常結束(不拋例外)', async () => { let page = 0 globalThis.fetch = async () => { page += 1 // 前 999 頁有資料,第 1000 頁(MAX_PAGES)回空陣列,應正常結束 return jsonResponse(page < 1000 ? [{ id: page }] : []) } const client = new GiteaClient({}) const items = await client.fetchAllPages('https://example.com/api') assert.equal(items.length, 999) }) test('deleteResource 回傳 HTTP 狀態碼', async () => { globalThis.fetch = async (url, opts) => { assert.equal(opts.method, 'DELETE') return { status: 204 } } const client = new GiteaClient({}) assert.equal(await client.deleteResource('https://example.com/api/1'), 204) })