template-monorepo/test/k6/README.md

156 lines
9.6 KiB
Markdown
Raw Permalink 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.

# k6 API tests
完整的 Gateway API smoke + journey 測試套件。**所有對外端點**都至少在 `smoke/``journeys/` 裡有一發(含登入 MFA、忘記/改密碼、註冊 resume
## TL;DR
```bash
make k6-up # dockermongo + redis + mailhog + postgres + zitadel
make k6-wait # 等 ZITADEL ready + 把 PAT 寫到 env file
make k6-gateway & # 起 gateway背景吃 etc/gateway.k6.yaml
make k6-seed-admin # rbac journey 才需要seed tenant_admin user
make k6-all # 跑 smoke + journey
make k6-down # 清掉
```
單跑一個檔(記得先載入環境變數):
```bash
source deploy/zitadel/machinekey/k6.env
k6 run test/k6/smoke/health.js
k6 run test/k6/journeys/email_register_full.js
```
## 環境變數
| 變數 | 預設 | 說明 |
|---|---|---|
| `BASE_URL` | `http://localhost:8888` | Gateway base URL |
| `MAILHOG_URL` | `http://localhost:8025` | MailHog HTTP API撈 email OTP |
| `REDIS_ADDR` | `localhost:6379` | Redis 位址(撈 SMS OTP |
| `TENANT_SLUG` | `k6-tenant` | register payload tenant |
| `INVITE_CODE` | `K6INVITE` | tenant 開啟 invite 時用 |
| `ADMIN_EMAIL` / `ADMIN_PASSWORD` | — | rbac journey seeded admin |
| `OTP_POLL_INTERVAL_MS` | `300` | OTP poll 頻率 |
| `OTP_POLL_TIMEOUT_MS` | `5000` | OTP poll 超時 |
| `RESEND_COOLDOWN_SECONDS` | `60` | 與 `gateway.k6.yaml` OTP 重送冷卻一致cooldown smoke 會 sleep |
## 目錄結構
```
test/k6/
├── README.md
├── lib/ # 共用 helper
│ ├── config.js # 環境變數 + unique() + SUCCESS_CODE
│ ├── http.js # get/post/...、checkEnvelope、withBearer
│ ├── otp.js # fetchEmailOTP (MailHog) / fetchSMSOTP (Redis)
│ ├── totp.js # HMAC-SHA1 TOTPP4
│ ├── auth.js # register / confirm / login / MFA / password helper
│ ├── member.js # TOTP enroll / change password helper
│ └── seed.js # tenant + invite + admin role bootstrapP5
├── smoke/ # 每個端點至少一發
│ ├── health.js # GET /api/v1/health
│ ├── auth_public.js # register / login / refresh / social-start (+negative)
│ ├── auth_register_resume.js # register/resumehappy + 404/409/429/400
│ ├── auth_password.js # password/forgot + resethappy + 各種 negative
│ ├── auth_login_mfa.js # login/mfaMFA 前置 + negative
│ ├── auth_bearer.js # logout
│ ├── member.js # me / patch / verify / TOTP / change-password negative
│ ├── permission_read.js # catalog / me
│ └── permission_admin.js # roles CRUD / role-permissions / user-roles / mappings / policy reload
└── journeys/ # 完整流程
├── email_register_full.js # register → confirm OTP(MailHog) → me → patch → logout
├── register_resume_full.js # register → resume → confirm → me
├── password_forgot_reset_full.js # forgot → reset → login新密碼
├── login_mfa_full.js # enroll TOTP → login MFA → me
├── change_password_full.js # change password → login新密碼
├── login_refresh.js # login → refresh → me → logout
├── email_verify.js # register → confirm → email verify start → confirm
├── phone_verify.js # register → confirm → phone verify (Redis OTP)
├── totp_full.js # enroll → confirm → verify → backup-codes → disable
├── rbac_admin.js # role CRUD → assign → policy reload → me/permissions
└── token_exchange.js # ZITADEL id_token → CloudEP JWT
```
## OTP 怎麼撈
- **Email** → `make k6-up` 含 MailHog:1025 SMTP / :8025 HTTP API。Gateway 把 OTP 寄到 MailHogk6 透過 `/api/v2/search?kind=to&query=<email>` 撈到信件,從 body 抓 6 位數。
- **SMS** → mock provider 把 body 寫到 `dev:notification:last:sms:<phone>`(見 [mock_sender.go](../../internal/model/notification/provider/sms/mock_sender.go) `WithMockRedis`k6 用 `k6/experimental/redis` 直接讀。
兩者都有 `OTP_POLL_TIMEOUT_MS`(預設 5 秒)保護,超時直接 fail。
## 覆蓋率39 endpoints
| 模組 | 端點 | 覆蓋 |
|---|---|---|
| Normal | `GET /api/v1/health` | smoke/health |
| Auth 公開 | `POST /register` | journeys/email_register_full + smoke/auth_public |
| Auth 公開 | `POST /register/confirm` | journeys/email_register_full |
| Auth 公開 | `POST /register/resend` | smoke/auth_public |
| Auth 公開 | `POST /register/resume` | smoke/auth_register_resume + journeys/register_resume_full |
| Auth 公開 | `POST /password/forgot` | smoke/auth_password + journeys/password_forgot_reset_full |
| Auth 公開 | `POST /password/reset` | smoke/auth_password + journeys/password_forgot_reset_full |
| Auth 公開 | `POST /login/mfa` | smoke/auth_login_mfa + journeys/login_mfa_full |
| Auth 公開 | `POST /register/social/start` | smoke/auth_public (happy) |
| Auth 公開 | `GET /register/social/callback` | smoke/auth_public (negative — TODO happy) |
| Auth 公開 | `POST /login` | journeys/login_refresh + smoke/auth_public (negative) |
| Auth 公開 | `POST /token/refresh` | journeys/login_refresh + smoke/auth_public (negative) |
| Auth 公開 | `POST /token/exchange` | journeys/token_exchange (negative — TODO happy) |
| Auth 公開 | `POST /login/social/start` | smoke/auth_public (happy) |
| Auth 公開 | `GET /login/social/callback` | smoke/auth_public (negative — TODO happy) |
| Auth Bearer | `POST /logout` | smoke/auth_bearer + journeys/email_register_full |
| Member | `GET /me` | smoke/member + all journeys |
| Member | `PATCH /me` | smoke/member + journeys/email_register_full |
| Member | `POST /me/verifications/email/start` | smoke/member + journeys/email_verify |
| Member | `POST /me/verifications/email/confirm` | smoke/member (negative) + journeys/email_verify (happy) |
| Member | `POST /me/verifications/phone/start` | smoke/member + journeys/phone_verify |
| Member | `POST /me/verifications/phone/confirm` | smoke/member (negative) + journeys/phone_verify (happy) |
| Member | `GET /me/totp` | smoke/member + journeys/totp_full |
| Member | `POST /me/totp/enroll-start` | smoke/member + journeys/totp_full |
| Member | `POST /me/totp/enroll-confirm` | smoke/member (negative) + journeys/totp_full (happy) |
| Member | `POST /me/totp/verify` | smoke/member (negative) + journeys/totp_full (happy) |
| Member | `POST /me/totp/backup-codes` | smoke/member (negative) + journeys/totp_full (happy) |
| Member | `DELETE /me/totp` | smoke/member + journeys/totp_full |
| Member | `POST /me/password` | smoke/member (negative) + journeys/change_password_full |
| Perm 讀 | `GET /permissions/catalog` | smoke/permission_read + journeys/rbac_admin |
| Perm 讀 | `GET /permissions/me` | smoke/permission_read + journeys/rbac_admin |
| Perm 管理 | `GET /roles` | smoke/permission_admin + journeys/rbac_admin |
| Perm 管理 | `POST /roles` | smoke/permission_admin + journeys/rbac_admin |
| Perm 管理 | `PATCH /roles/:id` | smoke/permission_admin + journeys/rbac_admin |
| Perm 管理 | `DELETE /roles/:id` | smoke/permission_admin + journeys/rbac_admin |
| Perm 管理 | `GET /roles/:id/permissions` | smoke/permission_admin + journeys/rbac_admin |
| Perm 管理 | `PUT /roles/:id/permissions` | smoke/permission_admin + journeys/rbac_admin |
| Perm 管理 | `GET /users/:uid/roles` | smoke/permission_admin + journeys/rbac_admin |
| Perm 管理 | `POST /users/:uid/roles` | smoke/permission_admin + journeys/rbac_admin |
| Perm 管理 | `DELETE /users/:uid/roles/:role_id` | smoke/permission_admin + journeys/rbac_admin |
| Perm 管理 | `GET /role-mappings` | smoke/permission_admin |
| Perm 管理 | `PUT /role-mappings` | smoke/permission_admin |
| Perm 管理 | `DELETE /role-mappings` | smoke/permission_admin |
| Perm 管理 | `POST /policy/reload` | smoke/permission_admin + journeys/rbac_admin |
## RBAC 系列(需 admin seed
`journeys/rbac_admin.js` 需要 `ADMIN_EMAIL` / `ADMIN_PASSWORD` 環境變數。`make k6-seed-admin`
會跑 [cmd/k6-seed-admin](../../cmd/k6-seed-admin/main.go)
1. 用 API 註冊一個固定的 `k6-admin@k6.local`
2. 從 MailHog 撈 OTP 完成 confirm
3. 寫入 permission catalog + 預設 system roles透過 `internal/model/permission/seed`
4. 指派 `tenant_admin` 給該 UID
5.`ADMIN_EMAIL / ADMIN_PASSWORD / ADMIN_UID` 寫到 `k6.env`
`k6-seed-admin` 是冪等的,重跑沒事。
沒跑 seed 時,`rbac_admin.js` 會印 skip notice 並 exit 0admin endpoint 的路由存在性
已由 `smoke/permission_admin.js` 涵蓋)。
## 已知無法純 k6 跑的
- `GET /api/v1/auth/register/social/callback``GET /api/v1/auth/login/social/callback` happy path 需要 Google OAuth UI 跳轉,純 k6 無法走完。Smoke 只覆蓋 negative無效 state → 400
- `POST /api/v1/auth/token/exchange` happy path 需要一個來自 ZITADEL 的有效 `id_token`;目前 `make k6-up` 的 ZITADEL bootstrap 只建 service account PAT沒有開 OIDC client password grant因此 happy 路徑 TODOsmoke 只跑 negative。要開`deploy/zitadel/steps.yaml` 加 Application + password grant再用 `/oauth/v2/token` 拿 id_token。
## 不要 commit 的東西
- `deploy/zitadel/machinekey/zitadel-admin-sa.token` / `.json` 已在 `.gitignore`
- `etc/gateway.k6.yaml` 內的 secret 都是固定 dev 值,本機 / CI 可用,**勿** 上 prod。