template-monorepo/docs/e2e-testing.md

266 lines
10 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.

# Gateway E2E 測試指南
本文件列出 **所有 HTTP API 測試情境****一鍵跑完整 E2E** 的方式。自動化測試程式在 `test/e2e/`build tag: `e2e`)。
---
## 一鍵完整測試(推薦)
**全新 Docker volume** 開始,依序:起 Mongo/Redis → 建 index → seed 資料 → 起 Gateway → 跑 E2E → **關閉並刪除 volume**
```bash
cd gateway
make e2e-full
```
等同於:
```bash
bash scripts/e2e-run.sh
```
成功時最後一行:`>> E2E OK`
### 流程圖
```mermaid
flowchart LR
A[docker compose down -v] --> B[up mongo + redis]
B --> C[cmd/mongo-index]
C --> D[cmd/e2e-seed]
D --> E[gateway :18888]
E --> F[go test -tags=e2e]
F --> G[stop gateway + down -v]
```
### 其他指令
| 指令 | 用途 |
|------|------|
| `make e2e-up` | 起環境 + seed + Gateway**不跑測試**(本機除錯) |
| `make test-e2e` | 對**已啟動**的 Gateway 跑 E2E需先有 `state.json` |
| `make e2e-casbin` | 以 `Permission.Casbin.Enabled: true` 跑 RBAC reload / deny E2E |
| `make e2e-down` | 停 Gateway + `docker compose down -v` |
| `E2E_KEEP_DOCKER=1 make e2e-full` | 測完**保留** Docker方便查 Mongo/Redis |
### E2E 專用設定
| 檔案 | 說明 |
|------|------|
| `test/e2e/fixtures/e2e.yaml` | Port **18888**、DB **gateway_e2e**、Notification mock、Casbin **關閉** |
| `test/e2e/fixtures/e2e.casbin.yaml` | 同上,但 Casbin **開啟**,搭配 `make e2e-casbin` |
| `test/e2e/fixtures/state.json` | seed 產生的 tenant / uid / JWTgitignore執行後生成 |
---
## 測試覆蓋矩陣(一目瞭然)
圖例:**✅ 自動 E2E** · **⏭ 需 ZITADEL / 手動** · **🔧 基礎設施**
### 基礎設施
| ID | 情境 | 自動 | 測試檔 |
|----|------|:----:|--------|
| INF-01 | Mongo + Redis docker healthy | ✅ | `scripts/e2e-run.sh` |
| INF-02 | 全模組 Mongo index | ✅ | `cmd/mongo-index` |
| INF-03 | E2E tenant + member + permission + JWT seed | ✅ | `cmd/e2e-seed` |
| INF-04 | Gateway 監聽 :18888 | ✅ | `scripts/e2e-run.sh` |
### Normal
| ID | Method | Path | 情境 | 自動 | 測試 |
|----|--------|------|------|:----:|------|
| N-01 | GET | `/api/v1/health` | Ping 200 | ✅ | `TestHealth_Ping` |
| N-02 | GET | `/api/v1/health` | 無需 Bearer | ✅ | `TestHealth_NoAuthRequired` |
### Auth`/api/v1/auth`
| ID | Method | Path | 情境 | 自動 | 測試 / 備註 |
|----|--------|------|------|:----:|-------------|
| A-01 | POST | `/register` | Email 註冊 | ⏭ | 需 **ZITADEL** + invite |
| A-02 | POST | `/register/confirm` | OTP 確認 | ⏭ | 同上 |
| A-03 | POST | `/register/resend` | 重發 OTP | ⏭ | 同上 |
| A-04 | POST | `/register/social/start` | 社交註冊 | ⏭ | 需 ZITADEL OAuth |
| A-05 | GET | `/register/social/callback` | 社交註冊 callback | ⏭ | 需 ZITADEL |
| A-06 | POST | `/login` | 密碼登入 | ⏭ | 需 ZITADEL ROPG |
| A-07 | POST | `/login/social/start` | 社交登入 | ⏭ | 需 ZITADEL |
| A-08 | GET | `/login/social/callback` | 社交登入 callback | ⏭ | 需 ZITADEL |
| A-09 | POST | `/token/exchange` | id_token 換 JWT | ⏭ | 需 ZITADEL |
| A-10 | POST | `/token/refresh` | 刷新 token | ✅ | `TestZZZ_AuthTokenRefreshAndLogout`(最後跑) |
| A-11 | POST | `/logout` | 登出黑名單 jti | ✅ | 同上(同一測試內連續驗證) |
| A-12 | GET | `/members/me`(無 Bearer | 401 | ✅ | `TestAuth_MissingBearer_401` |
| A-13 | POST | `/register`、`/login`、`/token/refresh`、`/login/social/start` | 公開 Auth validation 400 | ✅ | `TestAuth_PublicValidationErrors`(不需 ZITADEL |
> E2E 透過 `cmd/e2e-seed` 直接核發 JWT不走 ZITADEL因此 A-01A-09 列為手動staging 測試。
### Member`/api/v1/members`,需 Bearer
| ID | Method | Path | 情境 | 自動 | 測試 |
|----|--------|------|------|:----:|------|
| M-01 | GET | `/me` | 讀 profile | ✅ | `TestMember_GetMe` |
| M-02 | PATCH | `/me` | 更新 display_name | ✅ | `TestMember_UpdateMe` |
| M-03 | POST | `/me/verifications/email/start` | 發起 email OTP | ✅ | `TestMember_EmailVerification_FullFlow` |
| M-04 | POST | `/me/verifications/email/confirm` | 確認 email OTP | ✅ | 同上(`GATEWAY_E2E=1` 從 Redis 取碼) |
| M-05 | POST | `/me/verifications/phone/start` | 發起 phone OTP | ✅ | `TestMember_PhoneVerification_FullFlow` |
| M-06 | POST | `/me/verifications/phone/confirm` | 確認 phone OTP | ✅ | 同上(`GATEWAY_E2E=1` 從 Redis 取碼) |
| M-07 | GET | `/me/totp` | TOTP 狀態 | ✅ | `TestMember_TOTP_Status` |
| M-08 | POST | `/me/totp/enroll-start` | 開始綁定 | ✅ | `TestMember_TOTP_FullFlow`(解析 `otpauth_url` |
| M-09 | POST | `/me/totp/enroll-confirm` | 確認綁定 | ✅ | 同上 |
| M-10 | POST | `/me/totp/verify` | Step-up 驗碼 + replay 防護 | ✅ | 同上 |
| M-11 | DELETE | `/me/totp` | 解除綁定 | ✅ | 同上 |
| M-12 | POST | `/me/totp/backup-codes` | 重產備援碼 | ✅ | 同上 |
### Permission`/api/v1/permissions`,需 Bearer
| ID | Method | Path | Middleware | 情境 | 自動 | 測試 |
|----|--------|------|------------|------|:----:|------|
| P-01 | GET | `/catalog` | AuthJWT | 權限樹 | ✅ | `TestPermission_Catalog` |
| P-02 | GET | `/me` | AuthJWT | 當前 user 權限 | ✅ | `TestPermission_Me` |
| P-03 | GET | `/roles` | AuthJWT+Casbin* | 列角色 | ✅ | `TestPermission_RoleCRUD` |
| P-04 | POST | `/roles` | AuthJWT+Casbin* | 建角色 | ✅ | 同上 |
| P-05 | PATCH | `/roles/:id` | AuthJWT+Casbin* | 更新角色 | ✅ | 同上 |
| P-06 | DELETE | `/roles/:id` | AuthJWT+Casbin* | 刪角色 | ✅ | 同上 |
| P-07 | GET | `/roles/:id/permissions` | AuthJWT+Casbin* | 讀角色權限 | ✅ | `TestPermission_RolePermissions` |
| P-08 | PUT | `/roles/:id/permissions` | AuthJWT+Casbin* | 取代角色權限 | ✅ | 同上 |
| P-09 | GET | `/users/:uid/roles` | AuthJWT+Casbin* | 列 user 角色 | ✅ | `TestPermission_AssignUserRole` |
| P-10 | POST | `/users/:uid/roles` | AuthJWT+Casbin* | 指派角色 | ✅ | 同上 |
| P-11 | DELETE | `/users/:uid/roles/:role_id` | AuthJWT+Casbin* | 撤銷角色 | ✅ | 同上 |
| P-12 | GET/PUT/DELETE | `/role-mappings` | AuthJWT+Casbin* | 外部映射 CRUD | ✅ | `TestPermission_RoleMappingCRUD` |
| P-13 | POST | `/policy/reload` | AuthJWT+Casbin | 重載 policy | ✅ | `TestPermission_CasbinRBAC``make e2e-casbin` |
| P-14 | GET | `/roles` | AuthJWT+Casbin | no-role user RBAC denied 403 | ✅ | 同上 |
\* 預設 `make e2e-full` 使用 `Permission.Casbin.Enabled: false`Casbin middleware **放行**rbac=nil passthrough。`make e2e-casbin` 會改用 `e2e.casbin.yaml`,並額外驗證 policy reload 與 no-role 403。
### Notification無 HTTP API
| ID | 情境 | 自動 | 備註 |
|----|------|:----:|------|
| NT-01 | 同步 Sendmock email | 🔧 | `make notify-test METHOD=email-send MOCK=1` |
| NT-02 | 異步 Enqueue + Worker | 🔧 | `make notify-test METHOD=email-enqueue MOCK=1` |
| NT-03 | Member email OTP 寄送 | ✅ | 含在 M-03/M-04mock provider |
---
## 統計摘要
| 類別 | 自動 E2E | 待擴充 / 手動 |
|------|:--------:|:-------------:|
| Normal | 2 | 0 |
| Auth | 4 | 9ZITADEL |
| Member | 12 | 0 |
| Permission | 14 | 0 |
| **合計** | **32** | **9** |
> Auth refresh/logout 會撤銷 JWT因此腳本分兩輪跑`member/permission` 先用 seed token最後才跑 `TestZZZ_AuthTokenRefreshAndLogout`。
---
## 手動 / 延伸測試
### Auth 全链路(需 ZITADEL
1. 設定 `etc/gateway.dev.yaml``Zitadel.*`
2. `make deps-up && make run-dev`
3.`docs/auth-unified-registration.md` 跑 register → confirm → login
### TOTP 互動測試
```bash
make deps-up
make totp-test STEP=flow
```
### Notification
見 [`docs/notification-testing.md`](notification-testing.md)。
---
## 環境變數
| 變數 | 預設 | 說明 |
|------|------|------|
| `GATEWAY_E2E` | `1`(腳本內) | 開啟 OTP 寫入 Redis `e2e:otp:{challenge_id}` |
| `E2E_STATE_FILE` | `test/e2e/fixtures/state.json` | seed 輸出路徑 |
| `E2E_BASE_URL` | `http://127.0.0.1:18888` | 覆寫 Gateway URL |
| `E2E_KEEP_DOCKER` | — | `1` = 測完不 down -v |
| `GATEWAY_PORT` | `18888` | health check 用 |
| `E2E_ROLE` | `tenant_owner` | 覆寫 seed 指派給主測試使用者的 system role |
| `E2E_TEST_PATTERN` | `Test(Auth_\|Health\|Member\|Permission)` | 覆寫第一輪 `go test -run` pattern |
| `E2E_CASBIN` | — | `1` = 執行 Casbin 專用 assertion`make e2e-casbin` 已設定) |
---
## 目錄結構
```
gateway/
├── cmd/e2e-seed/ # E2E 資料 + JWT seed
├── scripts/
│ ├── e2e-run.sh # 一鍵完整流程
│ ├── e2e-up.sh # 只起環境
│ └── e2e-down.sh # 關閉
├── test/e2e/
│ ├── fixtures/
│ │ ├── e2e.yaml # E2E 設定
│ │ ├── e2e.casbin.yaml # Casbin enabled E2E 設定
│ │ └── state.json # 生成gitignore
│ ├── client.go # HTTP helper
│ ├── health_test.go
│ ├── auth_test.go
│ ├── member_test.go
│ └── permission_test.go
└── docs/e2e-testing.md # 本文件
```
---
## CI 建議
```yaml
# 範例 GitHub Actions job
- name: E2E
run: make e2e-full
working-directory: gateway
- name: E2E Casbin
run: make e2e-casbin
working-directory: gateway
```
PR 門檻:`make check`unit+ `make e2e-full`(整合)+ `make e2e-casbin`RBAC enforcement
---
## 常見問題
**Q: `missing e2e otp for challenge`**
A: Gateway 必須以 `GATEWAY_E2E=1` 啟動(`e2e-run.sh` 已設定)。
**Q: `connection refused :18888`**
A: 先 `make e2e-up` 或確認沒有其他 process 佔用 18888。
**Q: 跑完 `make e2e-full` 後 Gateway 還在 :18888**
A: 舊版用 `go run` 背景跑,`kill $!` 只殺 wrapper、子行程會留著。現已改為編譯 `.cache/e2e-gateway` 再啟動cleanup 會依 **pid 檔 + port + orphan** 三層關閉。若仍有殘留:
```bash
make e2e-down
# 或
lsof -ti tcp:18888 | xargs kill -9
```
**Q: 與 `make run-dev`:8888衝突**
A: E2E 固定用 **18888** + **gateway_e2e** DB互不影響。
**Q: 如何只跑單一測試?**
```bash
make e2e-up
GATEWAY_E2E=1 go test -tags=e2e -v -count=1 ./test/e2e/ -run TestMember_GetMe
make e2e-down
```