test(release-cleanup): 補上結束碼、sanitizeBody 與 logger 輸出測試

新增子行程驗證 index.js 失敗時以非零狀態結束、fetchAllPages 錯誤訊息
清理控制字元與長度限制、以及 logger 各輸出函式格式與 stdout/stderr 分流測試。
測試共 56 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-06-26 11:29:30 +08:00
co-authored by Claude Opus 4.8
parent 87db9030d6
commit 826b559006
3 changed files with 88 additions and 1 deletions
+20
View File
@@ -122,6 +122,26 @@ test('fetchAllPages 在頁數超過 MAX_PAGES 時中止以避免無限迴圈', a
) )
}) })
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('deleteResource 回傳 HTTP 狀態碼', async () => { test('deleteResource 回傳 HTTP 狀態碼', async () => {
globalThis.fetch = async (url, opts) => { globalThis.fetch = async (url, opts) => {
assert.equal(opts.method, 'DELETE') assert.equal(opts.method, 'DELETE')
+55 -1
View File
@@ -1,11 +1,65 @@
import { test, afterEach } from 'node:test' import { test, afterEach } from 'node:test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { failError } from '../logger.js' import {
separator,
section,
info,
success,
warn,
fail,
failError,
} from '../logger.js'
const realWrite = process.stderr.write const realWrite = process.stderr.write
const realStdoutWrite = process.stdout.write
afterEach(() => { afterEach(() => {
process.stderr.write = realWrite process.stderr.write = realWrite
process.stdout.write = realStdoutWrite
})
// 攔截寫入 stdout 的內容
function captureStdout(fn) {
const chunks = []
process.stdout.write = (chunk) => {
chunks.push(String(chunk))
return true
}
try {
fn()
} finally {
process.stdout.write = realStdoutWrite
}
return chunks.join('')
}
test('info/success/warn 以正確前綴寫入 stdout', () => {
assert.equal(captureStdout(() => info('hi')), '[INFO] hi\n')
assert.equal(captureStdout(() => success('ok')), '[OK] ok\n')
assert.equal(captureStdout(() => warn('careful')), '[WARN] careful\n')
})
test('separator 輸出等號分隔線', () => {
const out = captureStdout(() => separator())
assert.match(out, /^\n={50}\n$/)
})
test('section 輸出分隔線、標題與虛線', () => {
const out = captureStdout(() => section('參數檢查'))
assert.match(out, /={50}\n參數檢查\n-{50}\n/)
})
test('fail 寫入 stderr 而非 stdout', () => {
const stdout = captureStdout(() => {
const chunks = []
process.stderr.write = (chunk) => {
chunks.push(String(chunk))
return true
}
fail('boom')
assert.equal(chunks.join(''), '[ERR] boom\n')
})
assert.equal(stdout, '') // fail 不應寫入 stdout
}) })
// 攔截寫入 stderr 的內容 // 攔截寫入 stderr 的內容
+13
View File
@@ -1,5 +1,7 @@
import { test, afterEach } from 'node:test' import { test, afterEach } from 'node:test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { main } from '../index.js' import { main } from '../index.js'
const realFetch = globalThis.fetch const realFetch = globalThis.fetch
@@ -41,3 +43,14 @@ test('main 在必填環境變數缺失時 reject(供整合層級驗證錯誤處
}) })
await assert.rejects(() => main(), /GITEA_SERVER_URL is required/) await assert.rejects(() => main(), /GITEA_SERVER_URL is required/)
}) })
test('直接執行 index.js 在設定錯誤時以非零狀態碼結束', () => {
const indexPath = fileURLToPath(new URL('../index.js', import.meta.url))
// 僅帶 PATH,不提供任何必填環境變數,使 main 拋錯並觸發 process.exit(1)
const result = spawnSync(process.execPath, [indexPath], {
env: { PATH: process.env.PATH },
encoding: 'utf8',
})
assert.equal(result.status, 1)
assert.match(result.stderr, /GITEA_SERVER_URL is required/)
})