❯❯ 綠燈的邊界:R2 CRC32、跨租戶越權、CI 沒跑 unit:三個真實事故
📍 流水線位置|入口 → flow → 型別 → mock → 【測試】 → UI → vibe → 上線(綠燈的邊界)

上一篇把自動化 E2E 測試定為不說謊的系統主規格。但測試能驗證的,終究只有被明確寫進斷言的條款;那些沒被涵蓋的邊界條件與環境變數,它一概無法保證。
最可怕的不是測試沒過,而是測試全部綠燈,生產環境卻依然炸了。
以下是開發過程中發生的三起真實事故:生產環境功能失效、內部稽核挖出的安全漏洞,以及 CI 管道組態缺失。這三起案例在問題爆發前,E2E 自動化測試全都高掛綠燈。
在部署至生產環境後,圖片上傳介面(包含交通資訊圖、賓客祝福照片與喜餅圖檔)突然全面回傳 HTTP 400 失敗,使用者反覆重試也無法完成上傳。
明明測試全過,為什麼線上會爆掉?
仔細排查後發現,根因並非產品自身的業務邏輯出錯,而是第三方套件的預設行為變更:@aws-sdk/client-s3 套件自 3.729.0 版本起,預設對 PutObjectCommand 自動計算並附加 CRC32 校驗和(Checksum)。當後端在沒有 Request Body 的情況下生成 Presigned URL 時,SDK 便自動計算了「空內容」的校驗和,並將其一併簽入 URL 參數中。
結果,當前端拿著這個 Presigned URL 向 Cloudflare R2 發起實體檔案上傳時,Cloudflare R2 一比對 Header 與 Payload,發現實體檔案內容與簽名中的 CRC32 校驗和不符,當場拒絕請求並回傳 HTTP 400 錯誤。
那麼,E2E 測試為什麼沒抓到?因為在測試環境中, r2Enabled 組態設定為false,測試資料流走的是本地的 Base64 模擬路徑(Data URL),根本沒有對 R2 儲存服務發起真實的網路請求。
這種 Mock 設計本意是為了降低測試對外部雲端服務的相依性,但也帶來了意外的的代價:真實環境下的實體傳送路徑,在測試鏈結中完全處於未驗證狀態。(最終修復方案是調整 SDK 組態,將校驗和計算改為只有在必要時才觸發。)
第二個事故是在全系統的安全稽核期間發現的。早期撰寫的部分 API Handler 只有依據 Request 傳入的單一 ID 進行 Drizzle ORM 資料庫查詢,卻完全沒有比對該筆資料是否隸屬於當前 Session 所在的婚禮租戶(weddingId)。
這意味著什麼?只要是已登入的使用者,只需枚舉或推測資料 ID(例如測試資料常採用的 guest-001 格式),就能直接跨租戶存取、甚至修改與刪除其他婚禮的賓客與系統資料。
在該 Issue 追蹤紀錄中包含以下明確結論:
lint / typecheck / 211 條 e2e 全綠也抓不到此類問題。
E2E 測試之所以對這個漏洞完全失敏,主因有二:
驗證機制未啟動:
凍結的主規格測試套件預設運行於 authMode=open 模式下,並沒有模擬實體權限校驗。
多租戶狀態缺失:
自動化測試環境中只有注入單一婚禮租戶的資料,缺乏「多租戶同時存在且持 A 身分存取 B 資料」的邊界條件。
這個安全隱患在單租戶測試環境下被完美掩蓋,直到系統啟用「管理員代建帳號」、正式進入多租戶並存階段後,才暴露出這個高風險的越權漏洞。
在 main 主分支上,其實有 5 顆單元測試(Unit Test)長期默默亮著紅燈,但我第一時間完全沒有察覺。
排查後才發現,問題出在 CI/CD 管道(GitHub Actions)的設定檔裡,我只建置了 build、eslint 與自訂規格審查流程,卻忘了將 test:unit 指令納入 Pull Request 的必經檢查點。
測試程式碼明明早就寫好,也抓得到錯誤,卻因為沒有被整合進構建管道而失去防護效用。
後續追查發現,這 5 顆單元測試之所以失敗,是因為環境設定發生了「漂移(Drift)」:測試腳本假設的 apiBase 路徑前綴與專案現行預設值脫節,導致請求全數打在不存在的路由而回傳 HTTP 404。修復方式只需在測試環境中補上固定 apiBase 即可,產品本體程式碼並沒有缺陷。然而,就因為 CI 管道出現了這個組態缺口,這 5 顆隱形紅燈就在主分支上被忽視了許久。
綜合上述三起案例,歸納自動化測試失效的根本原因:
| 事故案例 | E2E 綠燈為什麼失效 |
|---|---|
| R2 圖片上傳失效 | 測試環境將實體雲端服務路徑抽象化(r2Enabled=false),導致真實上傳路徑未被覆蓋。 |
| 跨租戶越權漏洞 | 測試環境缺乏多租戶並存的資料條件,且執行於開放權限模式(authMode=open)。 |
| Unit 測試失敗未察 | 單元測試腳本未被配置於 CI/CD 管道的強制檢核節點(Gatekeeper)中。 |

上述問題本質上都是測試守備範圍(Test Scope)與環境條件的侷限,而不是測試工具本身有錯。自動化測試的綠燈,永遠只代表「在當前的測試環境與斷言條件下,系統表現符合預期」,絕不等於生產環境的絕對可靠。
針對這些測試盲點,我隨後補強了相對應的防守機制:
針對外部服務相依性:
建立專屬單元測試(test/unit/r2-presign.spec.ts),直接斷言 Presigned URL 產出結果不得包含校驗和參數,避免 SDK 版本升級再次引入預設行為更迭。
針對授權與權限邊界:
徹底清查 Handler 層的租戶檢查邏輯,於 33 支 API 端點補強 weddingId 約束條件,並將跨租戶存取攔截測試(security-tenant-scope-1.spec.ts)納入 Vibe 測試區進行持續監控。
針對 CI 流水線缺口:
修訂 GitHub Actions 工作流程,將單元測試納入 PR 合併前的強制阻擋條件(Blocker),並將執行順序調整至 build 步驟之前。

經歷這一輪邊界問題的修復與驗證,我對測試燈號的本質建立了更精確的工程認知:
紅燈非常直接,精準指著一個確定存在的壞點;綠燈卻更像是沉默,它對守備範圍之外的世界一字不提。將系統的沉默誤認品品質的承諾,是我過去的盲點。
下一篇將回到建構面:既然綠燈的可靠性完全取決於「我們究竟要求它驗證什麼」,那麼在自動化流水線中,測試合約(Test Contract)究竟該如何設計,才能精確且極大化綠燈的架構價值?
🎒 最小一步|打開你專案的 CI 設定檔(如
.github/workflows/),清查實際執行的測試指令,看有沒有已寫好的測試套件(Unit、Integration)根本沒被納入 PR 阻擋。
📎 本篇證據|Issue #50(R2 CRC32 checksum 異動分析)、Issue #48(跨婚禮越權存取漏洞檢討報告)、Issue #76(CI 流水線漏未執行test:unit檢討紀錄)