chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:54 +08:00
commit bf9395e022
2349 changed files with 588574 additions and 0 deletions
@@ -0,0 +1,108 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package calendar
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
// TestCalendar_CreateEvent tests the workflow of creating a calendar event.
func TestCalendar_CreateEvent(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
suffix := clie2e.GenerateSuffix()
eventSummary := "lark-cli-e2e-event-" + suffix
eventDescription := "test event description"
startAt := time.Now().UTC().Add(1 * time.Hour).Truncate(time.Minute)
endAt := startAt.Add(1 * time.Hour)
startTime := startAt.Format(time.RFC3339)
endTime := endAt.Format(time.RFC3339)
var eventID string
var deletedEvent bool
calendarID := getPrimaryCalendarID(t, ctx)
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,
"--start", startTime,
"--end", endTime,
"--calendar-id", calendarID,
"--description", eventDescription,
},
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
eventID = gjson.Get(result.Stdout, "data.event_id").String()
require.NotEmpty(t, eventID)
parentT.Cleanup(func() {
if eventID == "" || deletedEvent {
return
}
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("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"},
DefaultAs: "bot",
Params: map[string]any{
"calendar_id": calendarID,
"event_id": eventID,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, eventSummary, gjson.Get(result.Stdout, "data.event.summary").String())
assert.Equal(t, eventDescription, gjson.Get(result.Stdout, "data.event.description").String())
assert.Equal(t, unixSecondsRFC3339(startAt), gjson.Get(result.Stdout, "data.event.start_time.timestamp").String())
assert.Equal(t, unixSecondsRFC3339(endAt), gjson.Get(result.Stdout, "data.event.end_time.timestamp").String())
})
t.Run("delete event as bot", func(t *testing.T) {
require.NotEmpty(t, eventID)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"calendar", "events", "delete"},
DefaultAs: "bot",
Params: map[string]any{
"calendar_id": calendarID,
"event_id": eventID,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
deletedEvent = true
})
}
@@ -0,0 +1,136 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package calendar
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
// TestCalendar_ManageCalendar tests the workflow of managing calendars.
func TestCalendar_ManageCalendar(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
suffix := clie2e.GenerateSuffix()
calendarSummary := "lark-cli-e2e-cal-" + suffix
updatedCalendarSummary := calendarSummary + "-updated"
calendarDescription := "test calendar created by e2e"
var createdCalendarID string
var deletedCalendar bool
t.Run("get primary calendar as bot", func(t *testing.T) {
primaryCalendarID := getPrimaryCalendarID(t, ctx)
require.NotEmpty(t, primaryCalendarID)
})
t.Run("create calendar as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"calendar", "calendars", "create"},
DefaultAs: "bot",
Data: map[string]any{
"summary": calendarSummary,
"description": calendarDescription,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
createdCalendarID = gjson.Get(result.Stdout, "data.calendar.calendar_id").String()
require.NotEmpty(t, createdCalendarID)
parentT.Cleanup(func() {
if createdCalendarID == "" || deletedCalendar {
return
}
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"calendar", "calendars", "delete"},
DefaultAs: "bot",
Params: map[string]any{
"calendar_id": createdCalendarID,
},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete calendar "+createdCalendarID, deleteResult, deleteErr)
})
})
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"},
DefaultAs: "bot",
Params: map[string]any{
"calendar_id": createdCalendarID,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, createdCalendarID, gjson.Get(result.Stdout, "data.calendar_id").String())
assert.Equal(t, calendarSummary, gjson.Get(result.Stdout, "data.summary").String())
assert.Equal(t, calendarDescription, gjson.Get(result.Stdout, "data.description").String())
})
t.Run("update calendar as bot", func(t *testing.T) {
require.NotEmpty(t, createdCalendarID)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"calendar", "calendars", "patch"},
DefaultAs: "bot",
Params: map[string]any{
"calendar_id": createdCalendarID,
},
Data: map[string]any{
"summary": updatedCalendarSummary,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
})
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"},
DefaultAs: "bot",
Params: map[string]any{
"calendar_id": createdCalendarID,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, updatedCalendarSummary, gjson.Get(result.Stdout, "data.summary").String())
})
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"},
DefaultAs: "bot",
Params: map[string]any{
"calendar_id": createdCalendarID,
},
Yes: true,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
deletedCalendar = true
})
}
@@ -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, true)
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, true)
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)
})
}
@@ -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")
})
}
@@ -0,0 +1,59 @@
// 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/require"
"github.com/tidwall/gjson"
)
func TestCalendar_UpdateDryRun(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
t.Setenv("LARKSUITE_CLI_APP_ID", "app")
t.Setenv("LARKSUITE_CLI_APP_SECRET", "secret")
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"calendar", "+update",
"--calendar-id", "cal_dry",
"--event-id", "evt_dry",
"--summary", "updated dry-run",
"--start", "2026-04-25T10:00:00+08:00",
"--end", "2026-04-25T11:00:00+08:00",
"--remove-attendee-ids", "ou_old,omm_oldroom",
"--add-attendee-ids", "ou_new,oc_group,omm_newroom",
"--notify=false",
"--dry-run",
},
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
out := result.Stdout
require.Equal(t, "PATCH", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out)
require.Equal(t, "/open-apis/calendar/v4/calendars/cal_dry/events/evt_dry", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out)
require.Equal(t, "updated dry-run", gjson.Get(out, "api.0.body.summary").String(), "stdout:\n%s", out)
require.False(t, gjson.Get(out, "api.0.body.need_notification").Bool(), "stdout:\n%s", out)
require.Equal(t, "POST", gjson.Get(out, "api.1.method").String(), "stdout:\n%s", out)
require.Equal(t, "/open-apis/calendar/v4/calendars/cal_dry/events/evt_dry/attendees/batch_delete", gjson.Get(out, "api.1.url").String(), "stdout:\n%s", out)
require.Equal(t, "ou_old", gjson.Get(out, `api.1.body.delete_ids.#(type=="user").user_id`).String(), "stdout:\n%s", out)
require.Equal(t, "omm_oldroom", gjson.Get(out, `api.1.body.delete_ids.#(type=="resource").room_id`).String(), "stdout:\n%s", out)
require.Equal(t, "POST", gjson.Get(out, "api.2.method").String(), "stdout:\n%s", out)
require.Equal(t, "/open-apis/calendar/v4/calendars/cal_dry/events/evt_dry/attendees", gjson.Get(out, "api.2.url").String(), "stdout:\n%s", out)
require.Equal(t, "ou_new", gjson.Get(out, `api.2.body.attendees.#(type=="user").user_id`).String(), "stdout:\n%s", out)
require.Equal(t, "oc_group", gjson.Get(out, `api.2.body.attendees.#(type=="chat").chat_id`).String(), "stdout:\n%s", out)
require.Equal(t, "omm_newroom", gjson.Get(out, `api.2.body.attendees.#(type=="resource").room_id`).String(), "stdout:\n%s", out)
}
@@ -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_UpdateEventWorkflow(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
clie2e.SkipWithoutUserToken(t)
suffix := clie2e.GenerateSuffix()
calendarID := getPrimaryCalendarID(t, ctx)
userOpenID := getCurrentUserOpenIDForCalendar(t, ctx)
startAt := time.Now().UTC().Add(2 * time.Hour).Truncate(time.Minute)
endAt := startAt.Add(30 * time.Minute)
updatedStartAt := startAt.Add(30 * time.Minute)
updatedEndAt := updatedStartAt.Add(45 * time.Minute)
createdSummary := "lark-cli-e2e-update-before-" + suffix
updatedSummary := "lark-cli-e2e-update-after-" + suffix
updatedDescription := "updated by calendar update workflow"
var eventID string
var deletedEvent bool
t.Run("create event as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"calendar", "+create",
"--summary", createdSummary,
"--start", startAt.Format(time.RFC3339),
"--end", endAt.Format(time.RFC3339),
"--calendar-id", calendarID,
},
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() {
if eventID == "" || deletedEvent {
return
}
cleanupCtx, cleanupCancel := clie2e.CleanupContext()
defer cleanupCancel()
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("update event and add attendee as bot", func(t *testing.T) {
require.NotEmpty(t, eventID)
require.NotEmpty(t, userOpenID)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"calendar", "+update",
"--event-id", eventID,
"--calendar-id", calendarID,
"--summary", updatedSummary,
"--description", updatedDescription,
"--start", updatedStartAt.Format(time.RFC3339),
"--end", updatedEndAt.Format(time.RFC3339),
"--add-attendee-ids", userOpenID,
"--notify=false",
},
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, eventID, gjson.Get(result.Stdout, "data.event_id").String())
})
t.Run("verify updated event as bot", func(t *testing.T) {
require.NotEmpty(t, eventID)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"calendar", "events", "get"},
DefaultAs: "bot",
Params: map[string]any{
"calendar_id": calendarID,
"event_id": eventID,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, updatedSummary, gjson.Get(result.Stdout, "data.event.summary").String())
assert.Equal(t, updatedDescription, gjson.Get(result.Stdout, "data.event.description").String())
assert.Equal(t, unixSecondsRFC3339(updatedStartAt), gjson.Get(result.Stdout, "data.event.start_time.timestamp").String())
assert.Equal(t, unixSecondsRFC3339(updatedEndAt), gjson.Get(result.Stdout, "data.event.end_time.timestamp").String())
})
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"},
DefaultAs: "bot",
Params: map[string]any{
"calendar_id": calendarID,
"event_id": eventID,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
deletedEvent = true
})
}
@@ -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
View File
@@ -0,0 +1,43 @@
# Calendar CLI E2E Coverage
## Metrics
- Denominator: 23 leaf commands
- Covered: 11
- Coverage: 47.8%
## 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`, and `calendar calendars patch`; key `t.Run(...)` proof points are `get primary calendar as bot`, `create calendar as bot`, `get created calendar 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 | | none | removed from the live workflow because tenant history made list latency non-deterministic |
| ✓ | 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 |
+67
View File
@@ -0,0 +1,67 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package calendar
import (
"context"
"strconv"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func getPrimaryCalendarID(t *testing.T, ctx context.Context) string {
t.Helper()
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"calendar", "calendars", "primary"},
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
calendarID := gjson.Get(result.Stdout, "data.calendars.0.calendar.calendar_id").String()
require.NotEmpty(t, calendarID, "stdout:\n%s", result.Stdout)
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, true)
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)
}