Files
CFDivePlatform/openspec/changes/auth-tests/design.md
T
a620906209 6f446f601f test(auth): 新增三角色登入/註冊/登出、P2 帳號鎖定、OAuth state 驗證測試
新增 34 個 Feature 測試(AuthLoginTest 19、AuthLockoutTest 11、
AuthOAuthTest 4)涵蓋 member/provider/admin 的註冊、登入、登出與跨角色
隔離;P2 帳號鎖定的 Fixed Window 計數、companion cache key 與 email
正規化;以及 P2 OAuth state 的 CSRF 驗證與一次性消耗。

同時新增 .gitea/workflows/test.yml 在 push/PR 時於 CI 自動執行測試
(含 GD 等擴充套件安裝),並補上對應的 OpenSpec 變更文件
(proposal/design/spec/tasks,34 項驗收情境與測試 1:1 對應)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 04:01:33 +08:00

3.9 KiB
Raw Blame History

Context

Auth 模組已實作三角色(member / provider / admin)的登入、註冊、登出,以及 P2 安全強化(帳號鎖定、OAuth state 驗證)。目前 tests/Feature/ 只有 AuthRateLimitTestthrottle)和 TokenRefreshTestrefresh token),主流程與安全邏輯完全無覆蓋。

測試環境:phpunit.xml 已設定 DB_CONNECTION=sqliteDB_DATABASE=:memory:Cache 使用 array driverQueue 使用 sync

Goals / Non-Goals

Goals:

  • 補齊登入/註冊/登出三角色的 happy path 與 guard 測試
  • 覆蓋 P2 帳號鎖定邏輯(Fixed Window、Email 正規化、role 隔離)
  • 覆蓋 P2 OAuth state 驗證(state 缺失 / 不符 / 正確)
  • 每個測試檔案獨立、可單獨執行

Non-Goals:

  • 不測試 Token Refresh(已有 TokenRefreshTest
  • 不測試 Rate Limiting(已有 AuthRateLimitTest
  • 不測試前端 Vue 元件
  • 不測試 Admin CRUD(屬於別的 change 範疇)

Decisions

決策 1:三支測試檔案,各自對應一個關注點

  • AuthLoginTest.php:登入 / 註冊 / 登出三角色的 happy path + guard
  • AuthLockoutTest.phpP2 帳號鎖定邏輯
  • AuthOAuthTest.phpP2 OAuth state 驗證

理由:單一檔案職責清晰,出錯時可快速定位;不同測試的 setUp 需求差異大(Lockout 需 Cache::flush、OAuth 需 Socialite mock)。

決策 2Lockout 測試使用 Cache facade,不 mock

phpunit.xml 已設 CACHE_STORE=arrayCache 行為完全真實。 setUp() 呼叫 Cache::flush() 確保測試間隔離。

理由Lockout 邏輯的 bug 通常出在 Cache key 命名或 TTL 操作,用真實 array cache 才能抓到;mock 只會讓測試通過而 bug 繼續存在。

決策 3OAuth 測試使用 Socialite mock,並以 shouldNotReceive 驗證安全不變式

OAuth callback 依賴 Google 回應,測試環境無法真實呼叫,使用 Socialite Facade mock。

Mock 在 OAuth 測試中有兩個用途:

  • state 正確時shouldReceive('driver->user') 回傳假 Google user,讓登入流程走完
  • state 缺失 / 不符時shouldNotReceive('driver->user') 確認 Socialite 不被呼叫

第二個用途是這組測試的核心安全不變式:CSRF 檢查失敗時,系統不得向 OAuth provider 取 user。如果只斷言 redirect URL 含 error=oauth_failed 而不驗證 Socialite 未被呼叫,測試無法偵測「state 檢查失敗但仍繼續執行 OAuth 流程」這類回歸。

理由OAuth state 的設計意圖不只是「最後回錯誤」,而是在 CSRF 檢查點提前中止整個流程;shouldNotReceive 是唯一能驗證這個 invariant 的手段。

決策 4:共用 helper 方法直接定義在各測試 class 內

三支測試各自有 createMember()createProvider()createAdmin() 等 helper。

理由:測試共 3 個檔案,規模不大,不值得抽 BaseTestCase。一旦需要改 helper 邏輯,在各自檔案改更直觀,不會影響其他測試。

Risks / Trade-offs

  • OAuth redirect 比對handleGoogleCallback 最終用 redirect() 回前端 URL,測試需用 assertRedirect() 而非 assertStatus()URL 中含 error=oauth_failed 做斷言。→ 使用 assertRedirectContains('error=oauth_failed') 避免硬寫前端 URL。
  • Lockout TTL 在測試中array cache 不適合可靠驗證時間流逝後的自動過期,因此不測 cache driver 的 TTL 行為本身。測試只驗證「當 lockout cache entry 不存在時,登入流程恢復允許」;清除狀態以 Cache::forget() 模擬,不等待真實時間。這不等同於「TTL 到期自動解鎖」的驗證,兩者不可混淆。
  • Admin 帳號在鎖定測試的範圍AuthController 的 loginAdmin 沒有帳號鎖定邏輯(P2 只加了 member/provider),不在 AuthLockoutTest 測試範圍。→ 在 AuthLoginTest 只測 Admin 的 happy path 與角色 guard。

Open Questions

  • 無(設計已足夠完整開始實作)