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:
co-authored by
Claude Opus 4.8
parent
87db9030d6
commit
826b559006
@@ -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
@@ -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 的內容
|
||||||
|
|||||||
@@ -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/)
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user