From 826b5590060d79fd40e528a76a0daf20964a5e20 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 26 Jun 2026 11:29:30 +0800 Subject: [PATCH] =?UTF-8?q?test(release-cleanup):=20=E8=A3=9C=E4=B8=8A?= =?UTF-8?q?=E7=B5=90=E6=9D=9F=E7=A2=BC=E3=80=81sanitizeBody=20=E8=88=87=20?= =?UTF-8?q?logger=20=E8=BC=B8=E5=87=BA=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增子行程驗證 index.js 失敗時以非零狀態結束、fetchAllPages 錯誤訊息 清理控制字元與長度限制、以及 logger 各輸出函式格式與 stdout/stderr 分流測試。 測試共 56 項全數通過。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/test/gitea-client.test.js | 20 +++++++++++++ app/test/logger.test.js | 56 ++++++++++++++++++++++++++++++++++- app/test/main.test.js | 13 ++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/app/test/gitea-client.test.js b/app/test/gitea-client.test.js index 6b701a6..deb6c01 100644 --- a/app/test/gitea-client.test.js +++ b/app/test/gitea-client.test.js @@ -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 () => { globalThis.fetch = async (url, opts) => { assert.equal(opts.method, 'DELETE') diff --git a/app/test/logger.test.js b/app/test/logger.test.js index edd0bcd..3fdc373 100644 --- a/app/test/logger.test.js +++ b/app/test/logger.test.js @@ -1,11 +1,65 @@ import { test, afterEach } from 'node:test' 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 realStdoutWrite = process.stdout.write afterEach(() => { 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 的內容 diff --git a/app/test/main.test.js b/app/test/main.test.js index 00e8d74..e7286ab 100644 --- a/app/test/main.test.js +++ b/app/test/main.test.js @@ -1,5 +1,7 @@ import { test, afterEach } from 'node:test' import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' import { main } from '../index.js' const realFetch = globalThis.fetch @@ -41,3 +43,14 @@ test('main 在必填環境變數缺失時 reject(供整合層級驗證錯誤處 }) 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/) +})