Files
CFDivePlatform/openspec/changes/auth-tests/specs/auth-test-coverage/spec.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

152 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## ADDED Requirements
### Requirement: Auth 登入/註冊/登出測試覆蓋(三角色)
測試套件 SHALL 對 member、provider、admin 三個角色各自驗證以下場景,確保回歸時能即時偵測。
#### Scenario: Member 註冊成功
- **WHEN** 送出有效的 name / email / password / password_confirmation 至 `POST /api/member/register`
- **THEN** 回傳 HTTP 201body 包含 `{ status: true, data: { user } }`DB 存在對應 member 用戶
#### Scenario: Member 重複 Email 註冊失敗
- **WHEN** 送出已存在的 email 至 `POST /api/member/register`
- **THEN** 回傳 HTTP 422
#### Scenario: Member 登入成功
- **WHEN** 送出正確的 email / password 至 `POST /api/member/login`
- **THEN** 回傳 HTTP 200body 包含 `{ status: true, data: { token, token_type: "Bearer", user } }`
#### Scenario: Member 登入密碼錯誤
- **WHEN** 送出存在的 email 但錯誤的 password
- **THEN** 回傳 HTTP 401`{ status: false, message: "電子郵件或密碼錯誤" }`
#### Scenario: Member 非活躍帳號拒絕登入
- **WHEN** 送出 is_active=false 帳號的正確帳密
- **THEN** 回傳 HTTP 403
#### Scenario: Provider 帳號無法呼叫 Member 登入
- **WHEN** role=provider 的帳號嘗試 `POST /api/member/login`
- **THEN** 回傳 HTTP 401`{ status: false, message: "電子郵件或密碼錯誤" }`(查詢以 role 過濾,跨角色帳號視同不存在,不洩漏帳號實際所屬角色)
#### Scenario: Member 登出成功
- **WHEN** 已登入 Member 送出 `POST /api/member/logout`(帶有效 Bearer token
- **THEN** 回傳 HTTP 200token 被撤銷(後續以此 token 請求受保護資源應得 401)
#### Scenario: Provider 登出成功
- **WHEN** 已登入 Provider 送出 `POST /api/provider/logout`(帶有效 Bearer token
- **THEN** 回傳 HTTP 200token 被撤銷(後續以此 token 請求受保護資源應得 401)
#### Scenario: Admin 登出成功
- **WHEN** 已登入 Admin 送出 `POST /api/admin/logout`(帶有效 Bearer token
- **THEN** 回傳 HTTP 200token 被撤銷(後續以此 token 請求受保護資源應得 401)
#### Scenario: Provider 帳號無法呼叫 Admin 登入
- **WHEN** role=provider 的帳號嘗試 `POST /api/admin/login`
- **THEN** 回傳 HTTP 401`{ status: false, message: "電子郵件或密碼錯誤" }`(查詢以 role 過濾,跨角色帳號視同不存在)
#### Scenario: Provider 註冊成功
- **WHEN** 送出有效的 name / email / password / password_confirmation 至 `POST /api/provider/register`
- **THEN** 回傳 HTTP 201body 包含 `{ status: true, data: { user } }`DB 存在對應 provider 用戶
#### Scenario: Provider 重複 Email 註冊失敗
- **WHEN** 送出已存在的 email 至 `POST /api/provider/register`
- **THEN** 回傳 HTTP 422
#### Scenario: Provider 登入成功
- **WHEN** 送出正確的 email / password 至 `POST /api/provider/login`
- **THEN** 回傳 HTTP 200body 包含 `{ status: true, data: { token, token_type: "Bearer", user } }`
#### Scenario: Provider 非活躍帳號拒絕登入
- **WHEN** 送出 is_active=false 的 provider 帳號正確帳密
- **THEN** 回傳 HTTP 403
#### Scenario: Member 帳號無法呼叫 Provider 登入
- **WHEN** role=member 的帳號嘗試 `POST /api/provider/login`
- **THEN** 回傳 HTTP 401`{ status: false, message: "電子郵件或密碼錯誤" }`(查詢以 role 過濾,跨角色帳號視同不存在)
#### Scenario: Admin 註冊成功
- **WHEN** 送出有效的 name / email / password / password_confirmation 至 `POST /api/admin/register`
- **THEN** 回傳 HTTP 201body 包含 `{ status: true, data: { user } }`DB 存在對應 admin 用戶
#### Scenario: Admin 重複 Email 註冊失敗
- **WHEN** 送出已存在的 email 至 `POST /api/admin/register`
- **THEN** 回傳 HTTP 422
#### Scenario: Admin 登入成功
- **WHEN** 送出正確的 email / password 至 `POST /api/admin/login`
- **THEN** 回傳 HTTP 200body 包含 `{ status: true, data: { token, token_type: "Bearer", user } }`
#### Scenario: Admin 登入非 admin 帳號失敗
- **WHEN** role=member 帳號嘗試 `POST /api/admin/login`
- **THEN** 回傳 HTTP 401`{ status: false, message: "電子郵件或密碼錯誤" }`(查詢以 role 過濾,跨角色帳號視同不存在)
---
### Requirement: 帳號鎖定測試覆蓋(P2 Fixed Window
測試套件 SHALL 驗證帳號鎖定邏輯的全部關鍵路徑,以防止回歸破壞安全機制。
#### Scenario: 失敗次數未達閾值不鎖定
- **WHEN** 同一帳號連續登入失敗 4 次
- **THEN** 每次均回傳 HTTP 401,帳號未鎖定
#### Scenario: 第 5 次失敗當場觸發鎖定
- **WHEN** 同一帳號連續登入失敗第 5 次
- **THEN** 回傳 HTTP 423body 包含 `{ status: false, locked_until: "<ISO8601>" }`
#### Scenario: 鎖定期間任何登入均被拒絕
- **WHEN** 帳號已鎖定,使用者送出(含正確密碼的)登入請求
- **THEN** 回傳 HTTP 423,不驗證密碼
#### Scenario: 不存在帳號不累計失敗計數
- **WHEN** 以不存在的 email 連續嘗試登入 10 次
- **THEN** 每次均回傳 HTTP 401Cache 中不建立計數 key,第 10 次後帳號不鎖定
#### Scenario: 登入成功後清除失敗計數
- **WHEN** 帳號失敗 3 次後成功登入,再失敗 4 次
- **THEN** 失敗計數從成功後重新累積,第 4 次仍回傳 401(不觸發鎖定)
#### Scenario: Email 大小寫視為同一帳號
- **WHEN** 同一帳號分別用 `user@example.com` 失敗 3 次、`User@Example.COM` 失敗 2 次
- **THEN** 第 5 次失敗(累計)觸發鎖定,回傳 HTTP 423
#### Scenario: Email 前後空白視為同一帳號
- **WHEN** 同一帳號分別用 `user@example.com` 失敗 3 次、` user@example.com `(含前後空白)失敗 2 次
- **THEN** 第 5 次失敗(累計)觸發鎖定,回傳 HTTP 423
#### Scenario: 鎖定 cache entry 不存在時帳號恢復可登入
- **WHEN** 帳號已鎖定,鎖定的 cache entry 被移除(模擬 TTL 自然過期)
- **THEN** 帳號可正常登入,回傳 HTTP 200
#### Scenario: 跨角色嘗試不會汙染或鎖定正確角色帳號
> 註:原情境假設「同一 email 同時擁有 member 與 provider 帳號」,但 `users.email` 在 DB 層級為全域 unique`database/migrations/2025_05_06_065906_create_orgin_table.php:18`),此前提不可能成立,故改寫為下列可達成、且驗證相同安全性質(角色間鎖定計數互相隔離)的版本。
- **WHEN** 一個僅存在 provider 帳號的 email,先對 `/api/member/login` 送出 4 次失敗嘗試,再對 `/api/provider/login` 送出 4 次失敗嘗試
- **THEN** member namespace 不建立計數 key(帳號不存在於該 namespace);provider namespace 計數累積為 4、未達閾值不鎖定;provider 帳號仍可用正確密碼正常登入
#### Scenario: Fixed Window — 失敗不延長 TTL
- **WHEN** 帳號失敗 4 次後,第 4 次失敗發生於 TTL 將到期前
- **THEN** Cache TTL 不被重設,`locked_until` 仍為第一次失敗時寫入的時間
#### Scenario: `locked_until` 來自 companion key
- **WHEN** 帳號觸發鎖定,回傳 HTTP 423
- **THEN** response body 的 `locked_until` 等於 `login_expires_at:{role}:{email}` cache key 的值
---
### Requirement: OAuth State 驗證測試覆蓋(P2
測試套件 SHALL 驗證 OAuth callback 的 state 比對邏輯,涵蓋正常、缺失、不符三種情境。
#### Scenario: State 缺失時 redirect 至錯誤頁,且不呼叫 Socialite
- **WHEN** 直接呼叫 `GET /auth/google/callback` 而不帶 `state` 參數(session 無 `oauth_state`
- **THEN** 後端 redirect 至包含 `error=oauth_failed` 的 URL,不繼續登入流程;Socialite 的 `user()` 不被呼叫
#### Scenario: State 不符時 redirect 至錯誤頁,且不呼叫 Socialite
- **WHEN** callback 帶 `state=wrong_value`,但 session 中 `oauth_state` 為不同值
- **THEN** 後端 redirect 至包含 `error=oauth_failed` 的 URLSocialite 的 `user()` 不被呼叫
#### Scenario: State 正確時完成 OAuth 登入
- **WHEN** callback 帶的 `state` 與 session 中 `oauth_state` 一致,且 Socialite 回傳有效 Google user
- **THEN** 後端完成登入,redirect 至前端並附帶 tokenURL fragment `#token=...`
#### Scenario: State 成功驗證後從 session 消耗
- **WHEN** callback 帶正確 `state` 完成一次登入後,再次以相同 `state` 呼叫 callback
- **THEN** session 中已無 `oauth_state`,視為 state 缺失,redirect 至 `error=oauth_failed`