From ca5b8433620fcddfb60ee8b45c0ab3155e302cbf Mon Sep 17 00:00:00 2001 From: Hank Date: Mon, 8 Jun 2026 04:02:05 +0800 Subject: [PATCH] =?UTF-8?q?chore(openspec):=20=E6=AD=B8=E6=AA=94=20auth-te?= =?UTF-8?q?sts=20=E8=AE=8A=E6=9B=B4=E4=B8=A6=E5=BB=BA=E7=AB=8B=20auth-test?= =?UTF-8?q?-coverage=20=E4=B8=BB=E8=A6=8F=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 34 項驗收情境與測試已 1:1 對應且全數通過,將變更移至 openspec/changes/archive/2026-06-07-auth-tests/,並依其內容建立 openspec/specs/auth-test-coverage/spec.md 作為主規格文件。 Co-Authored-By: Claude Sonnet 4.6 --- .../2026-06-07-auth-tests}/.openspec.yaml | 0 .../2026-06-07-auth-tests}/design.md | 0 .../2026-06-07-auth-tests}/proposal.md | 0 .../specs/auth-test-coverage/spec.md | 0 .../2026-06-07-auth-tests}/tasks.md | 0 openspec/specs/auth-test-coverage/spec.md | 155 ++++++++++++++++++ 6 files changed, 155 insertions(+) rename openspec/changes/{auth-tests => archive/2026-06-07-auth-tests}/.openspec.yaml (100%) rename openspec/changes/{auth-tests => archive/2026-06-07-auth-tests}/design.md (100%) rename openspec/changes/{auth-tests => archive/2026-06-07-auth-tests}/proposal.md (100%) rename openspec/changes/{auth-tests => archive/2026-06-07-auth-tests}/specs/auth-test-coverage/spec.md (100%) rename openspec/changes/{auth-tests => archive/2026-06-07-auth-tests}/tasks.md (100%) create mode 100644 openspec/specs/auth-test-coverage/spec.md diff --git a/openspec/changes/auth-tests/.openspec.yaml b/openspec/changes/archive/2026-06-07-auth-tests/.openspec.yaml similarity index 100% rename from openspec/changes/auth-tests/.openspec.yaml rename to openspec/changes/archive/2026-06-07-auth-tests/.openspec.yaml diff --git a/openspec/changes/auth-tests/design.md b/openspec/changes/archive/2026-06-07-auth-tests/design.md similarity index 100% rename from openspec/changes/auth-tests/design.md rename to openspec/changes/archive/2026-06-07-auth-tests/design.md diff --git a/openspec/changes/auth-tests/proposal.md b/openspec/changes/archive/2026-06-07-auth-tests/proposal.md similarity index 100% rename from openspec/changes/auth-tests/proposal.md rename to openspec/changes/archive/2026-06-07-auth-tests/proposal.md diff --git a/openspec/changes/auth-tests/specs/auth-test-coverage/spec.md b/openspec/changes/archive/2026-06-07-auth-tests/specs/auth-test-coverage/spec.md similarity index 100% rename from openspec/changes/auth-tests/specs/auth-test-coverage/spec.md rename to openspec/changes/archive/2026-06-07-auth-tests/specs/auth-test-coverage/spec.md diff --git a/openspec/changes/auth-tests/tasks.md b/openspec/changes/archive/2026-06-07-auth-tests/tasks.md similarity index 100% rename from openspec/changes/auth-tests/tasks.md rename to openspec/changes/archive/2026-06-07-auth-tests/tasks.md diff --git a/openspec/specs/auth-test-coverage/spec.md b/openspec/specs/auth-test-coverage/spec.md new file mode 100644 index 0000000..ca8ae36 --- /dev/null +++ b/openspec/specs/auth-test-coverage/spec.md @@ -0,0 +1,155 @@ +# auth-test-coverage Specification + +## Purpose +TBD - created by archiving change auth-tests. Update Purpose after archive. +## Requirements +### Requirement: Auth 登入/註冊/登出測試覆蓋(三角色) +測試套件 SHALL 對 member、provider、admin 三個角色各自驗證以下場景,確保回歸時能即時偵測。 + +#### Scenario: Member 註冊成功 +- **WHEN** 送出有效的 name / email / password / password_confirmation 至 `POST /api/member/register` +- **THEN** 回傳 HTTP 201,body 包含 `{ 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 200,body 包含 `{ 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 200,token 被撤銷(後續以此 token 請求受保護資源應得 401) + +#### Scenario: Provider 登出成功 +- **WHEN** 已登入 Provider 送出 `POST /api/provider/logout`(帶有效 Bearer token) +- **THEN** 回傳 HTTP 200,token 被撤銷(後續以此 token 請求受保護資源應得 401) + +#### Scenario: Admin 登出成功 +- **WHEN** 已登入 Admin 送出 `POST /api/admin/logout`(帶有效 Bearer token) +- **THEN** 回傳 HTTP 200,token 被撤銷(後續以此 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 201,body 包含 `{ 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 200,body 包含 `{ 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 201,body 包含 `{ 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 200,body 包含 `{ 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 423,body 包含 `{ status: false, locked_until: "" }` + +#### Scenario: 鎖定期間任何登入均被拒絕 +- **WHEN** 帳號已鎖定,使用者送出(含正確密碼的)登入請求 +- **THEN** 回傳 HTTP 423,不驗證密碼 + +#### Scenario: 不存在帳號不累計失敗計數 +- **WHEN** 以不存在的 email 連續嘗試登入 10 次 +- **THEN** 每次均回傳 HTTP 401,Cache 中不建立計數 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` 的 URL;Socialite 的 `user()` 不被呼叫 + +#### Scenario: State 正確時完成 OAuth 登入 +- **WHEN** callback 帶的 `state` 與 session 中 `oauth_state` 一致,且 Socialite 回傳有效 Google user +- **THEN** 後端完成登入,redirect 至前端並附帶 token(URL fragment `#token=...`) + +#### Scenario: State 成功驗證後從 session 消耗 +- **WHEN** callback 帶正確 `state` 完成一次登入後,再次以相同 `state` 呼叫 callback +- **THEN** session 中已無 `oauth_state`,視為 state 缺失,redirect 至 `error=oauth_failed` +