template-monorepo/test/e2e/journey_rbac_test.go

148 lines
5.4 KiB
Go
Raw Normal View History

test(e2e): 加 banner / e2e-list / k6 風格 user journey 讓「我有哪些測試、現在在測什麼」一眼看得到,並補上跨 endpoint 的狀態流測試: 每個測試開頭印中文 banner - 新增 e2eStep(t, id, method, path, desc) helper(test/e2e/setup_test.go) - 17 個 contract test 開頭加 banner,go test -v 會逐個顯示 ▶ [M-01] GET /api/v1/members/me — 讀 profile(tenant/uid/status) - 對外 ID 與 docs/e2e-testing.md 的測試覆蓋矩陣對齊 新增 make e2e-list - scripts/e2e-list.sh 掃 _test.go,分兩節印 contract tests + journeys; 每個 journey 列出所有 step ID + 描述(Step 用 ▶、SkipStep 用 ⊘) scripts 彩色 step banner + optional MailHog - scripts/e2e-lib.sh 抽共用 helpers(e2e_step/info/ok/warn、e2e_print_services) - e2e-run.sh / e2e-up.sh 改用 step banner + 服務面板(執行完印出 Mongo/Redis/ Gateway/MailHog 的 URL) - E2E_WITH_SMTP=1 會額外起 MailHog(http://localhost:8025),方便肉眼確認流程 k6 風格 user journey - 新增 test/e2e/journey.go:NewJourney + Step + SkipStep + Summary, 任一步 fail 自動 skip 後續,輸出 ▶ [J-x.y] 階層 banner - J-1 Tenant Owner 入職第一天(12 steps):/me → PATCH → email verify → phone verify → TOTP enroll/verify/replay/disable - J-2 Tenant Admin 建 qa_engineer 角色 → 指派 → 二人視角驗證 → 撤銷(8 steps) - J-3 Session 生命週期 refresh → /me → logout → 舊 token 401(4 steps,ZZZ 排最後) - J-4 完整註冊 → 登入(5 steps stub,標 SkipStep;接 ZITADEL container 後改 Step 即可) - make e2e-journey / make test-e2e-journey 拆獨立 target;e2e-run.sh 透過 E2E_MODE=journey + E2E_TEST_PATTERN_ZZZ 切換 docs/e2e-testing.md - 首節改為「我現在有哪些測試?make e2e-list」並附 banner 範例輸出 - 加 Journeys 章節:journey 列表、執行範例、失敗時的輸出、寫新 journey 範本 - 補 e2e-journey / test-e2e-journey / E2E_WITH_SMTP 環境變數 Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 09:18:36 +00:00
//go:build e2e
package e2e
import (
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/require"
)
// TestJourney_TenantAdminCustomRole 模擬 Tenant Admin 為一個新工種「qa_engineer」
// 從零建出可用角色的流程:
// 建 Role → 從 catalog 取 perm id → 全量 PUT 權限 → 設 IdP Mapping → 指派給 user
// → 用該 user 視角看 /permissions/me 確認權限生效 → 撤銷 → 刪 mapping → 刪 role
//
// 整段都用 seed owner tokenno-role user 用來驗證「被指派後拿到權限」。
func TestJourney_TenantAdminCustomRole(t *testing.T) {
j := NewJourney(t, "J-2", "Tenant Admin 從零建立 qa_engineer 角色 → 指派 → 驗證 → 撤銷")
defer j.Summary()
owner := NewClient(t)
noRole := NewNoRoleClient(t)
var (
roleKey = "journey_qa_engineer"
externalKey = fmt.Sprintf("journey-qa-group-%s", noRole.Fixture.UID)
roleID string
permissionID string
)
// 清掉殘留(前一輪可能 fail 沒走到清理 step
t.Cleanup(func() {
_, _ = owner.Do(t, http.MethodDelete, "/api/v1/permissions/role-mappings", map[string]string{
"external_source": "zitadel",
"external_key": externalKey,
}, true)
if roleID != "" {
_, _ = owner.Do(t, http.MethodDelete, "/api/v1/permissions/users/"+noRole.Fixture.UID+"/roles/"+roleID, nil, true)
_, _ = owner.Do(t, http.MethodDelete, "/api/v1/permissions/roles/"+roleID, nil, true)
}
})
j.Step("1", "POST /permissions/roles — 建立 qa_engineer 角色", func(t *testing.T) {
env := owner.DoExpectOK(t, http.MethodPost, "/api/v1/permissions/roles", map[string]string{
"key": roleKey,
"display_name": "QA Engineer",
"status": "open",
}, true)
var role struct {
ID string `json:"id"`
Key string `json:"key"`
}
require.NoError(t, json.Unmarshal(env.Data, &role))
require.Equal(t, roleKey, role.Key)
require.NotEmpty(t, role.ID)
roleID = role.ID
})
j.Step("2", "GET /permissions/catalog — 從平台 catalog 撈第一個 leaf permission id", func(t *testing.T) {
permissionID = firstCatalogPermissionID(t, owner)
require.NotEmpty(t, permissionID)
})
j.Step("3", "PUT /permissions/roles/:id/permissions — 把選到的 permission 灌進 role", func(t *testing.T) {
require.NotEmpty(t, roleID)
require.NotEmpty(t, permissionID)
owner.DoExpectOK(t, http.MethodPut, "/api/v1/permissions/roles/"+roleID+"/permissions", map[string][]string{
"permission_ids": {permissionID},
}, true)
// 讀回比對parent closure 會把祖先一起加進去,所以集合 >= 1 + 必含 permissionID
env := owner.DoExpectOK(t, http.MethodGet, "/api/v1/permissions/roles/"+roleID+"/permissions", nil, true)
var list struct {
Permissions []struct {
ID string `json:"id"`
} `json:"permissions"`
}
require.NoError(t, json.Unmarshal(env.Data, &list))
found := false
for _, p := range list.Permissions {
if p.ID == permissionID {
found = true
break
}
}
require.True(t, found)
})
j.Step("4", "PUT /permissions/role-mappings — 設定 zitadel:qa-group → qa_engineer 對映", func(t *testing.T) {
env := owner.DoExpectOK(t, http.MethodPut, "/api/v1/permissions/role-mappings", map[string]string{
"external_source": "zitadel",
"external_key": externalKey,
"internal_role_key": roleKey,
}, true)
var mapping struct {
ExternalKey string `json:"external_key"`
InternalRoleKey string `json:"internal_role_key"`
}
require.NoError(t, json.Unmarshal(env.Data, &mapping))
require.Equal(t, externalKey, mapping.ExternalKey)
require.Equal(t, roleKey, mapping.InternalRoleKey)
})
j.Step("5", "POST /permissions/users/:uid/roles — 把 qa_engineer 手動指派給 no-role user", func(t *testing.T) {
require.NotEmpty(t, roleID)
owner.DoExpectOK(t, http.MethodPost, "/api/v1/permissions/users/"+noRole.Fixture.UID+"/roles", map[string]string{
"role_id": roleID,
}, true)
})
j.Step("6", "GET /permissions/me (no-role user 視角) — 確認新角色 + 權限都拿到了", func(t *testing.T) {
env := noRole.DoExpectOK(t, http.MethodGet, "/api/v1/permissions/me?include_tree=false", nil, true)
var data struct {
Roles []string `json:"roles"`
Permissions map[string]string `json:"permissions"`
}
require.NoError(t, json.Unmarshal(env.Data, &data))
require.Contains(t, data.Roles, roleKey, "no-role user should now have qa_engineer")
require.NotEmpty(t, data.Permissions, "expected non-empty permissions after role assignment")
})
j.Step("7", "DELETE /permissions/users/:uid/roles/:id — 撤銷指派", func(t *testing.T) {
require.NotEmpty(t, roleID)
owner.DoExpectOK(t, http.MethodDelete, "/api/v1/permissions/users/"+noRole.Fixture.UID+"/roles/"+roleID, nil, true)
env := noRole.DoExpectOK(t, http.MethodGet, "/api/v1/permissions/me?include_tree=false", nil, true)
var data struct {
Roles []string `json:"roles"`
}
require.NoError(t, json.Unmarshal(env.Data, &data))
require.NotContains(t, data.Roles, roleKey)
})
j.Step("8", "DELETE /permissions/role-mappings + /roles — 收尾刪 mapping + role", func(t *testing.T) {
owner.DoExpectOK(t, http.MethodDelete, "/api/v1/permissions/role-mappings", map[string]string{
"external_source": "zitadel",
"external_key": externalKey,
}, true)
require.NotEmpty(t, roleID)
owner.DoExpectOK(t, http.MethodDelete, "/api/v1/permissions/roles/"+roleID, nil, true)
// 標記已清t.Cleanup 那邊就不會再重複打
roleID = ""
})
}