From dccdb71505ba2e9e4850710fdcd158b21e2952ee Mon Sep 17 00:00:00 2001 From: "zhao.yuxuan" Date: Tue, 7 Apr 2026 20:00:07 +0800 Subject: [PATCH 1/3] test: add mail and wiki shortcut e2e coverage Change-Id: I43922a6cce5a671e842e48e57e8fb77aae738647 --- tests/cli_e2e/mail/helpers_test.go | 35 ++++ .../cli_e2e/mail/mail_bot_constraints_test.go | 66 +++++++ .../cli_e2e/mail/mail_read_reference_test.go | 71 +++++++ .../mail/mail_triage_permission_test.go | 32 ++++ .../cli_e2e/mail/mail_user_reference_test.go | 48 +++++ tests/cli_e2e/wiki/helpers_test.go | 27 +++ tests/cli_e2e/wiki/wiki_workflow_test.go | 176 ++++++++++++++++++ 7 files changed, 455 insertions(+) create mode 100644 tests/cli_e2e/mail/helpers_test.go create mode 100644 tests/cli_e2e/mail/mail_bot_constraints_test.go create mode 100644 tests/cli_e2e/mail/mail_read_reference_test.go create mode 100644 tests/cli_e2e/mail/mail_triage_permission_test.go create mode 100644 tests/cli_e2e/mail/mail_user_reference_test.go create mode 100644 tests/cli_e2e/wiki/helpers_test.go create mode 100644 tests/cli_e2e/wiki/wiki_workflow_test.go diff --git a/tests/cli_e2e/mail/helpers_test.go b/tests/cli_e2e/mail/helpers_test.go new file mode 100644 index 00000000..a6d4b6da --- /dev/null +++ b/tests/cli_e2e/mail/helpers_test.go @@ -0,0 +1,35 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package mail + +import ( + "strings" + "testing" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func mailJSONPayload(t *testing.T, result *clie2e.Result) string { + t.Helper() + + raw := strings.TrimSpace(result.Stdout) + if raw == "" { + raw = strings.TrimSpace(result.Stderr) + } + + start := strings.LastIndex(raw, "\n{") + if start >= 0 { + start++ + } else { + start = strings.Index(raw, "{") + } + require.NotEqualf(t, -1, start, "json payload not found:\nstdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) + + payload := raw[start:] + require.Truef(t, gjson.Valid(payload), "invalid json payload:\n%s", payload) + + return payload +} diff --git a/tests/cli_e2e/mail/mail_bot_constraints_test.go b/tests/cli_e2e/mail/mail_bot_constraints_test.go new file mode 100644 index 00000000..b9758f9e --- /dev/null +++ b/tests/cli_e2e/mail/mail_bot_constraints_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package mail + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMail_BotIdentityConstraints(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + testCases := []struct { + name string + args []string + }{ + { + name: "watch", + args: []string{"mail", "+watch", "--print-output-schema"}, + }, + { + name: "reply", + args: []string{"mail", "+reply", "--message-id", "msg_001", "--body", "hello"}, + }, + { + name: "reply-all", + args: []string{"mail", "+reply-all", "--message-id", "msg_001", "--body", "hello"}, + }, + { + name: "send", + args: []string{"mail", "+send", "--subject", "hello", "--to", "alice@example.com", "--body", "body"}, + }, + { + name: "draft-create", + args: []string{"mail", "+draft-create", "--subject", "hello", "--body", "body"}, + }, + { + name: "draft-edit", + args: []string{"mail", "+draft-edit", "--print-patch-template"}, + }, + { + name: "forward", + args: []string{"mail", "+forward", "--message-id", "msg_001", "--to", "alice@example.com"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: tc.args, + DefaultAs: "bot", + }) + require.NoError(t, err) + assert.Equal(t, 1, result.ExitCode, "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) + assert.Contains(t, result.Stderr, "--as bot is not supported", "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) + assert.Contains(t, result.Stderr, "only supports: user", "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) + }) + } +} diff --git a/tests/cli_e2e/mail/mail_read_reference_test.go b/tests/cli_e2e/mail/mail_read_reference_test.go new file mode 100644 index 00000000..ff485132 --- /dev/null +++ b/tests/cli_e2e/mail/mail_read_reference_test.go @@ -0,0 +1,71 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package mail + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestMail_ReadShortcutReferenceOutputs(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + testCases := []struct { + name string + req clie2e.Request + key string + }{ + { + name: "message print output schema", + req: clie2e.Request{ + Args: []string{"mail", "+message", "--message-id", "msg_dummy", "--print-output-schema"}, + DefaultAs: "bot", + }, + key: "data.fields.message_id", + }, + { + name: "messages print output schema", + req: clie2e.Request{ + Args: []string{"mail", "+messages", "--message-ids", "msg1,msg2", "--print-output-schema"}, + DefaultAs: "bot", + }, + key: "data.messages_extra_fields.total", + }, + { + name: "thread print output schema", + req: clie2e.Request{ + Args: []string{"mail", "+thread", "--thread-id", "thr_dummy", "--print-output-schema"}, + DefaultAs: "bot", + }, + key: "data.thread_extra_fields.thread_id", + }, + { + name: "triage print filter schema", + req: clie2e.Request{ + Args: []string{"mail", "+triage", "--print-filter-schema"}, + DefaultAs: "bot", + }, + key: "data.fields.folder.type", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, tc.req) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + payload := mailJSONPayload(t, result) + assert.True(t, gjson.Get(payload, tc.key).Exists(), "stdout:\n%s", result.Stdout) + }) + } +} diff --git a/tests/cli_e2e/mail/mail_triage_permission_test.go b/tests/cli_e2e/mail/mail_triage_permission_test.go new file mode 100644 index 00000000..f7b83a42 --- /dev/null +++ b/tests/cli_e2e/mail/mail_triage_permission_test.go @@ -0,0 +1,32 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package mail + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestMail_TriagePermissionConstraint_Bot(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"mail", "+triage", "--max", "1", "--format", "json"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + assert.Equal(t, 1, result.ExitCode, "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) + + payload := mailJSONPayload(t, result) + assert.Equal(t, "permission", gjson.Get(payload, "error.type").String()) + assert.Equal(t, "bot", gjson.Get(payload, "identity").String()) + assert.Contains(t, gjson.Get(payload, "error.message").String(), "mail:user_mailbox.message:readonly") +} diff --git a/tests/cli_e2e/mail/mail_user_reference_test.go b/tests/cli_e2e/mail/mail_user_reference_test.go new file mode 100644 index 00000000..203bc83f --- /dev/null +++ b/tests/cli_e2e/mail/mail_user_reference_test.go @@ -0,0 +1,48 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package mail + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestMail_UserOnlyReferenceOutputs(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + t.Run("watch print output schema", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"mail", "+watch", "--print-output-schema"}, + DefaultAs: "user", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + + payload := mailJSONPayload(t, result) + assert.True(t, gjson.Get(payload, "metadata.message.message_id").Exists(), "stdout:\n%s", result.Stdout) + assert.True(t, gjson.Get(payload, "full.message.attachments").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("draft edit print patch template", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"mail", "+draft-edit", "--print-patch-template"}, + DefaultAs: "user", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + payload := mailJSONPayload(t, result) + assert.Equal(t, "user", gjson.Get(payload, "identity").String()) + assert.True(t, gjson.Get(payload, "data.template.ops").Exists(), "stdout:\n%s", result.Stdout) + assert.True(t, gjson.Get(payload, "data.supported_ops_by_group").Exists(), "stdout:\n%s", result.Stdout) + }) +} diff --git a/tests/cli_e2e/wiki/helpers_test.go b/tests/cli_e2e/wiki/helpers_test.go new file mode 100644 index 00000000..c93c94d0 --- /dev/null +++ b/tests/cli_e2e/wiki/helpers_test.go @@ -0,0 +1,27 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package wiki + +import ( + "context" + "testing" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func createWikiNode(t *testing.T, ctx context.Context, req clie2e.Request) gjson.Result { + t.Helper() + + result, err := clie2e.RunCmd(ctx, req) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, 0) + + node := gjson.Get(result.Stdout, "data.node") + require.True(t, node.Exists(), "stdout:\n%s", result.Stdout) + + return node +} diff --git a/tests/cli_e2e/wiki/wiki_workflow_test.go b/tests/cli_e2e/wiki/wiki_workflow_test.go new file mode 100644 index 00000000..87466f2c --- /dev/null +++ b/tests/cli_e2e/wiki/wiki_workflow_test.go @@ -0,0 +1,176 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package wiki + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestWiki_Workflow_Bot(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + suffix := time.Now().UTC().Format("20060102-150405") + createdTitle := "lark-cli-e2e-wiki-create-" + suffix + copiedTitle := "lark-cli-e2e-wiki-copy-" + suffix + + var spaceID string + var createdNodeToken string + var createdObjToken string + var copiedNodeToken string + + t.Run("create node", func(t *testing.T) { + node := createWikiNode(t, ctx, clie2e.Request{ + Args: []string{"wiki", "nodes", "create"}, + DefaultAs: "bot", + Params: map[string]any{ + "space_id": "my_library", + }, + Data: map[string]any{ + "node_type": "origin", + "obj_type": "docx", + "title": createdTitle, + }, + }) + + spaceID = node.Get("space_id").String() + createdNodeToken = node.Get("node_token").String() + createdObjToken = node.Get("obj_token").String() + require.NotEmpty(t, spaceID) + require.NotEmpty(t, createdNodeToken) + require.NotEmpty(t, createdObjToken) + assert.Equal(t, createdTitle, node.Get("title").String()) + assert.Equal(t, "origin", node.Get("node_type").String()) + assert.Equal(t, "docx", node.Get("obj_type").String()) + }) + + t.Run("get created node", func(t *testing.T) { + require.NotEmpty(t, createdNodeToken, "node token should be created before get_node") + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"wiki", "spaces", "get_node"}, + DefaultAs: "bot", + Params: map[string]any{ + "token": createdNodeToken, + "obj_type": "wiki", + }, + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, 0) + + assert.Equal(t, createdNodeToken, gjson.Get(result.Stdout, "data.node.node_token").String()) + assert.Equal(t, createdObjToken, gjson.Get(result.Stdout, "data.node.obj_token").String()) + assert.Equal(t, createdTitle, gjson.Get(result.Stdout, "data.node.title").String()) + assert.Equal(t, spaceID, gjson.Get(result.Stdout, "data.node.space_id").String()) + }) + + t.Run("get space", func(t *testing.T) { + require.NotEmpty(t, spaceID, "space ID should be available before get") + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"wiki", "spaces", "get"}, + DefaultAs: "bot", + Params: map[string]any{ + "space_id": spaceID, + }, + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, 0) + + assert.Equal(t, spaceID, gjson.Get(result.Stdout, "data.space.space_id").String()) + assert.NotEmpty(t, gjson.Get(result.Stdout, "data.space.name").String(), "stdout:\n%s", result.Stdout) + }) + + t.Run("list spaces", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"wiki", "spaces", "list"}, + DefaultAs: "bot", + Params: map[string]any{ + "page_size": 1, + }, + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, 0) + + assert.True(t, gjson.Get(result.Stdout, "data.page_token").Exists(), "stdout:\n%s", result.Stdout) + assert.True(t, gjson.Get(result.Stdout, "data.items").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("list nodes and find created node", func(t *testing.T) { + require.NotEmpty(t, spaceID, "space ID should be available before list") + require.NotEmpty(t, createdNodeToken, "node token should be available before list") + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"wiki", "nodes", "list"}, + DefaultAs: "bot", + Params: map[string]any{ + "space_id": spaceID, + "page_size": 50, + }, + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, 0) + + nodeItem := gjson.Get(result.Stdout, `data.items.#(node_token=="`+createdNodeToken+`")`) + assert.True(t, nodeItem.Exists(), "stdout:\n%s", result.Stdout) + assert.Equal(t, createdTitle, nodeItem.Get("title").String()) + assert.Equal(t, createdObjToken, nodeItem.Get("obj_token").String()) + }) + + t.Run("copy node", func(t *testing.T) { + require.NotEmpty(t, spaceID, "space ID should be available before copy") + require.NotEmpty(t, createdNodeToken, "node token should be available before copy") + + copiedNode := createWikiNode(t, ctx, clie2e.Request{ + Args: []string{"wiki", "nodes", "copy"}, + DefaultAs: "bot", + Params: map[string]any{ + "space_id": spaceID, + "node_token": createdNodeToken, + }, + Data: map[string]any{ + "target_space_id": spaceID, + "title": copiedTitle, + }, + }) + + copiedNodeToken = copiedNode.Get("node_token").String() + require.NotEmpty(t, copiedNodeToken) + assert.Equal(t, copiedTitle, copiedNode.Get("title").String()) + assert.Equal(t, spaceID, copiedNode.Get("space_id").String()) + assert.NotEqual(t, createdNodeToken, copiedNodeToken) + }) + + t.Run("list nodes and find copied node", func(t *testing.T) { + require.NotEmpty(t, spaceID, "space ID should be available before second list") + require.NotEmpty(t, copiedNodeToken, "copied node token should be available before second list") + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"wiki", "nodes", "list"}, + DefaultAs: "bot", + Params: map[string]any{ + "space_id": spaceID, + "page_size": 50, + }, + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, 0) + + nodeItem := gjson.Get(result.Stdout, `data.items.#(node_token=="`+copiedNodeToken+`")`) + assert.True(t, nodeItem.Exists(), "stdout:\n%s", result.Stdout) + assert.Equal(t, copiedTitle, nodeItem.Get("title").String()) + }) +} From 3ab45fb1f2e86b3806a810f7188816d01d5cd831 Mon Sep 17 00:00:00 2001 From: "zhao.yuxuan" Date: Tue, 7 Apr 2026 20:42:54 +0800 Subject: [PATCH 2/3] test: remove mail shortcut e2e coverage Change-Id: Ic1f8f4fc4a28499c20154b3f7e3a4b5f4ddc46f2 --- tests/cli_e2e/mail/helpers_test.go | 35 --------- .../cli_e2e/mail/mail_bot_constraints_test.go | 66 ----------------- .../cli_e2e/mail/mail_read_reference_test.go | 71 ------------------- .../mail/mail_triage_permission_test.go | 32 --------- .../cli_e2e/mail/mail_user_reference_test.go | 48 ------------- 5 files changed, 252 deletions(-) delete mode 100644 tests/cli_e2e/mail/helpers_test.go delete mode 100644 tests/cli_e2e/mail/mail_bot_constraints_test.go delete mode 100644 tests/cli_e2e/mail/mail_read_reference_test.go delete mode 100644 tests/cli_e2e/mail/mail_triage_permission_test.go delete mode 100644 tests/cli_e2e/mail/mail_user_reference_test.go diff --git a/tests/cli_e2e/mail/helpers_test.go b/tests/cli_e2e/mail/helpers_test.go deleted file mode 100644 index a6d4b6da..00000000 --- a/tests/cli_e2e/mail/helpers_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2026 Lark Technologies Pte. Ltd. -// SPDX-License-Identifier: MIT - -package mail - -import ( - "strings" - "testing" - - clie2e "github.com/larksuite/cli/tests/cli_e2e" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" -) - -func mailJSONPayload(t *testing.T, result *clie2e.Result) string { - t.Helper() - - raw := strings.TrimSpace(result.Stdout) - if raw == "" { - raw = strings.TrimSpace(result.Stderr) - } - - start := strings.LastIndex(raw, "\n{") - if start >= 0 { - start++ - } else { - start = strings.Index(raw, "{") - } - require.NotEqualf(t, -1, start, "json payload not found:\nstdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) - - payload := raw[start:] - require.Truef(t, gjson.Valid(payload), "invalid json payload:\n%s", payload) - - return payload -} diff --git a/tests/cli_e2e/mail/mail_bot_constraints_test.go b/tests/cli_e2e/mail/mail_bot_constraints_test.go deleted file mode 100644 index b9758f9e..00000000 --- a/tests/cli_e2e/mail/mail_bot_constraints_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2026 Lark Technologies Pte. Ltd. -// SPDX-License-Identifier: MIT - -package mail - -import ( - "context" - "testing" - "time" - - clie2e "github.com/larksuite/cli/tests/cli_e2e" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMail_BotIdentityConstraints(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - t.Cleanup(cancel) - - testCases := []struct { - name string - args []string - }{ - { - name: "watch", - args: []string{"mail", "+watch", "--print-output-schema"}, - }, - { - name: "reply", - args: []string{"mail", "+reply", "--message-id", "msg_001", "--body", "hello"}, - }, - { - name: "reply-all", - args: []string{"mail", "+reply-all", "--message-id", "msg_001", "--body", "hello"}, - }, - { - name: "send", - args: []string{"mail", "+send", "--subject", "hello", "--to", "alice@example.com", "--body", "body"}, - }, - { - name: "draft-create", - args: []string{"mail", "+draft-create", "--subject", "hello", "--body", "body"}, - }, - { - name: "draft-edit", - args: []string{"mail", "+draft-edit", "--print-patch-template"}, - }, - { - name: "forward", - args: []string{"mail", "+forward", "--message-id", "msg_001", "--to", "alice@example.com"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result, err := clie2e.RunCmd(ctx, clie2e.Request{ - Args: tc.args, - DefaultAs: "bot", - }) - require.NoError(t, err) - assert.Equal(t, 1, result.ExitCode, "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) - assert.Contains(t, result.Stderr, "--as bot is not supported", "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) - assert.Contains(t, result.Stderr, "only supports: user", "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) - }) - } -} diff --git a/tests/cli_e2e/mail/mail_read_reference_test.go b/tests/cli_e2e/mail/mail_read_reference_test.go deleted file mode 100644 index ff485132..00000000 --- a/tests/cli_e2e/mail/mail_read_reference_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2026 Lark Technologies Pte. Ltd. -// SPDX-License-Identifier: MIT - -package mail - -import ( - "context" - "testing" - "time" - - clie2e "github.com/larksuite/cli/tests/cli_e2e" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" -) - -func TestMail_ReadShortcutReferenceOutputs(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - t.Cleanup(cancel) - - testCases := []struct { - name string - req clie2e.Request - key string - }{ - { - name: "message print output schema", - req: clie2e.Request{ - Args: []string{"mail", "+message", "--message-id", "msg_dummy", "--print-output-schema"}, - DefaultAs: "bot", - }, - key: "data.fields.message_id", - }, - { - name: "messages print output schema", - req: clie2e.Request{ - Args: []string{"mail", "+messages", "--message-ids", "msg1,msg2", "--print-output-schema"}, - DefaultAs: "bot", - }, - key: "data.messages_extra_fields.total", - }, - { - name: "thread print output schema", - req: clie2e.Request{ - Args: []string{"mail", "+thread", "--thread-id", "thr_dummy", "--print-output-schema"}, - DefaultAs: "bot", - }, - key: "data.thread_extra_fields.thread_id", - }, - { - name: "triage print filter schema", - req: clie2e.Request{ - Args: []string{"mail", "+triage", "--print-filter-schema"}, - DefaultAs: "bot", - }, - key: "data.fields.folder.type", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result, err := clie2e.RunCmd(ctx, tc.req) - require.NoError(t, err) - result.AssertExitCode(t, 0) - result.AssertStdoutStatus(t, true) - - payload := mailJSONPayload(t, result) - assert.True(t, gjson.Get(payload, tc.key).Exists(), "stdout:\n%s", result.Stdout) - }) - } -} diff --git a/tests/cli_e2e/mail/mail_triage_permission_test.go b/tests/cli_e2e/mail/mail_triage_permission_test.go deleted file mode 100644 index f7b83a42..00000000 --- a/tests/cli_e2e/mail/mail_triage_permission_test.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2026 Lark Technologies Pte. Ltd. -// SPDX-License-Identifier: MIT - -package mail - -import ( - "context" - "testing" - "time" - - clie2e "github.com/larksuite/cli/tests/cli_e2e" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" -) - -func TestMail_TriagePermissionConstraint_Bot(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - t.Cleanup(cancel) - - result, err := clie2e.RunCmd(ctx, clie2e.Request{ - Args: []string{"mail", "+triage", "--max", "1", "--format", "json"}, - DefaultAs: "bot", - }) - require.NoError(t, err) - assert.Equal(t, 1, result.ExitCode, "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) - - payload := mailJSONPayload(t, result) - assert.Equal(t, "permission", gjson.Get(payload, "error.type").String()) - assert.Equal(t, "bot", gjson.Get(payload, "identity").String()) - assert.Contains(t, gjson.Get(payload, "error.message").String(), "mail:user_mailbox.message:readonly") -} diff --git a/tests/cli_e2e/mail/mail_user_reference_test.go b/tests/cli_e2e/mail/mail_user_reference_test.go deleted file mode 100644 index 203bc83f..00000000 --- a/tests/cli_e2e/mail/mail_user_reference_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2026 Lark Technologies Pte. Ltd. -// SPDX-License-Identifier: MIT - -package mail - -import ( - "context" - "testing" - "time" - - clie2e "github.com/larksuite/cli/tests/cli_e2e" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" -) - -func TestMail_UserOnlyReferenceOutputs(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - t.Cleanup(cancel) - - t.Run("watch print output schema", func(t *testing.T) { - result, err := clie2e.RunCmd(ctx, clie2e.Request{ - Args: []string{"mail", "+watch", "--print-output-schema"}, - DefaultAs: "user", - }) - require.NoError(t, err) - result.AssertExitCode(t, 0) - - payload := mailJSONPayload(t, result) - assert.True(t, gjson.Get(payload, "metadata.message.message_id").Exists(), "stdout:\n%s", result.Stdout) - assert.True(t, gjson.Get(payload, "full.message.attachments").Exists(), "stdout:\n%s", result.Stdout) - }) - - t.Run("draft edit print patch template", func(t *testing.T) { - result, err := clie2e.RunCmd(ctx, clie2e.Request{ - Args: []string{"mail", "+draft-edit", "--print-patch-template"}, - DefaultAs: "user", - }) - require.NoError(t, err) - result.AssertExitCode(t, 0) - result.AssertStdoutStatus(t, true) - - payload := mailJSONPayload(t, result) - assert.Equal(t, "user", gjson.Get(payload, "identity").String()) - assert.True(t, gjson.Get(payload, "data.template.ops").Exists(), "stdout:\n%s", result.Stdout) - assert.True(t, gjson.Get(payload, "data.supported_ops_by_group").Exists(), "stdout:\n%s", result.Stdout) - }) -} From 819d09e301cbcaafae2e199c51d219a7afbc4b84 Mon Sep 17 00:00:00 2001 From: "zhao.yuxuan" Date: Tue, 7 Apr 2026 20:44:09 +0800 Subject: [PATCH 3/3] test: add base shortcut e2e coverage Change-Id: I966c752cec04d52c6af76348b9227ec9e97f4ca4 --- tests/cli_e2e/base/base_core_workflow_test.go | 41 ++ .../base/base_dashboard_form_workflow_test.go | 265 +++++++++++ .../base/base_role_workflow_workflow_test.go | 176 +++++++ .../base_table_record_view_workflow_test.go | 438 ++++++++++++++++++ tests/cli_e2e/base/helpers_test.go | 392 ++++++++++++++++ 5 files changed, 1312 insertions(+) create mode 100644 tests/cli_e2e/base/base_core_workflow_test.go create mode 100644 tests/cli_e2e/base/base_dashboard_form_workflow_test.go create mode 100644 tests/cli_e2e/base/base_role_workflow_workflow_test.go create mode 100644 tests/cli_e2e/base/base_table_record_view_workflow_test.go create mode 100644 tests/cli_e2e/base/helpers_test.go diff --git a/tests/cli_e2e/base/base_core_workflow_test.go b/tests/cli_e2e/base/base_core_workflow_test.go new file mode 100644 index 00000000..3d76de4b --- /dev/null +++ b/tests/cli_e2e/base/base_core_workflow_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestBase_CoreWorkflow(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + t.Cleanup(cancel) + + baseToken := createBase(t, ctx, uniqueName("lark-cli-e2e-base")) + + t.Run("get base", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+base-get", "--base-token", baseToken}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot base get capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.NotEmpty(t, gjson.Get(result.Stdout, "data.base.name").String(), "stdout:\n%s", result.Stdout) + }) + + t.Run("copy base", func(t *testing.T) { + copiedToken := copyBase(t, ctx, baseToken, uniqueName("lark-cli-e2e-base-copy")) + assert.NotEqual(t, baseToken, copiedToken) + }) +} diff --git a/tests/cli_e2e/base/base_dashboard_form_workflow_test.go b/tests/cli_e2e/base/base_dashboard_form_workflow_test.go new file mode 100644 index 00000000..38d81073 --- /dev/null +++ b/tests/cli_e2e/base/base_dashboard_form_workflow_test.go @@ -0,0 +1,265 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestBase_DashboardWorkflow(t *testing.T) { + parentT := t + ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute) + t.Cleanup(cancel) + + baseToken := createBase(t, ctx, uniqueName("lark-cli-e2e-base-dashboard")) + tableID, _, _ := createTable(t, parentT, ctx, baseToken, uniqueName("DashboardTable"), `[{"name":"Amount","type":"number"}]`, "") + dashboardID := createDashboard(t, parentT, ctx, baseToken, uniqueName("Sales Dashboard")) + blockID := createBlock(t, parentT, ctx, baseToken, dashboardID, "Amount Stats", "statistics", `{"table_name":"DashboardTable","series":[{"field_name":"Amount","rollup":"sum"}],"count_all":true}`) + + t.Run("dashboard list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+dashboard-list", "--base-token", baseToken}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot dashboard list capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.items.#(dashboard_id==\""+dashboardID+"\")").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("dashboard get", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+dashboard-get", "--base-token", baseToken, "--dashboard-id", dashboardID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot dashboard get capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, dashboardID, gjson.Get(result.Stdout, "data.dashboard.dashboard_id").String()) + }) + + t.Run("dashboard update", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+dashboard-update", "--base-token", baseToken, "--dashboard-id", dashboardID, "--name", "Sales Dashboard Updated", "--theme-style", "SimpleBlue"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot dashboard update capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, "Sales Dashboard Updated", gjson.Get(result.Stdout, "data.dashboard.name").String()) + }) + + t.Run("dashboard block list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+dashboard-block-list", "--base-token", baseToken, "--dashboard-id", dashboardID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot dashboard block list capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.items.#(block_id==\""+blockID+"\")").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("dashboard block get", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+dashboard-block-get", "--base-token", baseToken, "--dashboard-id", dashboardID, "--block-id", blockID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot dashboard block get capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, blockID, gjson.Get(result.Stdout, "data.block.block_id").String()) + }) + + t.Run("dashboard block update", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+dashboard-block-update", "--base-token", baseToken, "--dashboard-id", dashboardID, "--block-id", blockID, "--name", "Amount Stats Updated", "--data-config", `{"table_name":"DashboardTable","series":[{"field_name":"Amount","rollup":"SUM"}],"count_all":true}`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot dashboard block update capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, "Amount Stats Updated", gjson.Get(result.Stdout, "data.block.name").String()) + }) + + t.Run("dashboard block delete", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+dashboard-block-delete", "--base-token", baseToken, "--dashboard-id", dashboardID, "--block-id", blockID, "--yes"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot dashboard block delete capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, blockID, gjson.Get(result.Stdout, "data.block_id").String()) + }) + + t.Run("dashboard delete", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+dashboard-delete", "--base-token", baseToken, "--dashboard-id", dashboardID, "--yes"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot dashboard delete capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, dashboardID, gjson.Get(result.Stdout, "data.dashboard_id").String()) + }) + + _ = tableID +} + +func TestBase_FormWorkflow(t *testing.T) { + parentT := t + ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute) + t.Cleanup(cancel) + + baseToken := createBase(t, ctx, uniqueName("lark-cli-e2e-base-form")) + tableID, _, _ := createTable(t, parentT, ctx, baseToken, uniqueName("FormTable"), `[{"name":"Name","type":"text"}]`, "") + formID := createForm(t, parentT, ctx, baseToken, tableID, uniqueName("Survey")) + + var questionID string + + t.Run("form get", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+form-get", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot form get capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, formID, gjson.Get(result.Stdout, "data.id").String()) + }) + + t.Run("form list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+form-list", "--base-token", baseToken, "--table-id", tableID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot form list capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.forms.#(id==\""+formID+"\")").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("form update", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+form-update", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--name", "Survey Updated", "--description", "updated description"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot form update capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, "Survey Updated", gjson.Get(result.Stdout, "data.name").String()) + }) + + t.Run("form questions create", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+form-questions-create", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--questions", `[{"type":"text","title":"Your Name","required":true}]`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot form question create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + questionID = gjson.Get(result.Stdout, "data.questions.0.id").String() + require.NotEmpty(t, questionID, "stdout:\n%s", result.Stdout) + }) + + t.Run("form questions list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+form-questions-list", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot form question list capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.questions.#(id==\""+questionID+"\")").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("form questions update", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+form-questions-update", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--questions", `[{"id":"` + questionID + `","title":"Your Name Updated","required":true}]`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot form question update capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, "Your Name Updated", gjson.Get(result.Stdout, "data.questions.0.title").String()) + }) + + t.Run("form questions delete", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+form-questions-delete", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--question-ids", `["` + questionID + `"]`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot form question delete capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, true, gjson.Get(result.Stdout, "data.deleted").Bool(), "stdout:\n%s", result.Stdout) + }) + + t.Run("form delete", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+form-delete", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--yes"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot form delete capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, true, gjson.Get(result.Stdout, "data.deleted").Bool(), "stdout:\n%s", result.Stdout) + }) +} diff --git a/tests/cli_e2e/base/base_role_workflow_workflow_test.go b/tests/cli_e2e/base/base_role_workflow_workflow_test.go new file mode 100644 index 00000000..4b4ad9ee --- /dev/null +++ b/tests/cli_e2e/base/base_role_workflow_workflow_test.go @@ -0,0 +1,176 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestBase_RoleAdvpermAndWorkflowCoverage(t *testing.T) { + parentT := t + ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute) + t.Cleanup(cancel) + + baseToken := createBase(t, ctx, uniqueName("lark-cli-e2e-base-admin")) + + t.Run("advperm enable", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+advperm-enable", "--base-token", baseToken}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot advanced permission enable capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + }) + + roleID := createRole(t, parentT, ctx, baseToken, `{"role_name":"Reviewer","role_type":"custom_role"}`) + + t.Run("role list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+role-list", "--base-token", baseToken}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot role list capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.#(role_id==\""+roleID+"\")").Exists() || gjson.Get(result.Stdout, "data.data.#(role_id==\""+roleID+"\")").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("role get", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+role-get", "--base-token", baseToken, "--role-id", roleID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot role get capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, roleID, gjson.Get(result.Stdout, "data.role_id").String()) + }) + + t.Run("role update", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+role-update", "--base-token", baseToken, "--role-id", roleID, "--json", `{"role_name":"Reviewer Updated"}`, "--yes"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot role update capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, "Reviewer Updated", gjson.Get(result.Stdout, "data.role_name").String()) + }) + + workflowID := createWorkflow(t, ctx, baseToken, `{"title":"My Workflow","steps":[]}`) + + t.Run("workflow list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+workflow-list", "--base-token", baseToken}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot workflow list capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.items.#(workflow_id==\""+workflowID+"\")").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("workflow get", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+workflow-get", "--base-token", baseToken, "--workflow-id", workflowID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot workflow get capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, workflowID, gjson.Get(result.Stdout, "data.workflow_id").String()) + }) + + t.Run("workflow update", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+workflow-update", "--base-token", baseToken, "--workflow-id", workflowID, "--json", `{"title":"My Workflow Updated","steps":[]}`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot workflow update capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, "My Workflow Updated", gjson.Get(result.Stdout, "data.title").String()) + }) + + t.Run("workflow enable", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+workflow-enable", "--base-token", baseToken, "--workflow-id", workflowID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot workflow enable capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + }) + + t.Run("workflow disable", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+workflow-disable", "--base-token", baseToken, "--workflow-id", workflowID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot workflow disable capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + }) + + t.Run("role delete", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+role-delete", "--base-token", baseToken, "--role-id", roleID, "--yes"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot role delete capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + }) + + t.Run("advperm disable", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+advperm-disable", "--base-token", baseToken, "--yes"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot advanced permission disable capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + }) +} diff --git a/tests/cli_e2e/base/base_table_record_view_workflow_test.go b/tests/cli_e2e/base/base_table_record_view_workflow_test.go new file mode 100644 index 00000000..182cebe2 --- /dev/null +++ b/tests/cli_e2e/base/base_table_record_view_workflow_test.go @@ -0,0 +1,438 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestBase_TableFieldRecordViewWorkflow(t *testing.T) { + parentT := t + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + baseToken := createBase(t, ctx, uniqueName("lark-cli-e2e-base-main")) + tableID, primaryFieldID, primaryViewID := createTable(t, parentT, ctx, baseToken, uniqueName("Orders"), `[{"name":"Name","type":"text"}]`, `{"name":"Main","type":"grid"}`) + require.NotEmpty(t, primaryFieldID) + require.NotEmpty(t, primaryViewID) + + statusFieldID := createField(t, parentT, ctx, baseToken, tableID, `{"name":"Status","type":"select","multiple":false,"options":[{"name":"Open","hue":"Blue"},{"name":"Closed","hue":"Green"}]}`) + noteFieldID := createField(t, parentT, ctx, baseToken, tableID, `{"name":"Note","type":"text"}`) + attachmentFieldID := createField(t, parentT, ctx, baseToken, tableID, `{"name":"Files","type":"attachment"}`) + dueFieldID := createField(t, parentT, ctx, baseToken, tableID, `{"name":"Due","type":"datetime","style":{"format":"yyyy/MM/dd"}}`) + + recordID := createRecord(t, parentT, ctx, baseToken, tableID, `{"fields":{"Name":"Alice","Status":"Open","Note":"Seed row"}}`) + galleryViewID := createView(t, parentT, ctx, baseToken, tableID, `{"name":"Gallery","type":"gallery"}`) + calendarViewID := createView(t, parentT, ctx, baseToken, tableID, `{"name":"Calendar","type":"calendar"}`) + deleteViewID := createView(t, parentT, ctx, baseToken, tableID, `{"name":"DeleteMe","type":"grid"}`) + + t.Run("table list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+table-list", "--base-token", baseToken}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot table list capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.items.#(table_id==\""+tableID+"\")").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("table get", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+table-get", "--base-token", baseToken, "--table-id", tableID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot table get capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, tableID, gjson.Get(result.Stdout, "data.table.id").String()) + }) + + t.Run("table update", func(t *testing.T) { + newName := uniqueName("Orders-Renamed") + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+table-update", "--base-token", baseToken, "--table-id", tableID, "--name", newName}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot table update capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, newName, gjson.Get(result.Stdout, "data.table.name").String()) + }) + + t.Run("field list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+field-list", "--base-token", baseToken, "--table-id", tableID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot field list capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.items.#(field_id==\""+statusFieldID+"\")").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("field get", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+field-get", "--base-token", baseToken, "--table-id", tableID, "--field-id", statusFieldID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot field get capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, statusFieldID, gjson.Get(result.Stdout, "data.field.id").String()) + }) + + t.Run("field update", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+field-update", "--base-token", baseToken, "--table-id", tableID, "--field-id", noteFieldID, "--json", `{"name":"Note Updated","type":"text"}`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot field update capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, "Note Updated", gjson.Get(result.Stdout, "data.field.name").String()) + }) + + t.Run("field search options", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+field-search-options", "--base-token", baseToken, "--table-id", tableID, "--field-id", statusFieldID, "--keyword", "Op", "--limit", "10"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot field option search capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.GreaterOrEqual(t, len(gjson.Get(result.Stdout, "data.options").Array()), 1, "stdout:\n%s", result.Stdout) + }) + + t.Run("record list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+record-list", "--base-token", baseToken, "--table-id", tableID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot record list capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.items.#(record_id==\""+recordID+"\")").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("record get", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+record-get", "--base-token", baseToken, "--table-id", tableID, "--record-id", recordID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot record get capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, recordID, gjson.Get(result.Stdout, "data.record.record_id").String()) + }) + + t.Run("record update", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+record-upsert", "--base-token", baseToken, "--table-id", tableID, "--record-id", recordID, "--json", `{"fields":{"Status":"Closed","Note Updated":"Done"}}`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot record update capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, recordID, gjson.Get(result.Stdout, "data.record.record_id").String()) + }) + + t.Run("record history list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+record-history-list", "--base-token", baseToken, "--table-id", tableID, "--record-id", recordID, "--page-size", "10"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot record history capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.items.#").Int() >= 0, "stdout:\n%s", result.Stdout) + }) + + t.Run("record upload attachment", func(t *testing.T) { + filePath := writeTempAttachment(t, "hello attachment") + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+record-upload-attachment", "--base-token", baseToken, "--table-id", tableID, "--record-id", recordID, "--field-id", attachmentFieldID, "--file", filePath, "--name", "attachment.txt"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot attachment upload capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, true, gjson.Get(result.Stdout, "data.updated").Bool(), "stdout:\n%s", result.Stdout) + }) + + t.Run("data query", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+data-query", "--base-token", baseToken, "--dsl", `{"dimensions":[{"field_name":"Status"}]}`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot base data query capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + }) + + t.Run("view list", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-list", "--base-token", baseToken, "--table-id", tableID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot view list capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(result.Stdout, "data.items.#(view_id==\""+galleryViewID+"\")").Exists(), "stdout:\n%s", result.Stdout) + }) + + t.Run("view get", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-get", "--base-token", baseToken, "--table-id", tableID, "--view-id", primaryViewID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot view get capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, primaryViewID, gjson.Get(result.Stdout, "data.view.id").String()) + }) + + t.Run("view rename", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-rename", "--base-token", baseToken, "--table-id", tableID, "--view-id", deleteViewID, "--name", "DeleteSoon"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot view rename capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, "DeleteSoon", gjson.Get(result.Stdout, "data.view.name").String()) + }) + + t.Run("view set and get filter", func(t *testing.T) { + setResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-set-filter", "--base-token", baseToken, "--table-id", tableID, "--view-id", primaryViewID, "--json", `{"conditions":[{"field_name":"Status"}]}`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if setResult.ExitCode != 0 { + skipIfBaseUnavailable(t, setResult, "requires bot view filter update capability") + } + setResult.AssertExitCode(t, 0) + setResult.AssertStdoutStatus(t, true) + + getResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-get-filter", "--base-token", baseToken, "--table-id", tableID, "--view-id", primaryViewID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if getResult.ExitCode != 0 { + skipIfBaseUnavailable(t, getResult, "requires bot view filter read capability") + } + getResult.AssertExitCode(t, 0) + getResult.AssertStdoutStatus(t, true) + assert.True(t, gjson.Get(getResult.Stdout, "data.filter.conditions.0").Exists(), "stdout:\n%s", getResult.Stdout) + }) + + t.Run("view set and get group", func(t *testing.T) { + setResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-set-group", "--base-token", baseToken, "--table-id", tableID, "--view-id", primaryViewID, "--json", `[{"field":"` + statusFieldID + `","desc":false}]`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if setResult.ExitCode != 0 { + skipIfBaseUnavailable(t, setResult, "requires bot view group update capability") + } + setResult.AssertExitCode(t, 0) + setResult.AssertStdoutStatus(t, true) + + getResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-get-group", "--base-token", baseToken, "--table-id", tableID, "--view-id", primaryViewID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if getResult.ExitCode != 0 { + skipIfBaseUnavailable(t, getResult, "requires bot view group read capability") + } + getResult.AssertExitCode(t, 0) + getResult.AssertStdoutStatus(t, true) + }) + + t.Run("view set and get sort", func(t *testing.T) { + setResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-set-sort", "--base-token", baseToken, "--table-id", tableID, "--view-id", primaryViewID, "--json", `[{"field":"` + statusFieldID + `","desc":true}]`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if setResult.ExitCode != 0 { + skipIfBaseUnavailable(t, setResult, "requires bot view sort update capability") + } + setResult.AssertExitCode(t, 0) + setResult.AssertStdoutStatus(t, true) + + getResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-get-sort", "--base-token", baseToken, "--table-id", tableID, "--view-id", primaryViewID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if getResult.ExitCode != 0 { + skipIfBaseUnavailable(t, getResult, "requires bot view sort read capability") + } + getResult.AssertExitCode(t, 0) + getResult.AssertStdoutStatus(t, true) + }) + + t.Run("view set and get timebar", func(t *testing.T) { + setResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-set-timebar", "--base-token", baseToken, "--table-id", tableID, "--view-id", calendarViewID, "--json", `{"start_time":"` + dueFieldID + `","title":"` + primaryFieldID + `"}`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if setResult.ExitCode != 0 { + skipIfBaseUnavailable(t, setResult, "requires bot view timebar update capability") + } + setResult.AssertExitCode(t, 0) + setResult.AssertStdoutStatus(t, true) + + getResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-get-timebar", "--base-token", baseToken, "--table-id", tableID, "--view-id", calendarViewID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if getResult.ExitCode != 0 { + skipIfBaseUnavailable(t, getResult, "requires bot view timebar read capability") + } + getResult.AssertExitCode(t, 0) + getResult.AssertStdoutStatus(t, true) + }) + + t.Run("view set and get card", func(t *testing.T) { + setResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-set-card", "--base-token", baseToken, "--table-id", tableID, "--view-id", galleryViewID, "--json", `{"cover_field":"` + attachmentFieldID + `"}`}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if setResult.ExitCode != 0 { + skipIfBaseUnavailable(t, setResult, "requires bot view card update capability") + } + setResult.AssertExitCode(t, 0) + setResult.AssertStdoutStatus(t, true) + + getResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-get-card", "--base-token", baseToken, "--table-id", tableID, "--view-id", galleryViewID}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if getResult.ExitCode != 0 { + skipIfBaseUnavailable(t, getResult, "requires bot view card read capability") + } + getResult.AssertExitCode(t, 0) + getResult.AssertStdoutStatus(t, true) + }) + + t.Run("record delete", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+record-delete", "--base-token", baseToken, "--table-id", tableID, "--record-id", recordID, "--yes"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot record delete capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, recordID, gjson.Get(result.Stdout, "data.record_id").String()) + }) + + t.Run("view delete", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-delete", "--base-token", baseToken, "--table-id", tableID, "--view-id", deleteViewID, "--yes"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot view delete capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, deleteViewID, gjson.Get(result.Stdout, "data.view_id").String()) + }) + + t.Run("field delete", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+field-delete", "--base-token", baseToken, "--table-id", tableID, "--field-id", noteFieldID, "--yes"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot field delete capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, noteFieldID, gjson.Get(result.Stdout, "data.field_id").String()) + }) + + t.Run("table delete", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+table-delete", "--base-token", baseToken, "--table-id", tableID, "--yes"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot table delete capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + assert.Equal(t, tableID, gjson.Get(result.Stdout, "data.table_id").String()) + }) +} diff --git a/tests/cli_e2e/base/helpers_test.go b/tests/cli_e2e/base/helpers_test.go new file mode 100644 index 00000000..10274adf --- /dev/null +++ b/tests/cli_e2e/base/helpers_test.go @@ -0,0 +1,392 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func baseJSONPayload(t *testing.T, result *clie2e.Result) string { + t.Helper() + + raw := strings.TrimSpace(result.Stdout) + if raw == "" { + raw = strings.TrimSpace(result.Stderr) + } + + start := strings.LastIndex(raw, "\n{") + if start >= 0 { + start++ + } else { + start = strings.Index(raw, "{") + } + require.NotEqualf(t, -1, start, "json payload not found:\nstdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) + + payload := raw[start:] + require.Truef(t, gjson.Valid(payload), "invalid json payload:\n%s", payload) + return payload +} + +func skipIfBaseUnavailable(t *testing.T, result *clie2e.Result, reason string) { + t.Helper() + + payload := baseJSONPayload(t, result) + errType := gjson.Get(payload, "error.type").String() + switch errType { + case "config", "missing_scope", "permission", "auth", "auth_error", "security_policy": + t.Skipf("%s: %s", reason, gjson.Get(payload, "error.message").String()) + } +} + +func createBase(t *testing.T, ctx context.Context, name string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+base-create", "--name", name, "--time-zone", "Asia/Shanghai"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot base create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + baseToken := gjson.Get(result.Stdout, "data.base.app_token").String() + if baseToken == "" { + baseToken = gjson.Get(result.Stdout, "data.base.base_token").String() + } + require.NotEmpty(t, baseToken, "stdout:\n%s", result.Stdout) + return baseToken +} + +func copyBase(t *testing.T, ctx context.Context, baseToken string, name string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+base-copy", "--base-token", baseToken, "--name", name, "--without-content", "--time-zone", "Asia/Shanghai"}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot base copy capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + copiedToken := gjson.Get(result.Stdout, "data.base.app_token").String() + if copiedToken == "" { + copiedToken = gjson.Get(result.Stdout, "data.base.base_token").String() + } + require.NotEmpty(t, copiedToken, "stdout:\n%s", result.Stdout) + return copiedToken +} + +func createTable(t *testing.T, parentT *testing.T, ctx context.Context, baseToken string, name string, fieldsJSON string, viewJSON string) (tableID string, primaryFieldID string, primaryViewID string) { + t.Helper() + + args := []string{"base", "+table-create", "--base-token", baseToken, "--name", name} + if fieldsJSON != "" { + args = append(args, "--fields", fieldsJSON) + } + if viewJSON != "" { + args = append(args, "--view", viewJSON) + } + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: args, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot table create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + tableID = gjson.Get(result.Stdout, "data.table.id").String() + if tableID == "" { + tableID = gjson.Get(result.Stdout, "data.table.table_id").String() + } + require.NotEmpty(t, tableID, "stdout:\n%s", result.Stdout) + + primaryFieldID = gjson.Get(result.Stdout, "data.fields.0.id").String() + if primaryFieldID == "" { + primaryFieldID = gjson.Get(result.Stdout, "data.fields.0.field_id").String() + } + + primaryViewID = gjson.Get(result.Stdout, "data.views.0.id").String() + if primaryViewID == "" { + primaryViewID = gjson.Get(result.Stdout, "data.views.0.view_id").String() + } + + parentT.Cleanup(func() { + deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{ + Args: []string{"base", "+table-delete", "--base-token", baseToken, "--table-id", tableID, "--yes"}, + DefaultAs: "bot", + }) + if deleteErr != nil || deleteResult.ExitCode != 0 { + parentT.Logf("best-effort table cleanup skipped: table=%s err=%v stdout=%s stderr=%s", tableID, deleteErr, deleteResult.Stdout, deleteResult.Stderr) + } + }) + + return tableID, primaryFieldID, primaryViewID +} + +func createField(t *testing.T, parentT *testing.T, ctx context.Context, baseToken string, tableID string, body string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+field-create", "--base-token", baseToken, "--table-id", tableID, "--json", body}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot field create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + fieldID := gjson.Get(result.Stdout, "data.field.id").String() + if fieldID == "" { + fieldID = gjson.Get(result.Stdout, "data.field.field_id").String() + } + require.NotEmpty(t, fieldID, "stdout:\n%s", result.Stdout) + + parentT.Cleanup(func() { + deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{ + Args: []string{"base", "+field-delete", "--base-token", baseToken, "--table-id", tableID, "--field-id", fieldID, "--yes"}, + DefaultAs: "bot", + }) + if deleteErr != nil || deleteResult.ExitCode != 0 { + parentT.Logf("best-effort field cleanup skipped: field=%s err=%v stdout=%s stderr=%s", fieldID, deleteErr, deleteResult.Stdout, deleteResult.Stderr) + } + }) + + return fieldID +} + +func createRecord(t *testing.T, parentT *testing.T, ctx context.Context, baseToken string, tableID string, body string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+record-upsert", "--base-token", baseToken, "--table-id", tableID, "--json", body}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot record create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + recordID := gjson.Get(result.Stdout, "data.record.record_id").String() + require.NotEmpty(t, recordID, "stdout:\n%s", result.Stdout) + + parentT.Cleanup(func() { + deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{ + Args: []string{"base", "+record-delete", "--base-token", baseToken, "--table-id", tableID, "--record-id", recordID, "--yes"}, + DefaultAs: "bot", + }) + if deleteErr != nil || deleteResult.ExitCode != 0 { + parentT.Logf("best-effort record cleanup skipped: record=%s err=%v stdout=%s stderr=%s", recordID, deleteErr, deleteResult.Stdout, deleteResult.Stderr) + } + }) + + return recordID +} + +func createView(t *testing.T, parentT *testing.T, ctx context.Context, baseToken string, tableID string, body string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+view-create", "--base-token", baseToken, "--table-id", tableID, "--json", body}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot view create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + viewID := gjson.Get(result.Stdout, "data.views.0.id").String() + if viewID == "" { + viewID = gjson.Get(result.Stdout, "data.views.0.view_id").String() + } + require.NotEmpty(t, viewID, "stdout:\n%s", result.Stdout) + + parentT.Cleanup(func() { + deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{ + Args: []string{"base", "+view-delete", "--base-token", baseToken, "--table-id", tableID, "--view-id", viewID, "--yes"}, + DefaultAs: "bot", + }) + if deleteErr != nil || deleteResult.ExitCode != 0 { + parentT.Logf("best-effort view cleanup skipped: view=%s err=%v stdout=%s stderr=%s", viewID, deleteErr, deleteResult.Stdout, deleteResult.Stderr) + } + }) + + return viewID +} + +func createDashboard(t *testing.T, parentT *testing.T, ctx context.Context, baseToken string, name string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+dashboard-create", "--base-token", baseToken, "--name", name}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot dashboard create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + dashboardID := gjson.Get(result.Stdout, "data.dashboard.dashboard_id").String() + require.NotEmpty(t, dashboardID, "stdout:\n%s", result.Stdout) + + parentT.Cleanup(func() { + deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{ + Args: []string{"base", "+dashboard-delete", "--base-token", baseToken, "--dashboard-id", dashboardID, "--yes"}, + DefaultAs: "bot", + }) + if deleteErr != nil || deleteResult.ExitCode != 0 { + parentT.Logf("best-effort dashboard cleanup skipped: dashboard=%s err=%v stdout=%s stderr=%s", dashboardID, deleteErr, deleteResult.Stdout, deleteResult.Stderr) + } + }) + + return dashboardID +} + +func createBlock(t *testing.T, parentT *testing.T, ctx context.Context, baseToken string, dashboardID string, name string, blockType string, dataConfig string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+dashboard-block-create", "--base-token", baseToken, "--dashboard-id", dashboardID, "--name", name, "--type", blockType, "--data-config", dataConfig}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot dashboard block create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + blockID := gjson.Get(result.Stdout, "data.block.block_id").String() + require.NotEmpty(t, blockID, "stdout:\n%s", result.Stdout) + + parentT.Cleanup(func() { + deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{ + Args: []string{"base", "+dashboard-block-delete", "--base-token", baseToken, "--dashboard-id", dashboardID, "--block-id", blockID, "--yes"}, + DefaultAs: "bot", + }) + if deleteErr != nil || deleteResult.ExitCode != 0 { + parentT.Logf("best-effort block cleanup skipped: block=%s err=%v stdout=%s stderr=%s", blockID, deleteErr, deleteResult.Stdout, deleteResult.Stderr) + } + }) + + return blockID +} + +func createForm(t *testing.T, parentT *testing.T, ctx context.Context, baseToken string, tableID string, name string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+form-create", "--base-token", baseToken, "--table-id", tableID, "--name", name}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot form create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + formID := gjson.Get(result.Stdout, "data.id").String() + require.NotEmpty(t, formID, "stdout:\n%s", result.Stdout) + + parentT.Cleanup(func() { + deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{ + Args: []string{"base", "+form-delete", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--yes"}, + DefaultAs: "bot", + }) + if deleteErr != nil || deleteResult.ExitCode != 0 { + parentT.Logf("best-effort form cleanup skipped: form=%s err=%v stdout=%s stderr=%s", formID, deleteErr, deleteResult.Stdout, deleteResult.Stderr) + } + }) + + return formID +} + +func createRole(t *testing.T, parentT *testing.T, ctx context.Context, baseToken string, body string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+role-create", "--base-token", baseToken, "--json", body}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot role create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + roleID := gjson.Get(result.Stdout, "data.role_id").String() + require.NotEmpty(t, roleID, "stdout:\n%s", result.Stdout) + + parentT.Cleanup(func() { + deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{ + Args: []string{"base", "+role-delete", "--base-token", baseToken, "--role-id", roleID, "--yes"}, + DefaultAs: "bot", + }) + if deleteErr != nil || deleteResult.ExitCode != 0 { + parentT.Logf("best-effort role cleanup skipped: role=%s err=%v stdout=%s stderr=%s", roleID, deleteErr, deleteResult.Stdout, deleteResult.Stderr) + } + }) + + return roleID +} + +func createWorkflow(t *testing.T, ctx context.Context, baseToken string, body string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{"base", "+workflow-create", "--base-token", baseToken, "--json", body}, + DefaultAs: "bot", + }) + require.NoError(t, err) + if result.ExitCode != 0 { + skipIfBaseUnavailable(t, result, "requires bot workflow create capability") + } + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + workflowID := gjson.Get(result.Stdout, "data.workflow_id").String() + require.NotEmpty(t, workflowID, "stdout:\n%s", result.Stdout) + return workflowID +} + +func writeTempAttachment(t *testing.T, content string) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "attachment.txt") + err := os.WriteFile(path, []byte(content), 0o644) + require.NoError(t, err) + return path +} + +func uniqueName(prefix string) string { + return prefix + "-" + time.Now().UTC().Format("20060102-150405") +}