test: 整併測試至 app/test 並解決 AI 審查 findings #8

Closed
jiantw83 wants to merge 23 commits from test/consolidate-app-test-20260626-111823 into develop
2 changed files with 17 additions and 3 deletions
Showing only changes of commit b8804c5218 - Show all commits
+14 -2
View File
@@ -55,20 +55,32 @@ function assertHttpUrl(name, value) {
}
}
// 驗證 repository 為 owner/repo 格式(僅允許字母數字與 . _ -,且拒絕 . 與 .. 路徑穿越段)
function assertRepository(name, value) {
const parts = value.split('/');
const valid = parts.length === 2
&& parts.every((part) => /^[A-Za-z0-9._-]+$/.test(part) && part !== '.' && part !== '..');
Ghost marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔴 嚴重
審查員:Maya
問題:新增的 assertRepository 函數對 GITEA_REPOSITORY 的檢查邏輯並未測試到所有邊界條件,例如:包含特殊字元(非字母數字點下底線連字號)、極長字串段(> 100 字元)、以及完全空白的 value
建議:請在 app/test/config.test.js 中增加針對非法字元、超過 100 字元的字串段,以及空字串的測試案例,確保 assertRepository 的防禦性功能完全被驗證。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:新增的 `assertRepository` 函數對 `GITEA_REPOSITORY` 的檢查邏輯並未測試到所有邊界條件,例如:包含特殊字元(非字母數字點下底線連字號)、極長字串段(> 100 字元)、以及完全空白的 `value`。 **建議**:請在 `app/test/config.test.js` 中增加針對非法字元、超過 100 字元的字串段,以及空字串的測試案例,確保 `assertRepository` 的防禦性功能完全被驗證。
Outdated
Review

嚴重等級🟡 警告
審查員:Mage
問題:在 assertRepository 中,parts.every 僅檢查了單一區段長度,未對總體長度(owner/repo)做任何限制。
建議:建議在分割前,先對 value 的總長度進行合理範圍的限制。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `assertRepository` 中,`parts.every` 僅檢查了單一區段長度,未對總體長度(`owner/repo`)做任何限制。 **建議**:建議在分割前,先對 `value` 的總長度進行合理範圍的限制。
if (!valid) {
Ghost marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔴 嚴重
審查員:Mage
問題:在 assertRepository 函數中,part !== '.' && part !== '..' 的檢查雖然能防止路徑穿越,但正規表達式 ^[A-Za-z0-9._-]+$ 仍然允許 . 開頭或結尾的檔案名稱(例如 .foofoo.),這在某些檔案系統中可能導致意外行為或被視為隱藏檔案,且未明確禁止僅由 . 組成的名稱(儘管後面有手動排除)。
建議:建議調整正規表達式,禁止名稱以點號開頭或結尾,例如使用 /^[A-Za-z0-9_-]+([.][A-Za-z0-9_-]+)*$/ 來確保點號只能存在於中間。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:在 `assertRepository` 函數中,`part !== '.' && part !== '..'` 的檢查雖然能防止路徑穿越,但正規表達式 `^[A-Za-z0-9._-]+$` 仍然允許 `.` 開頭或結尾的檔案名稱(例如 `.foo` 或 `foo.`),這在某些檔案系統中可能導致意外行為或被視為隱藏檔案,且未明確禁止僅由 `.` 組成的名稱(儘管後面有手動排除)。 **建議**:建議調整正規表達式,禁止名稱以點號開頭或結尾,例如使用 `/^[A-Za-z0-9_-]+([.][A-Za-z0-9_-]+)*$/` 來確保點號只能存在於中間。
Outdated
Review

嚴重等級🟡 警告
審查員:Leo
問題:在 assertRepository 函式中使用了硬編碼的 100 作為長度限制,這是一個魔術數字,難以維護且無法從環境配置中調整。
建議:建議將 100 抽離為常數(例如 MAX_REPO_NAME_LENGTH),提高可維護性。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:在 `assertRepository` 函式中使用了硬編碼的 `100` 作為長度限制,這是一個魔術數字,難以維護且無法從環境配置中調整。 **建議**:建議將 `100` 抽離為常數(例如 `MAX_REPO_NAME_LENGTH`),提高可維護性。
Outdated
Review

嚴重等級🔵 建議
審查員:Bard
問題:正規表達式 /^[A-Za-z0-9._-]+$/ 描述稍顯冗長,且未利用字元類別的特性,略顯不夠優雅。
建議:考慮簡化寫法,或確保該格式驗證邏輯與專案其他地方一致並抽取為常數。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:正規表達式 `/^[A-Za-z0-9._-]+$/` 描述稍顯冗長,且未利用字元類別的特性,略顯不夠優雅。 **建議**:考慮簡化寫法,或確保該格式驗證邏輯與專案其他地方一致並抽取為常數。
Outdated
Review

嚴重等級🔵 建議
審查員:Leo
問題:驗證邏輯 parts.every 內包含過多條件,邏輯稍顯複雜,閱讀起來需要花點時間拆解。
建議:將 parts.every 的回呼函數提取為具名函數(例如 isValidPart),以提升代碼的可讀性與未來維護的清晰度。

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:驗證邏輯 parts.every 內包含過多條件,邏輯稍顯複雜,閱讀起來需要花點時間拆解。 **建議**:將 parts.every 的回呼函數提取為具名函數(例如 isValidPart),以提升代碼的可讀性與未來維護的清晰度。
Review

嚴重等級🔵 建議
審查員:Leo
問題:在 assertGiteaRepositoryFormat 函式中,當 value.split('/') 的長度不為 2 時,拋出的錯誤訊息僅籠統地說「格式錯誤」。若使用者輸入了包含多個斜線或完全沒有斜線的字串,這類訊息對修正環境變數幫助有限。
建議:建議區分「格式不符」與「內容不符」的錯誤細節,例如提示「必須為 owner/repo 格式,包含一個斜線」。

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:在 `assertGiteaRepositoryFormat` 函式中,當 `value.split('/')` 的長度不為 2 時,拋出的錯誤訊息僅籠統地說「格式錯誤」。若使用者輸入了包含多個斜線或完全沒有斜線的字串,這類訊息對修正環境變數幫助有限。 **建議**:建議區分「格式不符」與「內容不符」的錯誤細節,例如提示「必須為 owner/repo 格式,包含一個斜線」。
throw new Error(`${name} 格式錯誤,必須為 owner/repo`);
}
Ghost marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🟡 警告
審查員:Rogue
問題:在 every 迴圈中重複宣告與編譯 Regex /^[A-Za-z0-9._-]+$/,若該函數被頻繁呼叫,將造成無謂的 CPU 與記憶體浪費。
建議:將 Regex 移至函數外層宣告為常數。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:在 `every` 迴圈中重複宣告與編譯 Regex `/^[A-Za-z0-9._-]+$/`,若該函數被頻繁呼叫,將造成無謂的 CPU 與記憶體浪費。 **建議**:將 Regex 移至函數外層宣告為常數。
Review

嚴重等級🟡 警告
審查員:Bard
問題:函數 assertRepository 命名較為通用,但其實際行為僅在驗證 Gitea 的 owner/repo 格式。命名未能直接體現其檢查邏輯與該領域規則。
建議:建議重新命名為 assertGiteaRepositoryFormat,讓開發者一眼就能看出該函數在檢查特定的 Gitea 倉庫格式規範。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:函數 `assertRepository` 命名較為通用,但其實際行為僅在驗證 Gitea 的 `owner/repo` 格式。命名未能直接體現其檢查邏輯與該領域規則。 **建議**:建議重新命名為 `assertGiteaRepositoryFormat`,讓開發者一眼就能看出該函數在檢查特定的 Gitea 倉庫格式規範。
}
/**
* 從環境變數載入並驗證執行所需的設定。
*
* GITEA_SERVER_URL 與 GITEA_REPOSITORY 為必填,未設定時會拋出錯誤;GITEA_SERVER_URL
* 另需為合法的 http/https URLGITEA_TOKEN 為非必填,未設定時為 null;IS_BETA 會被正規化為布林值。
* 另需為合法的 http/https URLGITEA_REPOSITORY 另需為 owner/repo 格式;
* GITEA_TOKEN 為非必填,未設定時為 null;IS_BETA 會被正規化為布林值。
*
* @param {Object} [env=process.env] - 環境變數來源物件,預設為 process.env。
* @returns {{ serverUrl: string, repository: string, token: (string|null), isBeta: boolean }} 已驗證的設定物件。
* @throws {Error} 當 GITEA_SERVER_URL 或 GITEA_REPOSITORY 未設定,或 GITEA_SERVER_URL 非合法 http/https URL 時拋出。
* @throws {Error} 當必填項未設定、GITEA_SERVER_URL 非合法 http/https URL,或 GITEA_REPOSITORY 非 owner/repo 格式時拋出。
*/
function loadConfig(env = process.env) {
const serverUrl = requireEnv('GITEA_SERVER_URL', env.GITEA_SERVER_URL);
assertHttpUrl('GITEA_SERVER_URL', serverUrl);
const repository = requireEnv('GITEA_REPOSITORY', env.GITEA_REPOSITORY);
assertRepository('GITEA_REPOSITORY', repository);
const token = isUnset(env.GITEA_TOKEN) ? null : env.GITEA_TOKEN;
const isBeta = normalizeBetaFlag(env.IS_BETA);
+3 -1
View File
1
@@ -63,7 +63,9 @@ async function fetchReleases(baseUrl, options = {}) {
try {
pageJson = JSON.parse(text);
} catch {
throw new Error(`release API 回傳資料無法解析 (page=${page})`);
// 附上截斷的回傳內容片段,便於除錯回傳格式異常
const snippet = text.slice(0, 200);
Ghost marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔵 建議
審查員:Leo
問題:硬編碼了截斷字串長度 200,這是一個魔術數字,若未來需要調整截斷長度以容納更多偵錯資訊或減少輸出,維護者需要直接修改此處邏輯。
建議:建議定義一個具名的常數(例如 API_ERROR_SNIPPET_LENGTH),讓此數字具備語意,且未來調整時只需改動一處。

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:硬編碼了截斷字串長度 `200`,這是一個魔術數字,若未來需要調整截斷長度以容納更多偵錯資訊或減少輸出,維護者需要直接修改此處邏輯。 **建議**:建議定義一個具名的常數(例如 `API_ERROR_SNIPPET_LENGTH`),讓此數字具備語意,且未來調整時只需改動一處。
throw new Error(`release API 回傳資料無法解析 (page=${page}): ${snippet}`);
Ghost marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔵 建議
審查員:Assassin
問題:將未經清洗的 API 回應片段直接納入 Error 物件,若 API 返回敏感資訊(內部路徑、堆疊追蹤、使用者資料),可能導致敏感資訊外洩。
建議:錯誤報告應僅包含概括性的描述,若必須包含內容片段,務必先對片段進行內容脫敏或格式化處理。

**嚴重等級**:🔵 建議 **審查員**:Assassin **問題**:將未經清洗的 API 回應片段直接納入 `Error` 物件,若 API 返回敏感資訊(內部路徑、堆疊追蹤、使用者資料),可能導致敏感資訊外洩。 **建議**:錯誤報告應僅包含概括性的描述,若必須包含內容片段,務必先對片段進行內容脫敏或格式化處理。
}
Ghost marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🟡 警告
審查員:Maya
問題:雖然實作了針對多位元組字元的截斷處理邏輯,但目前的測試案例僅使用 ASCII 字元('x'),缺乏對於包含多位元組字元(如 Emoji 或特殊符號)的真實邊界情境驗證,無法確保在截斷邊界處不會產生亂碼或非預期的行為。
建議:請在 app/test/releases.test.js 中新增一個測試案例,使用包含多位元組字元(例如 Emoji 或代理對字元)的長字串作為 fetch 回應內容,並驗證截斷後的內容片段是否完整且無亂碼。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:雖然實作了針對多位元組字元的截斷處理邏輯,但目前的測試案例僅使用 ASCII 字元('x'),缺乏對於包含多位元組字元(如 Emoji 或特殊符號)的真實邊界情境驗證,無法確保在截斷邊界處不會產生亂碼或非預期的行為。 **建議**:請在 app/test/releases.test.js 中新增一個測試案例,使用包含多位元組字元(例如 Emoji 或代理對字元)的長字串作為 fetch 回應內容,並驗證截斷後的內容片段是否完整且無亂碼。
Ghost marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🟡 警告
審查員:Leo
問題:為了處理多位元組字元而使用 Array.from(text),如果回傳的錯誤內容(text)非常巨大(例如幾十 MB 的 HTML 錯誤頁面),會瞬間配置大量記憶體,可能導致效能瓶頸。
建議:若預期回應可能非常大,建議先檢查 text.length 是否遠大於限制,或是考慮使用更輕量的字串處理方式來避免不必要的記憶體配置。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:為了處理多位元組字元而使用 Array.from(text),如果回傳的錯誤內容(text)非常巨大(例如幾十 MB 的 HTML 錯誤頁面),會瞬間配置大量記憶體,可能導致效能瓶頸。 **建議**:若預期回應可能非常大,建議先檢查 text.length 是否遠大於限制,或是考慮使用更輕量的字串處理方式來避免不必要的記憶體配置。
Outdated
Review

嚴重等級🔵 建議
審查員:Leo
問題:在錯誤訊息中進行字串截斷處理,程式碼寫得較為複雜,使用了 Array.from(boundedText).slice(...)。雖然考慮了 UTF-16 碼元問題,但這段邏輯與核心業務功能(fetchReleases)混在一起,未來維護者若需調整錯誤訊息格式,容易誤傷功能。
建議:將此段截斷邏輯抽取為一個獨立的工具函式,例如 truncateString(text, limit),放在 utils 檔案中以提升可讀性與測試獨立性。

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:在錯誤訊息中進行字串截斷處理,程式碼寫得較為複雜,使用了 Array.from(boundedText).slice(...)。雖然考慮了 UTF-16 碼元問題,但這段邏輯與核心業務功能(fetchReleases)混在一起,未來維護者若需調整錯誤訊息格式,容易誤傷功能。 **建議**:將此段截斷邏輯抽取為一個獨立的工具函式,例如 truncateString(text, limit),放在 utils 檔案中以提升可讀性與測試獨立性。
if (pageJson === null) {
1