mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Feat/cli e2e tests with UAT (#528)
* test: expand and stabilize cli e2e workflows * ci: run deadcode with test entrypoints
This commit is contained in:
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -153,14 +153,14 @@ jobs:
|
||||
run: |
|
||||
# Analyze current HEAD (strip line:col for stable diff across line shifts)
|
||||
# Filter "go: downloading ..." lines to avoid false diffs from module cache state
|
||||
go run golang.org/x/tools/cmd/deadcode@v0.31.0 ./... 2>&1 | \
|
||||
go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./... 2>&1 | \
|
||||
grep -v '^go: ' | \
|
||||
sed 's/:[0-9][0-9]*:[0-9][0-9]*:/:/' | sort > /tmp/dc-head.txt
|
||||
|
||||
# Analyze base branch via worktree
|
||||
git worktree add -q /tmp/dc-base "origin/${{ github.base_ref }}"
|
||||
(cd /tmp/dc-base && python3 scripts/fetch_meta.py && \
|
||||
go run golang.org/x/tools/cmd/deadcode@v0.31.0 ./... 2>&1 | \
|
||||
go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./... 2>&1 | \
|
||||
grep -v '^go: ' | \
|
||||
sed 's/:[0-9][0-9]*:[0-9][0-9]*:/:/' | sort > /tmp/dc-base.txt) || {
|
||||
echo "::warning::Failed to analyze base branch — skipping incremental dead code check"
|
||||
@@ -209,6 +209,7 @@ jobs:
|
||||
env:
|
||||
TEST_BOT1_APP_ID: ${{ secrets.TEST_BOT1_APP_ID }}
|
||||
TEST_BOT1_APP_SECRET: ${{ secrets.TEST_BOT1_APP_SECRET }}
|
||||
TEST_USER_ACCESS_TOKEN: ${{ secrets.TEST_USER_ACCESS_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
@@ -229,6 +230,8 @@ jobs:
|
||||
- name: Run CLI E2E tests
|
||||
env:
|
||||
LARK_CLI_BIN: ${{ github.workspace }}/lark-cli
|
||||
LARKSUITE_CLI_APP_ID: ${{ env.TEST_BOT1_APP_ID }}
|
||||
LARKSUITE_CLI_USER_ACCESS_TOKEN: ${{ env.TEST_USER_ACCESS_TOKEN }}
|
||||
run: |
|
||||
packages=$(go list ./tests/cli_e2e/... | grep -v '^github.com/larksuite/cli/tests/cli_e2e$' | grep -v '/demo$')
|
||||
if [ -z "$packages" ]; then
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestBase_BasicWorkflow(t *testing.T) {
|
||||
baseName := "lark-cli-e2e-base-basic-" + clie2e.GenerateSuffix()
|
||||
baseToken := createBaseWithRetry(t, ctx, baseName)
|
||||
|
||||
t.Run("get base", func(t *testing.T) {
|
||||
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",
|
||||
@@ -49,7 +49,7 @@ func TestBase_BasicWorkflow(t *testing.T) {
|
||||
`{"name":"Main","type":"grid"}`,
|
||||
)
|
||||
|
||||
t.Run("get table", func(t *testing.T) {
|
||||
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",
|
||||
@@ -61,7 +61,7 @@ func TestBase_BasicWorkflow(t *testing.T) {
|
||||
assert.Equal(t, tableName, gjson.Get(result.Stdout, "data.table.name").String())
|
||||
})
|
||||
|
||||
t.Run("list tables and find created table", func(t *testing.T) {
|
||||
t.Run("list tables and find created table as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+table-list", "--base-token", baseToken},
|
||||
DefaultAs: "bot",
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestBase_RoleWorkflow(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list", func(t *testing.T) {
|
||||
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",
|
||||
@@ -81,7 +81,7 @@ func TestBase_RoleWorkflow(t *testing.T) {
|
||||
require.NotEmpty(t, roleID, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get", func(t *testing.T) {
|
||||
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{
|
||||
@@ -98,7 +98,7 @@ func TestBase_RoleWorkflow(t *testing.T) {
|
||||
assert.Equal(t, roleID, gjson.Get(rolePayload, "role_id").String())
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
t.Run("update role as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, roleID, "role ID should be resolved before update")
|
||||
|
||||
updatedRoleName := roleName + " Updated"
|
||||
|
||||
91
tests/cli_e2e/base/coverage.md
Normal file
91
tests/cli_e2e/base/coverage.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Base CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 73 leaf commands
|
||||
- Covered: 10
|
||||
- Coverage: 13.7%
|
||||
|
||||
## 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`.
|
||||
- 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 +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 | | none | field workflows not covered |
|
||||
| ✕ | 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 | | none | record workflows not covered |
|
||||
| ✕ | 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 |
|
||||
@@ -31,7 +31,7 @@ func TestCalendar_CreateEvent(t *testing.T) {
|
||||
var eventID string
|
||||
calendarID := getPrimaryCalendarID(t, ctx)
|
||||
|
||||
t.Run("create event with shortcut", func(t *testing.T) {
|
||||
t.Run("create event with shortcut as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "+create",
|
||||
"--summary", eventSummary,
|
||||
@@ -50,7 +50,7 @@ func TestCalendar_CreateEvent(t *testing.T) {
|
||||
require.NotEmpty(t, eventID)
|
||||
})
|
||||
|
||||
t.Run("verify event created", func(t *testing.T) {
|
||||
t.Run("verify event created as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, eventID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "events", "get"},
|
||||
@@ -69,7 +69,7 @@ func TestCalendar_CreateEvent(t *testing.T) {
|
||||
assert.Equal(t, unixSecondsRFC3339(endAt), gjson.Get(result.Stdout, "data.event.end_time.timestamp").String())
|
||||
})
|
||||
|
||||
t.Run("delete event", func(t *testing.T) {
|
||||
t.Run("delete event as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, eventID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "events", "delete"},
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestCalendar_ManageCalendar(t *testing.T) {
|
||||
|
||||
var createdCalendarID string
|
||||
|
||||
t.Run("list calendars", func(t *testing.T) {
|
||||
t.Run("list calendars as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "list"},
|
||||
DefaultAs: "bot",
|
||||
@@ -37,12 +37,12 @@ func TestCalendar_ManageCalendar(t *testing.T) {
|
||||
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) {
|
||||
t.Run("get primary calendar as bot", func(t *testing.T) {
|
||||
primaryCalendarID := getPrimaryCalendarID(t, ctx)
|
||||
require.NotEmpty(t, primaryCalendarID)
|
||||
})
|
||||
|
||||
t.Run("create calendar", func(t *testing.T) {
|
||||
t.Run("create calendar as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "create"},
|
||||
DefaultAs: "bot",
|
||||
@@ -59,7 +59,7 @@ func TestCalendar_ManageCalendar(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
})
|
||||
|
||||
t.Run("get created calendar", func(t *testing.T) {
|
||||
t.Run("get created calendar as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "get"},
|
||||
@@ -76,7 +76,7 @@ func TestCalendar_ManageCalendar(t *testing.T) {
|
||||
assert.Equal(t, calendarDescription, gjson.Get(result.Stdout, "data.description").String())
|
||||
})
|
||||
|
||||
t.Run("find created calendar in list", func(t *testing.T) {
|
||||
t.Run("find created calendar in list as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "list"},
|
||||
@@ -88,7 +88,7 @@ func TestCalendar_ManageCalendar(t *testing.T) {
|
||||
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) {
|
||||
t.Run("update calendar as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "patch"},
|
||||
@@ -105,7 +105,7 @@ func TestCalendar_ManageCalendar(t *testing.T) {
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
})
|
||||
|
||||
t.Run("verify updated calendar", func(t *testing.T) {
|
||||
t.Run("verify updated calendar as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "get"},
|
||||
@@ -120,7 +120,7 @@ func TestCalendar_ManageCalendar(t *testing.T) {
|
||||
assert.Equal(t, updatedCalendarSummary, gjson.Get(result.Stdout, "data.summary").String())
|
||||
})
|
||||
|
||||
t.Run("delete calendar", func(t *testing.T) {
|
||||
t.Run("delete calendar as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdCalendarID)
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "delete"},
|
||||
|
||||
134
tests/cli_e2e/calendar/calendar_personal_event_workflow_test.go
Normal file
134
tests/cli_e2e/calendar/calendar_personal_event_workflow_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
func TestCalendar_PersonalEventWorkflowAsUser(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
eventSummary := "lark-cli-e2e-personal-event-" + suffix
|
||||
eventDescription := "created by calendar personal event workflow"
|
||||
startAt := time.Now().UTC().Add(24 * time.Hour).Truncate(time.Minute)
|
||||
endAt := startAt.Add(30 * time.Minute)
|
||||
startTime := startAt.Format(time.RFC3339)
|
||||
endTime := endAt.Format(time.RFC3339)
|
||||
|
||||
var calendarID string
|
||||
var eventID string
|
||||
|
||||
t.Run("get primary calendar as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "primary"},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("create personal event with shortcut as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, calendarID, "calendar should be loaded before creating an event")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"calendar", "+create",
|
||||
"--summary", eventSummary,
|
||||
"--start", startTime,
|
||||
"--end", endTime,
|
||||
"--calendar-id", calendarID,
|
||||
"--description", eventDescription,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
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, "stdout:\n%s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"calendar", "events", "delete"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{
|
||||
"calendar_id": calendarID,
|
||||
"event_id": eventID,
|
||||
},
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete event "+eventID, deleteResult, deleteErr)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("get created event as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, calendarID, "calendar should be loaded before getting an event")
|
||||
require.NotEmpty(t, eventID, "event should be created before reading it back")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "events", "get"},
|
||||
DefaultAs: "user",
|
||||
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, eventID, gjson.Get(result.Stdout, "data.event.event_id").String())
|
||||
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("find created event in agenda as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, eventID, "event should be created before checking agenda")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"calendar", "+agenda",
|
||||
"--start", startTime,
|
||||
"--end", endTime,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
matchedEvent := gjson.Get(result.Stdout, `data.#(event_id=="`+eventID+`")`)
|
||||
require.True(t, matchedEvent.Exists(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, eventSummary, matchedEvent.Get("summary").String())
|
||||
|
||||
agendaStart, parseErr := time.Parse(time.RFC3339, matchedEvent.Get("start_time.datetime").String())
|
||||
require.NoError(t, parseErr, "stdout:\n%s", result.Stdout)
|
||||
agendaEnd, parseErr := time.Parse(time.RFC3339, matchedEvent.Get("end_time.datetime").String())
|
||||
require.NoError(t, parseErr, "stdout:\n%s", result.Stdout)
|
||||
assert.True(t, agendaStart.Equal(startAt), "stdout:\n%s", result.Stdout)
|
||||
assert.True(t, agendaEnd.Equal(endAt), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
214
tests/cli_e2e/calendar/calendar_rsvp_workflow_test.go
Normal file
214
tests/cli_e2e/calendar/calendar_rsvp_workflow_test.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
func requireFreebusyEntry(t *testing.T, stdout string, startAt time.Time, endAt time.Time, expectedRSVP string) {
|
||||
t.Helper()
|
||||
|
||||
var matched gjson.Result
|
||||
for _, item := range gjson.Parse(stdout).Get("data").Array() {
|
||||
itemStart, err := time.Parse(time.RFC3339, item.Get("start_time").String())
|
||||
require.NoError(t, err, "stdout:\n%s", stdout)
|
||||
itemEnd, err := time.Parse(time.RFC3339, item.Get("end_time").String())
|
||||
require.NoError(t, err, "stdout:\n%s", stdout)
|
||||
|
||||
if !itemStart.Equal(startAt) || !itemEnd.Equal(endAt) {
|
||||
continue
|
||||
}
|
||||
if item.Get("rsvp_status").String() != expectedRSVP {
|
||||
continue
|
||||
}
|
||||
matched = item
|
||||
break
|
||||
}
|
||||
|
||||
require.True(t, matched.Exists(), "expected freebusy entry start=%s end=%s rsvp=%s in stdout:\n%s", startAt.Format(time.RFC3339), endAt.Format(time.RFC3339), expectedRSVP, stdout)
|
||||
assert.Equal(t, expectedRSVP, matched.Get("rsvp_status").String(), "stdout:\n%s", stdout)
|
||||
}
|
||||
|
||||
func TestCalendar_RSVPWorkflowAsUser(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
userOpenID := getCurrentUserOpenIDForCalendar(t, ctx)
|
||||
calendarID := getPrimaryCalendarID(t, ctx)
|
||||
startAt := time.Now().UTC().Add(2 * time.Hour).Truncate(time.Minute)
|
||||
endAt := startAt.Add(30 * time.Minute)
|
||||
startTime := startAt.Format(time.RFC3339)
|
||||
endTime := endAt.Format(time.RFC3339)
|
||||
var eventID string
|
||||
|
||||
t.Run("query freebusy as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"calendar", "+freebusy",
|
||||
"--start", startTime,
|
||||
"--end", endTime,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
data := gjson.Get(result.Stdout, "data")
|
||||
require.True(t, data.IsArray() || data.Type == gjson.Null, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("create invite-only event as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"calendar", "+create",
|
||||
"--summary", "lark-cli-e2e-calendar-rsvp-" + clie2e.GenerateSuffix(),
|
||||
"--start", startTime,
|
||||
"--end", endTime,
|
||||
"--calendar-id", calendarID,
|
||||
"--attendee-ids", userOpenID,
|
||||
},
|
||||
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, "stdout:\n%s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"calendar", "events", "delete"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"calendar_id": calendarID,
|
||||
"event_id": eventID,
|
||||
},
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete event "+eventID, deleteResult, deleteErr)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("reply tentative as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, eventID, "event should be created before RSVP")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"calendar", "+rsvp",
|
||||
"--calendar-id", calendarID,
|
||||
"--event-id", eventID,
|
||||
"--rsvp-status", "tentative",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
require.Equal(t, calendarID, gjson.Get(result.Stdout, "data.calendar_id").String(), "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, eventID, gjson.Get(result.Stdout, "data.event_id").String(), "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, "tentative", gjson.Get(result.Stdout, "data.rsvp_status").String())
|
||||
})
|
||||
|
||||
t.Run("verify tentative freebusy as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"calendar", "+freebusy",
|
||||
"--start", startTime,
|
||||
"--end", endTime,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 || !gjson.Get(result.Stdout, "status").Bool() {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Parse(result.Stdout).Get("data").Array() {
|
||||
itemStart, err := time.Parse(time.RFC3339, item.Get("start_time").String())
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
itemEnd, err := time.Parse(time.RFC3339, item.Get("end_time").String())
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if itemStart.Equal(startAt) && itemEnd.Equal(endAt) && item.Get("rsvp_status").String() == "tentative" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
requireFreebusyEntry(t, result.Stdout, startAt, endAt, "tentative")
|
||||
})
|
||||
|
||||
t.Run("reply accept as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"calendar", "+rsvp",
|
||||
"--calendar-id", calendarID,
|
||||
"--event-id", eventID,
|
||||
"--rsvp-status", "accept",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
require.Equal(t, calendarID, gjson.Get(result.Stdout, "data.calendar_id").String(), "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, eventID, gjson.Get(result.Stdout, "data.event_id").String(), "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, "accept", gjson.Get(result.Stdout, "data.rsvp_status").String())
|
||||
})
|
||||
|
||||
t.Run("verify accepted freebusy as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"calendar", "+freebusy",
|
||||
"--start", startTime,
|
||||
"--end", endTime,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 || !gjson.Get(result.Stdout, "status").Bool() {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Parse(result.Stdout).Get("data").Array() {
|
||||
itemStart, err := time.Parse(time.RFC3339, item.Get("start_time").String())
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
itemEnd, err := time.Parse(time.RFC3339, item.Get("end_time").String())
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if itemStart.Equal(startAt) && itemEnd.Equal(endAt) && item.Get("rsvp_status").String() == "accept" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
requireFreebusyEntry(t, result.Stdout, startAt, endAt, "accept")
|
||||
})
|
||||
}
|
||||
57
tests/cli_e2e/calendar/calendar_view_agenda_test.go
Normal file
57
tests/cli_e2e/calendar/calendar_view_agenda_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
func TestCalendar_ViewAgenda(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
calendarID := getCurrentUserPrimaryCalendarID(t, ctx)
|
||||
|
||||
t.Run("view today agenda as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "+agenda", "--calendar-id", calendarID},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
assert.True(t, gjson.Get(result.Stdout, "data").IsArray(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("view agenda with date range as user", func(t *testing.T) {
|
||||
startDate := time.Now().UTC().Format("2006-01-02")
|
||||
endDate := time.Now().UTC().AddDate(0, 0, 7).Format("2006-01-02")
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "+agenda", "--calendar-id", calendarID, "--start", startDate, "--end", endDate},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
assert.True(t, gjson.Get(result.Stdout, "data").IsArray(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("view agenda with pretty format as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "+agenda"},
|
||||
DefaultAs: "user",
|
||||
Format: "pretty",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
})
|
||||
}
|
||||
43
tests/cli_e2e/calendar/coverage.md
Normal file
43
tests/cli_e2e/calendar/coverage.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Calendar CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 23 leaf commands
|
||||
- Covered: 12
|
||||
- Coverage: 52.2%
|
||||
|
||||
## Summary
|
||||
- TestCalendar_ViewAgenda: proves the user shortcut `calendar +agenda`; key `t.Run(...)` proof points are `view today agenda as user`, `view agenda with date range as user`, and `view agenda with pretty format as user`.
|
||||
- TestCalendar_PersonalEventWorkflowAsUser: proves a self-contained user event workflow across `calendar calendars primary`, `calendar +create`, `calendar events get`, and `calendar +agenda`; key `t.Run(...)` proof points are `get primary calendar as user`, `create personal event with shortcut as user`, `get created event as user`, and `find created event in agenda as user`.
|
||||
- TestCalendar_RSVPWorkflowAsUser: proves the user shortcuts `calendar +freebusy` and `calendar +rsvp`; key `t.Run(...)` proof points are `query freebusy as user`, `reply tentative as user`, `verify tentative freebusy as user`, `reply accept as user`, and `verify accepted freebusy as user`.
|
||||
- TestCalendar_CreateEvent: proves `calendar +create`, `calendar events get`, and `calendar events delete`; key `t.Run(...)` proof points are `create event with shortcut as bot`, `verify event created as bot`, and `delete event as bot`.
|
||||
- TestCalendar_ManageCalendar: proves `calendar calendars primary`, `calendar calendars create`, `calendar calendars get`, `calendar calendars list`, and `calendar calendars patch`; key `t.Run(...)` proof points are `get primary calendar as bot`, `create calendar as bot`, `get created calendar as bot`, `find created calendar in list as bot`, and `update calendar as bot`.
|
||||
- Cleanup note: `calendar calendars delete` is part of the calendar lifecycle workflow and is counted as covered because the workflow proves the full shared-calendar lifecycle.
|
||||
- Blocked area: direct `event.attendees *` APIs, `calendar calendars search`, `calendar events create|instance_view|patch|search`, `calendar freebusys list`, and planning shortcuts `calendar +room-find` / `calendar +suggestion` still need deterministic workflows; the planning shortcuts currently depend on live tenant availability and room inventory, so they remain uncovered.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✓ | calendar +agenda | shortcut | calendar_view_agenda_test.go::TestCalendar_ViewAgenda; calendar_personal_event_workflow_test.go::TestCalendar_PersonalEventWorkflowAsUser/find created event in agenda as user | default today; `--start`; `--end`; `--format pretty` | user identity readback plus general agenda view |
|
||||
| ✓ | calendar +create | shortcut | calendar_create_event_test.go::TestCalendar_CreateEvent/create event with shortcut as bot; calendar_personal_event_workflow_test.go::TestCalendar_PersonalEventWorkflowAsUser/create personal event with shortcut as user | `--summary`; `--start`; `--end`; `--calendar-id`; `--description` | bot and user workflow coverage |
|
||||
| ✓ | calendar +freebusy | shortcut | calendar_rsvp_workflow_test.go::TestCalendar_RSVPWorkflowAsUser/query freebusy as user; calendar_rsvp_workflow_test.go::TestCalendar_RSVPWorkflowAsUser/verify tentative freebusy as user; calendar_rsvp_workflow_test.go::TestCalendar_RSVPWorkflowAsUser/verify accepted freebusy as user | default current user; `--start`; `--end` | user identity flow |
|
||||
| ✕ | calendar +room-find | shortcut | | none | no deterministic self-contained workflow yet; output depends on live room inventory |
|
||||
| ✓ | calendar +rsvp | shortcut | calendar_rsvp_workflow_test.go::TestCalendar_RSVPWorkflowAsUser/reply tentative as user; calendar_rsvp_workflow_test.go::TestCalendar_RSVPWorkflowAsUser/reply accept as user | `--calendar-id`; `--event-id`; `--rsvp-status` | user reply flow |
|
||||
| ✕ | calendar +suggestion | shortcut | | none | no deterministic self-contained workflow yet; output depends on live availability suggestions |
|
||||
| ✓ | calendar calendars create | api | calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/create calendar as bot | `summary`; `description` in `--data` | |
|
||||
| ✓ | calendar calendars delete | api | calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/delete calendar as bot | `calendar_id` in `--params` | |
|
||||
| ✓ | calendar calendars get | api | calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/get created calendar as bot; calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/verify updated calendar as bot | `calendar_id` in `--params` | |
|
||||
| ✓ | calendar calendars list | api | calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/list calendars as bot; calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/find created calendar in list as bot | none | |
|
||||
| ✓ | calendar calendars patch | api | calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/update calendar as bot | `calendar_id` in `--params`; `summary` in `--data` | |
|
||||
| ✓ | calendar calendars primary | api | calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/get primary calendar as bot; calendar_personal_event_workflow_test.go::TestCalendar_PersonalEventWorkflowAsUser/get primary calendar as user | none | bot and user primary calendar lookup |
|
||||
| ✕ | calendar calendars search | api | | none | no search workflow yet |
|
||||
| ✕ | calendar events create | api | | none | only covered indirectly through `calendar +create` |
|
||||
| ✓ | calendar events delete | api | calendar_create_event_test.go::TestCalendar_CreateEvent/delete event as bot | `calendar_id`; `event_id` in `--params` | |
|
||||
| ✓ | calendar events get | api | calendar_create_event_test.go::TestCalendar_CreateEvent/verify event created as bot; calendar_personal_event_workflow_test.go::TestCalendar_PersonalEventWorkflowAsUser/get created event as user | `calendar_id`; `event_id` in `--params` | bot and user read-after-write coverage |
|
||||
| ✕ | calendar events instance_view | api | | none | `+agenda` is indirect orchestration, not direct API coverage |
|
||||
| ✕ | calendar events patch | api | | none | no direct event-update workflow yet |
|
||||
| ✕ | calendar events search | api | | none | no search workflow yet |
|
||||
| ✕ | calendar freebusys list | api | | none | no direct freebusy API workflow yet |
|
||||
| ✕ | calendar event.attendees batch_delete | api | | none | requires an isolated attendee lifecycle workflow |
|
||||
| ✕ | calendar event.attendees create | api | | none | requires an isolated attendee lifecycle workflow |
|
||||
| ✕ | calendar event.attendees list | api | | none | requires an isolated attendee lifecycle workflow |
|
||||
@@ -30,6 +30,38 @@ func getPrimaryCalendarID(t *testing.T, ctx context.Context) string {
|
||||
return calendarID
|
||||
}
|
||||
|
||||
func getCurrentUserPrimaryCalendarID(t *testing.T, ctx context.Context) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"calendar", "calendars", "primary"},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
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 getCurrentUserOpenIDForCalendar(t *testing.T, ctx context.Context) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"contact", "+get-user"},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
openID := gjson.Get(result.Stdout, "data.user.open_id").String()
|
||||
require.NotEmpty(t, openID, "stdout:\n%s", result.Stdout)
|
||||
return openID
|
||||
}
|
||||
|
||||
func unixSecondsRFC3339(t time.Time) string {
|
||||
return strconv.FormatInt(t.Unix(), 10)
|
||||
}
|
||||
|
||||
94
tests/cli_e2e/contact/contact_lookup_workflow_test.go
Normal file
94
tests/cli_e2e/contact/contact_lookup_workflow_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package contact
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
)
|
||||
|
||||
func TestContact_LookupWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
var selfOpenID string
|
||||
|
||||
t.Run("get self as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"contact", "+get-user"},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
selfOpenID = gjson.Get(result.Stdout, "data.user.open_id").String()
|
||||
require.NotEmpty(t, selfOpenID, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get self by open id as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, selfOpenID, "self open_id should be populated before get-by-id")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"contact", "+get-user", "--user-id", selfOpenID},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
require.Equal(t, selfOpenID, gjson.Get(result.Stdout, "data.user.user_id").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContact_LookupWorkflowAsBot(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
var targetOpenID string
|
||||
|
||||
t.Run("discover user via api as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"api", "get", "/open-apis/contact/v3/users"},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if result.ExitCode != 0 {
|
||||
stderrLower := strings.ToLower(result.Stderr)
|
||||
if strings.Contains(stderrLower, "permission denied") || strings.Contains(stderrLower, "99991679") {
|
||||
t.Skipf("skip bot contact workflow due to missing bot contact permissions: %s", result.Stderr)
|
||||
}
|
||||
}
|
||||
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 open id as bot", func(t *testing.T) {
|
||||
if targetOpenID == "" {
|
||||
t.Skip("skip bot get-user-by-id because discover-user-via-api did not provide targetOpenID")
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"contact", "+get-user", "--user-id", targetOpenID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
require.Equal(t, targetOpenID, gjson.Get(result.Stdout, "data.user.open_id").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
18
tests/cli_e2e/contact/coverage.md
Normal file
18
tests/cli_e2e/contact/coverage.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Contact CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 2 leaf commands
|
||||
- Covered: 1
|
||||
- Coverage: 50.0%
|
||||
|
||||
## Summary
|
||||
- TestContact_LookupWorkflowAsUser: proves the user lookup workflow through `get self as user` and `get self by open id as user`; reads the current user first and round-trips the returned `open_id` back into `+get-user`.
|
||||
- TestContact_LookupWorkflowAsBot: proves bot lookup through `discover user via api as bot` and `get user by open id as bot`; the raw API discovery step is fixture setup only and does not affect the domain denominator.
|
||||
- Blocked area: `contact +search-user` did not reliably return the current user in UAT even when queried with self-derived identifiers, so it remains uncovered rather than being counted from a flaky tenant-dependent assertion.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✓ | contact +get-user | shortcut | contact_lookup_workflow_test.go::TestContact_LookupWorkflowAsUser/get self as user; contact_lookup_workflow_test.go::TestContact_LookupWorkflowAsUser/get self by open id as user; contact_lookup_workflow_test.go::TestContact_LookupWorkflowAsBot/get user by open id as bot | self lookup; `--user-id <open_id>` | |
|
||||
| ✕ | contact +search-user | shortcut | | none | UAT did not reliably return the current user for self-derived queries, so stable write-after-read style proof is not available |
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -25,9 +24,46 @@ import (
|
||||
const EnvBinaryPath = "LARK_CLI_BIN"
|
||||
const projectRootMarkerDir = "tests"
|
||||
const cliBinaryName = "lark-cli"
|
||||
const defaultIdentity = "bot"
|
||||
const CleanupTimeout = 30 * time.Second
|
||||
|
||||
var defaultAsInitOnce sync.Once
|
||||
func SkipWithoutUserToken(t *testing.T) {
|
||||
t.Helper()
|
||||
if os.Getenv("LARKSUITE_CLI_USER_ACCESS_TOKEN") != "" {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result, err := RunCmd(ctx, Request{
|
||||
Args: []string{"auth", "status", "--verify"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Skipf("skipped: LARKSUITE_CLI_USER_ACCESS_TOKEN not set and failed to check local user login via `lark-cli auth status --verify`: %v", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
t.Skipf("skipped: LARKSUITE_CLI_USER_ACCESS_TOKEN not set and local user login check failed: exit=%d stderr=%s", result.ExitCode, strings.TrimSpace(result.Stderr))
|
||||
}
|
||||
|
||||
stdout := strings.TrimSpace(result.Stdout)
|
||||
if stdout == "" {
|
||||
t.Skip("skipped: LARKSUITE_CLI_USER_ACCESS_TOKEN not set and `lark-cli auth status --verify` returned empty stdout")
|
||||
}
|
||||
if !gjson.Valid(stdout) {
|
||||
t.Skipf("skipped: LARKSUITE_CLI_USER_ACCESS_TOKEN not set and `lark-cli auth status --verify` returned non-JSON stdout: %s", stdout)
|
||||
}
|
||||
|
||||
if identity := gjson.Get(stdout, "identity").String(); identity != "user" {
|
||||
t.Skip("skipped: LARKSUITE_CLI_USER_ACCESS_TOKEN not set and local auth is not a verified user login")
|
||||
}
|
||||
if verified := gjson.Get(stdout, "verified"); verified.Exists() && !verified.Bool() {
|
||||
verifyErr := gjson.Get(stdout, "verifyError").String()
|
||||
if verifyErr != "" {
|
||||
t.Skipf("skipped: LARKSUITE_CLI_USER_ACCESS_TOKEN not set and local user login verification failed: %s", verifyErr)
|
||||
}
|
||||
t.Skip("skipped: LARKSUITE_CLI_USER_ACCESS_TOKEN not set and local user login verification failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Request describes one lark-cli invocation.
|
||||
type Request struct {
|
||||
@@ -46,6 +82,8 @@ type Request struct {
|
||||
DefaultAs string
|
||||
// Format is optional and becomes --format <format> when non-empty.
|
||||
Format string
|
||||
// WorkDir is optional and becomes the child process working directory when non-empty.
|
||||
WorkDir string
|
||||
}
|
||||
|
||||
// Result captures process execution output.
|
||||
@@ -74,19 +112,15 @@ func RunCmd(ctx context.Context, req Request) (*Result, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Best-effort initialization only. Failing to set default-as should not hide
|
||||
// the actual command-under-test result, because some environments may still
|
||||
// run the target CLI flow successfully without this convenience setup.
|
||||
defaultAsInitOnce.Do(func() {
|
||||
_ = setDefaultAs(ctx, binaryPath, defaultIdentity)
|
||||
})
|
||||
|
||||
args, err := BuildArgs(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, binaryPath, args...)
|
||||
if req.WorkDir != "" {
|
||||
cmd.Dir = req.WorkDir
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
@@ -166,6 +200,72 @@ func GenerateSuffix() string {
|
||||
return fmt.Sprintf("%s-%09d", now.Format("20060102-150405"), now.Nanosecond())
|
||||
}
|
||||
|
||||
// CleanupContext returns a bounded context for teardown operations so cleanup
|
||||
// cannot outlive the test indefinitely when the remote API stalls.
|
||||
func CleanupContext() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), CleanupTimeout)
|
||||
}
|
||||
|
||||
// ReportCleanupFailure emits a uniform cleanup error with command output.
|
||||
func ReportCleanupFailure(parentT *testing.T, prefix string, result *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
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
parentT.Errorf("%s failed: exit=%d stdout=%s stderr=%s", prefix, result.ExitCode, result.Stdout, result.Stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func isCleanupSuppressedResult(result *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
|
||||
}
|
||||
|
||||
errType := gjson.Get(payload, "error.type").String()
|
||||
errMessage := strings.ToLower(gjson.Get(payload, "error.message").String())
|
||||
errDetailType := gjson.Get(payload, "error.detail.type").String()
|
||||
errCode := gjson.Get(payload, "error.code").Int()
|
||||
|
||||
if errDetailType == "not_found" || strings.Contains(errMessage, "not found") || strings.Contains(errMessage, "http 404") {
|
||||
return true
|
||||
}
|
||||
|
||||
return errType == "api_error" && (errCode == 800004135 || strings.Contains(errMessage, " limited"))
|
||||
}
|
||||
|
||||
// ResolveBinaryPath finds the CLI binary path using request, env, then PATH.
|
||||
func ResolveBinaryPath(req Request) (string, error) {
|
||||
if req.BinaryPath != "" {
|
||||
@@ -259,16 +359,6 @@ func findProjectRootDir() (string, error) {
|
||||
return "", fmt.Errorf("project root not found from cwd using marker %q", projectRootMarkerDir)
|
||||
}
|
||||
|
||||
func setDefaultAs(ctx context.Context, binaryPath string, identity string) error {
|
||||
cmd := exec.CommandContext(ctx, binaryPath, "config", "default-as", identity)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("set default-as %q: %w; stderr: %s", identity, err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func exitCode(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
|
||||
@@ -7,11 +7,8 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -101,10 +98,55 @@ func TestBuildArgs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSkipWithoutUserToken(t *testing.T) {
|
||||
t.Run("returns immediately when env user access token exists", func(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_USER_ACCESS_TOKEN", "uat-from-env")
|
||||
|
||||
ran := false
|
||||
ok := t.Run("inner", func(t *testing.T) {
|
||||
SkipWithoutUserToken(t)
|
||||
ran = true
|
||||
})
|
||||
require.True(t, ok)
|
||||
assert.True(t, ran)
|
||||
})
|
||||
|
||||
t.Run("accepts verified local auth status", func(t *testing.T) {
|
||||
fake := newFakeCLI(t)
|
||||
t.Setenv("LARKSUITE_CLI_USER_ACCESS_TOKEN", "")
|
||||
t.Setenv(EnvBinaryPath, fake.BinaryPath)
|
||||
t.Setenv("FAKE_AUTH_STATUS_STDOUT", `{"identity":"user","verified":true}`)
|
||||
t.Setenv("FAKE_AUTH_STATUS_EXIT_CODE", "0")
|
||||
|
||||
ran := false
|
||||
ok := t.Run("inner", func(t *testing.T) {
|
||||
SkipWithoutUserToken(t)
|
||||
ran = true
|
||||
})
|
||||
require.True(t, ok)
|
||||
assert.True(t, ran)
|
||||
})
|
||||
|
||||
t.Run("skips when local auth is not user", func(t *testing.T) {
|
||||
fake := newFakeCLI(t)
|
||||
t.Setenv("LARKSUITE_CLI_USER_ACCESS_TOKEN", "")
|
||||
t.Setenv(EnvBinaryPath, fake.BinaryPath)
|
||||
t.Setenv("FAKE_AUTH_STATUS_STDOUT", `{"identity":"bot","verified":false}`)
|
||||
t.Setenv("FAKE_AUTH_STATUS_EXIT_CODE", "0")
|
||||
|
||||
ran := false
|
||||
ok := t.Run("inner", func(t *testing.T) {
|
||||
SkipWithoutUserToken(t)
|
||||
ran = true
|
||||
})
|
||||
require.True(t, ok)
|
||||
assert.False(t, ran)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunCmd(t *testing.T) {
|
||||
t.Run("returns stdout json on success", func(t *testing.T) {
|
||||
resetDefaultAsInitForTest()
|
||||
fake := newFakeCLI(t, "auto")
|
||||
fake := newFakeCLI(t)
|
||||
result, err := RunCmd(context.Background(), Request{
|
||||
BinaryPath: fake.BinaryPath,
|
||||
Args: []string{"--stdout-json", `{"ok":true}`},
|
||||
@@ -119,8 +161,7 @@ func TestRunCmd(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("captures stderr and exit code on failure", func(t *testing.T) {
|
||||
resetDefaultAsInitForTest()
|
||||
fake := newFakeCLI(t, "auto")
|
||||
fake := newFakeCLI(t)
|
||||
result, err := RunCmd(context.Background(), Request{
|
||||
BinaryPath: fake.BinaryPath,
|
||||
Args: []string{"--stderr-json", `{"ok":false}`, "--exit", "3"},
|
||||
@@ -134,45 +175,8 @@ func TestRunCmd(t *testing.T) {
|
||||
assert.Equal(t, false, errMap["ok"])
|
||||
})
|
||||
|
||||
t.Run("defaults default-as to bot", func(t *testing.T) {
|
||||
resetDefaultAsInitForTest()
|
||||
fake := newFakeCLI(t, "auto")
|
||||
result, err := RunCmd(context.Background(), Request{
|
||||
BinaryPath: fake.BinaryPath,
|
||||
Args: []string{"emit-default-as"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
assert.Equal(t, "bot", strings.TrimSpace(result.Stdout))
|
||||
assert.Equal(t, "bot\n", fake.ReadState(t))
|
||||
assert.Equal(t, 1, fake.ReadSetCount(t))
|
||||
})
|
||||
|
||||
t.Run("initializes default-as only once per binary", func(t *testing.T) {
|
||||
resetDefaultAsInitForTest()
|
||||
fake := newFakeCLI(t, "auto")
|
||||
first, err := RunCmd(context.Background(), Request{
|
||||
BinaryPath: fake.BinaryPath,
|
||||
Args: []string{"emit-default-as"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
first.AssertExitCode(t, 0)
|
||||
assert.Equal(t, "bot", strings.TrimSpace(first.Stdout))
|
||||
|
||||
second, err := RunCmd(context.Background(), Request{
|
||||
BinaryPath: fake.BinaryPath,
|
||||
Args: []string{"emit-default-as"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
second.AssertExitCode(t, 0)
|
||||
assert.Equal(t, "bot", strings.TrimSpace(second.Stdout))
|
||||
assert.Equal(t, "bot\n", fake.ReadState(t))
|
||||
assert.Equal(t, 1, fake.ReadSetCount(t))
|
||||
})
|
||||
|
||||
t.Run("passes explicit default-as as flag and command-line value wins", func(t *testing.T) {
|
||||
resetDefaultAsInitForTest()
|
||||
fake := newFakeCLI(t, "auto")
|
||||
t.Run("passes explicit default-as as flag", func(t *testing.T) {
|
||||
fake := newFakeCLI(t)
|
||||
result, err := RunCmd(context.Background(), Request{
|
||||
BinaryPath: fake.BinaryPath,
|
||||
Args: []string{"emit-arg", "--as"},
|
||||
@@ -181,13 +185,10 @@ func TestRunCmd(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
assert.Equal(t, "user", strings.TrimSpace(result.Stdout))
|
||||
assert.Equal(t, "bot\n", fake.ReadState(t))
|
||||
assert.Equal(t, 1, fake.ReadSetCount(t))
|
||||
})
|
||||
|
||||
t.Run("asserts stdout code payloads", func(t *testing.T) {
|
||||
resetDefaultAsInitForTest()
|
||||
fake := newFakeCLI(t, "auto")
|
||||
fake := newFakeCLI(t)
|
||||
result, err := RunCmd(context.Background(), Request{
|
||||
BinaryPath: fake.BinaryPath,
|
||||
Args: []string{"--stdout-json", `{"code":0,"data":{"id":"x"}}`},
|
||||
@@ -198,27 +199,8 @@ func TestRunCmd(t *testing.T) {
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
})
|
||||
|
||||
t.Run("default-as init respects context cancellation", func(t *testing.T) {
|
||||
resetDefaultAsInitForTest()
|
||||
fake := newFakeCLI(t, "auto")
|
||||
t.Setenv("FAKE_DEFAULT_AS_SLEEP", "1")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
result, err := RunCmd(ctx, Request{
|
||||
BinaryPath: fake.BinaryPath,
|
||||
Args: []string{"emit-default-as"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Error(t, result.RunErr)
|
||||
assert.ErrorIs(t, result.RunErr, context.DeadlineExceeded)
|
||||
assert.Equal(t, 0, fake.ReadSetCount(t))
|
||||
})
|
||||
|
||||
t.Run("passes stdin to process", func(t *testing.T) {
|
||||
resetDefaultAsInitForTest()
|
||||
fake := newFakeCLI(t, "auto")
|
||||
fake := newFakeCLI(t)
|
||||
result, err := RunCmd(context.Background(), Request{
|
||||
BinaryPath: fake.BinaryPath,
|
||||
Args: []string{"emit-stdin"},
|
||||
@@ -232,49 +214,19 @@ func TestRunCmd(t *testing.T) {
|
||||
|
||||
type fakeCLI struct {
|
||||
BinaryPath string
|
||||
statePath string
|
||||
countPath string
|
||||
}
|
||||
|
||||
func newFakeCLI(t *testing.T, initialDefaultAs string) fakeCLI {
|
||||
func newFakeCLI(t *testing.T) fakeCLI {
|
||||
t.Helper()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
statePath := filepath.Join(tmpDir, "default-as.txt")
|
||||
countPath := filepath.Join(tmpDir, "set-count.txt")
|
||||
require.NoError(t, os.WriteFile(statePath, []byte(initialDefaultAs+"\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(countPath, []byte("0\n"), 0o644))
|
||||
|
||||
script := `#!/bin/sh
|
||||
state_file="__STATE_FILE__"
|
||||
count_file="__COUNT_FILE__"
|
||||
|
||||
if [ ! -f "$state_file" ]; then
|
||||
echo "auto" > "$state_file"
|
||||
fi
|
||||
|
||||
if [ "$1" = "config" ] && [ "$2" = "default-as" ]; then
|
||||
if [ "$#" -eq 2 ]; then
|
||||
value=$(tr -d '\r\n' < "$state_file")
|
||||
echo "default-as: $value"
|
||||
exit 0
|
||||
if [ "$1" = "auth" ] && [ "$2" = "status" ] && [ "$3" = "--verify" ]; then
|
||||
if [ -n "$FAKE_AUTH_STATUS_STDOUT" ]; then
|
||||
echo "$FAKE_AUTH_STATUS_STDOUT"
|
||||
fi
|
||||
if [ "$#" -eq 3 ]; then
|
||||
if [ -n "$FAKE_DEFAULT_AS_SLEEP" ]; then
|
||||
sleep "$FAKE_DEFAULT_AS_SLEEP"
|
||||
fi
|
||||
count=$(tr -d '\r\n' < "$count_file")
|
||||
count=$((count + 1))
|
||||
echo "$count" > "$count_file"
|
||||
echo "$3" > "$state_file"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$1" = "emit-default-as" ]; then
|
||||
tr -d '\r\n' < "$state_file"
|
||||
echo
|
||||
exit 0
|
||||
exit "${FAKE_AUTH_STATUS_EXIT_CODE:-0}"
|
||||
fi
|
||||
|
||||
if [ "$1" = "emit-arg" ]; then
|
||||
@@ -318,34 +270,14 @@ done
|
||||
exit "$exit_code"
|
||||
`
|
||||
|
||||
script = strings.ReplaceAll(script, "__STATE_FILE__", statePath)
|
||||
script = strings.ReplaceAll(script, "__COUNT_FILE__", countPath)
|
||||
binaryPath := filepath.Join(tmpDir, "fake-"+cliBinaryName)
|
||||
require.NoError(t, os.WriteFile(binaryPath, []byte(script), 0o755))
|
||||
|
||||
return fakeCLI{
|
||||
BinaryPath: binaryPath,
|
||||
statePath: statePath,
|
||||
countPath: countPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (f fakeCLI) ReadState(t *testing.T) string {
|
||||
t.Helper()
|
||||
stateBytes, err := os.ReadFile(f.statePath)
|
||||
require.NoError(t, err)
|
||||
return string(stateBytes)
|
||||
}
|
||||
|
||||
func (f fakeCLI) ReadSetCount(t *testing.T) int {
|
||||
t.Helper()
|
||||
countBytes, err := os.ReadFile(f.countPath)
|
||||
require.NoError(t, err)
|
||||
count, err := strconv.Atoi(strings.TrimSpace(string(countBytes)))
|
||||
require.NoError(t, err)
|
||||
return count
|
||||
}
|
||||
|
||||
func assertSamePath(t *testing.T, want string, got string) {
|
||||
t.Helper()
|
||||
gotReal, err := filepath.EvalSymlinks(got)
|
||||
@@ -362,7 +294,3 @@ func mustWriteExecutable(t *testing.T, path string) string {
|
||||
require.NoError(t, err)
|
||||
return absPath
|
||||
}
|
||||
|
||||
func resetDefaultAsInitForTest() {
|
||||
defaultAsInitOnce = sync.Once{}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,10 @@ func TestDemo_TaskLifecycle(t *testing.T) {
|
||||
|
||||
var taskGUID string
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
t.Run("create as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+create"},
|
||||
Args: []string{"task", "+create"},
|
||||
DefaultAs: "bot",
|
||||
Data: map[string]any{
|
||||
"summary": createdSummary,
|
||||
"description": createdDescription,
|
||||
@@ -42,25 +43,24 @@ func TestDemo_TaskLifecycle(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "stdout:\n%s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{
|
||||
Args: []string{"task", "tasks", "delete"},
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "delete"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
if deleteErr != nil {
|
||||
parentT.Errorf("delete task %s: %v", taskGUID, deleteErr)
|
||||
return
|
||||
}
|
||||
if deleteResult.ExitCode != 0 {
|
||||
parentT.Errorf("delete task %s failed: exit=%d stderr=%s", taskGUID, deleteResult.ExitCode, deleteResult.Stderr)
|
||||
}
|
||||
clie2e.ReportCleanupFailure(parentT, "delete task "+taskGUID, deleteResult, deleteErr)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
t.Run("update as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be created before update")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+update", "--task-id", taskGUID},
|
||||
Args: []string{"task", "+update", "--task-id", taskGUID},
|
||||
DefaultAs: "bot",
|
||||
Data: map[string]any{
|
||||
"summary": updatedSummary,
|
||||
"description": updatedDescription,
|
||||
@@ -71,12 +71,13 @@ func TestDemo_TaskLifecycle(t *testing.T) {
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("get", func(t *testing.T) {
|
||||
t.Run("get as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be created before get")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
26
tests/cli_e2e/docs/coverage.md
Normal file
26
tests/cli_e2e/docs/coverage.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Docs CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 8 leaf commands
|
||||
- Covered: 3
|
||||
- Coverage: 37.5%
|
||||
|
||||
## Summary
|
||||
- TestDocs_CreateAndFetchWorkflow: proves `docs +create` and `docs +fetch`; key `t.Run(...)` proof points are `create as bot` and `fetch as bot`.
|
||||
- TestDocs_CreateAndFetchWorkflowAsUser: proves the same shortcut pair with UAT injection via `create as user` and `fetch as user`; creates its own Drive folder fixture first, then reads back the created doc by token.
|
||||
- TestDocs_UpdateWorkflow: proves `docs +update` via `update-title-and-content as bot`, then re-fetches the same doc in `verify as bot` to assert persisted title/content changes.
|
||||
- Setup note: docs workflows create a Drive folder through `drive files create_folder` in `helpers_test.go`; that helper is external to the docs domain and is not counted here.
|
||||
- Blocked area: media and search shortcuts still need deterministic fixtures and local file orchestration.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✓ | docs +create | shortcut | docs/helpers_test.go::createDocWithRetry; docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflowAsUser/create as user | `--folder-token`; `--title`; `--markdown` | helper asserts returned doc id |
|
||||
| ✓ | docs +fetch | shortcut | docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflow/fetch as bot; docs_update_test.go::TestDocs_UpdateWorkflow/verify as bot; docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflowAsUser/fetch as user | `--doc <docToken>` | |
|
||||
| ✕ | docs +media-download | shortcut | | none | no media fixture workflow yet |
|
||||
| ✕ | docs +media-insert | shortcut | | none | requires deterministic upload fixture and rollback assertions |
|
||||
| ✕ | docs +media-preview | shortcut | | none | requires deterministic media fixture |
|
||||
| ✕ | docs +search | shortcut | | none | search results are ambient and not yet stabilized for E2E |
|
||||
| ✓ | docs +update | shortcut | docs_update_test.go::TestDocs_UpdateWorkflow/update-title-and-content as bot | `--doc`; `--mode overwrite`; `--markdown`; `--new-title` | |
|
||||
| ✕ | docs +whiteboard-update | shortcut | | none | requires whiteboard fixture and DSL-specific assertions |
|
||||
@@ -9,26 +9,28 @@ import (
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
drivee2e "github.com/larksuite/cli/tests/cli_e2e/drive"
|
||||
"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) {
|
||||
func TestDocs_CreateAndFetchWorkflowAsBot(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
parentT := t
|
||||
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)
|
||||
folderToken := drivee2e.CreateDriveFolder(t, parentT, ctx, folderName, "bot", "")
|
||||
var docToken string
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
docToken = createDocWithRetry(t, ctx, folderToken, docTitle, docContent)
|
||||
docToken = createDocWithRetry(t, parentT, ctx, folderToken, docTitle, docContent, "bot")
|
||||
})
|
||||
|
||||
t.Run("fetch", func(t *testing.T) {
|
||||
@@ -46,3 +48,35 @@ func TestDocs_CreateAndFetchWorkflow(t *testing.T) {
|
||||
assert.Equal(t, docTitle, gjson.Get(result.Stdout, "data.title").String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDocs_CreateAndFetchWorkflowAsUser(t *testing.T) {
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
folderName := "lark-cli-e2e-user-docs-folder-" + suffix
|
||||
docTitle := "lark-cli-e2e-user-docs-" + suffix
|
||||
docContent := "# User Test Document\n\nCreated with user access token."
|
||||
var docToken string
|
||||
folderToken := drivee2e.CreateDriveFolder(t, parentT, ctx, folderName, "user", "")
|
||||
|
||||
t.Run("create as user", func(t *testing.T) {
|
||||
docToken = createDocWithRetry(t, parentT, ctx, folderToken, docTitle, docContent, "user")
|
||||
})
|
||||
|
||||
t.Run("fetch as user", 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},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
assert.Equal(t, docTitle, gjson.Get(result.Stdout, "data.title").String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
drivee2e "github.com/larksuite/cli/tests/cli_e2e/drive"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
|
||||
// TestDocs_UpdateWorkflow tests the create, update, and verify lifecycle.
|
||||
func TestDocs_UpdateWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
@@ -26,14 +28,14 @@ func TestDocs_UpdateWorkflow(t *testing.T) {
|
||||
originalContent := "# Original\n\nThis is the original content."
|
||||
updatedContent := "# Updated\n\nThis is the updated content."
|
||||
|
||||
folderToken := createDocsFolderWithRetry(t, ctx, folderName)
|
||||
folderToken := drivee2e.CreateDriveFolder(t, parentT, ctx, folderName, "bot", "")
|
||||
var docToken string
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
docToken = createDocWithRetry(t, ctx, folderToken, originalTitle, originalContent)
|
||||
t.Run("create as bot", func(t *testing.T) {
|
||||
docToken = createDocWithRetry(t, parentT, ctx, folderToken, originalTitle, originalContent, "bot")
|
||||
})
|
||||
|
||||
t.Run("update-title-and-content", func(t *testing.T) {
|
||||
t.Run("update-title-and-content as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, docToken, "document token should be created before update")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
@@ -44,13 +46,14 @@ func TestDocs_UpdateWorkflow(t *testing.T) {
|
||||
"--markdown", updatedContent,
|
||||
"--new-title", updatedTitle,
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("verify", func(t *testing.T) {
|
||||
t.Run("verify as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, docToken, "document token should be created before verify")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
@@ -58,6 +61,7 @@ func TestDocs_UpdateWorkflow(t *testing.T) {
|
||||
"docs", "+fetch",
|
||||
"--doc", docToken,
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
@@ -12,43 +12,42 @@ import (
|
||||
"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 {
|
||||
func createDocWithRetry(t *testing.T, parentT *testing.T, ctx context.Context, folderToken string, title string, markdown string, defaultAs string) string {
|
||||
t.Helper()
|
||||
|
||||
require.NotEmpty(t, folderToken, "folder token is required")
|
||||
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"docs", "+create",
|
||||
"--folder-token", folderToken,
|
||||
"--title", title,
|
||||
"--markdown", markdown,
|
||||
},
|
||||
}, clie2e.RetryOptions{})
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
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)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{
|
||||
"drive", "+delete",
|
||||
"--file-token", docToken,
|
||||
"--type", "docx",
|
||||
"--yes",
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete doc "+docToken, deleteResult, deleteErr)
|
||||
})
|
||||
|
||||
return docToken
|
||||
}
|
||||
|
||||
44
tests/cli_e2e/drive/coverage.md
Normal file
44
tests/cli_e2e/drive/coverage.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Drive CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 28 leaf commands
|
||||
- Covered: 1
|
||||
- Coverage: 3.6%
|
||||
|
||||
## Summary
|
||||
- TestDrive_FilesCreateFolderWorkflow: proves `drive files create_folder` in `create_folder as bot`; helper asserts the returned folder token and registers best-effort cleanup via `drive files delete`.
|
||||
- Cleanup note: `drive files delete` is only exercised in cleanup and is intentionally left uncovered.
|
||||
- Blocked area: upload, export, comment, permission, subscription, and reply flows still need deterministic remote fixtures and filesystem setup.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✕ | drive +add-comment | shortcut | | none | no comment workflow yet |
|
||||
| ✕ | drive +delete | shortcut | | none | no primary delete workflow yet |
|
||||
| ✕ | drive +download | shortcut | | none | no file fixture workflow yet |
|
||||
| ✕ | drive +export | shortcut | | none | no export workflow yet |
|
||||
| ✕ | drive +export-download | shortcut | | none | no export-download workflow yet |
|
||||
| ✕ | drive +import | shortcut | | none | no import workflow yet |
|
||||
| ✕ | drive +move | shortcut | | none | no move workflow yet |
|
||||
| ✕ | drive +task_result | shortcut | | none | no async task-result workflow yet |
|
||||
| ✕ | drive +upload | shortcut | | none | no upload workflow yet |
|
||||
| ✕ | drive file.comment.replys create | api | | none | no reply workflow yet |
|
||||
| ✕ | drive file.comment.replys delete | api | | none | no reply workflow yet |
|
||||
| ✕ | drive file.comment.replys list | api | | none | no reply workflow yet |
|
||||
| ✕ | drive file.comment.replys update | api | | none | no reply workflow yet |
|
||||
| ✕ | drive file.comments create_v2 | api | | none | no file comment workflow yet |
|
||||
| ✕ | drive file.comments list | api | | none | no file comment workflow yet |
|
||||
| ✕ | drive file.comments patch | api | | none | no file comment workflow yet |
|
||||
| ✕ | drive file.statistics get | api | | none | no statistics workflow yet |
|
||||
| ✕ | drive file.view_records list | api | | none | no view-record workflow yet |
|
||||
| ✕ | drive files copy | api | | none | no file copy workflow yet |
|
||||
| ✓ | drive files create_folder | api | drive_files_workflow_test.go::TestDrive_FilesCreateFolderWorkflow/create_folder as bot | `name`; empty `folder_token` in `--data` | |
|
||||
| ✕ | drive files list | api | | none | no list workflow yet |
|
||||
| ✕ | drive metas batch_query | api | | none | no metadata workflow yet |
|
||||
| ✕ | drive permission.members auth | api | | none | permission workflows not covered |
|
||||
| ✕ | drive permission.members create | api | | none | permission workflows not covered |
|
||||
| ✕ | drive permission.members transfer_owner | api | | none | permission workflows not covered |
|
||||
| ✕ | drive user remove_subscription | api | | none | subscription workflows not covered |
|
||||
| ✕ | drive user subscription | api | | none | subscription workflows not covered |
|
||||
| ✕ | drive user subscription_status | api | | none | subscription workflows not covered |
|
||||
@@ -18,10 +18,12 @@ func TestDrive_FilesCreateFolderWorkflow(t *testing.T) {
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
parentFolderName := "lark-cli-e2e-drive-parent-" + suffix
|
||||
folderName := "lark-cli-e2e-drive-folder-" + suffix
|
||||
parentFolderToken := createDriveFolder(t, parentT, ctx, parentFolderName, "")
|
||||
|
||||
t.Run("create_folder", func(t *testing.T) {
|
||||
folderToken := createDriveFolder(t, parentT, ctx, folderName)
|
||||
t.Run("create_folder as bot", func(t *testing.T) {
|
||||
folderToken := createDriveFolder(t, parentT, ctx, folderName, parentFolderToken)
|
||||
if folderToken == "" {
|
||||
t.Fatalf("folder token should be available")
|
||||
}
|
||||
|
||||
55
tests/cli_e2e/drive/helpers.go
Normal file
55
tests/cli_e2e/drive/helpers.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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/tidwall/gjson"
|
||||
)
|
||||
|
||||
// CreateDriveFolder creates a Drive folder, optionally under a parent folder, and
|
||||
// deletes it during parent cleanup.
|
||||
func CreateDriveFolder(t *testing.T, parentT *testing.T, ctx context.Context, name string, defaultAs string, parentFolderToken string) string {
|
||||
t.Helper()
|
||||
|
||||
if defaultAs == "" {
|
||||
defaultAs = "bot"
|
||||
}
|
||||
|
||||
args := []string{"drive", "+create-folder", "--name", name}
|
||||
if parentFolderToken != "" {
|
||||
args = append(args, "--folder-token", parentFolderToken)
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: args,
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create drive folder %q: %v", name, err)
|
||||
}
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
folderToken := gjson.Get(result.Stdout, "data.folder_token").String()
|
||||
if folderToken == "" {
|
||||
t.Fatalf("drive folder token should not be empty, stdout:\n%s", result.Stdout)
|
||||
}
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmdWithRetry(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"drive", "+delete", "--file-token", folderToken, "--type", "folder", "--yes"},
|
||||
DefaultAs: defaultAs,
|
||||
}, clie2e.RetryOptions{})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete drive folder "+folderToken, deleteResult, deleteErr)
|
||||
})
|
||||
|
||||
return folderToken
|
||||
}
|
||||
@@ -7,38 +7,12 @@ 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 {
|
||||
func createDriveFolder(t *testing.T, parentT *testing.T, ctx context.Context, name string, parentFolderToken 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"},
|
||||
})
|
||||
})
|
||||
|
||||
folderToken := CreateDriveFolder(t, parentT, ctx, name, "bot", parentFolderToken)
|
||||
require.NotEmpty(t, folderToken)
|
||||
return folderToken
|
||||
}
|
||||
|
||||
78
tests/cli_e2e/im/chat_message_workflow_test.go
Normal file
78
tests/cli_e2e/im/chat_message_workflow_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestIM_ChatMessageWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "im-chat-" + suffix
|
||||
messageText := "im-chat-msg-" + suffix
|
||||
var chatID string
|
||||
var messageID string
|
||||
|
||||
t.Run("create chat as user", func(t *testing.T) {
|
||||
chatID = createChatAs(t, parentT, ctx, chatName, "user")
|
||||
})
|
||||
|
||||
t.Run("send message as user", func(t *testing.T) {
|
||||
messageID = sendMessageAs(t, ctx, chatID, messageText, "user")
|
||||
})
|
||||
|
||||
t.Run("list chat messages as user", func(t *testing.T) {
|
||||
startTime := time.Now().UTC().Add(-10 * time.Minute).Format(time.RFC3339)
|
||||
endTime := time.Now().UTC().Add(10 * time.Minute).Format(time.RFC3339)
|
||||
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+chat-messages-list",
|
||||
"--chat-id", chatID,
|
||||
"--start", startTime,
|
||||
"--end", endTime,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Get(result.Stdout, "data.messages").Array() {
|
||||
if item.Get("message_id").String() == messageID && strings.Contains(item.Get("content").String(), messageText) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
var found bool
|
||||
for _, item := range gjson.Get(result.Stdout, "data.messages").Array() {
|
||||
if item.Get("message_id").String() != messageID {
|
||||
continue
|
||||
}
|
||||
require.True(t, strings.Contains(item.Get("content").String(), messageText), "stdout:\n%s", result.Stdout)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
require.True(t, found, "expected message %s in stdout:\n%s", messageID, result.Stdout)
|
||||
})
|
||||
}
|
||||
@@ -27,34 +27,37 @@ func TestIM_ChatUpdateWorkflow(t *testing.T) {
|
||||
|
||||
chatID := createChat(t, parentT, ctx, originalName)
|
||||
|
||||
t.Run("update chat name", func(t *testing.T) {
|
||||
t.Run("update chat name as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+chat-update",
|
||||
"--chat-id", chatID,
|
||||
"--name", updatedName,
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("update chat description", func(t *testing.T) {
|
||||
t.Run("update chat description as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+chat-update",
|
||||
"--chat-id", chatID,
|
||||
"--description", updatedDescription,
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("get updated chat", func(t *testing.T) {
|
||||
t.Run("get updated chat as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "chats", "get"},
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
Args: []string{"im", "chats", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -76,10 +79,11 @@ func TestIM_ChatsGetWorkflow(t *testing.T) {
|
||||
|
||||
chatID := createChat(t, parentT, ctx, chatName)
|
||||
|
||||
t.Run("get chat info", func(t *testing.T) {
|
||||
t.Run("get chat info as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "chats", "get"},
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
Args: []string{"im", "chats", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Logf("chats get result: %s", result.Stdout)
|
||||
@@ -105,10 +109,11 @@ func TestIM_ChatsLinkWorkflow(t *testing.T) {
|
||||
|
||||
chatID := createChat(t, parentT, ctx, chatName)
|
||||
|
||||
t.Run("get chat share link", func(t *testing.T) {
|
||||
t.Run("get chat share link as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "chats", "link"},
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
Args: []string{"im", "chats", "link"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
Data: map[string]any{
|
||||
"validity_period": "week",
|
||||
},
|
||||
|
||||
49
tests/cli_e2e/im/coverage.md
Normal file
49
tests/cli_e2e/im/coverage.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# IM CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 29 leaf commands
|
||||
- Covered: 9
|
||||
- Coverage: 31.0%
|
||||
|
||||
## Summary
|
||||
- TestIM_ChatUpdateWorkflow: proves `im +chat-create`, `im +chat-update`, and `im chats get`; key `t.Run(...)` proof points are `update chat name as bot`, `update chat description as bot`, and `get updated chat as bot`.
|
||||
- TestIM_ChatsGetWorkflow: proves `im chats get` on a fresh chat fixture via `get chat info as bot`.
|
||||
- TestIM_ChatsLinkWorkflow: proves `im chats link` via `get chat share link as bot`.
|
||||
- TestIM_ChatMessageWorkflowAsUser: proves the user chat message flow through `create chat as user`, `send message as user`, and `list chat messages as user` with the created message ID and content asserted from read-after-write output.
|
||||
- TestIM_MessageGetWorkflowAsUser: proves user message readback through `batch get message as user` after creating a fresh chat and sending a unique message.
|
||||
- TestIM_MessageReplyWorkflowAsBot: proves threaded reply flow through `reply to message in thread as bot` and `list thread replies as bot`, reading back the reply from `im +threads-messages-list`.
|
||||
- Blocked area: `im +chat-search` did not reliably return freshly created private chats in UAT, and `im +messages-search` did not reliably index freshly sent messages in time for a deterministic read-after-write assertion, so both remain uncovered.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✓ | im +chat-create | shortcut | im/chat_message_workflow_test.go::TestIM_ChatMessageWorkflowAsUser/create chat as user; im/chat_workflow_test.go::TestIM_ChatUpdateWorkflow; im/chat_workflow_test.go::TestIM_ChatsGetWorkflow; im/chat_workflow_test.go::TestIM_ChatsLinkWorkflow; im/message_get_workflow_test.go::TestIM_MessageGetWorkflowAsUser; im/message_reply_workflow_test.go::TestIM_MessageReplyWorkflowAsBot | `--name`; `--type private` | covered via workflow setup with created chat IDs asserted |
|
||||
| ✓ | im +chat-messages-list | shortcut | im/chat_message_workflow_test.go::TestIM_ChatMessageWorkflowAsUser/list chat messages as user; im/message_reply_workflow_test.go::TestIM_MessageReplyWorkflowAsBot/list thread replies as bot | `--chat-id`; `--start`; `--end` | reads back created message and discovers thread ID |
|
||||
| ✕ | im +chat-search | shortcut | | none | UAT did not reliably return freshly created private chats, so it is left uncovered |
|
||||
| ✓ | im +chat-update | shortcut | im/chat_workflow_test.go::TestIM_ChatUpdateWorkflow/update chat name as bot; im/chat_workflow_test.go::TestIM_ChatUpdateWorkflow/update chat description as bot | `--chat-id`; `--name`; `--description` | |
|
||||
| ✓ | im +messages-mget | shortcut | im/message_get_workflow_test.go::TestIM_MessageGetWorkflowAsUser/batch get message as user | `--message-ids` | verifies sent message content by ID |
|
||||
| ✓ | im +messages-reply | shortcut | im/message_reply_workflow_test.go::TestIM_MessageReplyWorkflowAsBot/reply to message in thread as bot | `--message-id`; `--text`; `--reply-in-thread` | reply is read back via thread list |
|
||||
| ✕ | im +messages-resources-download | shortcut | | none | needs a stable image/file message fixture plus file_key proof; left uncovered |
|
||||
| ✕ | im +messages-search | shortcut | | none | freshly sent messages were not indexed deterministically in UAT time for a stable read-after-write proof |
|
||||
| ✓ | im +messages-send | shortcut | im/chat_message_workflow_test.go::TestIM_ChatMessageWorkflowAsUser/send message as user; im/message_get_workflow_test.go::TestIM_MessageGetWorkflowAsUser; im/message_reply_workflow_test.go::TestIM_MessageReplyWorkflowAsBot | `--chat-id`; `--text` | covered where returned message IDs feed follow-up reads |
|
||||
| ✓ | im +threads-messages-list | shortcut | im/message_reply_workflow_test.go::TestIM_MessageReplyWorkflowAsBot/list thread replies as bot | `--thread` | proves threaded reply is persisted |
|
||||
| ✕ | im chat.members create | api | | none | no member mutation workflow yet |
|
||||
| ✕ | im chat.members get | api | | none | no member get workflow yet |
|
||||
| ✕ | im chats create | api | | none | only covered indirectly through `+chat-create` |
|
||||
| ✓ | im chats get | api | im/chat_workflow_test.go::TestIM_ChatUpdateWorkflow/get updated chat as bot; im/chat_workflow_test.go::TestIM_ChatsGetWorkflow/get chat info as bot | `chat_id` in `--params` | |
|
||||
| ✓ | im chats link | api | im/chat_workflow_test.go::TestIM_ChatsLinkWorkflow/get chat share link as bot | `chat_id` in `--params`; `validity_period` in `--data` | |
|
||||
| ✕ | im chats list | api | | none | no chats list workflow yet |
|
||||
| ✕ | im chats update | api | | none | only covered indirectly through `+chat-update` |
|
||||
| ✕ | im images create | api | | none | no image upload workflow yet |
|
||||
| ✕ | im messages delete | api | | none | no recall workflow yet |
|
||||
| ✕ | im messages forward | api | | none | no forward workflow yet |
|
||||
| ✕ | im messages merge_forward | api | | none | no merge-forward workflow yet |
|
||||
| ✕ | im messages read_users | api | | none | no read-user workflow yet |
|
||||
| ✕ | im pins create | api | | none | pin workflows not covered |
|
||||
| ✕ | im pins delete | api | | none | pin workflows not covered |
|
||||
| ✕ | im pins list | api | | none | pin workflows not covered |
|
||||
| ✕ | im reactions batch_query | api | | none | reaction workflows not covered |
|
||||
| ✕ | im reactions create | api | | none | reaction workflows not covered |
|
||||
| ✕ | im reactions delete | api | | none | reaction workflows not covered |
|
||||
| ✕ | im reactions list | api | | none | reaction workflows not covered |
|
||||
@@ -17,12 +17,18 @@ import (
|
||||
// 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()
|
||||
return createChatAs(t, parentT, ctx, name, "bot")
|
||||
}
|
||||
|
||||
func createChatAs(t *testing.T, parentT *testing.T, ctx context.Context, name string, defaultAs string) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+chat-create",
|
||||
"--name", name,
|
||||
"--type", "private",
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -40,7 +46,12 @@ func createChat(t *testing.T, parentT *testing.T, ctx context.Context, name stri
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func sendMessage(t *testing.T, ctx context.Context, chatID string, text string) string {
|
||||
t.Helper()
|
||||
return sendMessageAs(t, ctx, chatID, text, "bot")
|
||||
}
|
||||
|
||||
func sendMessageAs(t *testing.T, ctx context.Context, chatID string, text string, defaultAs string) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
@@ -48,6 +59,7 @@ func sendMessage(t *testing.T, parentT *testing.T, ctx context.Context, chatID s
|
||||
"--chat-id", chatID,
|
||||
"--text", text,
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
45
tests/cli_e2e/im/message_get_workflow_test.go
Normal file
45
tests/cli_e2e/im/message_get_workflow_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestIM_MessageGetWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "im-lookup-" + suffix
|
||||
messageText := "im-msg-" + suffix
|
||||
|
||||
chatID := createChatAs(t, parentT, ctx, chatName, "user")
|
||||
messageID := sendMessageAs(t, ctx, chatID, messageText, "user")
|
||||
|
||||
t.Run("batch get message as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+messages-mget", "--message-ids", messageID},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
messages := gjson.Get(result.Stdout, "data.messages").Array()
|
||||
require.Len(t, messages, 1, "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, messageID, messages[0].Get("message_id").String(), "stdout:\n%s", result.Stdout)
|
||||
require.True(t, strings.Contains(messages[0].Get("content").String(), messageText), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
111
tests/cli_e2e/im/message_reply_workflow_test.go
Normal file
111
tests/cli_e2e/im/message_reply_workflow_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"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 TestIM_MessageReplyWorkflowAsBot(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 := "lark-cli-e2e-original-message-" + suffix
|
||||
replyText := "lark-cli-e2e-reply-text-" + suffix
|
||||
|
||||
chatID := createChat(t, parentT, ctx, chatName)
|
||||
messageID := sendMessage(t, ctx, chatID, originalMessage)
|
||||
|
||||
t.Run("reply to message in thread as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+messages-reply",
|
||||
"--message-id", messageID,
|
||||
"--text", replyText,
|
||||
"--reply-in-thread",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.message_id").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, chatID, gjson.Get(result.Stdout, "data.chat_id").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("list thread replies as bot", func(t *testing.T) {
|
||||
listResult, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+chat-messages-list",
|
||||
"--chat-id", chatID,
|
||||
"--start", time.Now().UTC().Add(-10 * time.Minute).Format(time.RFC3339),
|
||||
"--end", time.Now().UTC().Add(10 * time.Minute).Format(time.RFC3339),
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Get(result.Stdout, "data.messages").Array() {
|
||||
if item.Get("message_id").String() == messageID && item.Get("thread_id").String() != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
listResult.AssertExitCode(t, 0)
|
||||
listResult.AssertStdoutStatus(t, true)
|
||||
|
||||
var threadID string
|
||||
for _, item := range gjson.Get(listResult.Stdout, "data.messages").Array() {
|
||||
if item.Get("message_id").String() == messageID {
|
||||
threadID = item.Get("thread_id").String()
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, threadID, "expected thread_id for message %s in stdout:\n%s", messageID, listResult.Stdout)
|
||||
|
||||
threadResult, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+threads-messages-list", "--thread", threadID},
|
||||
DefaultAs: "bot",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Get(result.Stdout, "data.messages").Array() {
|
||||
if strings.Contains(item.Get("content").String(), replyText) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
threadResult.AssertExitCode(t, 0)
|
||||
threadResult.AssertStdoutStatus(t, true)
|
||||
|
||||
var found bool
|
||||
for _, item := range gjson.Get(threadResult.Stdout, "data.messages").Array() {
|
||||
if strings.Contains(item.Get("content").String(), replyText) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "expected reply content in stdout:\n%s", threadResult.Stdout)
|
||||
})
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
79
tests/cli_e2e/mail/coverage.md
Normal file
79
tests/cli_e2e/mail/coverage.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Mail CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 62 leaf commands
|
||||
- Covered: 13
|
||||
- Coverage: 21.0%
|
||||
|
||||
## Summary
|
||||
- TestMail_DraftLifecycleWorkflowAsUser: proves a self-contained user draft workflow across `mail user_mailboxes profile`, `mail +draft-create`, `mail user_mailbox.drafts list`, `mail user_mailbox.drafts get`, `mail +draft-edit`, and `mail user_mailbox.drafts delete`; key `t.Run(...)` proof points are `get mailbox profile as user`, `create draft with shortcut as user`, `list draft as user`, `get created draft as user`, `inspect created draft as user`, `update draft subject with shortcut as user`, `inspect updated draft as user`, `delete draft as user`, and `verify draft removed from list as user`.
|
||||
- TestMail_SendWorkflowAsUser: proves a self-contained self-mail workflow across `mail +send`, `mail +triage`, `mail +message`, `mail +messages`, `mail +thread`, `mail +reply`, and `mail +forward`; key `t.Run(...)` proof points are `send mail to self with shortcut as user`, `find self sent mail in triage as user`, `get sent message as user`, `get received message as user`, `get both self sent messages as user`, `get self send thread as user`, `reply to received message with shortcut as user`, `inspect reply draft as user`, `forward received message with shortcut as user`, and `inspect forward draft as user`.
|
||||
- Blocked area: `mail +reply-all` is still uncovered because the self-send workflow produces only self-recipient traffic and reply-all’s recipient expansion becomes degenerate after self-address exclusion; `+signature`, `+watch`, event commands, and many raw message/thread mutation APIs still need dedicated tenant-aware workflows.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✓ | mail +draft-create | shortcut | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/create draft with shortcut as user | `--subject`; `--body`; `--plain-text` | creates a new self-owned draft without relying on external recipients |
|
||||
| ✓ | mail +draft-edit | shortcut | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/inspect created draft as user; mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/update draft subject with shortcut as user; mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/inspect updated draft as user | `--draft-id`; `--mailbox me`; `--inspect`; `--set-subject` | shortcut proves readback projection and subject update |
|
||||
| ✓ | mail +forward | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/forward received message with shortcut as user; mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/inspect forward draft as user | `--message-id`; `--to`; `--body`; `--plain-text` | uses self-generated inbox message as source and inspects forwarded draft projection |
|
||||
| ✓ | mail +message | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/get sent message as user; mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/get received message as user | `--mailbox me`; `--message-id` | verifies both SENT and INBOX copies after self-send |
|
||||
| ✓ | mail +messages | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/get both self sent messages as user | `--mailbox me`; `--message-ids` | batch reads both sent and received message copies |
|
||||
| ✓ | mail +reply | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/reply to received message with shortcut as user; mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/inspect reply draft as user | `--message-id`; `--body`; `--plain-text` | creates reply draft from self-generated inbox message and inspects quoted content |
|
||||
| ✕ | mail +reply-all | shortcut | | none | self-send traffic leaves no stable non-self recipient set for deterministic reply-all assertions |
|
||||
| ✓ | mail +send | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/send mail to self with shortcut as user | `--to`; `--subject`; `--body`; `--plain-text`; `--confirm-send` | self-send creates both sent and inbox copies for follow-up assertions |
|
||||
| ✕ | mail +signature | shortcut | | none | signature availability is mailbox-configuration dependent |
|
||||
| ✓ | mail +thread | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/get self send thread as user | `--mailbox me`; `--thread-id` | verifies readback of the sent-message thread created by self-send |
|
||||
| ✓ | mail +triage | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/find self sent mail in triage as user | `--mailbox me`; `--query`; `--max`; `--format data` | polls until self-sent subject becomes searchable and captures sent/inbox message ids |
|
||||
| ✕ | mail +watch | shortcut | | none | requires websocket event subscription setup and external mail delivery |
|
||||
| ✕ | mail multi_entity search | api | | none | requires deterministic searchable contact entities |
|
||||
| ✕ | mail user_mailbox.drafts create | api | | none | only covered indirectly through `mail +draft-create` |
|
||||
| ✓ | mail user_mailbox.drafts delete | api | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/delete draft as user | `user_mailbox_id`; `draft_id` in `--params` | explicit lifecycle delete plus read-after-delete list check |
|
||||
| ✓ | mail user_mailbox.drafts get | api | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/get created draft as user | `user_mailbox_id`; `draft_id` in `--params` | asserts persisted draft id, subject, and draft state |
|
||||
| ✓ | mail user_mailbox.drafts list | api | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/list draft as user; mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/verify draft removed from list as user | `user_mailbox_id`; `page_size` in `--params` | proves create visibility and delete removal |
|
||||
| ✕ | mail user_mailbox.drafts send | api | | none | draft send needs recipient-side or send-status assertions to be deterministic |
|
||||
| ✕ | mail user_mailbox.drafts update | api | | none | only covered indirectly through `mail +draft-edit` |
|
||||
| ✕ | mail user_mailbox.drafts cancel_scheduled_send | api | | none | requires a scheduled-send draft lifecycle |
|
||||
| ✕ | mail user_mailbox.event subscribe | api | | none | requires event subscription setup |
|
||||
| ✕ | mail user_mailbox.event subscription | api | | none | requires event subscription setup |
|
||||
| ✕ | mail user_mailbox.event unsubscribe | api | | none | requires event subscription setup |
|
||||
| ✕ | mail user_mailbox.folders create | api | | none | folder lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.folders delete | api | | none | folder lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.folders get | api | | none | folder lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.folders list | api | | none | folder lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.folders patch | api | | none | folder lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.labels create | api | | none | label lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.labels delete | api | | none | label lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.labels get | api | | none | label lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.labels list | api | | none | label lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.labels patch | api | | none | label lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.mail_contacts create | api | | none | contact lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.mail_contacts delete | api | | none | contact lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.mail_contacts list | api | | none | contact lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.mail_contacts patch | api | | none | contact lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.message.attachments download_url | api | | none | requires an existing message attachment |
|
||||
| ✕ | mail user_mailbox.messages batch_get | api | | none | requires existing message ids |
|
||||
| ✕ | mail user_mailbox.messages batch_modify | api | | none | requires existing messages and mailbox folders/labels |
|
||||
| ✕ | mail user_mailbox.messages batch_trash | api | | none | requires existing messages |
|
||||
| ✕ | mail user_mailbox.messages get | api | | none | requires an existing message id |
|
||||
| ✕ | mail user_mailbox.messages list | api | | none | requires deterministic existing folder or label message inventory |
|
||||
| ✕ | mail user_mailbox.messages modify | api | | none | requires existing messages and mailbox folders/labels |
|
||||
| ✕ | mail user_mailbox.messages send_status | api | | none | requires a sent message id |
|
||||
| ✕ | mail user_mailbox.messages trash | api | | none | requires an existing message id |
|
||||
| ✕ | mail user_mailbox.rules create | api | | none | rule lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.rules delete | api | | none | rule lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.rules list | api | | none | rule lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.rules reorder | api | | none | rule lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.rules update | api | | none | rule lifecycle left for a dedicated workflow |
|
||||
| ✕ | mail user_mailbox.sent_messages get_recall_detail | api | | none | requires a recallable sent message |
|
||||
| ✕ | mail user_mailbox.sent_messages recall | api | | none | requires a delivered sent message within recall window |
|
||||
| ✕ | mail user_mailbox.settings send_as | api | | none | mailbox alias availability is tenant-configuration dependent |
|
||||
| ✕ | mail user_mailbox.threads batch_modify | api | | none | requires existing threads and mailbox folders/labels |
|
||||
| ✕ | mail user_mailbox.threads batch_trash | api | | none | requires existing thread ids |
|
||||
| ✕ | mail user_mailbox.threads get | api | | none | requires an existing thread id |
|
||||
| ✕ | mail user_mailbox.threads list | api | | none | requires deterministic existing folder or label thread inventory |
|
||||
| ✕ | mail user_mailbox.threads modify | api | | none | requires existing threads and mailbox folders/labels |
|
||||
| ✕ | mail user_mailbox.threads trash | api | | none | requires an existing thread id |
|
||||
| ✕ | mail user_mailboxes accessible_mailboxes | api | | none | mailbox visibility differs by tenant and shared-mailbox configuration |
|
||||
| ✓ | mail user_mailboxes profile | api | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/get mailbox profile as user | `user_mailbox_id=me` in `--params` | proves current mailbox identity before draft lifecycle |
|
||||
| ✕ | mail user_mailboxes search | api | | none | requires deterministic searchable mailbox content |
|
||||
211
tests/cli_e2e/mail/mail_draft_lifecycle_workflow_test.go
Normal file
211
tests/cli_e2e/mail/mail_draft_lifecycle_workflow_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestMail_DraftLifecycleWorkflowAsUser(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
originalSubject := "lark-cli-e2e-mail-draft-" + suffix
|
||||
updatedSubject := originalSubject + "-updated"
|
||||
originalBody := "draft lifecycle body " + suffix
|
||||
|
||||
const mailboxID = "me"
|
||||
|
||||
var draftID string
|
||||
var draftDeleted bool
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
if draftID == "" || draftDeleted {
|
||||
return
|
||||
}
|
||||
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"mail", "user_mailbox.drafts", "delete"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{
|
||||
"user_mailbox_id": mailboxID,
|
||||
"draft_id": draftID,
|
||||
},
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete draft "+draftID, result, err)
|
||||
})
|
||||
|
||||
t.Run("get mailbox profile as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"mail", "user_mailboxes", "profile"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"user_mailbox_id": mailboxID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
require.NotEmpty(t, gjson.Get(result.Stdout, "data.primary_email_address").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("create draft with shortcut as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+draft-create",
|
||||
"--subject", originalSubject,
|
||||
"--body", originalBody,
|
||||
"--plain-text",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
draftID = gjson.Get(result.Stdout, "data.draft_id").String()
|
||||
require.NotEmpty(t, draftID, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("list draft as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, draftID, "draft should be created before listing drafts")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"mail", "user_mailbox.drafts", "list"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{
|
||||
"user_mailbox_id": mailboxID,
|
||||
"page_size": 100,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
require.True(t, gjson.Get(result.Stdout, `data.items.#(id=="`+draftID+`")`).Exists(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get created draft as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, draftID, "draft should be created before get")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"mail", "user_mailbox.drafts", "get"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{
|
||||
"user_mailbox_id": mailboxID,
|
||||
"draft_id": draftID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
assert.Equal(t, draftID, gjson.Get(result.Stdout, "data.draft.id").String())
|
||||
assert.Equal(t, originalSubject, gjson.Get(result.Stdout, "data.draft.message.subject").String())
|
||||
assert.Equal(t, int64(3), gjson.Get(result.Stdout, "data.draft.message.message_state").Int())
|
||||
})
|
||||
|
||||
t.Run("inspect created draft as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, draftID, "draft should be created before inspect")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+draft-edit",
|
||||
"--draft-id", draftID,
|
||||
"--mailbox", mailboxID,
|
||||
"--inspect",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, draftID, gjson.Get(result.Stdout, "data.draft_id").String())
|
||||
assert.Equal(t, originalSubject, gjson.Get(result.Stdout, "data.projection.subject").String())
|
||||
assert.Equal(t, originalBody, gjson.Get(result.Stdout, "data.projection.body_text").String())
|
||||
})
|
||||
|
||||
t.Run("update draft subject with shortcut as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, draftID, "draft should be created before update")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+draft-edit",
|
||||
"--draft-id", draftID,
|
||||
"--mailbox", mailboxID,
|
||||
"--set-subject", updatedSubject,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, draftID, gjson.Get(result.Stdout, "data.draft_id").String())
|
||||
assert.Equal(t, updatedSubject, gjson.Get(result.Stdout, "data.projection.subject").String())
|
||||
assert.Equal(t, originalBody, gjson.Get(result.Stdout, "data.projection.body_text").String())
|
||||
})
|
||||
|
||||
t.Run("inspect updated draft as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+draft-edit",
|
||||
"--draft-id", draftID,
|
||||
"--mailbox", mailboxID,
|
||||
"--inspect",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, updatedSubject, gjson.Get(result.Stdout, "data.projection.subject").String())
|
||||
assert.Equal(t, originalBody, gjson.Get(result.Stdout, "data.projection.body_text").String())
|
||||
})
|
||||
|
||||
t.Run("delete draft as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"mail", "user_mailbox.drafts", "delete"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{
|
||||
"user_mailbox_id": mailboxID,
|
||||
"draft_id": draftID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
draftDeleted = true
|
||||
})
|
||||
|
||||
t.Run("verify draft removed from list as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"mail", "user_mailbox.drafts", "get"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{
|
||||
"user_mailbox_id": mailboxID,
|
||||
"draft_id": draftID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.ExitCode, "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr)
|
||||
assert.Equal(t, "not_found", gjson.Get(result.Stderr, "error.detail.type").String(), "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr)
|
||||
})
|
||||
}
|
||||
340
tests/cli_e2e/mail/mail_send_workflow_test.go
Normal file
340
tests/cli_e2e/mail/mail_send_workflow_test.go
Normal file
@@ -0,0 +1,340 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestMail_SendWorkflowAsUser(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
const mailboxID = "me"
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
subject := "mail-self-" + suffix
|
||||
body := "self send body " + suffix
|
||||
replyBody := "self send reply body " + suffix
|
||||
forwardBody := "self send forward body " + suffix
|
||||
|
||||
var primaryEmail string
|
||||
var sentMessageID string
|
||||
var threadID string
|
||||
var inboxMessageID string
|
||||
var replyDraftID string
|
||||
var forwardDraftID string
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
if replyDraftID != "" {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"mail", "user_mailbox.drafts", "delete"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{
|
||||
"user_mailbox_id": mailboxID,
|
||||
"draft_id": replyDraftID,
|
||||
},
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete reply draft "+replyDraftID, result, err)
|
||||
}
|
||||
|
||||
if forwardDraftID != "" {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"mail", "user_mailbox.drafts", "delete"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{
|
||||
"user_mailbox_id": mailboxID,
|
||||
"draft_id": forwardDraftID,
|
||||
},
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete forward draft "+forwardDraftID, result, err)
|
||||
}
|
||||
|
||||
var messageIDs []string
|
||||
if sentMessageID != "" {
|
||||
messageIDs = append(messageIDs, sentMessageID)
|
||||
}
|
||||
if inboxMessageID != "" && inboxMessageID != sentMessageID {
|
||||
messageIDs = append(messageIDs, inboxMessageID)
|
||||
}
|
||||
if len(messageIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"mail", "user_mailbox.messages", "batch_trash"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"user_mailbox_id": mailboxID},
|
||||
Data: map[string]any{"message_ids": messageIDs},
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "trash self-send messages", result, err)
|
||||
})
|
||||
|
||||
t.Run("get mailbox profile as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"mail", "user_mailboxes", "profile"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"user_mailbox_id": mailboxID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
primaryEmail = gjson.Get(result.Stdout, "data.primary_email_address").String()
|
||||
require.NotEmpty(t, primaryEmail, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("send mail to self with shortcut as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, primaryEmail, "mailbox profile should be loaded before self-send")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+send",
|
||||
"--to", primaryEmail,
|
||||
"--subject", subject,
|
||||
"--body", body,
|
||||
"--plain-text",
|
||||
"--confirm-send",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
sentMessageID = gjson.Get(result.Stdout, "data.message_id").String()
|
||||
threadID = gjson.Get(result.Stdout, "data.thread_id").String()
|
||||
require.NotEmpty(t, sentMessageID, "stdout:\n%s", result.Stdout)
|
||||
require.NotEmpty(t, threadID, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("find self sent mail in triage as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, subject, "subject should be set before triage")
|
||||
|
||||
var stdout string
|
||||
for attempt := 0; attempt < 12; attempt++ {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+triage",
|
||||
"--mailbox", mailboxID,
|
||||
"--query", subject,
|
||||
"--max", "10",
|
||||
"--format", "data",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
stdout = result.Stdout
|
||||
|
||||
messages := gjson.Get(stdout, "messages").Array()
|
||||
for _, item := range messages {
|
||||
if item.Get("subject").String() != subject {
|
||||
continue
|
||||
}
|
||||
messageID := item.Get("message_id").String()
|
||||
if messageID == "" {
|
||||
continue
|
||||
}
|
||||
if messageID != sentMessageID {
|
||||
inboxMessageID = messageID
|
||||
}
|
||||
}
|
||||
|
||||
if inboxMessageID != "" {
|
||||
require.GreaterOrEqual(t, int(gjson.Get(stdout, "count").Int()), 2, "stdout:\n%s", stdout)
|
||||
require.True(t, gjson.Get(stdout, `messages.#(message_id=="`+sentMessageID+`")`).Exists(), "stdout:\n%s", stdout)
|
||||
require.True(t, gjson.Get(stdout, `messages.#(message_id=="`+inboxMessageID+`")`).Exists(), "stdout:\n%s", stdout)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
t.Fatalf("failed to observe inbox copy for self-sent message in triage:\n%s", stdout)
|
||||
})
|
||||
|
||||
t.Run("get sent message as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, sentMessageID, "sent message id should be available before message read")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+message",
|
||||
"--mailbox", mailboxID,
|
||||
"--message-id", sentMessageID,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, sentMessageID, gjson.Get(result.Stdout, "data.message_id").String())
|
||||
assert.Equal(t, threadID, gjson.Get(result.Stdout, "data.thread_id").String())
|
||||
assert.Equal(t, subject, gjson.Get(result.Stdout, "data.subject").String())
|
||||
assert.Equal(t, body, gjson.Get(result.Stdout, "data.body_plain_text").String())
|
||||
assert.Equal(t, "SENT", gjson.Get(result.Stdout, "data.folder_id").String())
|
||||
assert.Equal(t, "sent", gjson.Get(result.Stdout, "data.message_state_text").String())
|
||||
assert.Equal(t, primaryEmail, gjson.Get(result.Stdout, "data.to.0.mail_address").String())
|
||||
})
|
||||
|
||||
t.Run("get received message as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, inboxMessageID, "inbox message id should be available before message read")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+message",
|
||||
"--mailbox", mailboxID,
|
||||
"--message-id", inboxMessageID,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, inboxMessageID, gjson.Get(result.Stdout, "data.message_id").String())
|
||||
assert.Equal(t, threadID, gjson.Get(result.Stdout, "data.thread_id").String())
|
||||
assert.Equal(t, subject, gjson.Get(result.Stdout, "data.subject").String())
|
||||
assert.Equal(t, body, gjson.Get(result.Stdout, "data.body_plain_text").String())
|
||||
assert.Equal(t, "INBOX", gjson.Get(result.Stdout, "data.folder_id").String())
|
||||
assert.Equal(t, "received", gjson.Get(result.Stdout, "data.message_state_text").String())
|
||||
assert.Equal(t, primaryEmail, gjson.Get(result.Stdout, "data.to.0.mail_address").String())
|
||||
})
|
||||
|
||||
t.Run("get both self sent messages as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+messages",
|
||||
"--mailbox", mailboxID,
|
||||
"--message-ids", sentMessageID + "," + inboxMessageID,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, int64(2), gjson.Get(result.Stdout, "data.total").Int())
|
||||
assert.Len(t, gjson.Get(result.Stdout, "data.unavailable_message_ids").Array(), 0, "stdout:\n%s", result.Stdout)
|
||||
assert.True(t, gjson.Get(result.Stdout, `data.messages.#(message_id=="`+sentMessageID+`")`).Exists(), "stdout:\n%s", result.Stdout)
|
||||
assert.True(t, gjson.Get(result.Stdout, `data.messages.#(message_id=="`+inboxMessageID+`")`).Exists(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get self send thread as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+thread",
|
||||
"--mailbox", mailboxID,
|
||||
"--thread-id", threadID,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, threadID, gjson.Get(result.Stdout, "data.thread_id").String())
|
||||
assert.Equal(t, int64(1), gjson.Get(result.Stdout, "data.message_count").Int(), "stdout:\n%s", result.Stdout)
|
||||
assert.True(t, gjson.Get(result.Stdout, `data.messages.#(message_id=="`+sentMessageID+`")`).Exists(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("reply to received message with shortcut as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+reply",
|
||||
"--message-id", inboxMessageID,
|
||||
"--body", replyBody,
|
||||
"--plain-text",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
replyDraftID = gjson.Get(result.Stdout, "data.draft_id").String()
|
||||
require.NotEmpty(t, replyDraftID, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("inspect reply draft as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+draft-edit",
|
||||
"--draft-id", replyDraftID,
|
||||
"--mailbox", mailboxID,
|
||||
"--inspect",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, "Re: "+subject, gjson.Get(result.Stdout, "data.projection.subject").String())
|
||||
assert.Equal(t, primaryEmail, gjson.Get(result.Stdout, "data.projection.to.0.address").String())
|
||||
assert.Contains(t, gjson.Get(result.Stdout, "data.projection.body_text").String(), replyBody)
|
||||
assert.Contains(t, gjson.Get(result.Stdout, "data.projection.body_text").String(), body)
|
||||
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.projection.in_reply_to").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("forward received message with shortcut as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+forward",
|
||||
"--message-id", inboxMessageID,
|
||||
"--to", primaryEmail,
|
||||
"--body", forwardBody,
|
||||
"--plain-text",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
forwardDraftID = gjson.Get(result.Stdout, "data.draft_id").String()
|
||||
require.NotEmpty(t, forwardDraftID, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("inspect forward draft as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"mail", "+draft-edit",
|
||||
"--draft-id", forwardDraftID,
|
||||
"--mailbox", mailboxID,
|
||||
"--inspect",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, "Fwd: "+subject, gjson.Get(result.Stdout, "data.projection.subject").String())
|
||||
assert.Equal(t, primaryEmail, gjson.Get(result.Stdout, "data.projection.to.0.address").String())
|
||||
assert.Contains(t, gjson.Get(result.Stdout, "data.projection.body_text").String(), forwardBody)
|
||||
assert.Contains(t, gjson.Get(result.Stdout, "data.projection.body_text").String(), body)
|
||||
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.projection.in_reply_to").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
44
tests/cli_e2e/sheets/coverage.md
Normal file
44
tests/cli_e2e/sheets/coverage.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Sheets CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 26 leaf commands
|
||||
- Covered: 14
|
||||
- Coverage: 53.8%
|
||||
|
||||
## Summary
|
||||
- TestSheets_CRUDE2EWorkflow: proves `+create`, `+info`, `+write`, `+read`, `+append`, `+find`, and `+export`; key `t.Run(...)` proof points are `create spreadsheet with +create as bot`, `read data with +read as bot`, `find cells with +find as bot`, and `export spreadsheet with +export as bot`.
|
||||
- TestSheets_CreateWorkflowAsUser: proves the UAT path for `sheets +create` and `sheets +info` through `create spreadsheet with +create as user` and `get spreadsheet info with +info as user`.
|
||||
- TestSheets_SpreadsheetsResource: proves direct `spreadsheets create`, `spreadsheets get`, and `spreadsheets patch`.
|
||||
- TestSheets_FilterWorkflow: proves `spreadsheet.sheet.filters create`, `get`, `update`, and `delete`, with supporting sheet setup through `+create`, `+info`, and `+write`.
|
||||
- Cleanup note: workflow-created spreadsheets are cleaned up via `drive +delete --type sheet`; those cleanup-only executions are not counted as command coverage because no testcase asserts delete behavior as the primary proof surface.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✕ | sheets +add-dimension | shortcut | | none | no dimension workflow yet |
|
||||
| ✓ | sheets +append | shortcut | sheets_crud_workflow_test.go::TestSheets_CRUDE2EWorkflow/append rows with +append as bot | `--spreadsheet-token`; `--sheet-id`; `--range`; `--values` | |
|
||||
| ✕ | sheets +batch-set-style | shortcut | | none | no style workflow yet |
|
||||
| ✓ | sheets +create | shortcut | sheets_crud_workflow_test.go::TestSheets_CRUDE2EWorkflow/create spreadsheet with +create as bot; sheets_filter_workflow_test.go::TestSheets_FilterWorkflow/create spreadsheet with initial data as bot; sheets_create_workflow_test.go::TestSheets_CreateWorkflowAsUser/create spreadsheet with +create as user | `--title` | |
|
||||
| ✕ | sheets +delete-dimension | shortcut | | none | no dimension workflow yet |
|
||||
| ✓ | sheets +export | shortcut | sheets_crud_workflow_test.go::TestSheets_CRUDE2EWorkflow/export spreadsheet with +export as bot | `--spreadsheet-token`; `--file-extension` | |
|
||||
| ✓ | sheets +find | shortcut | sheets_crud_workflow_test.go::TestSheets_CRUDE2EWorkflow/find cells with +find as bot | `--spreadsheet-token`; `--sheet-id`; `--find`; `--range` | |
|
||||
| ✓ | sheets +info | shortcut | sheets_crud_workflow_test.go::TestSheets_CRUDE2EWorkflow/get spreadsheet info with +info as bot; sheets_filter_workflow_test.go::TestSheets_FilterWorkflow/get sheet info as bot; sheets_create_workflow_test.go::TestSheets_CreateWorkflowAsUser/get spreadsheet info with +info as user | `--spreadsheet-token` | |
|
||||
| ✕ | sheets +insert-dimension | shortcut | | none | no dimension workflow yet |
|
||||
| ✕ | sheets +merge-cells | shortcut | | none | no merge workflow yet |
|
||||
| ✕ | sheets +move-dimension | shortcut | | none | no dimension workflow yet |
|
||||
| ✓ | sheets +read | shortcut | sheets_crud_workflow_test.go::TestSheets_CRUDE2EWorkflow/read data with +read as bot | `--spreadsheet-token`; `--sheet-id`; `--range` | |
|
||||
| ✕ | sheets +replace | shortcut | | none | no replace workflow yet |
|
||||
| ✕ | sheets +set-style | shortcut | | none | no style workflow yet |
|
||||
| ✕ | sheets +unmerge-cells | shortcut | | none | no merge workflow yet |
|
||||
| ✕ | sheets +update-dimension | shortcut | | none | no dimension workflow yet |
|
||||
| ✓ | sheets +write | shortcut | sheets_crud_workflow_test.go::TestSheets_CRUDE2EWorkflow/write data with +write as bot; sheets_filter_workflow_test.go::TestSheets_FilterWorkflow/write test data for filtering as bot | `--spreadsheet-token`; `--sheet-id`; `--range`; `--values` | |
|
||||
| ✕ | sheets +write-image | shortcut | | none | no image workflow yet |
|
||||
| ✓ | sheets spreadsheet.sheet.filters create | api | sheets_filter_workflow_test.go::TestSheets_FilterWorkflow/create filter with spreadsheet.sheet.filters create as bot | `spreadsheet_token`; `sheet_id` in `--params`; filter JSON in `--data` | |
|
||||
| ✓ | sheets spreadsheet.sheet.filters delete | api | sheets_filter_workflow_test.go::TestSheets_FilterWorkflow/delete filter with spreadsheet.sheet.filters delete as bot | `spreadsheet_token`; `sheet_id` in `--params` | |
|
||||
| ✓ | sheets spreadsheet.sheet.filters get | api | sheets_filter_workflow_test.go::TestSheets_FilterWorkflow/get filter with spreadsheet.sheet.filters get as bot | `spreadsheet_token`; `sheet_id` in `--params` | |
|
||||
| ✓ | sheets spreadsheet.sheet.filters update | api | sheets_filter_workflow_test.go::TestSheets_FilterWorkflow/update filter with spreadsheet.sheet.filters update as bot | `spreadsheet_token`; `sheet_id` in `--params`; filter JSON in `--data` | |
|
||||
| ✕ | sheets spreadsheet.sheets find | api | | none | no direct API workflow yet |
|
||||
| ✓ | sheets spreadsheets create | api | sheets_crud_workflow_test.go::TestSheets_SpreadsheetsResource/create spreadsheet with spreadsheets create as bot | `title` in `--data` | |
|
||||
| ✓ | sheets spreadsheets get | api | sheets_crud_workflow_test.go::TestSheets_SpreadsheetsResource/get spreadsheet with spreadsheets get as bot | `spreadsheet_token` in `--params` | |
|
||||
| ✓ | sheets spreadsheets patch | api | sheets_crud_workflow_test.go::TestSheets_SpreadsheetsResource/patch spreadsheet with spreadsheets patch as bot | `spreadsheet_token` in `--params`; title patch in `--data` | |
|
||||
53
tests/cli_e2e/sheets/helpers_test.go
Normal file
53
tests/cli_e2e/sheets/helpers_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
drivee2e "github.com/larksuite/cli/tests/cli_e2e/drive"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func createSpreadsheet(t *testing.T, parentT *testing.T, ctx context.Context, title string, defaultAs string) string {
|
||||
t.Helper()
|
||||
|
||||
folderToken := drivee2e.CreateDriveFolder(t, parentT, ctx, title+"-folder", defaultAs, "")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"sheets", "+create",
|
||||
"--title", title,
|
||||
"--folder-token", folderToken,
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
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, "stdout:\n%s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{
|
||||
"drive", "+delete",
|
||||
"--file-token", spreadsheetToken,
|
||||
"--type", "sheet",
|
||||
"--yes",
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete spreadsheet "+spreadsheetToken, deleteResult, deleteErr)
|
||||
})
|
||||
|
||||
return spreadsheetToken
|
||||
}
|
||||
46
tests/cli_e2e/sheets/sheets_create_workflow_test.go
Normal file
46
tests/cli_e2e/sheets/sheets_create_workflow_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
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 TestSheets_CreateWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
title := "lark-cli-e2e-user-sheets-" + suffix
|
||||
var spreadsheetToken string
|
||||
|
||||
t.Run("create spreadsheet with +create as user", func(t *testing.T) {
|
||||
spreadsheetToken = createSpreadsheet(t, parentT, ctx, title, "user")
|
||||
})
|
||||
|
||||
t.Run("get spreadsheet info with +info as user", 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},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
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())
|
||||
require.NotEmpty(t, gjson.Get(result.Stdout, "data.sheets.sheets.0.sheet_id").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
drivee2e "github.com/larksuite/cli/tests/cli_e2e/drive"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -27,27 +29,15 @@ func TestSheets_CRUDE2EWorkflow(t *testing.T) {
|
||||
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("create spreadsheet with +create as bot", func(t *testing.T) {
|
||||
spreadsheetToken = createSpreadsheet(t, parentT, ctx, "lark-cli-e2e-sheets-"+suffix, "bot")
|
||||
})
|
||||
|
||||
t.Run("get spreadsheet info with +info", func(t *testing.T) {
|
||||
t.Run("get spreadsheet info with +info as bot", 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},
|
||||
Args: []string{"sheets", "+info", "--spreadsheet-token", spreadsheetToken},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -58,7 +48,7 @@ func TestSheets_CRUDE2EWorkflow(t *testing.T) {
|
||||
require.NotEmpty(t, sheetID, "sheet_id should not be empty, stdout: %s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("write data with +write", func(t *testing.T) {
|
||||
t.Run("write data with +write as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
@@ -77,13 +67,14 @@ func TestSheets_CRUDE2EWorkflow(t *testing.T) {
|
||||
"--range", "A1:C3",
|
||||
"--values", string(valuesJSON),
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("read data with +read", func(t *testing.T) {
|
||||
t.Run("read data with +read as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
@@ -94,6 +85,7 @@ func TestSheets_CRUDE2EWorkflow(t *testing.T) {
|
||||
"--sheet-id", sheetID,
|
||||
"--range", "A1:C3",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -106,7 +98,7 @@ func TestSheets_CRUDE2EWorkflow(t *testing.T) {
|
||||
assert.Equal(t, "Alice", values.Array()[1].Array()[0].String())
|
||||
})
|
||||
|
||||
t.Run("append rows with +append", func(t *testing.T) {
|
||||
t.Run("append rows with +append as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
@@ -121,13 +113,14 @@ func TestSheets_CRUDE2EWorkflow(t *testing.T) {
|
||||
"--range", "A4:C4",
|
||||
"--values", string(valuesJSON),
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("find cells with +find", func(t *testing.T) {
|
||||
t.Run("find cells with +find as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
@@ -139,6 +132,7 @@ func TestSheets_CRUDE2EWorkflow(t *testing.T) {
|
||||
"--find", "Alice",
|
||||
"--range", fmt.Sprintf("%s!A1:C10", sheetID),
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -149,22 +143,32 @@ func TestSheets_CRUDE2EWorkflow(t *testing.T) {
|
||||
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) {
|
||||
t.Run("export spreadsheet with +export as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
outputDir := t.TempDir()
|
||||
outputPath := filepath.Join(outputDir, "export.xlsx")
|
||||
|
||||
// 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",
|
||||
"--output-path", "./export.xlsx",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
WorkDir: outputDir,
|
||||
})
|
||||
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)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
savedPath := gjson.Get(result.Stdout, "data.saved_path").String()
|
||||
require.NotEmpty(t, savedPath, "stdout:\n%s", result.Stdout)
|
||||
savedPathReal, err := filepath.EvalSymlinks(savedPath)
|
||||
require.NoError(t, err, "stdout:\n%s", result.Stdout)
|
||||
outputPathReal, err := filepath.EvalSymlinks(outputPath)
|
||||
require.NoError(t, err, "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, outputPathReal, savedPathReal, "stdout:\n%s", result.Stdout)
|
||||
assert.FileExists(t, outputPath, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -177,13 +181,16 @@ func TestSheets_SpreadsheetsResource(t *testing.T) {
|
||||
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"},
|
||||
t.Run("create spreadsheet with spreadsheets create as bot", func(t *testing.T) {
|
||||
folderToken := drivee2e.CreateDriveFolder(t, parentT, ctx, "lark-cli-e2e-sheets-resource-folder-"+suffix, "bot", "")
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheets", "create"},
|
||||
DefaultAs: "bot",
|
||||
Data: map[string]any{
|
||||
"title": "lark-cli-e2e-sheets-resource-" + suffix,
|
||||
"title": "lark-cli-e2e-sheets-resource-" + suffix,
|
||||
"folder_token": folderToken,
|
||||
},
|
||||
}, clie2e.RetryOptions{})
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
@@ -192,16 +199,29 @@ func TestSheets_SpreadsheetsResource(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token should not be empty, stdout: %s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
// Best-effort cleanup
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{
|
||||
"drive", "+delete",
|
||||
"--file-token", spreadsheetToken,
|
||||
"--type", "sheet",
|
||||
"--yes",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete spreadsheet "+spreadsheetToken, deleteResult, deleteErr)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("get spreadsheet with spreadsheets get", func(t *testing.T) {
|
||||
t.Run("get spreadsheet with spreadsheets get as bot", 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},
|
||||
Args: []string{"sheets", "spreadsheets", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"spreadsheet_token": spreadsheetToken},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -211,14 +231,15 @@ func TestSheets_SpreadsheetsResource(t *testing.T) {
|
||||
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.spreadsheet.url").String())
|
||||
})
|
||||
|
||||
t.Run("patch spreadsheet with spreadsheets patch", func(t *testing.T) {
|
||||
t.Run("patch spreadsheet with spreadsheets patch as bot", 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},
|
||||
Args: []string{"sheets", "spreadsheets", "patch"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"spreadsheet_token": spreadsheetToken},
|
||||
Data: map[string]any{"title": updatedTitle},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -226,8 +247,9 @@ func TestSheets_SpreadsheetsResource(t *testing.T) {
|
||||
|
||||
// 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},
|
||||
Args: []string{"sheets", "spreadsheets", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"spreadsheet_token": spreadsheetToken},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
getResult.AssertExitCode(t, 0)
|
||||
|
||||
@@ -25,28 +25,16 @@ func TestSheets_FilterWorkflow(t *testing.T) {
|
||||
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("create spreadsheet with initial data as bot", func(t *testing.T) {
|
||||
spreadsheetToken = createSpreadsheet(t, parentT, ctx, "lark-cli-e2e-sheets-filter-"+suffix, "bot")
|
||||
})
|
||||
|
||||
t.Run("get sheet info", func(t *testing.T) {
|
||||
t.Run("get sheet info as bot", 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},
|
||||
Args: []string{"sheets", "+info", "--spreadsheet-token", spreadsheetToken},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -56,7 +44,7 @@ func TestSheets_FilterWorkflow(t *testing.T) {
|
||||
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) {
|
||||
t.Run("write test data for filtering as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
@@ -77,13 +65,14 @@ func TestSheets_FilterWorkflow(t *testing.T) {
|
||||
"--range", "A1:C5",
|
||||
"--values", string(valuesJSON),
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
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) {
|
||||
t.Run("create filter with spreadsheet.sheet.filters create as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, spreadsheetToken, "spreadsheet token is required")
|
||||
require.NotEmpty(t, sheetID, "sheet_id is required")
|
||||
|
||||
@@ -98,7 +87,8 @@ func TestSheets_FilterWorkflow(t *testing.T) {
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheet.sheet.filters", "create"},
|
||||
Args: []string{"sheets", "spreadsheet.sheet.filters", "create"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"spreadsheet_token": spreadsheetToken,
|
||||
"sheet_id": sheetID,
|
||||
@@ -110,12 +100,13 @@ func TestSheets_FilterWorkflow(t *testing.T) {
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
})
|
||||
|
||||
t.Run("get filter with spreadsheet.sheet.filters get", func(t *testing.T) {
|
||||
t.Run("get filter with spreadsheet.sheet.filters get as bot", 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"},
|
||||
Args: []string{"sheets", "spreadsheet.sheet.filters", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"spreadsheet_token": spreadsheetToken,
|
||||
"sheet_id": sheetID,
|
||||
@@ -129,22 +120,22 @@ func TestSheets_FilterWorkflow(t *testing.T) {
|
||||
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) {
|
||||
t.Run("update filter with spreadsheet.sheet.filters update as bot", 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",
|
||||
"col": "C",
|
||||
"filter_type": "multiValue",
|
||||
"condition": map[string]any{
|
||||
"filter_type": "number",
|
||||
"compare_type": "greater",
|
||||
"expected": []any{80},
|
||||
"filter_type": "multiValue",
|
||||
"expected": []any{"A"},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"sheets", "spreadsheet.sheet.filters", "update"},
|
||||
Args: []string{"sheets", "spreadsheet.sheet.filters", "update"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"spreadsheet_token": spreadsheetToken,
|
||||
"sheet_id": sheetID,
|
||||
@@ -156,12 +147,13 @@ func TestSheets_FilterWorkflow(t *testing.T) {
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
})
|
||||
|
||||
t.Run("delete filter with spreadsheet.sheet.filters delete", func(t *testing.T) {
|
||||
t.Run("delete filter with spreadsheet.sheet.filters delete as bot", 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"},
|
||||
Args: []string{"sheets", "spreadsheet.sheet.filters", "delete"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"spreadsheet_token": spreadsheetToken,
|
||||
"sheet_id": sheetID,
|
||||
|
||||
17
tests/cli_e2e/slides/coverage.md
Normal file
17
tests/cli_e2e/slides/coverage.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Slides CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 2 leaf commands
|
||||
- Covered: 1
|
||||
- Coverage: 50.0%
|
||||
|
||||
## Summary
|
||||
- TestSlides_CreateWorkflowAsUser: proves the user slides workflow through `create presentation with slide as user` and `get created presentation xml as user`; creates a fresh presentation, asserts returned IDs, then reads back the XML content to prove the title and slide body persisted.
|
||||
- Blocked area: `slides +media-upload` is still uncovered because it needs a deterministic local image fixture plus XML follow-up proof that is separate from the base create/read workflow.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✓ | slides +create | shortcut | slides_create_workflow_test.go::TestSlides_CreateWorkflowAsUser/create presentation with slide as user | `--title`; `--slides ["<slide ...>"]` | read back through raw slides API to prove persisted XML |
|
||||
| ✕ | slides +media-upload | shortcut | | none | needs a stable local image fixture plus follow-up slide XML proof |
|
||||
86
tests/cli_e2e/slides/slides_create_workflow_test.go
Normal file
86
tests/cli_e2e/slides/slides_create_workflow_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package slides
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestSlides_CreateWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
title := "slides-e2e-" + suffix
|
||||
slideTitle := "Overview " + suffix
|
||||
slideBody := "Body " + suffix
|
||||
slideXML := `<slide xmlns="http://www.larkoffice.com/sml/2.0"><data><shape type="text" topLeftX="80" topLeftY="80" width="800" height="120"><content textType="title"><p>` + slideTitle + `</p></content></shape><shape type="text" topLeftX="80" topLeftY="200" width="800" height="180"><content textType="body"><p>` + slideBody + `</p></content></shape></data></slide>`
|
||||
|
||||
var presentationID string
|
||||
|
||||
t.Run("create presentation with slide as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"slides", "+create",
|
||||
"--title", title,
|
||||
"--slides", `["` + strings.ReplaceAll(slideXML, `"`, `\"`) + `"]`,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
presentationID = gjson.Get(result.Stdout, "data.xml_presentation_id").String()
|
||||
require.NotEmpty(t, presentationID, "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, title, gjson.Get(result.Stdout, "data.title").String(), "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, int64(1), gjson.Get(result.Stdout, "data.slides_added").Int(), "stdout:\n%s", result.Stdout)
|
||||
require.Len(t, gjson.Get(result.Stdout, "data.slide_ids").Array(), 1, "stdout:\n%s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{
|
||||
"drive", "+delete",
|
||||
"--file-token", presentationID,
|
||||
"--type", "slides",
|
||||
"--yes",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete presentation "+presentationID, deleteResult, deleteErr)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("get created presentation xml as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, presentationID, "presentation should be created before readback")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"api", "get", "/open-apis/slides_ai/v1/xml_presentations/" + presentationID},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"revision_id": -1},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
require.Equal(t, presentationID, gjson.Get(result.Stdout, "data.xml_presentation.presentation_id").String(), "stdout:\n%s", result.Stdout)
|
||||
content := gjson.Get(result.Stdout, "data.xml_presentation.content").String()
|
||||
require.Contains(t, content, "<title>"+title+"</title>", "stdout:\n%s", result.Stdout)
|
||||
require.Contains(t, content, slideTitle, "stdout:\n%s", result.Stdout)
|
||||
require.Contains(t, content, slideBody, "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
@@ -2,21 +2,25 @@
|
||||
|
||||
## Metrics
|
||||
- Denominator: 29 leaf commands
|
||||
- Covered: 10
|
||||
- Coverage: 34.5%
|
||||
- Covered: 14
|
||||
- Coverage: 48.3%
|
||||
|
||||
## Summary
|
||||
- TestTask_StatusWorkflow: creates a task via `task +create`, then proves `task +complete`, `task tasks get`, and `task +reopen` through `complete`, `get completed task`, `reopen`, and `get reopened task`; asserts `status` flips between `done` and `todo` and `completed_at` is set then cleared.
|
||||
- TestTask_ReminderWorkflow: creates a task with a due time via `task +create`, then proves `task +reminder` and `task tasks get` through `set reminder`, `get task with reminder`, `remove reminder`, and `get task without reminder`; asserts `relative_fire_minute=30`, reminder id presence, and reminder removal.
|
||||
- TestTask_CommentWorkflow: creates a task via `task +create`, runs `comment`, and asserts the returned comment id is non-empty; this is the direct proof for `task +comment`.
|
||||
- TestTask_TasklistWorkflow: runs `create tasklist with task`, then `get tasklist`, `list tasklist tasks`, and `get task`; proves `task +tasklist-create`, `task tasklists get`, `task tasklists tasks`, and `task tasks get` with seeded task payload and task-to-tasklist linkage.
|
||||
- TestTask_UpdateWorkflow: creates a task as `--as user`, proves `task +update` and `task tasks patch` through repeated read-after-write `task tasks get`, then completes it via `task +complete` and verifies the completed task state directly.
|
||||
- TestTask_TasklistWorkflowAsBot: runs `create tasklist with task`, then `get tasklist`, `list tasklist tasks`, and `get task`; proves `task +tasklist-create`, `task tasklists get`, `task tasklists tasks`, and `task tasks get` with seeded task payload and task-to-tasklist linkage.
|
||||
- TestTask_TasklistWorkflowAsUser: creates a tasklist as `--as user`, patches its name through `task tasklists patch`, then proves both `task tasklists get` and `task tasklists list` return the patched tasklist.
|
||||
- TestTask_TasklistAddTaskWorkflow: creates a standalone tasklist and task, runs `add task to tasklist`, then `list tasklist tasks` and `get task with tasklist link`; proves `task +tasklist-task-add`, `task tasklists tasks`, and `task tasks get`, including no failed tasks in the add response.
|
||||
- Cleanup path note: workflow-created tasks and tasklists are deleted through direct `task tasks delete` / `task tasklists delete` cleanup paths in `helpers_test.go::createTask`, `helpers_test.go::createTasklist`, and `tasklist_workflow_test.go::TestTask_TasklistWorkflow`, but those cleanup-only executions are not counted as command coverage because no testcase asserts delete behavior as the primary proof surface.
|
||||
- Cleanup path note: workflow-created tasks and tasklists are deleted through direct `task tasks delete` / `task tasklists delete` cleanup paths in `helpers_test.go::createTask`, `helpers_test.go::createTasklist`, `tasklist_workflow_test.go::TestTask_TasklistWorkflowAsBot`, and `tasklist_workflow_test.go::TestTask_TasklistWorkflowAsUser`, but those cleanup-only executions are not counted as command coverage because no testcase asserts delete behavior as the primary proof surface.
|
||||
- Blocked area: assignee, follower, and tasklist member mutations still require stable real-user `open_id` fixtures; the current suite is bot-safe only.
|
||||
- Blocked area: `task +get-my-tasks` still depends on `--as user` identity plus deterministic user-scoped data.
|
||||
- Blocked area: `task +get-my-tasks` and `task tasks list` did not return the workflow-created user task deterministically in UAT, so they are left uncovered instead of being counted from flaky list visibility.
|
||||
- Blocked area: the remaining user-oriented shortcuts still need deterministic user-owned fixtures or collaborator fixtures beyond the self-owned task created inside the testcase.
|
||||
- Gap pattern: direct `tasks create/delete/list/patch`, `tasklists create/delete/list/patch`, `members *`, and `subtasks *` APIs still lack deterministic direct-call workflows, so shortcut coverage does not count for those leaf commands.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✕ | task +assign | shortcut | | none | requires real assignee open_id fixtures; shortcut defaults to `--as user` |
|
||||
@@ -24,13 +28,13 @@
|
||||
| ✓ | task +complete | shortcut | task_status_workflow_test.go::TestTask_StatusWorkflow/complete | `--task-id` | |
|
||||
| ✓ | task +create | shortcut | task_status_workflow_test.go::TestTask_StatusWorkflow; task_comment_workflow_test.go::TestTask_CommentWorkflow; task_reminder_workflow_test.go::TestTask_ReminderWorkflow; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow | `summary` + `description`; `due.timestamp` + `due.is_all_day` | |
|
||||
| ✕ | task +followers | shortcut | | none | requires real follower open_id fixtures; shortcut defaults to `--as user` |
|
||||
| ✕ | task +get-my-tasks | shortcut | | none | depends on `--as user` identity and deterministic user-scoped task data |
|
||||
| ✕ | task +get-my-tasks | shortcut | | none | UAT did not return the workflow-created user task deterministically in my-tasks views |
|
||||
| ✓ | task +reminder | shortcut | task_reminder_workflow_test.go::TestTask_ReminderWorkflow/set reminder; task_reminder_workflow_test.go::TestTask_ReminderWorkflow/remove reminder | `--task-id --set 30m`; `--task-id --remove` | |
|
||||
| ✓ | task +reopen | shortcut | task_status_workflow_test.go::TestTask_StatusWorkflow/reopen | `--task-id` | |
|
||||
| ✓ | task +tasklist-create | shortcut | tasklist_workflow_test.go::TestTask_TasklistWorkflow/create tasklist with task; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow | `--name` only; `--name` plus task array in `--data` | |
|
||||
| ✓ | task +tasklist-create | shortcut | tasklist_workflow_test.go::TestTask_TasklistWorkflowAsBot/create tasklist with task as bot; tasklist_workflow_test.go::TestTask_TasklistWorkflowAsUser/create tasklist as user; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow | `--name` only; `--name` plus task array in `--data` | |
|
||||
| ✕ | task +tasklist-members | shortcut | | none | requires real member open_id fixtures to add, remove, or set tasklist members |
|
||||
| ✓ | task +tasklist-task-add | shortcut | tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow/add task to tasklist | `--tasklist-id`; `--task-id` | |
|
||||
| ✕ | task +update | shortcut | | none | no dedicated workflow yet for summary, description, or due-field mutation assertions |
|
||||
| ✓ | task +update | shortcut | task_update_workflow_test.go::TestTask_UpdateWorkflow/update task with shortcut as user | `--task-id`; `--summary`; `--description` | verified by follow-up `task tasks get` |
|
||||
| ✕ | task members add | api | | none | requires stable member fixtures and explicit direct API-body assertions |
|
||||
| ✕ | task members remove | api | | none | requires stable member fixtures and explicit direct API-body assertions |
|
||||
| ✕ | task subtasks create | api | | none | needs a parent-task workflow plus direct subtask payload assertions |
|
||||
@@ -38,13 +42,13 @@
|
||||
| ✕ | task tasklists add_members | api | | none | requires real member open_id fixtures and direct API coverage |
|
||||
| ✕ | task tasklists create | api | | none | only covered indirectly through `task +tasklist-create`; no direct API invocation yet |
|
||||
| ✕ | task tasklists delete | api | | none | only exercised in parent cleanup; no testcase asserts delete behavior or post-delete state as the primary proof |
|
||||
| ✓ | task tasklists get | api | tasklist_workflow_test.go::TestTask_TasklistWorkflow/get tasklist | `tasklist_guid` in `--params` | |
|
||||
| ✕ | task tasklists list | api | | none | needs isolated list or filter assertions against ambient tasklist data |
|
||||
| ✕ | task tasklists patch | api | | none | no dedicated direct tasklist-update workflow yet |
|
||||
| ✓ | task tasklists get | api | tasklist_workflow_test.go::TestTask_TasklistWorkflowAsBot/get tasklist as bot; tasklist_workflow_test.go::TestTask_TasklistWorkflowAsUser/get patched tasklist as user | `tasklist_guid` in `--params` | |
|
||||
| ✓ | task tasklists list | api | tasklist_workflow_test.go::TestTask_TasklistWorkflowAsUser/list tasklists and find patched tasklist as user | `page_size` | asserts the workflow-created tasklist appears with patched name |
|
||||
| ✓ | task tasklists patch | api | tasklist_workflow_test.go::TestTask_TasklistWorkflowAsUser/patch tasklist as user | `tasklist_guid` in `--params`; `name` in `--data` | verified by follow-up `task tasklists get` and `task tasklists list` |
|
||||
| ✕ | task tasklists remove_members | api | | none | requires real member open_id fixtures and direct API coverage |
|
||||
| ✓ | task tasklists tasks | api | tasklist_workflow_test.go::TestTask_TasklistWorkflow/list tasklist tasks; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow/list tasklist tasks | `tasklist_guid`; `page_size` | |
|
||||
| ✓ | task tasklists tasks | api | tasklist_workflow_test.go::TestTask_TasklistWorkflowAsBot/list tasklist tasks as bot; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow/list tasklist tasks | `tasklist_guid`; `page_size` | |
|
||||
| ✕ | task tasks create | api | | none | only covered indirectly through `task +create`; no direct API invocation yet |
|
||||
| ✕ | task tasks delete | api | | none | only exercised in parent cleanup; no testcase asserts delete behavior or post-delete state as the primary proof |
|
||||
| ✓ | task tasks get | api | task_status_workflow_test.go::TestTask_StatusWorkflow/get completed task; task_status_workflow_test.go::TestTask_StatusWorkflow/get reopened task; task_reminder_workflow_test.go::TestTask_ReminderWorkflow/get task with reminder; task_reminder_workflow_test.go::TestTask_ReminderWorkflow/get task without reminder; tasklist_workflow_test.go::TestTask_TasklistWorkflow/get task; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow/get task with tasklist link | `task_guid` in `--params`; assert status, reminders, summary, description, and tasklist link | |
|
||||
| ✕ | task tasks list | api | | none | needs isolated list or filter assertions against ambient task data |
|
||||
| ✕ | task tasks patch | api | | none | no dedicated direct task-update workflow yet |
|
||||
| ✓ | task tasks get | api | task_status_workflow_test.go::TestTask_StatusWorkflow/get completed task; task_status_workflow_test.go::TestTask_StatusWorkflow/get reopened task; task_reminder_workflow_test.go::TestTask_ReminderWorkflow/get task with reminder; task_reminder_workflow_test.go::TestTask_ReminderWorkflow/get task without reminder; tasklist_workflow_test.go::TestTask_TasklistWorkflowAsBot/get task as bot; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow/get task with tasklist link; task_update_workflow_test.go::TestTask_UpdateWorkflow/get created task as user; task_update_workflow_test.go::TestTask_UpdateWorkflow/get task updated by shortcut as user; task_update_workflow_test.go::TestTask_UpdateWorkflow/get task patched by api as user; task_update_workflow_test.go::TestTask_UpdateWorkflow/get completed task as user | `task_guid` in `--params`; assert status, reminders, summary, description, and tasklist link | |
|
||||
| ✕ | task tasks list | api | | none | UAT did not return the workflow-created user task deterministically in list views |
|
||||
| ✓ | task tasks patch | api | task_update_workflow_test.go::TestTask_UpdateWorkflow/patch task with api as user | `task_guid` in `--params`; `summary` + `description` in `--data` | verified by follow-up `task tasks get` |
|
||||
|
||||
@@ -15,6 +15,10 @@ import (
|
||||
func createTask(t *testing.T, parentT *testing.T, ctx context.Context, req clie2e.Request) string {
|
||||
t.Helper()
|
||||
|
||||
if req.DefaultAs == "" {
|
||||
req.DefaultAs = "bot"
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, req)
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -24,17 +28,15 @@ func createTask(t *testing.T, parentT *testing.T, ctx context.Context, req clie2
|
||||
require.NotEmpty(t, taskGUID, "stdout:\n%s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{
|
||||
Args: []string{"task", "tasks", "delete"},
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "delete"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
if deleteErr != nil {
|
||||
parentT.Errorf("delete task %s: %v", taskGUID, deleteErr)
|
||||
return
|
||||
}
|
||||
if deleteResult.ExitCode != 0 {
|
||||
parentT.Errorf("delete task %s failed: exit=%d stdout=%s stderr=%s", taskGUID, deleteResult.ExitCode, deleteResult.Stdout, deleteResult.Stderr)
|
||||
}
|
||||
clie2e.ReportCleanupFailure(parentT, "delete task "+taskGUID, deleteResult, deleteErr)
|
||||
})
|
||||
|
||||
return taskGUID
|
||||
@@ -43,6 +45,10 @@ func createTask(t *testing.T, parentT *testing.T, ctx context.Context, req clie2
|
||||
func createTasklist(t *testing.T, parentT *testing.T, ctx context.Context, req clie2e.Request) string {
|
||||
t.Helper()
|
||||
|
||||
if req.DefaultAs == "" {
|
||||
req.DefaultAs = "bot"
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, req)
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -52,17 +58,15 @@ func createTasklist(t *testing.T, parentT *testing.T, ctx context.Context, req c
|
||||
require.NotEmpty(t, tasklistGUID, "stdout:\n%s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "delete"},
|
||||
Params: map[string]any{"tasklist_guid": tasklistGUID},
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "delete"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"tasklist_guid": tasklistGUID},
|
||||
})
|
||||
if deleteErr != nil {
|
||||
parentT.Errorf("delete tasklist %s: %v", tasklistGUID, deleteErr)
|
||||
return
|
||||
}
|
||||
if deleteResult.ExitCode != 0 {
|
||||
parentT.Errorf("delete tasklist %s failed: exit=%d stdout=%s stderr=%s", tasklistGUID, deleteResult.ExitCode, deleteResult.Stdout, deleteResult.Stderr)
|
||||
}
|
||||
clie2e.ReportCleanupFailure(parentT, "delete tasklist "+tasklistGUID, deleteResult, deleteErr)
|
||||
})
|
||||
|
||||
return tasklistGUID
|
||||
|
||||
@@ -22,16 +22,18 @@ func TestTask_CommentWorkflow(t *testing.T) {
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
commentContent := "lark-cli-e2e-comment-" + suffix
|
||||
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
|
||||
Args: []string{"task", "+create"},
|
||||
Args: []string{"task", "+create"},
|
||||
DefaultAs: "bot",
|
||||
Data: map[string]any{
|
||||
"summary": "lark-cli-e2e-comment-task-" + suffix,
|
||||
"description": "created by tests/cli_e2e/task comment workflow",
|
||||
},
|
||||
})
|
||||
|
||||
t.Run("comment", func(t *testing.T) {
|
||||
t.Run("comment as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+comment", "--task-id", taskGUID, "--content", commentContent},
|
||||
Args: []string{"task", "+comment", "--task-id", taskGUID, "--content", commentContent},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
@@ -21,7 +21,8 @@ func TestTask_ReminderWorkflow(t *testing.T) {
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
|
||||
Args: []string{"task", "+create"},
|
||||
Args: []string{"task", "+create"},
|
||||
DefaultAs: "bot",
|
||||
Data: map[string]any{
|
||||
"summary": "lark-cli-e2e-reminder-" + suffix,
|
||||
"description": "created by tests/cli_e2e/task reminder workflow",
|
||||
@@ -32,9 +33,10 @@ func TestTask_ReminderWorkflow(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
t.Run("set reminder", func(t *testing.T) {
|
||||
t.Run("set reminder as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+reminder", "--task-id", taskGUID, "--set", "30m"},
|
||||
Args: []string{"task", "+reminder", "--task-id", taskGUID, "--set", "30m"},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -42,10 +44,11 @@ func TestTask_ReminderWorkflow(t *testing.T) {
|
||||
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.guid").String())
|
||||
})
|
||||
|
||||
t.Run("get task with reminder", func(t *testing.T) {
|
||||
t.Run("get task with reminder as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -56,9 +59,10 @@ func TestTask_ReminderWorkflow(t *testing.T) {
|
||||
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.task.reminders.0.id").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("remove reminder", func(t *testing.T) {
|
||||
t.Run("remove reminder as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+reminder", "--task-id", taskGUID, "--remove"},
|
||||
Args: []string{"task", "+reminder", "--task-id", taskGUID, "--remove"},
|
||||
DefaultAs: "bot",
|
||||
}, clie2e.RetryOptions{})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -66,10 +70,11 @@ func TestTask_ReminderWorkflow(t *testing.T) {
|
||||
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.guid").String())
|
||||
})
|
||||
|
||||
t.Run("get task without reminder", func(t *testing.T) {
|
||||
t.Run("get task without reminder as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
@@ -21,16 +21,18 @@ func TestTask_StatusWorkflow(t *testing.T) {
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
|
||||
Args: []string{"task", "+create"},
|
||||
Args: []string{"task", "+create"},
|
||||
DefaultAs: "bot",
|
||||
Data: map[string]any{
|
||||
"summary": "lark-cli-e2e-summary-" + suffix,
|
||||
"description": "created by tests/cli_e2e/task status workflow",
|
||||
},
|
||||
})
|
||||
|
||||
t.Run("complete", func(t *testing.T) {
|
||||
t.Run("complete as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+complete", "--task-id", taskGUID},
|
||||
Args: []string{"task", "+complete", "--task-id", taskGUID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -38,10 +40,11 @@ func TestTask_StatusWorkflow(t *testing.T) {
|
||||
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.guid").String())
|
||||
})
|
||||
|
||||
t.Run("get completed task", func(t *testing.T) {
|
||||
t.Run("get completed task as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -52,9 +55,10 @@ func TestTask_StatusWorkflow(t *testing.T) {
|
||||
assert.NotZero(t, gjson.Get(result.Stdout, "data.task.completed_at").Int(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("reopen", func(t *testing.T) {
|
||||
t.Run("reopen as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+reopen", "--task-id", taskGUID},
|
||||
Args: []string{"task", "+reopen", "--task-id", taskGUID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -62,10 +66,11 @@ func TestTask_StatusWorkflow(t *testing.T) {
|
||||
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.guid").String())
|
||||
})
|
||||
|
||||
t.Run("get reopened task", func(t *testing.T) {
|
||||
t.Run("get reopened task as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
189
tests/cli_e2e/task/task_update_workflow_test.go
Normal file
189
tests/cli_e2e/task/task_update_workflow_test.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package task
|
||||
|
||||
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 TestTask_UpdateWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
taskSummary := "lark-cli-e2e-user-my-task-" + suffix
|
||||
taskDescription := "created by tests/cli_e2e/task user workflow"
|
||||
updatedTaskSummary := "lark-cli-e2e-user-my-task-updated-" + suffix
|
||||
updatedTaskDescription := "updated by task +update user workflow"
|
||||
patchedTaskSummary := "lark-cli-e2e-user-my-task-patched-" + suffix
|
||||
patchedTaskDescription := "patched by task tasks patch user workflow"
|
||||
taskGUID := ""
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
if taskGUID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "delete"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete user task "+taskGUID, deleteResult, deleteErr)
|
||||
})
|
||||
|
||||
t.Run("create task as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+create"},
|
||||
DefaultAs: "user",
|
||||
Data: map[string]any{
|
||||
"summary": taskSummary,
|
||||
"description": taskDescription,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
taskGUID = gjson.Get(result.Stdout, "data.guid").String()
|
||||
require.NotEmpty(t, taskGUID, "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, "user", gjson.Get(result.Stdout, "identity").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get created task as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be created before get")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, taskSummary, gjson.Get(result.Stdout, "data.task.summary").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, taskDescription, gjson.Get(result.Stdout, "data.task.description").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, "todo", gjson.Get(result.Stdout, "data.task.status").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("update task with shortcut as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be created before update")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"task", "+update",
|
||||
"--task-id", taskGUID,
|
||||
"--summary", updatedTaskSummary,
|
||||
"--description", updatedTaskDescription,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.tasks.0.guid").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get task updated by shortcut as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be updated before get")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
assert.Equal(t, updatedTaskSummary, gjson.Get(result.Stdout, "data.task.summary").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, updatedTaskDescription, gjson.Get(result.Stdout, "data.task.description").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("patch task with api as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be updated before patch")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "patch"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
Data: map[string]any{
|
||||
"task": map[string]any{
|
||||
"summary": patchedTaskSummary,
|
||||
"description": patchedTaskDescription,
|
||||
},
|
||||
"update_fields": []string{"summary", "description"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get task patched by api as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be patched before get")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, patchedTaskSummary, gjson.Get(result.Stdout, "data.task.summary").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, patchedTaskDescription, gjson.Get(result.Stdout, "data.task.description").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("complete task as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be created before complete")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+complete", "--task-id", taskGUID},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.guid").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get completed task as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be completed before get")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, patchedTaskSummary, gjson.Get(result.Stdout, "data.task.summary").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, "done", gjson.Get(result.Stdout, "data.task.status").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.NotZero(t, gjson.Get(result.Stdout, "data.task.completed_at").Int(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
@@ -24,19 +24,22 @@ func TestTask_TasklistAddTaskWorkflow(t *testing.T) {
|
||||
taskSummary := "lark-cli-e2e-tasklist-add-task-" + suffix
|
||||
|
||||
tasklistGUID := createTasklist(t, parentT, ctx, clie2e.Request{
|
||||
Args: []string{"task", "+tasklist-create", "--name", tasklistName},
|
||||
Args: []string{"task", "+tasklist-create", "--name", tasklistName},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
|
||||
Args: []string{"task", "+create"},
|
||||
Args: []string{"task", "+create"},
|
||||
DefaultAs: "bot",
|
||||
Data: map[string]any{
|
||||
"summary": taskSummary,
|
||||
"description": "created by tests/cli_e2e/task tasklist add workflow",
|
||||
},
|
||||
})
|
||||
|
||||
t.Run("add task to tasklist", func(t *testing.T) {
|
||||
t.Run("add task to tasklist as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+tasklist-task-add", "--tasklist-id", tasklistGUID, "--task-id", taskGUID},
|
||||
Args: []string{"task", "+tasklist-task-add", "--tasklist-id", tasklistGUID, "--task-id", taskGUID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -47,9 +50,10 @@ func TestTask_TasklistAddTaskWorkflow(t *testing.T) {
|
||||
assert.False(t, gjson.Get(result.Stdout, "data.failed_tasks.0").Exists(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("list tasklist tasks", func(t *testing.T) {
|
||||
t.Run("list tasklist tasks as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "tasks"},
|
||||
Args: []string{"task", "tasklists", "tasks"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"tasklist_guid": tasklistGUID,
|
||||
"page_size": 50,
|
||||
@@ -64,10 +68,11 @@ func TestTask_TasklistAddTaskWorkflow(t *testing.T) {
|
||||
assert.Equal(t, taskSummary, taskItem.Get("summary").String())
|
||||
})
|
||||
|
||||
t.Run("get task with tasklist link", func(t *testing.T) {
|
||||
t.Run("get task with tasklist link as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestTask_TasklistWorkflow(t *testing.T) {
|
||||
func TestTask_TasklistWorkflowAsBot(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
@@ -27,9 +27,10 @@ func TestTask_TasklistWorkflow(t *testing.T) {
|
||||
var tasklistGUID string
|
||||
var taskGUID string
|
||||
|
||||
t.Run("create tasklist with task", func(t *testing.T) {
|
||||
t.Run("create tasklist with task as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+tasklist-create", "--name", tasklistName},
|
||||
Args: []string{"task", "+tasklist-create", "--name", tasklistName},
|
||||
DefaultAs: "bot",
|
||||
Data: []map[string]any{
|
||||
{
|
||||
"summary": taskSummary,
|
||||
@@ -47,40 +48,37 @@ func TestTask_TasklistWorkflow(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "stdout:\n%s", result.Stdout)
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{
|
||||
Args: []string{"task", "tasks", "delete"},
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "delete"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
if deleteErr != nil {
|
||||
parentT.Errorf("delete task %s: %v", taskGUID, deleteErr)
|
||||
return
|
||||
}
|
||||
if deleteResult.ExitCode != 0 {
|
||||
parentT.Errorf("delete task %s failed: exit=%d stdout=%s stderr=%s", taskGUID, deleteResult.ExitCode, deleteResult.Stdout, deleteResult.Stderr)
|
||||
}
|
||||
clie2e.ReportCleanupFailure(parentT, "delete task "+taskGUID, deleteResult, deleteErr)
|
||||
})
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
deleteResult, deleteErr := clie2e.RunCmd(context.Background(), clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "delete"},
|
||||
Params: map[string]any{"tasklist_guid": tasklistGUID},
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "delete"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"tasklist_guid": tasklistGUID},
|
||||
})
|
||||
if deleteErr != nil {
|
||||
parentT.Errorf("delete tasklist %s: %v", tasklistGUID, deleteErr)
|
||||
return
|
||||
}
|
||||
if deleteResult.ExitCode != 0 {
|
||||
parentT.Errorf("delete tasklist %s failed: exit=%d stdout=%s stderr=%s", tasklistGUID, deleteResult.ExitCode, deleteResult.Stdout, deleteResult.Stderr)
|
||||
}
|
||||
clie2e.ReportCleanupFailure(parentT, "delete tasklist "+tasklistGUID, deleteResult, deleteErr)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("get tasklist", func(t *testing.T) {
|
||||
t.Run("get tasklist as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, tasklistGUID, "tasklist GUID should be created before get")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "get"},
|
||||
Params: map[string]any{"tasklist_guid": tasklistGUID},
|
||||
Args: []string{"task", "tasklists", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"tasklist_guid": tasklistGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -89,12 +87,13 @@ func TestTask_TasklistWorkflow(t *testing.T) {
|
||||
assert.Equal(t, tasklistName, gjson.Get(result.Stdout, "data.tasklist.name").String())
|
||||
})
|
||||
|
||||
t.Run("list tasklist tasks", func(t *testing.T) {
|
||||
t.Run("list tasklist tasks as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, tasklistGUID, "tasklist GUID should be created before listing tasks")
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be created before listing tasks")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "tasks"},
|
||||
Args: []string{"task", "tasklists", "tasks"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"tasklist_guid": tasklistGUID,
|
||||
"page_size": 50,
|
||||
@@ -109,12 +108,13 @@ func TestTask_TasklistWorkflow(t *testing.T) {
|
||||
assert.Equal(t, taskSummary, taskItem.Get("summary").String())
|
||||
})
|
||||
|
||||
t.Run("get task", func(t *testing.T) {
|
||||
t.Run("get task as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, taskGUID, "task GUID should be created before get")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
Args: []string{"task", "tasks", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"task_guid": taskGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
@@ -126,3 +126,96 @@ func TestTask_TasklistWorkflow(t *testing.T) {
|
||||
assert.Equal(t, tasklistGUID, gjson.Get(result.Stdout, "data.task.tasklists.0.tasklist_guid").String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestTask_TasklistWorkflowAsUser(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
tasklistName := "lark-cli-e2e-user-tasklist-" + suffix
|
||||
patchedTasklistName := "lark-cli-e2e-user-tasklist-patched-" + suffix
|
||||
tasklistGUID := ""
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
if tasklistGUID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "delete"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"tasklist_guid": tasklistGUID},
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete user tasklist "+tasklistGUID, deleteResult, deleteErr)
|
||||
})
|
||||
|
||||
t.Run("create tasklist as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "+tasklist-create", "--name", tasklistName},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
tasklistGUID = gjson.Get(result.Stdout, "data.guid").String()
|
||||
require.NotEmpty(t, tasklistGUID, "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, "user", gjson.Get(result.Stdout, "identity").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("patch tasklist as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, tasklistGUID, "tasklist GUID should be created before patch")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "patch"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"tasklist_guid": tasklistGUID},
|
||||
Data: map[string]any{
|
||||
"tasklist": map[string]any{"name": patchedTasklistName},
|
||||
"update_fields": []string{"name"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
assert.Equal(t, tasklistGUID, gjson.Get(result.Stdout, "data.tasklist.guid").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("get patched tasklist as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, tasklistGUID, "tasklist GUID should be patched before get")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "get"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"tasklist_guid": tasklistGUID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
assert.Equal(t, tasklistGUID, gjson.Get(result.Stdout, "data.tasklist.guid").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, patchedTasklistName, gjson.Get(result.Stdout, "data.tasklist.name").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("list tasklists and find patched tasklist as user", func(t *testing.T) {
|
||||
require.NotEmpty(t, tasklistGUID, "tasklist GUID should be patched before list")
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"task", "tasklists", "list"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{"page_size": 50},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
tasklistItem := gjson.Get(result.Stdout, `data.items.#(guid=="`+tasklistGUID+`")`)
|
||||
require.True(t, tasklistItem.Exists(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, patchedTasklistName, tasklistItem.Get("name").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
|
||||
21
tests/cli_e2e/wiki/coverage.md
Normal file
21
tests/cli_e2e/wiki/coverage.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Wiki CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 6 leaf commands
|
||||
- Covered: 6
|
||||
- Coverage: 100.0%
|
||||
|
||||
## Summary
|
||||
- TestWiki_NodeWorkflow: proves the full currently-tested wiki domain surface; key `t.Run(...)` proof points are `create node as bot`, `get created node as bot`, `get space as bot`, `list spaces as bot`, `list nodes and find created node as bot`, `copy node as bot`, and `list nodes and find copied node as bot`.
|
||||
- The workflow covers both node creation/copy/listing and space lookup/listing with persisted token assertions.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✓ | wiki nodes copy | api | wiki_workflow_test.go::TestWiki_NodeWorkflow/copy node as bot | `space_id`; `node_token` in `--params`; target/title in `--data` | |
|
||||
| ✓ | wiki nodes create | api | wiki_workflow_test.go::TestWiki_NodeWorkflow/create node as bot | `space_id` in `--params`; `node_type`; `obj_type`; `title` in `--data` | |
|
||||
| ✓ | wiki nodes list | api | wiki/helpers_test.go::findWikiNodeByToken; wiki_workflow_test.go::TestWiki_NodeWorkflow/list nodes and find created node as bot; wiki_workflow_test.go::TestWiki_NodeWorkflow/list nodes and find copied node as bot | `space_id`; `page_size`; optional `page_token` | |
|
||||
| ✓ | wiki spaces get | api | wiki_workflow_test.go::TestWiki_NodeWorkflow/get space as bot | `space_id` in `--params` | |
|
||||
| ✓ | wiki spaces get_node | api | wiki_workflow_test.go::TestWiki_NodeWorkflow/get created node as bot | `token`; `obj_type` in `--params` | |
|
||||
| ✓ | wiki spaces list | api | wiki_workflow_test.go::TestWiki_NodeWorkflow/list spaces as bot | `page_size` in `--params` | |
|
||||
@@ -12,10 +12,14 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func createWikiNode(t *testing.T, ctx context.Context, req clie2e.Request) gjson.Result {
|
||||
func createWikiNode(t *testing.T, parentT *testing.T, ctx context.Context, spaceID string, data map[string]any) gjson.Result {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, req, clie2e.RetryOptions{})
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"api", "post", "/open-apis/wiki/v2/spaces/" + spaceID + "/nodes"},
|
||||
DefaultAs: "bot",
|
||||
Data: data,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
@@ -23,32 +27,87 @@ func createWikiNode(t *testing.T, ctx context.Context, req clie2e.Request) gjson
|
||||
node := gjson.Get(result.Stdout, "data.node")
|
||||
require.True(t, node.Exists(), "stdout:\n%s", result.Stdout)
|
||||
|
||||
nodeToken := node.Get("node_token").String()
|
||||
require.NotEmpty(t, nodeToken, "stdout:\n%s", result.Stdout)
|
||||
parentT.Cleanup(func() {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"api", "delete", "/open-apis/wiki/v2/spaces/" + spaceID + "/nodes/" + nodeToken},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete wiki node "+nodeToken, deleteResult, deleteErr)
|
||||
})
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func getWikiNode(t *testing.T, ctx context.Context, nodeToken string) gjson.Result {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"api", "get", "/open-apis/wiki/v2/spaces/get_node"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"token": nodeToken},
|
||||
})
|
||||
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 getWikiSpace(t *testing.T, ctx context.Context, spaceID string) gjson.Result {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"api", "get", "/open-apis/wiki/v2/spaces/" + spaceID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
space := gjson.Get(result.Stdout, "data.space")
|
||||
require.True(t, space.Exists(), "stdout:\n%s", result.Stdout)
|
||||
return space
|
||||
}
|
||||
|
||||
func listWikiSpaces(t *testing.T, ctx context.Context, pageSize int) gjson.Result {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"api", "get", "/open-apis/wiki/v2/spaces"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"page_size": pageSize},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
return gjson.Parse(result.Stdout)
|
||||
}
|
||||
|
||||
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 := ""
|
||||
lastStdout := ""
|
||||
seenPageTokens := map[string]struct{}{}
|
||||
for {
|
||||
params := map[string]any{
|
||||
"space_id": spaceID,
|
||||
"page_size": 50,
|
||||
}
|
||||
params := map[string]any{"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)
|
||||
if _, exists := seenPageTokens[pageToken]; exists {
|
||||
t.Fatalf("wiki list pagination loop detected for page_token %q, last stdout:\n%s", pageToken, lastStdout)
|
||||
}
|
||||
seenPageTokens[pageToken] = struct{}{}
|
||||
params["page_token"] = pageToken
|
||||
}
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"wiki", "nodes", "list"},
|
||||
Args: []string{"api", "get", "/open-apis/wiki/v2/spaces/" + spaceID + "/nodes"},
|
||||
DefaultAs: "bot",
|
||||
Params: params,
|
||||
})
|
||||
@@ -56,15 +115,16 @@ func findWikiNodeByToken(t *testing.T, ctx context.Context, spaceID string, node
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
node := gjson.Get(result.Stdout, `data.items.#(node_token=="`+nodeToken+`")`)
|
||||
lastStdout = result.Stdout
|
||||
parsed := gjson.Parse(result.Stdout)
|
||||
node := parsed.Get(`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)
|
||||
pageToken = parsed.Get("data.page_token").String()
|
||||
if pageToken == "" || !parsed.Get("data.has_more").Bool() {
|
||||
t.Fatalf("wiki node %q not found in listed pages, last stdout:\n%s", nodeToken, lastStdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,136 +15,126 @@ import (
|
||||
)
|
||||
|
||||
func TestWiki_NodeWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
parentTitle := "lark-cli-e2e-wiki-parent-" + suffix
|
||||
createdTitle := "lark-cli-e2e-wiki-create-" + suffix
|
||||
copiedTitle := "lark-cli-e2e-wiki-copy-" + suffix
|
||||
|
||||
var spaceID string
|
||||
var parentNodeToken string
|
||||
var createdNodeToken string
|
||||
var createdObjToken string
|
||||
var copiedNodeToken string
|
||||
var copiedSpaceID 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,
|
||||
},
|
||||
t.Run("create isolated parent node as bot", func(t *testing.T) {
|
||||
parentNode := createWikiNode(t, parentT, ctx, "my_library", map[string]any{
|
||||
"node_type": "origin",
|
||||
"obj_type": "docx",
|
||||
"title": parentTitle,
|
||||
})
|
||||
spaceID = parentNode.Get("space_id").String()
|
||||
parentNodeToken = parentNode.Get("node_token").String()
|
||||
require.NotEmpty(t, spaceID)
|
||||
require.NotEmpty(t, parentNodeToken)
|
||||
assert.Equal(t, parentTitle, parentNode.Get("title").String())
|
||||
})
|
||||
|
||||
t.Run("create node as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, parentNodeToken, "parent node token should be created before child node")
|
||||
|
||||
node := createWikiNode(t, parentT, ctx, spaceID, map[string]any{
|
||||
"node_type": "origin",
|
||||
"obj_type": "docx",
|
||||
"title": createdTitle,
|
||||
"parent_node_token": parentNodeToken,
|
||||
})
|
||||
|
||||
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())
|
||||
assert.Equal(t, parentNodeToken, node.Get("parent_node_token").String())
|
||||
})
|
||||
|
||||
t.Run("get created node", func(t *testing.T) {
|
||||
t.Run("get created node as bot", 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())
|
||||
node := getWikiNode(t, ctx, createdNodeToken)
|
||||
assert.Equal(t, createdNodeToken, node.Get("node_token").String())
|
||||
assert.Equal(t, createdObjToken, node.Get("obj_token").String())
|
||||
assert.Equal(t, createdTitle, node.Get("title").String())
|
||||
assert.Equal(t, spaceID, node.Get("space_id").String())
|
||||
})
|
||||
|
||||
t.Run("get space", func(t *testing.T) {
|
||||
t.Run("get isolated parent node as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, parentNodeToken, "parent node token should be created before get_node")
|
||||
node := getWikiNode(t, ctx, parentNodeToken)
|
||||
assert.Equal(t, parentNodeToken, node.Get("node_token").String())
|
||||
assert.Equal(t, parentTitle, node.Get("title").String())
|
||||
})
|
||||
|
||||
t.Run("get space as bot", 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)
|
||||
space := getWikiSpace(t, ctx, spaceID)
|
||||
assert.Equal(t, spaceID, space.Get("space_id").String())
|
||||
assert.NotEmpty(t, space.Get("name").String())
|
||||
})
|
||||
|
||||
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 spaces as bot", func(t *testing.T) {
|
||||
result := listWikiSpaces(t, ctx, 1)
|
||||
assert.True(t, result.Get("data.items").Exists(), "stdout:\n%s", result.Raw)
|
||||
})
|
||||
|
||||
t.Run("list nodes and find created node", func(t *testing.T) {
|
||||
t.Run("list nodes and find isolated parent node as bot", 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")
|
||||
require.NotEmpty(t, parentNodeToken, "parent 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())
|
||||
nodeItem := findWikiNodeByToken(t, ctx, spaceID, parentNodeToken)
|
||||
assert.Equal(t, parentTitle, nodeItem.Get("title").String())
|
||||
})
|
||||
|
||||
t.Run("copy node", func(t *testing.T) {
|
||||
t.Run("copy node as bot", 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"},
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"api", "post", "/open-apis/wiki/v2/spaces/" + spaceID + "/nodes/" + createdNodeToken + "/copy"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{
|
||||
"space_id": spaceID,
|
||||
"node_token": createdNodeToken,
|
||||
},
|
||||
Data: map[string]any{
|
||||
"target_space_id": spaceID,
|
||||
"title": copiedTitle,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, 0)
|
||||
|
||||
copiedNodeToken = copiedNode.Get("node_token").String()
|
||||
copiedNodeToken = gjson.Get(result.Stdout, "data.node.node_token").String()
|
||||
copiedSpaceID = gjson.Get(result.Stdout, "data.node.space_id").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)
|
||||
require.NotEmpty(t, copiedSpaceID)
|
||||
parentT.Cleanup(func() {
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
|
||||
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"api", "delete", "/open-apis/wiki/v2/spaces/" + copiedSpaceID + "/nodes/" + copiedNodeToken},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "delete copied wiki node "+copiedNodeToken, deleteResult, deleteErr)
|
||||
})
|
||||
})
|
||||
|
||||
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())
|
||||
t.Run("get copied node as bot", func(t *testing.T) {
|
||||
require.NotEmpty(t, copiedNodeToken, "copied node token should be available before verification")
|
||||
node := getWikiNode(t, ctx, copiedNodeToken)
|
||||
assert.Equal(t, copiedTitle, node.Get("title").String())
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user