mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
feat: add stable cli e2e tests (#401)
* feat: add stable bot-only cli e2e subset Change-Id: I62edf59d179e407954f65f82e94cf5dcf4938080 * fix: address review comments on stable cli e2e tests Change-Id: I4436100c30adf2694cd06953961f8d77f576fc1e * fix: reduce flakiness in drive and im e2e helpers Change-Id: I51e77d857f1fd9aec5ee34adf5045cc695239f21 * fix: document missing drive cleanup support Change-Id: I3d4f034145bd69fb7640e707fcda05146b8754c7 * style: unify e2e cleanup comments Change-Id: I40d906c9168754ad71ef9fb770ff4c340fc19beb * test: update e2e assertions Change-Id: I73c21b4b38d4ced7ea27cb327075957ec2b9a2a2 * test: stabilize cli e2e bot-only coverage Change-Id: Ied897c37c4f42e446d55d110461aa34ae198195d
This commit is contained in:
77
tests/cli_e2e/base/base_basic_workflow_test.go
Normal file
77
tests/cli_e2e/base/base_basic_workflow_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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", 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, primaryFieldID, primaryViewID := createTableWithRetry(
|
||||
t,
|
||||
parentT,
|
||||
ctx,
|
||||
baseToken,
|
||||
tableName,
|
||||
`[{"name":"Name","type":"text"}]`,
|
||||
`{"name":"Main","type":"grid"}`,
|
||||
)
|
||||
|
||||
t.Run("get table", 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", 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)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
assert.True(t, gjson.Get(result.Stdout, `data.tables.#(id=="`+tableID+`")`).Exists(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
require.NotEmpty(t, primaryFieldID)
|
||||
require.NotEmpty(t, primaryViewID)
|
||||
}
|
||||
127
tests/cli_e2e/base/base_role_workflow_test.go
Normal file
127
tests/cli_e2e/base/base_role_workflow_test.go
Normal file
@@ -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", 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", 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", 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())
|
||||
})
|
||||
|
||||
}
|
||||
164
tests/cli_e2e/base/helpers_test.go
Normal file
164
tests/cli_e2e/base/helpers_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// 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/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()
|
||||
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+base-create", "--name", name, "--time-zone", "Asia/Shanghai"},
|
||||
DefaultAs: "bot",
|
||||
}, 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)
|
||||
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()
|
||||
}
|
||||
86
tests/cli_e2e/calendar/calendar_create_event_test.go
Normal file
86
tests/cli_e2e/calendar/calendar_create_event_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package calendar
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// TestCalendar_CreateEvent tests the workflow of creating a calendar event.
|
||||
func TestCalendar_CreateEvent(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
eventSummary := "lark-cli-e2e-event-" + suffix
|
||||
eventDescription := "test event description"
|
||||
|
||||
startAt := time.Now().UTC().Add(1 * time.Hour).Truncate(time.Minute)
|
||||
endAt := startAt.Add(1 * time.Hour)
|
||||
startTime := startAt.Format(time.RFC3339)
|
||||
endTime := endAt.Format(time.RFC3339)
|
||||
|
||||
var eventID string
|
||||
calendarID := getPrimaryCalendarID(t, ctx)
|
||||
|
||||
t.Run("create event with shortcut", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "+create",
|
||||
"--summary", eventSummary,
|
||||
"--start", startTime,
|
||||
"--end", endTime,
|
||||
"--calendar-id", calendarID,
|
||||
"--description", eventDescription,
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
eventID = gjson.Get(result.Stdout, "data.event_id").String()
|
||||
require.NotEmpty(t, eventID)
|
||||
})
|
||||
|
||||
t.Run("verify event created", func(t *testing.T) {
|
||||
require.NotEmpty(t, eventID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "events", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"calendar_id": calendarID,
|
||||
"event_id": eventID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
assert.Equal(t, eventSummary, gjson.Get(result.Stdout, "data.event.summary").String())
|
||||
assert.Equal(t, eventDescription, gjson.Get(result.Stdout, "data.event.description").String())
|
||||
assert.Equal(t, unixSecondsRFC3339(startAt), gjson.Get(result.Stdout, "data.event.start_time.timestamp").String())
|
||||
assert.Equal(t, unixSecondsRFC3339(endAt), gjson.Get(result.Stdout, "data.event.end_time.timestamp").String())
|
||||
})
|
||||
|
||||
t.Run("delete event", func(t *testing.T) {
|
||||
require.NotEmpty(t, eventID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "events", "delete"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"calendar_id": calendarID,
|
||||
"event_id": eventID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
})
|
||||
}
|
||||
136
tests/cli_e2e/calendar/calendar_manage_calendar_test.go
Normal file
136
tests/cli_e2e/calendar/calendar_manage_calendar_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package calendar
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// TestCalendar_ManageCalendar tests the workflow of managing calendars.
|
||||
func TestCalendar_ManageCalendar(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
calendarSummary := "lark-cli-e2e-cal-" + suffix
|
||||
updatedCalendarSummary := calendarSummary + "-updated"
|
||||
calendarDescription := "test calendar created by e2e"
|
||||
|
||||
var createdCalendarID string
|
||||
|
||||
t.Run("list calendars", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "list"},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
require.NotEmpty(t, gjson.Get(result.Stdout, "data.calendar_list").Array(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get primary calendar", func(t *testing.T) {
|
||||
primaryCalendarID := getPrimaryCalendarID(t, ctx)
|
||||
require.NotEmpty(t, primaryCalendarID)
|
||||
})
|
||||
|
||||
t.Run("create calendar", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "create"},
|
||||
DefaultAs: "bot",
|
||||
Data: map[string]any{
|
||||
"summary": calendarSummary,
|
||||
"description": calendarDescription,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
createdCalendarID = gjson.Get(result.Stdout, "data.calendar.calendar_id").String()
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
})
|
||||
|
||||
t.Run("get created calendar", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"calendar_id": createdCalendarID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
assert.Equal(t, createdCalendarID, gjson.Get(result.Stdout, "data.calendar_id").String())
|
||||
assert.Equal(t, calendarSummary, gjson.Get(result.Stdout, "data.summary").String())
|
||||
assert.Equal(t, calendarDescription, gjson.Get(result.Stdout, "data.description").String())
|
||||
})
|
||||
|
||||
t.Run("find created calendar in list", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "list"},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
require.True(t, gjson.Get(result.Stdout, `data.calendar_list.#(calendar_id=="`+createdCalendarID+`")`).Exists(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("update calendar", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "patch"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"calendar_id": createdCalendarID,
|
||||
},
|
||||
Data: map[string]any{
|
||||
"summary": updatedCalendarSummary,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
})
|
||||
|
||||
t.Run("verify updated calendar", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"calendar_id": createdCalendarID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
assert.Equal(t, updatedCalendarSummary, gjson.Get(result.Stdout, "data.summary").String())
|
||||
})
|
||||
|
||||
t.Run("delete calendar", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "delete"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"calendar_id": createdCalendarID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
})
|
||||
}
|
||||
35
tests/cli_e2e/calendar/helpers_test.go
Normal file
35
tests/cli_e2e/calendar/helpers_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func getPrimaryCalendarID(t *testing.T, ctx context.Context) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "primary"},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
calendarID := gjson.Get(result.Stdout, "data.calendars.0.calendar.calendar_id").String()
|
||||
require.NotEmpty(t, calendarID, "stdout:\n%s", result.Stdout)
|
||||
return calendarID
|
||||
}
|
||||
|
||||
func unixSecondsRFC3339(t time.Time) string {
|
||||
return strconv.FormatInt(t.Unix(), 10)
|
||||
}
|
||||
51
tests/cli_e2e/contact/contact_shortcut_test.go
Normal file
51
tests/cli_e2e/contact/contact_shortcut_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package contact
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
)
|
||||
|
||||
func TestContact_GetUser_BotWorkflow(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
var targetOpenID string
|
||||
|
||||
t.Run("discover-user-via-api", func(t *testing.T) {
|
||||
// Bot identity cannot use +search-user or +get-user (self).
|
||||
// However, it CAN call the raw API to list users if it has contact permissions.
|
||||
// We use this to discover a real open_id for the next step.
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"api", "get", "/open-apis/contact/v3/users"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
targetOpenID = gjson.Get(result.Stdout, "data.items.0.open_id").String()
|
||||
|
||||
require.NotEmpty(t, targetOpenID, "expected to find at least one user via raw API")
|
||||
})
|
||||
|
||||
t.Run("get-user-by-id-as-bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, targetOpenID, "targetOpenID should be populated")
|
||||
// DefaultAs is automatically "bot" in the clie2e framework
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"contact", "+get-user", "--user-id", targetOpenID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
returnedID := gjson.Get(result.Stdout, "data.user.open_id").String()
|
||||
require.Equal(t, targetOpenID, returnedID)
|
||||
})
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -57,6 +58,15 @@ type Result struct {
|
||||
RunErr error
|
||||
}
|
||||
|
||||
// RetryOptions configures retry behavior for flaky external API calls.
|
||||
type RetryOptions struct {
|
||||
Attempts int
|
||||
InitialDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
BackoffMultiple int
|
||||
ShouldRetry func(*Result) bool
|
||||
}
|
||||
|
||||
// RunCmd executes lark-cli and captures stdout/stderr/exit code.
|
||||
func RunCmd(ctx context.Context, req Request) (*Result, error) {
|
||||
binaryPath, err := ResolveBinaryPath(req)
|
||||
@@ -99,6 +109,63 @@ func RunCmd(ctx context.Context, req Request) (*Result, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RunCmdWithRetry reruns a command when the result matches the configured retry condition.
|
||||
func RunCmdWithRetry(ctx context.Context, req Request, opts RetryOptions) (*Result, error) {
|
||||
if opts.Attempts <= 0 {
|
||||
opts.Attempts = 4
|
||||
}
|
||||
if opts.InitialDelay <= 0 {
|
||||
opts.InitialDelay = 1 * time.Second
|
||||
}
|
||||
if opts.MaxDelay <= 0 {
|
||||
opts.MaxDelay = 6 * time.Second
|
||||
}
|
||||
if opts.BackoffMultiple <= 1 {
|
||||
opts.BackoffMultiple = 2
|
||||
}
|
||||
if opts.ShouldRetry == nil {
|
||||
opts.ShouldRetry = func(result *Result) bool {
|
||||
return result != nil && result.ExitCode != 0
|
||||
}
|
||||
}
|
||||
|
||||
delay := opts.InitialDelay
|
||||
var lastResult *Result
|
||||
for attempt := 1; attempt <= opts.Attempts; attempt++ {
|
||||
result, err := RunCmd(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastResult = result
|
||||
if attempt == opts.Attempts || !opts.ShouldRetry(result) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
timer := time.NewTimer(delay)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return lastResult, nil
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
nextDelay := delay * time.Duration(opts.BackoffMultiple)
|
||||
if nextDelay > opts.MaxDelay {
|
||||
delay = opts.MaxDelay
|
||||
} else {
|
||||
delay = nextDelay
|
||||
}
|
||||
}
|
||||
|
||||
return lastResult, nil
|
||||
}
|
||||
|
||||
// GenerateSuffix returns a high-entropy UTC timestamp suffix suitable for remote test resource names.
|
||||
func GenerateSuffix() string {
|
||||
now := time.Now().UTC()
|
||||
return fmt.Sprintf("%s-%09d", now.Format("20060102-150405"), now.Nanosecond())
|
||||
}
|
||||
|
||||
// ResolveBinaryPath finds the CLI binary path using request, env, then PATH.
|
||||
func ResolveBinaryPath(req Request) (string, error) {
|
||||
if req.BinaryPath != "" {
|
||||
|
||||
48
tests/cli_e2e/docs/docs_create_fetch_test.go
Normal file
48
tests/cli_e2e/docs/docs_create_fetch_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package docs
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// TestDocs_CreateAndFetchWorkflow tests the create and fetch lifecycle.
|
||||
func TestDocs_CreateAndFetchWorkflow(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
folderName := "lark-cli-e2e-docs-folder-" + suffix
|
||||
docTitle := "lark-cli-e2e-docs-" + suffix
|
||||
docContent := "# Test Document\n\nThis document was created by lark-cli e2e test."
|
||||
|
||||
folderToken := createDocsFolderWithRetry(t, ctx, folderName)
|
||||
var docToken string
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
docToken = createDocWithRetry(t, ctx, folderToken, docTitle, docContent)
|
||||
})
|
||||
|
||||
t.Run("fetch", func(t *testing.T) {
|
||||
require.NotEmpty(t, docToken, "document token should be created before fetch")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"docs", "+fetch",
|
||||
"--doc", docToken,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
assert.Equal(t, docTitle, gjson.Get(result.Stdout, "data.title").String())
|
||||
})
|
||||
}
|
||||
67
tests/cli_e2e/docs/docs_update_test.go
Normal file
67
tests/cli_e2e/docs/docs_update_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package docs
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// TestDocs_UpdateWorkflow tests the create, update, and verify lifecycle.
|
||||
func TestDocs_UpdateWorkflow(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
folderName := "lark-cli-e2e-update-folder-" + suffix
|
||||
originalTitle := "lark-cli-e2e-update-" + suffix
|
||||
updatedTitle := "lark-cli-e2e-update-updated-" + suffix
|
||||
originalContent := "# Original\n\nThis is the original content."
|
||||
updatedContent := "# Updated\n\nThis is the updated content."
|
||||
|
||||
folderToken := createDocsFolderWithRetry(t, ctx, folderName)
|
||||
var docToken string
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
docToken = createDocWithRetry(t, ctx, folderToken, originalTitle, originalContent)
|
||||
})
|
||||
|
||||
t.Run("update-title-and-content", func(t *testing.T) {
|
||||
require.NotEmpty(t, docToken, "document token should be created before update")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"docs", "+update",
|
||||
"--doc", docToken,
|
||||
"--mode", "overwrite",
|
||||
"--markdown", updatedContent,
|
||||
"--new-title", updatedTitle,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("verify", func(t *testing.T) {
|
||||
require.NotEmpty(t, docToken, "document token should be created before verify")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"docs", "+fetch",
|
||||
"--doc", docToken,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
assert.Equal(t, updatedTitle, gjson.Get(result.Stdout, "data.title").String())
|
||||
})
|
||||
}
|
||||
54
tests/cli_e2e/docs/helpers_test.go
Normal file
54
tests/cli_e2e/docs/helpers_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package docs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func createDocsFolderWithRetry(t *testing.T, ctx context.Context, name string) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"drive", "files", "create_folder"},
|
||||
Data: map[string]any{
|
||||
"name": name,
|
||||
"folder_token": "",
|
||||
},
|
||||
}, clie2e.RetryOptions{})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
folderToken := gjson.Get(result.Stdout, "data.token").String()
|
||||
require.NotEmpty(t, folderToken, "stdout:\n%s", result.Stdout)
|
||||
|
||||
return folderToken
|
||||
}
|
||||
|
||||
func createDocWithRetry(t *testing.T, ctx context.Context, folderToken string, title string, markdown string) string {
|
||||
t.Helper()
|
||||
|
||||
require.NotEmpty(t, folderToken, "folder token is required")
|
||||
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"docs", "+create",
|
||||
"--folder-token", folderToken,
|
||||
"--title", title,
|
||||
"--markdown", markdown,
|
||||
},
|
||||
}, clie2e.RetryOptions{})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
docToken := gjson.Get(result.Stdout, "data.doc_id").String()
|
||||
require.NotEmpty(t, docToken, "stdout:\n%s", result.Stdout)
|
||||
return docToken
|
||||
}
|
||||
29
tests/cli_e2e/drive/drive_files_workflow_test.go
Normal file
29
tests/cli_e2e/drive/drive_files_workflow_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package drive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
)
|
||||
|
||||
// TestDrive_FilesCreateFolderWorkflow tests the files create_folder resource command.
|
||||
func TestDrive_FilesCreateFolderWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
folderName := "lark-cli-e2e-drive-folder-" + suffix
|
||||
|
||||
t.Run("create_folder", func(t *testing.T) {
|
||||
folderToken := createDriveFolder(t, parentT, ctx, folderName)
|
||||
if folderToken == "" {
|
||||
t.Fatalf("folder token should be available")
|
||||
}
|
||||
})
|
||||
}
|
||||
44
tests/cli_e2e/drive/helpers_test.go
Normal file
44
tests/cli_e2e/drive/helpers_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package drive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// createDriveFolder creates a private folder for the current workflow and
|
||||
// deletes it during cleanup.
|
||||
func createDriveFolder(t *testing.T, parentT *testing.T, ctx context.Context, name string) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"drive", "files", "create_folder"},
|
||||
DefaultAs: "bot",
|
||||
Data: map[string]any{
|
||||
"name": name,
|
||||
"folder_token": "",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
folderToken := gjson.Get(result.Stdout, "data.token").String()
|
||||
require.NotEmpty(t, folderToken, "stdout:\n%s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
clie2e.RunCmd(context.Background(), clie2e.Request{
|
||||
Args: []string{"drive", "files", "delete"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"file_token": folderToken, "type": "folder"},
|
||||
})
|
||||
})
|
||||
|
||||
return folderToken
|
||||
}
|
||||
124
tests/cli_e2e/im/chat_workflow_test.go
Normal file
124
tests/cli_e2e/im/chat_workflow_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// TestIM_ChatUpdateWorkflow tests the +chat-update shortcut.
|
||||
func TestIM_ChatUpdateWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
originalName := "lark-cli-e2e-im-update-" + suffix
|
||||
updatedName := originalName + "-updated"
|
||||
updatedDescription := "Updated description for e2e test"
|
||||
|
||||
chatID := createChat(t, parentT, ctx, originalName)
|
||||
|
||||
t.Run("update chat name", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+chat-update",
|
||||
"--chat-id", chatID,
|
||||
"--name", updatedName,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("update chat description", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+chat-update",
|
||||
"--chat-id", chatID,
|
||||
"--description", updatedDescription,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("get updated chat", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "chats", "get"},
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
assert.Equal(t, updatedName, gjson.Get(result.Stdout, "data.name").String())
|
||||
assert.Equal(t, updatedDescription, gjson.Get(result.Stdout, "data.description").String())
|
||||
})
|
||||
}
|
||||
|
||||
// TestIM_ChatsGetWorkflow tests the im chats get command.
|
||||
func TestIM_ChatsGetWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "lark-cli-e2e-chats-get-" + suffix
|
||||
|
||||
chatID := createChat(t, parentT, ctx, chatName)
|
||||
|
||||
t.Run("get chat info", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "chats", "get"},
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Logf("chats get result: %s", result.Stdout)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
dataExists := gjson.Get(result.Stdout, "data").Exists()
|
||||
require.True(t, dataExists, "data object should exist")
|
||||
|
||||
chatNameGot := gjson.Get(result.Stdout, "data.name").String()
|
||||
require.Equal(t, chatName, chatNameGot)
|
||||
})
|
||||
}
|
||||
|
||||
// TestIM_ChatsLinkWorkflow tests the im chats link command.
|
||||
func TestIM_ChatsLinkWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "lark-cli-e2e-chats-link-" + suffix
|
||||
|
||||
chatID := createChat(t, parentT, ctx, chatName)
|
||||
|
||||
t.Run("get chat share link", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "chats", "link"},
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
Data: map[string]any{
|
||||
"validity_period": "week",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
shareLink := gjson.Get(result.Stdout, "data.share_link").String()
|
||||
require.NotEmpty(t, shareLink, "share_link should not be empty")
|
||||
t.Logf("Generated share link: %s", shareLink)
|
||||
})
|
||||
}
|
||||
60
tests/cli_e2e/im/helpers_test.go
Normal file
60
tests/cli_e2e/im/helpers_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// createChat creates a private chat with the given name and returns the chatID.
|
||||
// The chat will be automatically cleaned up via parentT.Cleanup().
|
||||
// Note: Chat deletion is not available via lark-cli im command.
|
||||
func createChat(t *testing.T, parentT *testing.T, ctx context.Context, name string) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+chat-create",
|
||||
"--name", name,
|
||||
"--type", "private",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
chatID := gjson.Get(result.Stdout, "data.chat_id").String()
|
||||
require.NotEmpty(t, chatID, "chat_id should not be empty")
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
// No IM chat delete command is currently available in lark-cli,
|
||||
// so created chats are intentionally left in the test account.
|
||||
})
|
||||
|
||||
return chatID
|
||||
}
|
||||
|
||||
// sendMessage sends a text message to the specified chat and returns the messageID.
|
||||
func sendMessage(t *testing.T, parentT *testing.T, ctx context.Context, chatID string, text string) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+messages-send",
|
||||
"--chat-id", chatID,
|
||||
"--text", text,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
messageID := gjson.Get(result.Stdout, "data.message_id").String()
|
||||
require.NotEmpty(t, messageID, "message_id should not be empty")
|
||||
|
||||
return messageID
|
||||
}
|
||||
53
tests/cli_e2e/im/message_workflow_test.go
Normal file
53
tests/cli_e2e/im/message_workflow_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestIM_MessagesReplyWorkflow tests the +messages-reply shortcut.
|
||||
func TestIM_MessagesReplyWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "lark-cli-e2e-im-reply-" + suffix
|
||||
originalMessage := "Original message for reply test"
|
||||
replyText := "This is a reply"
|
||||
|
||||
chatID := createChat(t, parentT, ctx, chatName)
|
||||
messageID := sendMessage(t, parentT, ctx, chatID, originalMessage)
|
||||
|
||||
t.Run("reply to message with text", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+messages-reply",
|
||||
"--message-id", messageID,
|
||||
"--text", replyText,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("reply to message with markdown", func(t *testing.T) {
|
||||
markdownReply := "**Bold** reply"
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+messages-reply",
|
||||
"--message-id", messageID,
|
||||
"--markdown", markdownReply,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
}
|
||||
239
tests/cli_e2e/sheets/sheets_crud_workflow_test.go
Normal file
239
tests/cli_e2e/sheets/sheets_crud_workflow_test.go
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// TestSheets_CRUDE2EWorkflow tests the full lifecycle of spreadsheet operations
|
||||
// using all shortcut methods: +create, +read, +write, +append, +find, +info, +export
|
||||
func TestSheets_CRUDE2EWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
spreadsheetToken := ""
|
||||
sheetID := ""
|
||||
|
||||
t.Run("create spreadsheet with +create", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "+create", "--title", "lark-cli-e2e-sheets-" + suffix},
|
||||
}, clie2e.RetryOptions{})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
spreadsheetToken = gjson.Get(result.Stdout, "data.spreadsheet_token").String()
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token should not be empty, stdout: %s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
// Best-effort cleanup - spreadsheets don't have a direct delete shortcut
|
||||
// The spreadsheet will be cleaned up by the test environment if needed
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("get spreadsheet info with +info", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "+info", "--spreadsheet-token", spreadsheetToken},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, spreadsheetToken, gjson.Get(result.Stdout, "data.spreadsheet.spreadsheet.token").String())
|
||||
sheetID = gjson.Get(result.Stdout, "data.sheets.sheets.0.sheet_id").String()
|
||||
require.NotEmpty(t, sheetID, "sheet_id should not be empty, stdout: %s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("write data with +write", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
values := [][]any{
|
||||
{"Name", "Age", "City"},
|
||||
{"Alice", 25, "Beijing"},
|
||||
{"Bob", 30, "Shanghai"},
|
||||
}
|
||||
valuesJSON, _ := json.Marshal(values)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"sheets", "+write",
|
||||
"--spreadsheet-token", spreadsheetToken,
|
||||
"--sheet-id", sheetID,
|
||||
"--range", "A1:C3",
|
||||
"--values", string(valuesJSON),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("read data with +read", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"sheets", "+read",
|
||||
"--spreadsheet-token", spreadsheetToken,
|
||||
"--sheet-id", sheetID,
|
||||
"--range", "A1:C3",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
// Verify the data was written correctly
|
||||
values := gjson.Get(result.Stdout, "data.valueRange.values")
|
||||
require.True(t, values.IsArray(), "values should be an array, stdout: %s", result.Stdout)
|
||||
assert.Equal(t, "Name", values.Array()[0].Array()[0].String())
|
||||
assert.Equal(t, "Alice", values.Array()[1].Array()[0].String())
|
||||
})
|
||||
|
||||
t.Run("append rows with +append", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
values := [][]any{{"Charlie", 28, "Guangzhou"}}
|
||||
valuesJSON, _ := json.Marshal(values)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"sheets", "+append",
|
||||
"--spreadsheet-token", spreadsheetToken,
|
||||
"--sheet-id", sheetID,
|
||||
"--range", "A4:C4",
|
||||
"--values", string(valuesJSON),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("find cells with +find", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"sheets", "+find",
|
||||
"--spreadsheet-token", spreadsheetToken,
|
||||
"--sheet-id", sheetID,
|
||||
"--find", "Alice",
|
||||
"--range", fmt.Sprintf("%s!A1:C10", sheetID),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
assert.Equal(t, true, gjson.Get(result.Stdout, "ok").Bool(), "stdout:\n%s", result.Stdout)
|
||||
|
||||
matchedCells := gjson.Get(result.Stdout, "data.find_result.matched_cells")
|
||||
require.True(t, matchedCells.IsArray(), "matched_cells should be an array, stdout: %s", result.Stdout)
|
||||
assert.True(t, len(matchedCells.Array()) > 0, "should find at least one cell containing 'Alice'")
|
||||
})
|
||||
|
||||
t.Run("export spreadsheet with +export", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
|
||||
// Export is an async operation; verify it initiates correctly
|
||||
// The command may have filesystem race issues but the API call succeeds
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"sheets", "+export",
|
||||
"--spreadsheet-token", spreadsheetToken,
|
||||
"--file-extension", "xlsx",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Export initiates successfully and returns file_token even if there's a temp file race
|
||||
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.file_token").String(),
|
||||
"export should return file_token, stdout: %s", result.Stdout)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSheets_SpreadsheetsResource tests the spreadsheets resource methods
|
||||
func TestSheets_SpreadsheetsResource(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
spreadsheetToken := ""
|
||||
|
||||
t.Run("create spreadsheet with spreadsheets create", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheets", "create"},
|
||||
Data: map[string]any{
|
||||
"title": "lark-cli-e2e-sheets-resource-" + suffix,
|
||||
},
|
||||
}, clie2e.RetryOptions{})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
spreadsheetToken = gjson.Get(result.Stdout, "data.spreadsheet.spreadsheet_token").String()
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token should not be empty, stdout: %s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
// Best-effort cleanup
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("get spreadsheet with spreadsheets get", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheets", "get"},
|
||||
Params: map[string]any{"spreadsheet_token": spreadsheetToken},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
assert.Equal(t, spreadsheetToken, gjson.Get(result.Stdout, "data.spreadsheet.token").String())
|
||||
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.spreadsheet.url").String())
|
||||
})
|
||||
|
||||
t.Run("patch spreadsheet with spreadsheets patch", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
|
||||
updatedTitle := "lark-cli-e2e-sheets-patched-" + suffix
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheets", "patch"},
|
||||
Params: map[string]any{"spreadsheet_token": spreadsheetToken},
|
||||
Data: map[string]any{"title": updatedTitle},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
// Verify the title was updated by fetching the spreadsheet
|
||||
getResult, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheets", "get"},
|
||||
Params: map[string]any{"spreadsheet_token": spreadsheetToken},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
getResult.AssertExitCode(t, 0)
|
||||
getResult.AssertStdoutStatus(t, 0)
|
||||
|
||||
// Verify the title was actually updated
|
||||
assert.Equal(t, updatedTitle, gjson.Get(getResult.Stdout, "data.spreadsheet.title").String())
|
||||
})
|
||||
}
|
||||
174
tests/cli_e2e/sheets/sheets_filter_workflow_test.go
Normal file
174
tests/cli_e2e/sheets/sheets_filter_workflow_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// TestSheets_FilterWorkflow tests the spreadsheet sheet filter operations
|
||||
func TestSheets_FilterWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
spreadsheetToken := ""
|
||||
sheetID := ""
|
||||
|
||||
t.Run("create spreadsheet with initial data", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "+create", "--title", "lark-cli-e2e-sheets-filter-" + suffix},
|
||||
}, clie2e.RetryOptions{})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
spreadsheetToken = gjson.Get(result.Stdout, "data.spreadsheet_token").String()
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token should not be empty, stdout: %s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
// No sheets delete command is currently available in lark-cli,
|
||||
// so created spreadsheets are intentionally left in the test account.
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("get sheet info", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "+info", "--spreadsheet-token", spreadsheetToken},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
sheetID = gjson.Get(result.Stdout, "data.sheets.sheets.0.sheet_id").String()
|
||||
require.NotEmpty(t, sheetID, "sheet_id should not be empty, stdout: %s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("write test data for filtering", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
values := [][]any{
|
||||
{"Name", "Score", "Grade"},
|
||||
{"Alice", 85, "B"},
|
||||
{"Bob", 92, "A"},
|
||||
{"Charlie", 78, "C"},
|
||||
{"Diana", 95, "A"},
|
||||
}
|
||||
valuesJSON, _ := json.Marshal(values)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"sheets", "+write",
|
||||
"--spreadsheet-token", spreadsheetToken,
|
||||
"--sheet-id", sheetID,
|
||||
"--range", "A1:C5",
|
||||
"--values", string(valuesJSON),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("create filter with spreadsheet.sheet.filters create", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
filterData := map[string]any{
|
||||
"range": fmt.Sprintf("%s!A1:D5", sheetID),
|
||||
"col": "C",
|
||||
"filter_type": "multiValue",
|
||||
"condition": map[string]any{
|
||||
"filter_type": "multiValue",
|
||||
"expected": []any{"A", "B"},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheet.sheet.filters", "create"},
|
||||
Params: map[string]any{
|
||||
"spreadsheet_token": spreadsheetToken,
|
||||
"sheet_id": sheetID,
|
||||
},
|
||||
Data: filterData,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
})
|
||||
|
||||
t.Run("get filter with spreadsheet.sheet.filters get", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheet.sheet.filters", "get"},
|
||||
Params: map[string]any{
|
||||
"spreadsheet_token": spreadsheetToken,
|
||||
"sheet_id": sheetID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
filterInfo := gjson.Get(result.Stdout, "data.sheet_filter_info")
|
||||
require.True(t, filterInfo.Exists(), "filter info should exist, stdout: %s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("update filter with spreadsheet.sheet.filters update", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
filterData := map[string]any{
|
||||
"col": "B",
|
||||
"filter_type": "number",
|
||||
"condition": map[string]any{
|
||||
"filter_type": "number",
|
||||
"compare_type": "greater",
|
||||
"expected": []any{80},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheet.sheet.filters", "update"},
|
||||
Params: map[string]any{
|
||||
"spreadsheet_token": spreadsheetToken,
|
||||
"sheet_id": sheetID,
|
||||
},
|
||||
Data: filterData,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
})
|
||||
|
||||
t.Run("delete filter with spreadsheet.sheet.filters delete", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheet.sheet.filters", "delete"},
|
||||
Params: map[string]any{
|
||||
"spreadsheet_token": spreadsheetToken,
|
||||
"sheet_id": sheetID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
})
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func TestTask_CommentWorkflow(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := time.Now().UTC().Format("20060102-150405")
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
commentContent := "lark-cli-e2e-comment-" + suffix
|
||||
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
|
||||
Args: []string{"task", "+create"},
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestTask_ReminderWorkflow(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := time.Now().UTC().Format("20060102-150405")
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
|
||||
Args: []string{"task", "+create"},
|
||||
Data: map[string]any{
|
||||
@@ -57,9 +57,9 @@ func TestTask_ReminderWorkflow(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("remove reminder", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+reminder", "--task-id", taskGUID, "--remove"},
|
||||
})
|
||||
}, clie2e.RetryOptions{})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestTask_StatusWorkflow(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := time.Now().UTC().Format("20060102-150405")
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
|
||||
Args: []string{"task", "+create"},
|
||||
Data: map[string]any{
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestTask_TasklistAddTaskWorkflow(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := time.Now().UTC().Format("20060102-150405")
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
tasklistName := "lark-cli-e2e-tasklist-add-" + suffix
|
||||
taskSummary := "lark-cli-e2e-tasklist-add-task-" + suffix
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestTask_TasklistWorkflow(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := time.Now().UTC().Format("20060102-150405")
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
tasklistName := "lark-cli-e2e-tasklist-" + suffix
|
||||
taskSummary := "lark-cli-e2e-task-in-tasklist-" + suffix
|
||||
taskDescription := "created by tests/cli_e2e/task"
|
||||
|
||||
70
tests/cli_e2e/wiki/helpers_test.go
Normal file
70
tests/cli_e2e/wiki/helpers_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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.RunCmdWithRetry(ctx, req, clie2e.RetryOptions{})
|
||||
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
|
||||
}
|
||||
|
||||
func findWikiNodeByToken(t *testing.T, ctx context.Context, spaceID string, nodeToken string) gjson.Result {
|
||||
t.Helper()
|
||||
|
||||
require.NotEmpty(t, spaceID, "space ID is required")
|
||||
require.NotEmpty(t, nodeToken, "node token is required")
|
||||
|
||||
pageToken := ""
|
||||
seenPageTokens := map[string]struct{}{}
|
||||
for {
|
||||
params := map[string]any{
|
||||
"space_id": spaceID,
|
||||
"page_size": 50,
|
||||
}
|
||||
if pageToken != "" {
|
||||
if _, seen := seenPageTokens[pageToken]; seen {
|
||||
t.Fatalf("wiki node list pagination loop detected for space %q, repeated page_token %q", spaceID, pageToken)
|
||||
}
|
||||
seenPageTokens[pageToken] = struct{}{}
|
||||
params["page_token"] = pageToken
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"wiki", "nodes", "list"},
|
||||
DefaultAs: "bot",
|
||||
Params: params,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
node := gjson.Get(result.Stdout, `data.items.#(node_token=="`+nodeToken+`")`)
|
||||
if node.Exists() {
|
||||
return node
|
||||
}
|
||||
|
||||
hasMore := gjson.Get(result.Stdout, "data.has_more").Bool()
|
||||
pageToken = gjson.Get(result.Stdout, "data.page_token").String()
|
||||
if !hasMore || pageToken == "" {
|
||||
t.Fatalf("wiki node %q not found in listed pages, last stdout:\n%s", nodeToken, result.Stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
150
tests/cli_e2e/wiki/wiki_workflow_test.go
Normal file
150
tests/cli_e2e/wiki/wiki_workflow_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// 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_NodeWorkflow(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
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")
|
||||
|
||||
nodeItem := findWikiNodeByToken(t, ctx, spaceID, createdNodeToken)
|
||||
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")
|
||||
|
||||
nodeItem := findWikiNodeByToken(t, ctx, spaceID, copiedNodeToken)
|
||||
assert.Equal(t, copiedTitle, nodeItem.Get("title").String())
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user