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 14 additions and 7 deletions
Showing only changes of commit 4aa1393af2 - Show all commits
+10 -5
View File
@@ -59,13 +59,18 @@ function assertHttpUrl(name, value) {
const MAX_REPO_NAME_LENGTH = 100;
const REPO_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
// 驗證 repository 為 owner/repo 格式(拒絕 . .. 路徑穿越段、空段、過長或含非法字元的段)
// 單一 repository 區段是否合法:長度在範圍內、僅含允許字元,且非 . .. 路徑穿越段
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` 的總長度進行合理範圍的限制。
function isValidRepoSegment(part) {
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 格式,包含一個斜線」。
return part.length > 0
&& part.length <= MAX_REPO_NAME_LENGTH
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 倉庫格式規範。
&& REPO_SEGMENT_PATTERN.test(part)
&& part !== '.' && part !== '..';
}
// 驗證 repository 為 owner/repo 格式(恰兩段,且每段皆為合法區段)
function assertRepository(name, value) {
const parts = value.split('/');
const isRepoFormatValid = parts.length === 2
&& parts.every((part) => part.length > 0 && part.length <= MAX_REPO_NAME_LENGTH
&& REPO_SEGMENT_PATTERN.test(part)
&& part !== '.' && part !== '..');
const isRepoFormatValid = parts.length === 2 && parts.every(isValidRepoSegment);
if (!isRepoFormatValid) {
throw new Error(`${name} 格式錯誤,必須為 owner/repo`);
}
+4 -2
View File
1
@@ -66,8 +66,10 @@ async function fetchReleases(baseUrl, options = {}) {
try {
pageJson = JSON.parse(text);
Ghost marked this conversation as resolved Outdated
Outdated
Review

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

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

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

**嚴重等級**:🔵 建議 **審查員**:Assassin **問題**:將未經清洗的 API 回應片段直接納入 `Error` 物件,若 API 返回敏感資訊(內部路徑、堆疊追蹤、使用者資料),可能導致敏感資訊外洩。 **建議**:錯誤報告應僅包含概括性的描述,若必須包含內容片段,務必先對片段進行內容脫敏或格式化處理。
// 以字元(而非 UTF-16 碼元)截斷回傳內容片段,避免拆分多位元組字元造成亂碼
const contentSnippet = Array.from(text).slice(0, API_ERROR_SNIPPET_LENGTH).join('');
// 以 UTF-16 長度粗略上限截斷(避免將可能極大的回應整個陣列化),
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 回應內容,並驗證截斷後的內容片段是否完整且無亂碼。
// 再以字元(而非 UTF-16 碼元)精準截斷,避免拆分多位元組字元造成亂碼
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 檔案中以提升可讀性與測試獨立性。
const boundedText = text.slice(0, API_ERROR_SNIPPET_LENGTH * 2);
const contentSnippet = Array.from(boundedText).slice(0, API_ERROR_SNIPPET_LENGTH).join('');
Ghost marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🟡 警告
審查員:Mage
問題:雖然使用 Array.from(text) 可以正確處理多位元組字元(避免在 Unicode code point 中間截斷),但此操作會將整個回應內容轉為陣列,若 API 回傳的錯誤回應內容極大(例如數 MB 的 HTML 錯誤頁面),這會導致嚴重的記憶體耗用,甚至造成執行緒卡頓或記憶體溢位 (OOM)。
建議:應先對 text 進行長度檢查,或者改用 Bufferstring.substring() 搭配適當的 Unicode 感知邏輯來截斷,而不是將完整內容陣列化。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:雖然使用 `Array.from(text)` 可以正確處理多位元組字元(避免在 Unicode code point 中間截斷),但此操作會將整個回應內容轉為陣列,若 API 回傳的錯誤回應內容極大(例如數 MB 的 HTML 錯誤頁面),這會導致嚴重的記憶體耗用,甚至造成執行緒卡頓或記憶體溢位 (OOM)。 **建議**:應先對 `text` 進行長度檢查,或者改用 `Buffer` 或 `string.substring()` 搭配適當的 Unicode 感知邏輯來截斷,而不是將完整內容陣列化。
throw new Error(`release API 回傳資料無法解析 (page=${page}),回應內容片段:「${contentSnippet}`);
}