From 87b5899d192148270bd90f5aa6a5d07918166db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E6=B8=AF?= Date: Mon, 29 Jun 2026 11:36:15 +0800 Subject: [PATCH] feat: support calendar +get --- shortcuts/calendar/calendar_get.go | 279 ++++++++++++++++++ shortcuts/calendar/calendar_test.go | 198 ++++++++++++- shortcuts/calendar/shortcuts.go | 1 + skills/lark-calendar/SKILL.md | 12 + .../references/lark-calendar-get.md | 61 ++++ .../calendar/calendar_get_dryrun_test.go | 66 +++++ 6 files changed, 613 insertions(+), 4 deletions(-) create mode 100644 shortcuts/calendar/calendar_get.go create mode 100644 skills/lark-calendar/references/lark-calendar-get.md create mode 100644 tests/cli_e2e/calendar/calendar_get_dryrun_test.go diff --git a/shortcuts/calendar/calendar_get.go b/shortcuts/calendar/calendar_get.go new file mode 100644 index 000000000..a49fb1fbb --- /dev/null +++ b/shortcuts/calendar/calendar_get.go @@ -0,0 +1,279 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT +// +// calendar +get — get a single calendar event detail by calendar_id and event_id + +package calendar + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/larksuite/cli/errs" + "github.com/larksuite/cli/internal/output" + "github.com/larksuite/cli/internal/validate" + "github.com/larksuite/cli/shortcuts/common" +) + +// calendarEventTime mirrors start_time / end_time in the API response. +type calendarEventTime struct { + Date string `json:"date,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Timezone string `json:"timezone,omitempty"` +} + +// calendarEventVChat mirrors the vchat block in the API response. +type calendarEventVChat struct { + VCType string `json:"vc_type,omitempty"` + IconType string `json:"icon_type,omitempty"` + Description string `json:"description,omitempty"` + MeetingURL string `json:"meeting_url,omitempty"` +} + +// calendarEventLocation mirrors the location block in the API response. +type calendarEventLocation struct { + Name string `json:"name,omitempty"` + Address string `json:"address,omitempty"` + Latitude float64 `json:"latitude,omitempty"` + Longitude float64 `json:"longitude,omitempty"` +} + +// calendarEventReminder mirrors a reminder entry. +type calendarEventReminder struct { + Minutes int `json:"minutes"` +} + +// calendarEventOrganizer mirrors event_organizer. +type calendarEventOrganizer struct { + UserID string `json:"user_id,omitempty"` + DisplayName string `json:"display_name,omitempty"` +} + +// calendarEventAttachment mirrors a single attachment entry. +type calendarEventAttachment struct { + FileToken string `json:"file_token,omitempty"` + FileSize string `json:"file_size,omitempty"` + Name string `json:"name,omitempty"` +} + +// calendarEventCheckInTime mirrors check_in_start_time / check_in_end_time. +type calendarEventCheckInTime struct { + TimeType string `json:"time_type,omitempty"` + Duration int `json:"duration"` +} + +// calendarEventCheckIn mirrors event_check_in. +type calendarEventCheckIn struct { + EnableCheckIn bool `json:"enable_check_in"` + CheckInStartTime *calendarEventCheckInTime `json:"check_in_start_time,omitempty"` + CheckInEndTime *calendarEventCheckInTime `json:"check_in_end_time,omitempty"` + NeedNotifyAttendees bool `json:"need_notify_attendees"` +} + +// calendarEvent mirrors the event object inside the API response. +type calendarEvent struct { + EventID string `json:"event_id,omitempty"` + OrganizerCalendarID string `json:"organizer_calendar_id,omitempty"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + StartTime *calendarEventTime `json:"start_time,omitempty"` + EndTime *calendarEventTime `json:"end_time,omitempty"` + VChat *calendarEventVChat `json:"vchat,omitempty"` + Visibility string `json:"visibility,omitempty"` + AttendeeAbility string `json:"attendee_ability,omitempty"` + FreeBusyStatus string `json:"free_busy_status,omitempty"` + SelfRsvpStatus string `json:"self_rsvp_status,omitempty"` + Location *calendarEventLocation `json:"location,omitempty"` + Color int `json:"color,omitempty"` + Reminders []calendarEventReminder `json:"reminders,omitempty"` + Recurrence string `json:"recurrence,omitempty"` + Status string `json:"status,omitempty"` + IsException bool `json:"is_exception,omitempty"` + RecurringEventID string `json:"recurring_event_id,omitempty"` + CreateTime string `json:"create_time,omitempty"` + EventOrganizer *calendarEventOrganizer `json:"event_organizer,omitempty"` + AppLink string `json:"app_link,omitempty"` + Attachments []calendarEventAttachment `json:"attachments,omitempty"` + EventCheckIn *calendarEventCheckIn `json:"event_check_in,omitempty"` +} + +// parseCalendarEvent decodes the API response data into a typed calendarEvent. +func parseCalendarEvent(data map[string]any) (*calendarEvent, error) { + rawEvent, ok := data["event"] + if !ok || rawEvent == nil { + return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "calendar event response missing 'event' field") + } + raw, err := json.Marshal(rawEvent) + if err != nil { + return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "calendar event response: marshal failed: %s", err).WithCause(err) + } + var event calendarEvent + if err := json.Unmarshal(raw, &event); err != nil { + return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "calendar event response: unmarshal failed: %s", err).WithCause(err) + } + return &event, nil +} + +// buildCalendarEventOutput converts the typed event into the output map and +// applies the four transformation rules: +// 1. create_time -> RFC3339 +// 2. start_time / end_time timestamp -> datetime (RFC3339), drop timestamp +// 3. flatten event into the top-level result +// 4. when status != "cancelled", drop status (and adjust all-day end date) +func buildCalendarEventOutput(event *calendarEvent) (map[string]interface{}, error) { + raw, err := json.Marshal(event) + if err != nil { + return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "calendar event marshal failed: %s", err).WithCause(err) + } + var out map[string]interface{} + if err := json.Unmarshal(raw, &out); err != nil { + return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "calendar event unmarshal failed: %s", err).WithCause(err) + } + + if ctStr, ok := out["create_time"].(string); ok && ctStr != "" { + if ts, err := strconv.ParseInt(ctStr, 10, 64); err == nil { + out["create_time"] = time.Unix(ts, 0).Local().Format(time.RFC3339) + } + } + + if startMap, ok := out["start_time"].(map[string]interface{}); ok { + if tsStr, ok := startMap["timestamp"].(string); ok && tsStr != "" { + if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil { + startMap["datetime"] = time.Unix(ts, 0).Local().Format(time.RFC3339) + delete(startMap, "timestamp") + } + } + } + if endMap, ok := out["end_time"].(map[string]interface{}); ok { + if tsStr, ok := endMap["timestamp"].(string); ok && tsStr != "" { + if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil { + endMap["datetime"] = time.Unix(ts, 0).Local().Format(time.RFC3339) + delete(endMap, "timestamp") + } + } + // All-day event: end date is exclusive in the API; rewind by 1s and reformat. + if dt, _ := endMap["datetime"].(string); dt == "" { + if dateStr, ok := endMap["date"].(string); ok && dateStr != "" { + if t, err := time.ParseInLocation("2006-01-02", dateStr, time.UTC); err == nil { + endMap["date"] = t.Add(-1 * time.Second).Format("2006-01-02") + } + } + } + } + + if status, _ := out["status"].(string); status != "cancelled" { + delete(out, "status") + } + + return out, nil +} + +// CalendarGet gets a single calendar event detail. +var CalendarGet = common.Shortcut{ + Service: "calendar", + Command: "+get", + Description: "Get a single calendar event detail by calendar-id and event-id", + Risk: "read", + Scopes: []string{"calendar:calendar.event:read"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: []common.Flag{ + {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, + {Name: "event-id", Desc: "event ID", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if err := rejectCalendarAutoBotFallback(runtime); err != nil { + return err + } + for _, flag := range []string{"calendar-id", "event-id"} { + if val := strings.TrimSpace(runtime.Str(flag)); val != "" { + if err := common.RejectDangerousCharsTyped("--"+flag, val); err != nil { + return err + } + } + } + eventId := strings.TrimSpace(runtime.Str("event-id")) + if eventId == "" { + return errs.NewValidationError(errs.SubtypeInvalidArgument, "event-id cannot be empty").WithParam("--event-id") + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + calendarId := strings.TrimSpace(runtime.Str("calendar-id")) + d := common.NewDryRunAPI() + switch calendarId { + case "": + d.Desc("(calendar-id omitted) Will use primary calendar") + calendarId = "" + case "primary": + calendarId = "" + } + eventId := strings.TrimSpace(runtime.Str("event-id")) + return d. + GET("/open-apis/calendar/v4/calendars/:calendar_id/events/:event_id"). + Set("calendar_id", calendarId). + Set("event_id", eventId) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + calendarId := strings.TrimSpace(runtime.Str("calendar-id")) + if calendarId == "" { + calendarId = PrimaryCalendarIDStr + } + eventId := strings.TrimSpace(runtime.Str("event-id")) + + data, err := runtime.CallAPITyped("GET", + fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/%s", + validate.EncodePathSegment(calendarId), + validate.EncodePathSegment(eventId)), + nil, nil) + if err != nil { + return err + } + + event, err := parseCalendarEvent(data) + if err != nil { + return err + } + + out, err := buildCalendarEventOutput(event) + if err != nil { + return err + } + + runtime.OutFormat(out, nil, func(w io.Writer) { + summary, _ := out["summary"].(string) + if summary == "" { + summary = "(untitled)" + } + startMap, _ := out["start_time"].(map[string]interface{}) + endMap, _ := out["end_time"].(map[string]interface{}) + startStr, _ := startMap["datetime"].(string) + if startStr == "" { + startStr, _ = startMap["date"].(string) + } + endStr, _ := endMap["datetime"].(string) + if endStr == "" { + endStr, _ = endMap["date"].(string) + } + eventIdOut, _ := out["event_id"].(string) + freeBusyStatus, _ := out["free_busy_status"].(string) + selfRsvpStatus, _ := out["self_rsvp_status"].(string) + row := map[string]interface{}{ + "event_id": eventIdOut, + "summary": summary, + "start": startStr, + "end": endStr, + "free_busy_status": freeBusyStatus, + "self_rsvp_status": selfRsvpStatus, + } + output.PrintTable(w, []map[string]interface{}{row}) + fmt.Fprintln(w) + }) + return nil + }, +} diff --git a/shortcuts/calendar/calendar_test.go b/shortcuts/calendar/calendar_test.go index 970a141e3..d43dc7510 100644 --- a/shortcuts/calendar/calendar_test.go +++ b/shortcuts/calendar/calendar_test.go @@ -2234,17 +2234,17 @@ func TestResolveStartEnd_ExplicitValues(t *testing.T) { // Shortcuts() registration test // --------------------------------------------------------------------------- -func TestShortcuts_Returns9(t *testing.T) { +func TestShortcuts_Returns10(t *testing.T) { shortcuts := Shortcuts() - if len(shortcuts) != 9 { - t.Fatalf("expected 9 shortcuts, got %d", len(shortcuts)) + if len(shortcuts) != 10 { + t.Fatalf("expected 10 shortcuts, got %d", len(shortcuts)) } names := map[string]bool{} for _, s := range shortcuts { names[s.Command] = true } - for _, want := range []string{"+agenda", "+create", "+update", "+freebusy", "+room-find", "+rsvp", "+suggestion"} { + for _, want := range []string{"+agenda", "+create", "+update", "+freebusy", "+room-find", "+rsvp", "+suggestion", "+get"} { if !names[want] { t.Errorf("missing shortcut %s", want) } @@ -3108,3 +3108,193 @@ func TestSuggestion_RejectsDangerousTimezone_Typed(t *testing.T) { t.Errorf("param=%q, want --timezone", ve.Param) } } + +// --------------------------------------------------------------------------- +// CalendarGet tests +// --------------------------------------------------------------------------- + +func TestGet_Success_FlattensAndConvertsTimes(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/calendar/v4/calendars/cal_test123/events/evt_001", + Body: map[string]interface{}{ + "code": 0, "msg": "success", + "data": map[string]interface{}{ + "event": map[string]interface{}{ + "event_id": "evt_001", + "summary": "Daily Sync", + "create_time": "1602504000", + "start_time": map[string]interface{}{ + "timestamp": "1742515200", + "timezone": "Asia/Shanghai", + }, + "end_time": map[string]interface{}{ + "timestamp": "1742518800", + "timezone": "Asia/Shanghai", + }, + "status": "confirmed", + }, + }, + }, + }) + + err := mountAndRun(t, CalendarGet, []string{ + "+get", + "--calendar-id", "cal_test123", + "--event-id", "evt_001", + "--as", "bot", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := stdout.String() + // Expect flattened — fields appear directly under "data", not under "data.event" + if strings.Contains(out, "\"event\": {") { + t.Errorf("payload should be flattened (no event wrapper), got: %s", out) + } + if !strings.Contains(out, "\"event_id\": \"evt_001\"") { + t.Errorf("expected event_id in output, got: %s", out) + } + // status=confirmed should be dropped + if strings.Contains(out, "\"status\": \"confirmed\"") { + t.Errorf("status should be dropped when not cancelled, got: %s", out) + } + // timestamp must be replaced with datetime + if strings.Contains(out, "\"timestamp\":") { + t.Errorf("timestamp should be replaced with datetime, got: %s", out) + } + if !strings.Contains(out, "\"datetime\":") { + t.Errorf("expected datetime in output, got: %s", out) + } + // create_time must be RFC3339 (contain 'T' and timezone) + if !strings.Contains(out, "\"create_time\": \"2020-10-12T") { + t.Errorf("expected RFC3339 create_time, got: %s", out) + } +} + +func TestGet_CancelledStatus_PreservesStatus(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/calendar/v4/calendars/cal_test123/events/evt_002", + Body: map[string]interface{}{ + "code": 0, "msg": "success", + "data": map[string]interface{}{ + "event": map[string]interface{}{ + "event_id": "evt_002", + "summary": "Cancelled Meeting", + "create_time": "1602504000", + "start_time": map[string]interface{}{"timestamp": "1742515200"}, + "end_time": map[string]interface{}{"timestamp": "1742518800"}, + "status": "cancelled", + }, + }, + }, + }) + + err := mountAndRun(t, CalendarGet, []string{ + "+get", + "--calendar-id", "cal_test123", + "--event-id", "evt_002", + "--as", "bot", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := stdout.String() + if !strings.Contains(out, "\"status\": \"cancelled\"") { + t.Errorf("status should be preserved when cancelled, got: %s", out) + } +} + +func TestGet_AllDayEvent_AdjustsEndDate(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + + // All-day event: start 2025-03-21, end 2025-03-22 (exclusive in API). + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/calendar/v4/calendars/cal_test123/events/evt_003", + Body: map[string]interface{}{ + "code": 0, "msg": "success", + "data": map[string]interface{}{ + "event": map[string]interface{}{ + "event_id": "evt_003", + "summary": "All-day", + "start_time": map[string]interface{}{"date": "2025-03-21"}, + "end_time": map[string]interface{}{"date": "2025-03-22"}, + "status": "confirmed", + }, + }, + }, + }) + + err := mountAndRun(t, CalendarGet, []string{ + "+get", + "--calendar-id", "cal_test123", + "--event-id", "evt_003", + "--as", "bot", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := stdout.String() + // end date 2025-03-22 should rewind by 1s -> 2025-03-21 + if !strings.Contains(out, "\"date\": \"2025-03-21\"") { + t.Errorf("expected end date adjusted to 2025-03-21, got: %s", out) + } +} + +func TestGet_EmptyEventID_Typed(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarGet, []string{ + "+get", + "--event-id", " ", + "--as", "bot", + }, f, nil) + if err == nil { + t.Fatal("want error for empty event-id") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Param != "--event-id" { + t.Errorf("param=%q, want --event-id", ve.Param) + } +} + +func TestGet_MissingEventField_TypedInternal(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, defaultConfig()) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/calendar/v4/calendars/cal_test123/events/evt_404", + Body: map[string]interface{}{ + "code": 0, "msg": "success", + "data": map[string]interface{}{}, + }, + }) + + err := mountAndRun(t, CalendarGet, []string{ + "+get", + "--calendar-id", "cal_test123", + "--event-id", "evt_404", + "--as", "bot", + }, f, nil) + if err == nil { + t.Fatal("want error when event field is missing") + } + var ie *errs.InternalError + if !errors.As(err, &ie) { + t.Fatalf("want *errs.InternalError, got %T", err) + } + if ie.Subtype != errs.SubtypeInvalidResponse { + t.Errorf("subtype=%q, want invalid_response", ie.Subtype) + } +} diff --git a/shortcuts/calendar/shortcuts.go b/shortcuts/calendar/shortcuts.go index b927bf78e..92b63f7f3 100644 --- a/shortcuts/calendar/shortcuts.go +++ b/shortcuts/calendar/shortcuts.go @@ -17,5 +17,6 @@ func Shortcuts() []common.Shortcut { CalendarSuggestion, CalendarMeeting, CalendarSearchEvent, + CalendarGet, } } diff --git a/skills/lark-calendar/SKILL.md b/skills/lark-calendar/SKILL.md index 09c1e4977..267c144b3 100644 --- a/skills/lark-calendar/SKILL.md +++ b/skills/lark-calendar/SKILL.md @@ -32,6 +32,7 @@ lark-cli calendar +agenda --as user |----------|------| | [`+agenda`](references/lark-calendar-agenda.md) | 查看日程安排(默认今天) | | [`+search-event`](references/lark-calendar-search-event.md) | 按关键词、时间范围和参会人搜索日程, 仅返回 日程ID/主题/时间等信息,详情需走 `events get` | +| `+get` | 获取单个日程详情 | | [`+meeting`](references/lark-calendar-meeting.md) | 通过日程事件 ID 获取关联的视频会议信息(meeting_id、meeting_note),日程开过视频会议才会有meeting_id | | [`+create`](references/lark-calendar-create.md) | 创建日程并邀请参会人(ISO 8601 时间) | | [`+update`](references/lark-calendar-update.md) | 更新既有日程字段,或独立增量添加/移除参会人和会议室 | @@ -40,6 +41,17 @@ lark-cli calendar +agenda --as user | [`+rsvp`](references/lark-calendar-rsvp.md) | 回复日程(接受/拒绝/待定) | | [`+suggestion`](references/lark-calendar-suggestion.md) | 根据非明确时间或一段时间范围,推荐多个可用时间块方案 | +### calendar +get + +通过 `calendar_id` + `event_id` 获取**单个日程**的详情。只读,不修改任何数据。 + +## 命令 + +```bash +# calendar_id不传,默认primary +lark-cli calendar +get --calendar-id --event-id +``` + ## 前置条件路由 | 场景 | 前置要求 | diff --git a/skills/lark-calendar/references/lark-calendar-get.md b/skills/lark-calendar/references/lark-calendar-get.md new file mode 100644 index 000000000..38eaa4434 --- /dev/null +++ b/skills/lark-calendar/references/lark-calendar-get.md @@ -0,0 +1,61 @@ + +# calendar +get + +通过 `calendar_id` + `event_id` 获取**单个日程**的详情。只读,不修改任何数据。 + +## 命令 + +```bash +# 主日历(默认primary) +lark-cli calendar +get --event-id + +# 指定日历 +lark-cli calendar +get --calendar-id --event-id + +# 人类可读格式 +lark-cli calendar +get --event-id --format pretty + +# 预览 API 调用,不真实执行 +lark-cli calendar +get --event-id --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--calendar-id ` | 否 | 日历 ID。省略时使用主日历 `primary` | +| `--event-id ` | 是 | 日程 ID。重复性日程的某次实例必须传该实例的 `event_id`,禁止使用原循环日程的 `event_id` | +| `--format` | 否 | 输出格式:`json`(默认) \| `pretty` | +| `--dry-run` | 否 | 只打印将要发起的 API 请求,不真正调用 | + +## 输出字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `event_id` | string | 日程 ID | +| `organizer_calendar_id` | string | 组织者日历 ID | +| `summary` | string | 日程标题 | +| `description` | string | 日程描述 | +| `start_time.datetime` | string | 开始时间,**RFC3339(设备本地时区)**;非全天日程返回 | +| `start_time.date` | string | 开始日期,仅全天日程返回 | +| `start_time.timezone` | string | 时区(IANA),仅非全天日程返回 | +| `end_time.datetime` | string | 结束时间,RFC3339;非全天日程返回 | +| `end_time.date` | string | 结束日期,仅全天日程返回。**已按"包含"语义自动回卷为最后一天** | +| `end_time.timezone` | string | 时区(IANA),仅非全天日程返回 | +| `vchat` | object | 视频会议配置(`vc_type` / `meeting_url` / `icon_type` / `description`) | +| `visibility` | string | 可见性:`default` / `public` / `private` | +| `attendee_ability` | string | 参会人权限:`none` / `can_see_others` / `can_invite_others` / `can_modify_event` | +| `free_busy_status` | string | 忙闲状态:`busy` / `free` | +| `self_rsvp_status` | string | 当前用户的 RSVP:`needs_action` / `accept` / `decline` / `tentative` / `removed` | +| `location` | object | 地点(`name` / `address` / `latitude` / `longitude`) | +| `color` | int | 日程颜色(-1 为继承) | +| `reminders[]` | array | 提醒,单元素 `{minutes}` | +| `recurrence` | string | RRULE 字符串(仅循环日程) | +| `is_exception` | bool | 是否是循环日程的例外实例 | +| `recurring_event_id` | string | 原循环日程 ID(仅例外实例) | +| `create_time` | string | 创建时间,**RFC3339(设备本地时区)** | +| `event_organizer` | object | 创建人(`user_id` / `display_name`) | +| `app_link` | string | 飞书内部唤起链接 | +| `attachments[]` | array | 附件(`file_token` / `file_size` / `name`) | +| `event_check_in` | object | 签到配置 | +| `status` | string | **仅当 `status == "cancelled"` 才返回**,非取消日程会从输出中删除 | diff --git a/tests/cli_e2e/calendar/calendar_get_dryrun_test.go b/tests/cli_e2e/calendar/calendar_get_dryrun_test.go new file mode 100644 index 000000000..45a7d1843 --- /dev/null +++ b/tests/cli_e2e/calendar/calendar_get_dryrun_test.go @@ -0,0 +1,66 @@ +// 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_GetDryRun(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", "+get", + "--calendar-id", "cal_dry", + "--event-id", "evt_dry", + "--dry-run", + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + + out := result.Stdout + require.Equal(t, "GET", 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, "cal_dry", gjson.Get(out, "calendar_id").String(), "stdout:\n%s", out) + require.Equal(t, "evt_dry", gjson.Get(out, "event_id").String(), "stdout:\n%s", out) +} + +func TestCalendar_GetDryRun_DefaultPrimary(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", "+get", + "--event-id", "evt_dry", + "--dry-run", + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + + out := result.Stdout + require.Equal(t, "", gjson.Get(out, "calendar_id").String(), "stdout:\n%s", out) +}