Files

569 lines
24 KiB
Go
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.
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
import (
"context"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/spf13/cobra"
)
// fakeDevAppRunner 捕获 runDevAppTool 派发的 invocation,用于断言 LeafSpec.Call
// 装配出的 toolArgs 与手写版逐字等价。这是 catalog drift 不覆盖的运行时维度。
type fakeDevAppRunner struct {
got executor.Invocation
}
func prepareDevAppLeafTestCommand(cmd *cobra.Command) *cobra.Command {
ctx, _ := output.WithResultStore(context.Background())
cmd.SetContext(ctx)
return cmd
}
func (f *fakeDevAppRunner) Run(_ context.Context, inv executor.Invocation) (executor.Result, error) {
f.got = inv
// 空 Response:normalizeDevAppServiceResult 无 "content" 时早返回,安全。
return executor.Result{Invocation: inv, Response: map[string]any{}}, nil
}
// TestDevAppCredentialsGetLeafDispatchesTrimmedArgs 验证迁移到 LeafSpec 后:
// toolArgs 键/值/trim 与手写版等价,且 PostMount 设上了 schema 注解与 NoArgs。
func TestDevAppCredentialsGetLeafDispatchesTrimmedArgs(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppCredentialsGetCommand(r))
// 含首尾空白:验证 LeafFlag.Trim 等价手写 devAppStringFlag 的 TrimSpace。
if err := cmd.Flags().Set("unified-app-id", " APP-123 "); err != nil {
t.Fatal(err)
}
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
if r.got.Tool != devAppCredentialsGetTool {
t.Fatalf("tool = %q, want %q", r.got.Tool, devAppCredentialsGetTool)
}
if v, _ := r.got.Params["unifiedAppId"].(string); v != "APP-123" {
t.Fatalf("unifiedAppId = %q, want trimmed \"APP-123\"", v)
}
if cmd.Annotations["mcp-tool"] != devAppCredentialsGetTool {
t.Fatalf("mcp-tool annotation = %q (PostMount 未生效)", cmd.Annotations["mcp-tool"])
}
if cmd.Args == nil {
t.Fatal("Args == nil, want NoArgs via PostMount")
}
}
// TestDevAppLifecycleLeafWriteGuardAndArgs 验证框架确认门拦截/放行 + toolArgs 装配。
func TestDevAppLifecycleLeafWriteGuardAndArgs(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppEnableCommand(r))
if err := cmd.Flags().Set("unified-app-id", "APP-9"); err != nil {
t.Fatal(err)
}
// 无 --yes / --dry-run:SafetySpec 的 user_required 确认必须拦下。
if err := cmd.RunE(cmd, nil); err == nil {
t.Fatal("RunE() without --yes: want write-guard error, got nil")
}
// devAppYes 在 cmd.Flags 里找 "yes";注册并置位,守卫放行。
cmd.Flags().Bool("yes", false, "")
if err := cmd.Flags().Set("yes", "true"); err != nil {
t.Fatal(err)
}
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() with --yes: error = %v", err)
}
if r.got.Tool != devAppEnableTool {
t.Fatalf("tool = %q, want %q", r.got.Tool, devAppEnableTool)
}
if v, _ := r.got.Params["unifiedAppId"].(string); v != "APP-9" {
t.Fatalf("unifiedAppId = %q, want \"APP-9\"", v)
}
}
// TestDevAppCredentialsGetMissingRequired 验证自定义必填报错(apperrors)逐字保留。
func TestDevAppCredentialsGetMissingRequired(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := newDevAppCredentialsGetCommand(r)
err := cmd.RunE(cmd, nil)
if err == nil {
t.Fatal("want required error for missing --unified-app-id")
}
if !strings.Contains(err.Error(), "--unified-app-id 为必填") {
t.Fatalf("err = %v, want 含 \"--unified-app-id 为必填\"", err)
}
}
// TestDevAppVersionLeafs 验证 version get/status/check-approval 的 toolArgs
// 装配:双必填 trim + check-approval 经 Call 注入 precheckOnly=true 常量。
func TestDevAppVersionLeafs(t *testing.T) {
cases := []struct {
name string
build func(*fakeDevAppRunner) *cobra.Command
tool string
precheckOnly any
}{
{"get", func(r *fakeDevAppRunner) *cobra.Command { return newDevAppVersionGetCommand(r) }, devAppVersionDetailTool, nil},
{"status", func(r *fakeDevAppRunner) *cobra.Command { return newDevAppVersionStatusCommand(r) }, devAppVersionStatusTool, nil},
{"check-approval", func(r *fakeDevAppRunner) *cobra.Command { return newDevAppVersionCheckApprovalCommand(r) }, devAppVersionPublishTool, true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(c.build(r))
if err := cmd.Flags().Set("unified-app-id", " APP-1 "); err != nil {
t.Fatal(err)
}
if err := cmd.Flags().Set("version-id", " v9 "); err != nil {
t.Fatal(err)
}
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
if r.got.Tool != c.tool {
t.Fatalf("tool = %q, want %q", r.got.Tool, c.tool)
}
if v, _ := r.got.Params["unifiedAppId"].(string); v != "APP-1" {
t.Fatalf("unifiedAppId = %q, want trimmed \"APP-1\"", v)
}
if v, _ := r.got.Params["versionId"].(string); v != "v9" {
t.Fatalf("versionId = %q, want trimmed \"v9\"", v)
}
if c.precheckOnly != nil {
if v, _ := r.got.Params["precheckOnly"].(bool); v != c.precheckOnly {
t.Fatalf("precheckOnly = %v, want %v", r.got.Params["precheckOnly"], c.precheckOnly)
}
} else if _, present := r.got.Params["precheckOnly"]; present {
t.Fatalf("precheckOnly present = %v, want absent", r.got.Params["precheckOnly"])
}
})
}
}
// TestDevAppMemberListLeaf 验证 member list 的 unified-id 装配。
func TestDevAppMemberListLeaf(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppMemberListCommand(r))
if err := cmd.Flags().Set("unified-app-id", "APP-7"); err != nil {
t.Fatal(err)
}
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
if r.got.Tool != devAppMemberListTool {
t.Fatalf("tool = %q, want %q", r.got.Tool, devAppMemberListTool)
}
if v, _ := r.got.Params["unifiedAppId"].(string); v != "APP-7" {
t.Fatalf("unifiedAppId = %q, want \"APP-7\"", v)
}
}
// TestDevAppPermissionListLeaf 验证 ToUpper transform + auth-status 默认 + 命令别名。
func TestDevAppPermissionListLeaf(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppPermissionListCommand(r))
// 命令别名 "search" 经 PostMount 设回。
if !containsString(cmd.Aliases, "search") {
t.Fatalf("cmd.Aliases = %v, want contain search", cmd.Aliases)
}
_ = cmd.Flags().Set("unified-app-id", "APP-P")
_ = cmd.Flags().Set("scope-type", "app")
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
// auth-status 默认 ALL → ToUpper ALL。
if v, _ := r.got.Params["authStatus"].(string); v != "ALL" {
t.Fatalf("authStatus = %v, want ALL (default ToUpper)", r.got.Params["authStatus"])
}
// scope-type 小写 app → ToUpper APP。
if v, _ := r.got.Params["scopeType"].(string); v != "APP" {
t.Fatalf("scopeType = %v, want APP (ToUpper)", r.got.Params["scopeType"])
}
if _, present := r.got.Params["pageSize"]; !present {
t.Fatal("pageSize missing, want cursor injection")
}
}
// TestDevAppVersionPublishLeaf 验证 precheckOnly=false 常量 + confirmed-sensitive Changed() 语义。
func TestDevAppVersionPublishLeaf(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppVersionPublishCommand(r))
cmd.Flags().Bool("yes", false, "")
_ = cmd.Flags().Set("yes", "true")
_ = cmd.Flags().Set("unified-app-id", "APP-V")
_ = cmd.Flags().Set("version-id", "v1")
// confirmed-sensitive 未显式设:不入参。
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
if v, _ := r.got.Params["precheckOnly"].(bool); v != false {
t.Fatalf("precheckOnly = %v, want false", r.got.Params["precheckOnly"])
}
if _, present := r.got.Params["confirmedSensitive"]; present {
t.Fatalf("confirmedSensitive present = %v, want omitted (not Changed)", r.got.Params["confirmedSensitive"])
}
// 显式设 confirmed-sensitive=true:入参。
r2 := &fakeDevAppRunner{}
cmd2 := prepareDevAppLeafTestCommand(newDevAppVersionPublishCommand(r2))
cmd2.Flags().Bool("yes", false, "")
_ = cmd2.Flags().Set("yes", "true")
_ = cmd2.Flags().Set("unified-app-id", "APP-V")
_ = cmd2.Flags().Set("version-id", "v1")
_ = cmd2.Flags().Set("confirmed-sensitive", "true")
_ = cmd2.RunE(cmd2, nil)
if v, _ := r2.got.Params["confirmedSensitive"].(bool); v != true {
t.Fatalf("confirmedSensitive = %v, want true", r2.got.Params["confirmedSensitive"])
}
}
// TestDevAppGetLeafDualKey 验证二选一定位键。
func TestDevAppGetLeafDualKey(t *testing.T) {
// 都不传:报错(框架统一约束措辞)。
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppGetCommand(r))
if err := cmd.RunE(cmd, nil); err == nil ||
!strings.Contains(err.Error(), "请传入 --unified-app-id 或 --app-key") {
t.Fatalf("err = %v, want 二选一报错", err)
}
// 只 app-key:params 含 appKey,无 unifiedAppId。
_ = cmd.Flags().Set("app-key", "dingABC")
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
if v, _ := r.got.Params["appKey"].(string); v != "dingABC" {
t.Fatalf("appKey = %v, want dingABC", r.got.Params["appKey"])
}
if _, present := r.got.Params["unifiedAppId"]; present {
t.Fatalf("unifiedAppId present, want omitted")
}
}
// TestDevAppListLeaf 是 showcase:退役 devAppPutString/devAppPutInt/devAppFlagOrFallback。
// 验证 name/keyword 别名回退、LeafInt(!=0 入参)、cursor 注入、OmitEmpty。
func TestDevAppListLeaf(t *testing.T) {
// keyword 别名回退:只设 keyword,name 应取 keyword 值。
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppListCommand(r))
_ = cmd.Flags().Set("keyword", "DemoApp")
_ = cmd.Flags().Set("app-group-id", "5")
_ = cmd.Flags().Set("develop-type", "0") // 0:LeafInt 不入参
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
if v, _ := r.got.Params["name"].(string); v != "DemoApp" {
t.Fatalf("name = %q, want keyword-fallback \"DemoApp\"", v)
}
if v, _ := r.got.Params["appGroupId"].(int); v != 5 {
t.Fatalf("appGroupId = %v, want int(5)", r.got.Params["appGroupId"])
}
if _, present := r.got.Params["developType"]; present {
t.Fatalf("developType present = %v, want omitted (LeafInt 0)", r.got.Params["developType"])
}
if _, present := r.got.Params["pageSize"]; !present {
t.Fatal("pageSize missing, want cursor injection")
}
if _, present := r.got.Params["creator"]; present {
t.Fatalf("creator present, want omitted (OmitEmpty)")
}
// name 优先于 keyword。
r2 := &fakeDevAppRunner{}
cmd2 := prepareDevAppLeafTestCommand(newDevAppListCommand(r2))
_ = cmd2.Flags().Set("name", "Primary")
_ = cmd2.Flags().Set("keyword", "Fallback")
_ = cmd2.RunE(cmd2, nil)
if v, _ := r2.got.Params["name"].(string); v != "Primary" {
t.Fatalf("name = %q, want Primary (name beats keyword)", v)
}
}
// TestDevAppEventListLeaf 验证 cursor/pageSize 经 devAppCallCursor 工具注入。
func TestDevAppEventListLeaf(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppEventListCommand(r))
_ = cmd.Flags().Set("unified-app-id", "APP-E")
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
if v, _ := r.got.Params["unifiedAppId"].(string); v != "APP-E" {
t.Fatalf("unifiedAppId = %q, want APP-E", v)
}
// page-size 默认 20,由 registerDevAppCursorFlags + CallCursor 注入。
if v, present := r.got.Params["pageSize"]; !present || v != 20 {
t.Fatalf("pageSize = %v, want 20 from cursor tool", v)
}
// cursor 默认空:不入参。
if _, present := r.got.Params["cursor"]; present {
t.Fatalf("cursor present = %v, want omitted when empty", r.got.Params["cursor"])
}
// keyword 未设:OmitEmpty 省略。
if _, present := r.got.Params["keyword"]; present {
t.Fatalf("keyword present, want omitted (OmitEmpty)")
}
}
// TestDevAppUpdateLeaf 验证「至少一项」校验 + OmitEmpty 装配。
func TestDevAppUpdateLeaf(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppUpdateCommand(r))
cmd.Flags().Bool("yes", false, "")
_ = cmd.Flags().Set("yes", "true")
_ = cmd.Flags().Set("unified-app-id", "APP-U")
// 全空:至少一项拦(框架统一约束措辞)。
if err := cmd.RunE(cmd, nil); err == nil ||
!strings.Contains(err.Error(), "至少提供一项待更新字段:--name、--desc 或 --icon-media-id") {
t.Fatalf("err = %v, want 至少提供一项待更新字段:--name、--desc 或 --icon-media-id", err)
}
_ = cmd.Flags().Set("desc", " 新描述 ")
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
if v, _ := r.got.Params["desc"].(string); v != "新描述" {
t.Fatalf("desc = %q, want trimmed", v)
}
if _, present := r.got.Params["name"]; present {
t.Fatalf("name present, want omitted (OmitEmpty)")
}
}
// TestDevAppMemberAddLeaf 验证 userIds []string 注入 + memberType 装配 + 写守卫/必填顺序。
func TestDevAppMemberAddLeaf(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppMemberAddCommand(r))
_ = cmd.Flags().Set("unified-app-id", "APP-M")
// 无 --yes:写守卫拦。
if err := cmd.RunE(cmd, nil); err == nil {
t.Fatal("want write-guard error without --yes")
}
cmd.Flags().Bool("yes", false, "")
_ = cmd.Flags().Set("yes", "true")
// 缺 user-ids/member-type:必填拦。
if err := cmd.RunE(cmd, nil); err == nil ||
!strings.Contains(err.Error(), "--user-ids 为必填") {
t.Fatalf("err = %v, want --user-ids 为必填", err)
}
_ = cmd.Flags().Set("user-ids", "u1, u2")
if err := cmd.RunE(cmd, nil); err == nil ||
!strings.Contains(err.Error(), "--member-type 为必填") {
t.Fatalf("err = %v, want --member-type 为必填", err)
}
// 齐全:userIds []string + memberType 装配。
_ = cmd.Flags().Set("member-type", "DEVELOPER")
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
users, ok := r.got.Params["userIds"].([]string)
if !ok || len(users) != 2 || users[0] != "u1" || users[1] != "u2" {
t.Fatalf("userIds = %v, want [u1 u2]", r.got.Params["userIds"])
}
if v, _ := r.got.Params["memberType"].(string); v != "DEVELOPER" {
t.Fatalf("memberType = %v, want DEVELOPER", r.got.Params["memberType"])
}
}
// TestDevAppSecurityConfigLeaf 验证 3 个列表 flag 的 Call 注入 + 「至少一项」校验。
func TestDevAppSecurityConfigLeaf(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppSecurityConfigCommand(r))
cmd.Flags().Bool("yes", false, "")
_ = cmd.Flags().Set("yes", "true")
_ = cmd.Flags().Set("unified-app-id", "APP-S")
// 全空:至少一项拦。
if err := cmd.RunE(cmd, nil); err == nil ||
!strings.Contains(err.Error(), "至少提供一项安全配置:--ip-whitelist、--redirect-urls 或 --sso-urls") {
t.Fatalf("err = %v, want 至少提供一项安全配置:--ip-whitelist、--redirect-urls 或 --sso-urls", err)
}
// 设 ip-whitelist:注入 []string,其余省略。
_ = cmd.Flags().Set("ip-whitelist", "10.0.0.1; 10.0.0.2")
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
ips, ok := r.got.Params["ipWhitelist"].([]string)
if !ok || len(ips) != 2 || ips[0] != "10.0.0.1" || ips[1] != "10.0.0.2" {
t.Fatalf("ipWhitelist = %v, want [10.0.0.1 10.0.0.2]", r.got.Params["ipWhitelist"])
}
if _, present := r.got.Params["redirectUrls"]; present {
t.Fatalf("redirectUrls present, want omitted")
}
}
// TestDevAppPermissionAddLeaf 验证 scope-values 经 Call 注入为 []string + 写守卫/必填。
func TestDevAppPermissionAddLeaf(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppPermissionAddCommand(r))
// scope-values 在 PostMount 注册。
if err := cmd.Flags().Set("unified-app-id", "APP-3"); err != nil {
t.Fatal(err)
}
// 无 --yes 且无 scope-values:写守卫先拦。
if err := cmd.RunE(cmd, nil); err == nil {
t.Fatal("want write-guard error without --yes")
}
cmd.Flags().Bool("yes", false, "")
_ = cmd.Flags().Set("yes", "true")
// 有 --yes 但无 scope-values:必填校验拦。
if err := cmd.RunE(cmd, nil); err == nil ||
!strings.Contains(err.Error(), "--scope-values 为必填") {
t.Fatalf("err = %v, want --scope-values 为必填", err)
}
// 齐全:scopeValues 注入为 []string。
_ = cmd.Flags().Set("scope-values", "Contact.User.mobile, qyapi_robot_sendmsg")
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
scopes, ok := r.got.Params["scopeValues"].([]string)
if !ok || len(scopes) != 2 || scopes[0] != "Contact.User.mobile" || scopes[1] != "qyapi_robot_sendmsg" {
t.Fatalf("scopeValues = %v, want [Contact.User.mobile qyapi_robot_sendmsg]", r.got.Params["scopeValues"])
}
if v, _ := r.got.Params["unifiedAppId"].(string); v != "APP-3" {
t.Fatalf("unifiedAppId = %q, want APP-3", v)
}
}
// TestDevAppWebappConfigLeaf 验证 OmitEmpty 装配 + 「至少一项」跨 flag 校验。
func TestDevAppWebappConfigLeaf(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(newDevAppWebappConfigCommand(r))
cmd.Flags().Bool("yes", false, "")
_ = cmd.Flags().Set("yes", "true")
// 全空:写守卫放行后,「至少一项」必须拦。
if err := cmd.Flags().Set("unified-app-id", "APP-1"); err != nil {
t.Fatal(err)
}
err := cmd.RunE(cmd, nil)
if err == nil || !strings.Contains(err.Error(), "至少提供一项网页应用配置:--h5-page-type、--homepage-url、--pc-homepage-url 或 --omp-url") {
t.Fatalf("err = %v, want 至少提供一项网页应用配置:--h5-page-type、--homepage-url、--pc-homepage-url 或 --omp-url", err)
}
// 只设一项:该项入参,其余 OmitEmpty 省略。
if err := cmd.Flags().Set("homepage-url", " https://a.example.invalid "); err != nil {
t.Fatal(err)
}
r = &fakeDevAppRunner{}
cmd2 := prepareDevAppLeafTestCommand(newDevAppWebappConfigCommand(r))
cmd2.Flags().Bool("yes", false, "")
_ = cmd2.Flags().Set("yes", "true")
_ = cmd2.Flags().Set("unified-app-id", "APP-1")
_ = cmd2.Flags().Set("homepage-url", " https://a.example.invalid ")
if err := cmd2.RunE(cmd2, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
if v, _ := r.got.Params["homepageUrl"].(string); v != "https://a.example.invalid" {
t.Fatalf("homepageUrl = %q, want trimmed", v)
}
if _, present := r.got.Params["h5PageType"]; present {
t.Fatalf("h5PageType present = %v, want omitted (OmitEmpty)", r.got.Params["h5PageType"])
}
if _, present := r.got.Params["ompUrl"]; present {
t.Fatalf("ompUrl present = %v, want omitted (OmitEmpty)", r.got.Params["ompUrl"])
}
}
// TestDevAppLeafToolArgsTable 表驱动补齐剩余迁移命令的 toolArgs 断言。
// 覆盖此前无断言的:robot enable/disable、permission remove、event sub/unsub、
// member remove、version create、app create、robot config-get、lifecycle disable。
func TestDevAppLeafToolArgsTable(t *testing.T) {
cases := []struct {
name string
build func(executor.Runner) *cobra.Command
flags map[string]string
needsYes bool
wantTool string
wantParam map[string]any
wantAbsent []string // 应缺席的 params 键(OmitEmpty 校验)
}{
{"robot enable", newDevAppRobotEnableCommand, map[string]string{"unified-app-id": "APP-R"}, true, devAppRobotEnableTool, map[string]any{"unifiedAppId": "APP-R"}, nil},
{"robot disable", newDevAppRobotOfflineCommand, map[string]string{"unified-app-id": "APP-R"}, true, devAppRobotOfflineTool, map[string]any{"unifiedAppId": "APP-R"}, nil},
{"permission remove", newDevAppPermissionRemoveCommand, map[string]string{"unified-app-id": "APP-P", "scope-values": "a.b, c.d"}, true, devAppPermissionRmTool, map[string]any{"unifiedAppId": "APP-P", "scopeValues": []string{"a.b", "c.d"}}, nil},
{"event subscribe", newDevAppEventSubscribeCommand, map[string]string{"unified-app-id": "APP-E", "event-codes": "x, y"}, true, devAppEventSubscribeTool, map[string]any{"unifiedAppId": "APP-E", "eventCodes": []string{"x", "y"}}, nil},
{"event unsubscribe", newDevAppEventUnsubscribeCommand, map[string]string{"unified-app-id": "APP-E", "event-codes": "x, y"}, true, devAppEventUnsubscribeTool, map[string]any{"unifiedAppId": "APP-E", "eventCodes": []string{"x", "y"}}, nil},
{"member remove", newDevAppMemberRemoveCommand, map[string]string{"unified-app-id": "APP-M", "user-ids": "u1,u2", "member-type": "DEVELOPER"}, true, devAppMemberRemoveTool, map[string]any{"unifiedAppId": "APP-M", "userIds": []string{"u1", "u2"}, "memberType": "DEVELOPER"}, nil},
{"version create", newDevAppVersionCreateCommand, map[string]string{"unified-app-id": "APP-V", "desc": "新增机器人"}, true, devAppVersionCreateTool, map[string]any{"unifiedAppId": "APP-V", "desc": "新增机器人"}, []string{"version"}},
{"app create", newDevAppCreateCommand, map[string]string{"name": "DemoApp"}, true, devAppCreateTool, map[string]any{"name": "DemoApp"}, []string{"desc", "iconMediaId"}},
{"robot config get", newDevAppRobotConfigGetCommand, map[string]string{"unified-app-id": "APP-C"}, false, devAppRobotConfigGetTool, map[string]any{"unifiedAppId": "APP-C"}, nil},
{"lifecycle disable", newDevAppDisableCommand, map[string]string{"unified-app-id": "APP-D"}, true, devAppDisableTool, map[string]any{"unifiedAppId": "APP-D"}, nil},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := prepareDevAppLeafTestCommand(c.build(r))
if c.needsYes {
cmd.Flags().Bool("yes", false, "")
_ = cmd.Flags().Set("yes", "true")
}
for k, v := range c.flags {
if err := cmd.Flags().Set(k, v); err != nil {
t.Fatalf("set %s: %v", k, err)
}
}
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatalf("RunE() error = %v", err)
}
if r.got.Tool != c.wantTool {
t.Fatalf("tool = %q, want %q", r.got.Tool, c.wantTool)
}
for k, want := range c.wantParam {
if got := r.got.Params[k]; !reflect.DeepEqual(got, want) {
t.Fatalf("param %s = %#v (%T), want %#v (%T)", k, got, got, want, want)
}
}
for _, k := range c.wantAbsent {
if _, present := r.got.Params[k]; present {
t.Fatalf("param %s present = %#v, want absent (OmitEmpty)", k, r.got.Params[k])
}
}
})
}
}
// TestDevAppMemberRemoveValidateChain 覆盖 member remove Validate 钩子的逐级
// 必填校验:unified-app-id → user-ids → member-type。
func TestDevAppMemberRemoveValidateChain(t *testing.T) {
newCmd := func() *cobra.Command {
cmd := newDevAppMemberRemoveCommand(&fakeDevAppRunner{})
cmd.Flags().Bool("yes", false, "")
if err := cmd.Flags().Set("yes", "true"); err != nil {
t.Fatal(err)
}
return cmd
}
cmd := newCmd()
if err := cmd.RunE(cmd, nil); err == nil || !strings.Contains(err.Error(), "unified-app-id") {
t.Fatalf("missing unified-app-id err = %v", err)
}
cmd = newCmd()
if err := cmd.Flags().Set("unified-app-id", "APP-1"); err != nil {
t.Fatal(err)
}
if err := cmd.RunE(cmd, nil); err == nil || !strings.Contains(err.Error(), "user-ids") {
t.Fatalf("missing user-ids err = %v", err)
}
cmd = newCmd()
if err := cmd.Flags().Set("unified-app-id", "APP-1"); err != nil {
t.Fatal(err)
}
if err := cmd.Flags().Set("user-ids", "u1"); err != nil {
t.Fatal(err)
}
if err := cmd.RunE(cmd, nil); err == nil || !strings.Contains(err.Error(), "member-type") {
t.Fatalf("missing member-type err = %v", err)
}
}