chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestBase_AttachmentDryRun(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
workDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(workDir, "report.txt"), []byte("hello"), 0o600))
|
||||
require.NoError(t, os.Mkdir(filepath.Join(workDir, "downloads"), 0o700))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("upload", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+record-upload-attachment",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_x",
|
||||
"--record-id", "rec_x",
|
||||
"--field-id", "fld_att",
|
||||
"--file", "report.txt",
|
||||
"--dry-run",
|
||||
},
|
||||
WorkDir: workDir,
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_att", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "/open-apis/drive/v1/medias/upload_all", gjson.Get(out, "api.1.url").String(), out)
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/append_attachments", gjson.Get(out, "api.2.url").String(), out)
|
||||
require.Equal(t, "<uploaded_file_token>", gjson.Get(out, "api.2.body.attachments.rec_x.fld_att.0.file_token").String(), out)
|
||||
})
|
||||
|
||||
t.Run("download", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+record-download-attachment",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_x",
|
||||
"--record-id", "rec_x",
|
||||
"--file-token", "box_a",
|
||||
"--output", "downloads",
|
||||
"--dry-run",
|
||||
},
|
||||
WorkDir: workDir,
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/get_attachments", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "/open-apis/drive/v1/medias/%3Cfile_token%3E/download", gjson.Get(out, "api.1.url").String(), out)
|
||||
require.Equal(t, "<extra_info_if_present>", gjson.Get(out, "api.1.params.extra").String(), out)
|
||||
})
|
||||
|
||||
t.Run("download all", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+record-download-attachment",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_x",
|
||||
"--record-id", "rec_x",
|
||||
"--output", "downloads",
|
||||
"--dry-run",
|
||||
},
|
||||
WorkDir: workDir,
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/get_attachments", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "/open-apis/drive/v1/medias/%3Cfile_token%3E/download", gjson.Get(out, "api.1.url").String(), out)
|
||||
})
|
||||
|
||||
t.Run("remove", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+record-remove-attachment",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_x",
|
||||
"--record-id", "rec_x",
|
||||
"--field-id", "fld_att",
|
||||
"--file-token", "box_a",
|
||||
"--dry-run",
|
||||
},
|
||||
WorkDir: workDir,
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/remove_attachments", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "box_a", gjson.Get(out, "api.0.body.attachments.rec_x.fld_att.0.file_token").String(), out)
|
||||
})
|
||||
}
|
||||
|
||||
func setBaseDryRunConfigEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_APP_ID", "base_dryrun_test")
|
||||
t.Setenv("LARKSUITE_CLI_APP_SECRET", "base_dryrun_secret")
|
||||
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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_BasicWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
baseName := "lark-cli-e2e-base-basic-" + clie2e.GenerateSuffix()
|
||||
baseToken := createBaseWithRetry(t, ctx, baseName)
|
||||
|
||||
t.Run("get base as bot", 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)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
returnedBaseToken := gjson.Get(result.Stdout, "data.base.app_token").String()
|
||||
if returnedBaseToken == "" {
|
||||
returnedBaseToken = gjson.Get(result.Stdout, "data.base.base_token").String()
|
||||
}
|
||||
assert.Equal(t, baseToken, returnedBaseToken, "stdout:\n%s", result.Stdout)
|
||||
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.base.name").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
tableName := "lark-cli-e2e-table-basic-" + clie2e.GenerateSuffix()
|
||||
tableID, _, _ := createTableWithRetry(
|
||||
t,
|
||||
parentT,
|
||||
ctx,
|
||||
baseToken,
|
||||
tableName,
|
||||
`[{"name":"Name","type":"text"}]`,
|
||||
`{"name":"Main","type":"grid"}`,
|
||||
)
|
||||
|
||||
t.Run("get table as bot", 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)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
assert.Equal(t, tableID, gjson.Get(result.Stdout, "data.table.id").String())
|
||||
assert.Equal(t, tableName, gjson.Get(result.Stdout, "data.table.name").String())
|
||||
})
|
||||
|
||||
t.Run("list tables and find created table as bot", func(t *testing.T) {
|
||||
table := findBaseTableByID(t, ctx, baseToken, tableID)
|
||||
assert.Equal(t, tableID, table.Get("id").String())
|
||||
assert.Equal(t, tableName, table.Get("name").String())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// 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/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestBaseBlockDryRun(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("list all", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+base-block-list",
|
||||
"--base-token", "app_x",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/list", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out)
|
||||
require.False(t, gjson.Get(out, "api.0.body.parent_id").Exists(), out)
|
||||
})
|
||||
|
||||
t.Run("list folder", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+base-block-list",
|
||||
"--base-token", "app_x",
|
||||
"--parent-id", "blk_folder",
|
||||
"--type", "docx",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/list", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "blk_folder", gjson.Get(out, "api.0.body.parent_id").String(), out)
|
||||
require.False(t, gjson.Get(out, "api.0.body.type").Exists(), out)
|
||||
})
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+base-block-create",
|
||||
"--base-token", "app_x",
|
||||
"--type", "docx",
|
||||
"--name", "Spec",
|
||||
"--parent-id", "blk_folder",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out)
|
||||
require.Equal(t, "docx", gjson.Get(out, "api.0.body.type").String(), out)
|
||||
require.Equal(t, "Spec", gjson.Get(out, "api.0.body.name").String(), out)
|
||||
require.Equal(t, "blk_folder", gjson.Get(out, "api.0.body.parent_id").String(), out)
|
||||
})
|
||||
|
||||
t.Run("move root", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+base-block-move",
|
||||
"--base-token", "app_x",
|
||||
"--block-id", "blk_a",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a/move", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out)
|
||||
require.True(t, gjson.Get(out, "api.0.body.parent_id").Exists(), out)
|
||||
require.Equal(t, "Null", gjson.Get(out, "api.0.body.parent_id").Type.String(), out)
|
||||
})
|
||||
|
||||
t.Run("move after", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+base-block-move",
|
||||
"--base-token", "app_x",
|
||||
"--block-id", "blk_a",
|
||||
"--parent-id", "blk_folder",
|
||||
"--after-id", "blk_b",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a/move", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "blk_folder", gjson.Get(out, "api.0.body.parent_id").String(), out)
|
||||
require.Equal(t, "blk_b", gjson.Get(out, "api.0.body.after_id").String(), out)
|
||||
})
|
||||
|
||||
t.Run("rename", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+base-block-rename",
|
||||
"--base-token", "app_x",
|
||||
"--block-id", "blk_a",
|
||||
"--name", "Renamed",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a/rename", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out)
|
||||
require.Equal(t, "Renamed", gjson.Get(out, "api.0.body.name").String(), out)
|
||||
})
|
||||
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+base-block-delete",
|
||||
"--base-token", "app_x",
|
||||
"--block-id", "blk_a",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "DELETE", gjson.Get(out, "api.0.method").String(), out)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestBaseCreateDryRun(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+base-create",
|
||||
"--name", "Project Tracker",
|
||||
"--table-name", "Tasks",
|
||||
"--time-zone", "Asia/Shanghai",
|
||||
"--fields", `[{"name":"Title","type":"text"},{"name":"Status","type":"text"}]`,
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out)
|
||||
require.Equal(t, "Project Tracker", gjson.Get(out, "api.0.body.name").String(), out)
|
||||
require.Equal(t, "Asia/Shanghai", gjson.Get(out, "api.0.body.time_zone").String(), out)
|
||||
|
||||
require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", gjson.Get(out, "api.1.url").String(), out)
|
||||
require.Equal(t, "GET", gjson.Get(out, "api.1.method").String(), out)
|
||||
require.Equal(t, int64(0), gjson.Get(out, "api.1.params.offset").Int(), out)
|
||||
require.Equal(t, int64(100), gjson.Get(out, "api.1.params.limit").Int(), out)
|
||||
|
||||
require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", gjson.Get(out, "api.2.url").String(), out)
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.2.method").String(), out)
|
||||
require.Equal(t, "Tasks", gjson.Get(out, "api.2.body.name").String(), out)
|
||||
require.Equal(t, "Title", gjson.Get(out, "api.2.body.fields.0.name").String(), out)
|
||||
require.Equal(t, "Status", gjson.Get(out, "api.2.body.fields.1.name").String(), out)
|
||||
|
||||
require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables/%3Cdefault_table_id%3E", gjson.Get(out, "api.3.url").String(), out)
|
||||
require.Equal(t, "DELETE", gjson.Get(out, "api.3.method").String(), out)
|
||||
}
|
||||
|
||||
func TestBaseCreateDryRunTableNameOnlyRenamesDefaultTable(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+base-create",
|
||||
"--name", "Project Tracker",
|
||||
"--table-name", "Tasks",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out)
|
||||
require.Equal(t, "Project Tracker", gjson.Get(out, "api.0.body.name").String(), out)
|
||||
|
||||
require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", gjson.Get(out, "api.1.url").String(), out)
|
||||
require.Equal(t, "GET", gjson.Get(out, "api.1.method").String(), out)
|
||||
|
||||
require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables/%3Cdefault_table_id%3E", gjson.Get(out, "api.2.url").String(), out)
|
||||
require.Equal(t, "PATCH", gjson.Get(out, "api.2.method").String(), out)
|
||||
require.Equal(t, "Tasks", gjson.Get(out, "api.2.body.name").String(), out)
|
||||
require.False(t, gjson.Get(out, "api.3").Exists(), out)
|
||||
}
|
||||
|
||||
func TestBaseCreateDryRunFieldsOnlyUsesDefaultTableName(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+base-create",
|
||||
"--name", "Project Tracker",
|
||||
"--fields", `[{"name":"Title","type":"text"}]`,
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", gjson.Get(out, "api.2.url").String(), out)
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.2.method").String(), out)
|
||||
require.Equal(t, "Table 1", gjson.Get(out, "api.2.body.name").String(), out)
|
||||
require.Equal(t, "Title", gjson.Get(out, "api.2.body.fields.0.name").String(), out)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBaseDashboardBlockGetDataDryRun(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+dashboard-block-get-data",
|
||||
"--base-token", "app_x",
|
||||
"--block-id", "blk_chart",
|
||||
"--dry-run",
|
||||
},
|
||||
BinaryPath: "../../../lark-cli",
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
output := strings.TrimSpace(result.Stdout)
|
||||
assert.Contains(t, output, "/open-apis/base/v3/bases/app_x/dashboards/blocks/blk_chart/data")
|
||||
assert.Contains(t, output, `"method": "GET"`)
|
||||
assert.Contains(t, output, `"block_id": "blk_chart"`)
|
||||
assert.Contains(t, output, `"base_token": "app_x"`)
|
||||
}
|
||||
|
||||
func TestBaseDashboardBlockGetDataDryRun_MissingRequiredFlags(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+dashboard-block-get-data",
|
||||
"--dry-run",
|
||||
},
|
||||
BinaryPath: "../../../lark-cli",
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.ExitCode)
|
||||
assert.Contains(t, result.Stderr, "base-token")
|
||||
assert.Contains(t, result.Stderr, "block-id")
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestBaseFieldCreateDryRunArrayCompat(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+field-create",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_x",
|
||||
"--json", `[{"name":"A","type":"text"},{"name":"B","type":"text"}]`,
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
out := result.Stdout
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields", gjson.Get(out, "api.0.url").String(), out)
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out)
|
||||
require.Equal(t, "A", gjson.Get(out, "api.0.body.name").String(), out)
|
||||
require.Equal(t, "text", gjson.Get(out, "api.0.body.type").String(), out)
|
||||
|
||||
require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields", gjson.Get(out, "api.1.url").String(), out)
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.1.method").String(), out)
|
||||
require.Equal(t, "B", gjson.Get(out, "api.1.body.name").String(), out)
|
||||
require.Equal(t, "text", gjson.Get(out, "api.1.body.type").String(), out)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBaseFormDetailDryRun(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+form-detail",
|
||||
"--share-token", "shrXXXX",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
output := strings.TrimSpace(result.Stdout)
|
||||
assert.Contains(t, output, "/open-apis/base/v3/bases/tables/forms/detail")
|
||||
assert.Contains(t, output, `"share_token"`)
|
||||
assert.Contains(t, output, "shrXXXX")
|
||||
assert.Contains(t, output, `"method": "POST"`)
|
||||
}
|
||||
|
||||
func TestBaseFormDetailDryRun_MissingShareToken(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+form-detail",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.ExitCode)
|
||||
assert.Contains(t, result.Stderr, "share-token")
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestBaseListDryRunRejectsOutOfRangeLimit(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+table-list",
|
||||
"--base-token", "app_x",
|
||||
"--limit", "101",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 2)
|
||||
|
||||
require.Equal(t, "validation", gjson.Get(result.Stderr, "error.type").String(), result.Stderr)
|
||||
require.Equal(t, "invalid_argument", gjson.Get(result.Stderr, "error.subtype").String(), result.Stderr)
|
||||
require.Equal(t, "--limit", gjson.Get(result.Stderr, "error.param").String(), result.Stderr)
|
||||
require.Contains(t, gjson.Get(result.Stderr, "error.message").String(), "must be between 1 and 100")
|
||||
require.Empty(t, result.Stdout)
|
||||
}
|
||||
|
||||
func TestBaseListDryRunAcceptsPageSizeAliasForLimit(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+table-list",
|
||||
"--base-token", "app_x",
|
||||
"--page-size", "40",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
require.Equal(t, int64(0), gjson.Get(result.Stdout, "api.0.params.offset").Int(), result.Stdout)
|
||||
require.Equal(t, int64(40), gjson.Get(result.Stdout, "api.0.params.limit").Int(), result.Stdout)
|
||||
require.False(t, gjson.Get(result.Stdout, "api.0.params.page_size").Exists(), result.Stdout)
|
||||
}
|
||||
|
||||
func TestBaseListDryRunRejectsLimitPageSizeConflict(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+table-list",
|
||||
"--base-token", "app_x",
|
||||
"--limit", "20",
|
||||
"--page-size", "40",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 2)
|
||||
|
||||
require.Equal(t, "validation", gjson.Get(result.Stderr, "error.type").String(), result.Stderr)
|
||||
require.Equal(t, "invalid_argument", gjson.Get(result.Stderr, "error.subtype").String(), result.Stderr)
|
||||
require.Equal(t, "--page-size", gjson.Get(result.Stderr, "error.param").String(), result.Stderr)
|
||||
require.Contains(t, gjson.Get(result.Stderr, "error.message").String(), "mutually exclusive")
|
||||
require.Empty(t, result.Stdout)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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_RoleWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
baseToken := createBaseWithRetry(t, ctx, "lark-cli-e2e-base-role-"+clie2e.GenerateSuffix())
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+advperm-enable", "--base-token", baseToken},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
roleName := "Reviewer-" + clie2e.GenerateSuffix()
|
||||
createRole(t, ctx, baseToken, `{"role_name":"`+roleName+`","role_type":"custom_role"}`)
|
||||
roleID := ""
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
if roleID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cleanupCtx, cancel := cleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"base", "+role-delete", "--base-token", baseToken, "--role-id", roleID, "--yes"},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
if deleteErr != nil || deleteResult.ExitCode != 0 {
|
||||
reportCleanupFailure(parentT, "delete role "+roleID, deleteResult, deleteErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list as bot", 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)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
roleListPayload := gjson.Get(result.Stdout, "data.data").String()
|
||||
require.NotEmpty(t, roleListPayload, "stdout:\n%s", result.Stdout)
|
||||
assert.True(t, gjson.Valid(roleListPayload), "role list payload should be valid JSON: %s", roleListPayload)
|
||||
|
||||
roleItems := gjson.Get(roleListPayload, "base_roles").Array()
|
||||
assert.NotEmpty(t, roleItems, "role list should contain at least one role: %s", roleListPayload)
|
||||
|
||||
found := false
|
||||
for _, item := range roleItems {
|
||||
rolePayload := item.String()
|
||||
if !gjson.Valid(rolePayload) {
|
||||
continue
|
||||
}
|
||||
if gjson.Get(rolePayload, "role_name").String() == roleName {
|
||||
roleID = gjson.Get(rolePayload, "role_id").String()
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "stdout:\n%s", result.Stdout)
|
||||
require.NotEmpty(t, roleID, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get role as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, roleID, "role ID should be resolved before get")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+role-get", "--base-token", baseToken, "--role-id", roleID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
rolePayload := gjson.Get(result.Stdout, "data.data").String()
|
||||
require.NotEmpty(t, rolePayload, "stdout:\n%s", result.Stdout)
|
||||
require.True(t, gjson.Valid(rolePayload), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, roleID, gjson.Get(rolePayload, "role_id").String())
|
||||
})
|
||||
|
||||
t.Run("update role as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, roleID, "role ID should be resolved before update")
|
||||
|
||||
updatedRoleName := roleName + " Updated"
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+role-update", "--base-token", baseToken, "--role-id", roleID, "--json", `{"role_name":"` + updatedRoleName + `","role_type":"custom_role"}`, "--yes"},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
getResult, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+role-get", "--base-token", baseToken, "--role-id", roleID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
getResult.AssertExitCode(t, 0)
|
||||
getResult.AssertStdoutStatus(t, true)
|
||||
|
||||
rolePayload := gjson.Get(getResult.Stdout, "data.data").String()
|
||||
require.NotEmpty(t, rolePayload, "stdout:\n%s", getResult.Stdout)
|
||||
require.True(t, gjson.Valid(rolePayload), "stdout:\n%s", getResult.Stdout)
|
||||
assert.Equal(t, updatedRoleName, gjson.Get(rolePayload, "role_name").String())
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
# Base CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 78 leaf commands
|
||||
- Covered: 19
|
||||
- Coverage: 24.4%
|
||||
|
||||
## Summary
|
||||
- TestBase_BasicWorkflow: proves `+base-create`, `+base-get`, `+table-create`, `+table-get`, and `+table-list`; key `t.Run(...)` proof points are `get base as bot`, `get table as bot`, and `list tables and find created table as bot`.
|
||||
- TestBaseBlockDryRun: proves the five `+base-block-*` shortcuts request shapes without touching live data.
|
||||
- TestBaseFieldCreateDryRunArrayCompat: proves `+field-create` dry-run request shape for the internal JSON-array compatibility path.
|
||||
- TestBase_RoleWorkflow: proves `+advperm-enable`, `+role-create`, `+role-list`, `+role-get`, and `+role-update`; key `t.Run(...)` proof points are `list as bot`, `get as bot`, and `update as bot`.
|
||||
- Cleanup note: `+table-delete` and `+role-delete` only run in cleanup and are intentionally left uncovered.
|
||||
- Blocked area: dashboard, field, form, record, view, and workflow operations still lack deterministic create/read/update workflows in this suite.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✕ | base +advperm-disable | shortcut | | none | no disable workflow yet |
|
||||
| ✓ | base +advperm-enable | shortcut | base_role_workflow_test.go::TestBase_RoleWorkflow | `--base-token` | |
|
||||
| ✕ | base +base-copy | shortcut | | none | no copy workflow yet |
|
||||
| ✓ | base +base-create | shortcut | base/helpers_test.go::createBaseWithRetry | `--name`; `--time-zone` | helper asserts created base token |
|
||||
| ✓ | base +base-get | shortcut | base_basic_workflow_test.go::TestBase_BasicWorkflow/get base as bot | `--base-token` | |
|
||||
| ✓ | base +base-block-create | shortcut | base_block_dryrun_test.go::TestBaseBlockDryRun/create | `--base-token`; `--type`; `--name`; `--parent-id`; dry-run only | request shape only |
|
||||
| ✓ | base +base-block-delete | shortcut | base_block_dryrun_test.go::TestBaseBlockDryRun/delete | `--base-token`; `--block-id`; dry-run only | request shape only |
|
||||
| ✓ | base +base-block-list | shortcut | base_block_dryrun_test.go::TestBaseBlockDryRun/list all,list folder | `--base-token`; optional `--parent-id`; optional `--type`; dry-run only | request shape only |
|
||||
| ✓ | base +base-block-move | shortcut | base_block_dryrun_test.go::TestBaseBlockDryRun/move root,move after | `--base-token`; `--block-id`; optional `--parent-id`; `--after-id`; dry-run only | request shape only |
|
||||
| ✓ | base +base-block-rename | shortcut | base_block_dryrun_test.go::TestBaseBlockDryRun/rename | `--base-token`; `--block-id`; `--name`; dry-run only | request shape only |
|
||||
| ✕ | base +dashboard-arrange | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-block-create | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-block-delete | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-block-get | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-block-list | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-block-update | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-create | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-delete | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-get | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-list | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-update | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +data-query | shortcut | | none | no data-query assertions yet |
|
||||
| ✓ | base +field-create | shortcut | base_field_dryrun_test.go::TestBaseFieldCreateDryRunArrayCompat | `--base-token`; `--table-id`; `--json`; dry-run only | request shape only |
|
||||
| ✕ | base +field-delete | shortcut | | none | field workflows not covered |
|
||||
| ✕ | base +field-get | shortcut | | none | field workflows not covered |
|
||||
| ✕ | base +field-list | shortcut | | none | field workflows not covered |
|
||||
| ✕ | base +field-search-options | shortcut | | none | field workflows not covered |
|
||||
| ✕ | base +field-update | shortcut | | none | field workflows not covered |
|
||||
| ✕ | base +form-create | shortcut | | none | form workflows not covered |
|
||||
| ✕ | base +form-delete | shortcut | | none | form workflows not covered |
|
||||
| ✕ | base +form-get | shortcut | | none | form workflows not covered |
|
||||
| ✕ | base +form-list | shortcut | | none | form workflows not covered |
|
||||
| ✕ | base +form-questions-create | shortcut | | none | form workflows not covered |
|
||||
| ✕ | base +form-questions-delete | shortcut | | none | form workflows not covered |
|
||||
| ✕ | base +form-questions-list | shortcut | | none | form workflows not covered |
|
||||
| ✕ | base +form-questions-update | shortcut | | none | form workflows not covered |
|
||||
| ✕ | base +form-update | shortcut | | none | form workflows not covered |
|
||||
| ✕ | base +record-batch-create | shortcut | | none | record workflows not covered |
|
||||
| ✕ | base +record-batch-update | shortcut | | none | record workflows not covered |
|
||||
| ✕ | base +record-delete | shortcut | | none | record workflows not covered |
|
||||
| ✕ | base +record-get | shortcut | | none | record workflows not covered |
|
||||
| ✕ | base +record-history-list | shortcut | | none | record workflows not covered |
|
||||
| ✕ | base +record-list | shortcut | | none | record workflows not covered |
|
||||
| ✕ | base +record-search | shortcut | | none | record workflows not covered |
|
||||
| ✓ | base +record-upload-attachment | shortcut | base_attachment_dryrun_test.go::TestBase_AttachmentDryRun/upload | dry-run only | request shape only |
|
||||
| ✓ | base +record-download-attachment | shortcut | base_attachment_dryrun_test.go::TestBase_AttachmentDryRun/download | dry-run only | request shape only |
|
||||
| ✓ | base +record-remove-attachment | shortcut | base_attachment_dryrun_test.go::TestBase_AttachmentDryRun/remove | dry-run only | request shape only |
|
||||
| ✕ | base +record-upsert | shortcut | | none | record workflows not covered |
|
||||
| ✓ | base +role-create | shortcut | base/helpers_test.go::createRole | `--base-token`; `--json` | helper asserts created role id |
|
||||
| ✕ | base +role-delete | shortcut | | none | cleanup only |
|
||||
| ✓ | base +role-get | shortcut | base_role_workflow_test.go::TestBase_RoleWorkflow/get as bot | `--base-token`; `--role-id` | |
|
||||
| ✓ | base +role-list | shortcut | base_role_workflow_test.go::TestBase_RoleWorkflow/list as bot | `--base-token` | |
|
||||
| ✓ | base +role-update | shortcut | base_role_workflow_test.go::TestBase_RoleWorkflow/update as bot | `--base-token`; `--role-id`; `--json` | |
|
||||
| ✓ | base +table-create | shortcut | base/helpers_test.go::createTableWithRetry | `--base-token`; `--name`; optional `--fields`; optional `--view` | helper asserts table id |
|
||||
| ✕ | base +table-delete | shortcut | | none | cleanup only |
|
||||
| ✓ | base +table-get | shortcut | base_basic_workflow_test.go::TestBase_BasicWorkflow/get table as bot | `--base-token`; `--table-id` | |
|
||||
| ✓ | base +table-list | shortcut | base_basic_workflow_test.go::TestBase_BasicWorkflow/list tables and find created table as bot | `--base-token` | |
|
||||
| ✕ | base +table-update | shortcut | | none | no rename workflow yet |
|
||||
| ✕ | base +view-create | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-delete | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-get | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-get-card | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-get-filter | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-get-group | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-get-sort | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-get-timebar | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-get-visible-fields | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-list | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-rename | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-set-card | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-set-filter | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-set-group | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-set-sort | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-set-timebar | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +view-set-visible-fields | shortcut | | none | view workflows not covered |
|
||||
| ✕ | base +workflow-create | shortcut | | none | workflow CRUD not covered |
|
||||
| ✕ | base +workflow-disable | shortcut | | none | workflow CRUD not covered |
|
||||
| ✕ | base +workflow-enable | shortcut | | none | workflow CRUD not covered |
|
||||
| ✕ | base +workflow-get | shortcut | | none | workflow CRUD not covered |
|
||||
| ✕ | base +workflow-list | shortcut | | none | workflow CRUD not covered |
|
||||
| ✕ | base +workflow-update | shortcut | | none | workflow CRUD not covered |
|
||||
@@ -0,0 +1,219 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/larksuite/cli/tests/cli_e2e/drive"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const cleanupTimeout = 30 * time.Second
|
||||
|
||||
func reportCleanupFailure(parentT *testing.T, prefix string, result *clie2e.Result, err error) {
|
||||
parentT.Helper()
|
||||
|
||||
if err != nil {
|
||||
parentT.Errorf("%s: %v", prefix, err)
|
||||
return
|
||||
}
|
||||
if result == nil {
|
||||
parentT.Errorf("%s: nil result", prefix)
|
||||
return
|
||||
}
|
||||
if isCleanupSuppressedResult(result) {
|
||||
return
|
||||
}
|
||||
|
||||
parentT.Errorf("%s failed: exit=%d stdout=%s stderr=%s", prefix, result.ExitCode, result.Stdout, result.Stderr)
|
||||
}
|
||||
|
||||
func cleanupContext() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), cleanupTimeout)
|
||||
}
|
||||
|
||||
func isCleanupSuppressedResult(result *clie2e.Result) bool {
|
||||
if result == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
raw := strings.TrimSpace(result.Stdout)
|
||||
if raw == "" {
|
||||
raw = strings.TrimSpace(result.Stderr)
|
||||
}
|
||||
if raw == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
start := strings.LastIndex(raw, "\n{")
|
||||
if start >= 0 {
|
||||
start++
|
||||
} else {
|
||||
start = strings.Index(raw, "{")
|
||||
}
|
||||
if start < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
payload := raw[start:]
|
||||
if !gjson.Valid(payload) {
|
||||
return false
|
||||
}
|
||||
|
||||
if gjson.Get(payload, "error.type").String() != "api_error" {
|
||||
return false
|
||||
}
|
||||
|
||||
if gjson.Get(payload, "error.detail.type").String() == "not_found" ||
|
||||
strings.Contains(strings.ToLower(gjson.Get(payload, "error.message").String()), "not found") {
|
||||
return true
|
||||
}
|
||||
|
||||
return gjson.Get(payload, "error.code").Int() == 800004135 ||
|
||||
strings.Contains(strings.ToLower(gjson.Get(payload, "error.message").String()), " limited")
|
||||
}
|
||||
|
||||
func createBaseWithRetry(t *testing.T, ctx context.Context, name string) string {
|
||||
t.Helper()
|
||||
|
||||
const defaultAs = "bot"
|
||||
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+base-create", "--name", name, "--time-zone", "Asia/Shanghai"},
|
||||
DefaultAs: defaultAs,
|
||||
}, clie2e.RetryOptions{})
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
t.Cleanup(func() {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := drive.DeleteDriveResourceAndVerify(cleanupCtx, baseToken, "bitable", defaultAs)
|
||||
clie2e.ReportCleanupFailure(t, "delete base "+baseToken, deleteResult, deleteErr)
|
||||
})
|
||||
return baseToken
|
||||
}
|
||||
|
||||
func createTableWithRetry(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.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: args,
|
||||
DefaultAs: "bot",
|
||||
}, clie2e.RetryOptions{})
|
||||
require.NoError(t, err)
|
||||
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() {
|
||||
cleanupCtx, cancel := cleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"base", "+table-delete", "--base-token", baseToken, "--table-id", tableID, "--yes"},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
if deleteErr != nil || deleteResult.ExitCode != 0 {
|
||||
reportCleanupFailure(parentT, "delete table "+tableID, deleteResult, deleteErr)
|
||||
}
|
||||
})
|
||||
|
||||
return tableID, primaryFieldID, primaryViewID
|
||||
}
|
||||
|
||||
func createRole(t *testing.T, ctx context.Context, baseToken string, body string) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+role-create", "--base-token", baseToken, "--json", body},
|
||||
DefaultAs: "bot",
|
||||
}, clie2e.RetryOptions{})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
return gjson.Get(result.Stdout, "data.role_id").String()
|
||||
}
|
||||
|
||||
func findBaseTableByID(t *testing.T, ctx context.Context, baseToken string, tableID string) gjson.Result {
|
||||
t.Helper()
|
||||
|
||||
require.NotEmpty(t, baseToken, "base token is required")
|
||||
require.NotEmpty(t, tableID, "table ID is required")
|
||||
|
||||
const pageLimit = 50
|
||||
|
||||
offset := 0
|
||||
seenOffsets := map[int]struct{}{}
|
||||
for {
|
||||
if _, seen := seenOffsets[offset]; seen {
|
||||
t.Fatalf("base table list pagination loop detected for base %q, repeated offset %d", baseToken, offset)
|
||||
}
|
||||
seenOffsets[offset] = struct{}{}
|
||||
|
||||
args := []string{
|
||||
"base", "+table-list",
|
||||
"--base-token", baseToken,
|
||||
"--limit", strconv.Itoa(pageLimit),
|
||||
"--offset", strconv.Itoa(offset),
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: args,
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
table := gjson.Get(result.Stdout, `data.tables.#(id=="`+tableID+`")`)
|
||||
if table.Exists() {
|
||||
return table
|
||||
}
|
||||
|
||||
tables := gjson.Get(result.Stdout, "data.tables").Array()
|
||||
if len(tables) == 0 || len(tables) < pageLimit {
|
||||
t.Fatalf("table %q not found in listed pages, last stdout:\n%s", tableID, result.Stdout)
|
||||
}
|
||||
offset += len(tables)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user