//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 token;no-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 = "" }) }