266 lines
10 KiB
Markdown
266 lines
10 KiB
Markdown
|
|
# 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 / JWT(gitignore,執行後生成) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 測試覆蓋矩陣(一目瞭然)
|
|||
|
|
|
|||
|
|
圖例:**✅ 自動 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-01~A-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 | 同步 Send(mock 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-04(mock provider) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 統計摘要
|
|||
|
|
|
|||
|
|
| 類別 | 自動 E2E | 待擴充 / 手動 |
|
|||
|
|
|------|:--------:|:-------------:|
|
|||
|
|
| Normal | 2 | 0 |
|
|||
|
|
| Auth | 4 | 9(ZITADEL) |
|
|||
|
|
| 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
|
|||
|
|
```
|