Skip to content

Commit 78d92de

Browse files
feat: add calendar update shortcut (#678)
Change-Id: Ie2d4bde6cd28bbf4d7946db38c5c9be13edc6ba9
1 parent 8ec95a4 commit 78d92de

8 files changed

Lines changed: 1052 additions & 31 deletions

File tree

shortcuts/calendar/calendar_test.go

Lines changed: 263 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,260 @@ func TestCreate_WithAttendees_InvalidParamsWithDetail_RollsBack(t *testing.T) {
607607
}
608608
}
609609

610+
// ---------------------------------------------------------------------------
611+
// CalendarUpdate tests
612+
// ---------------------------------------------------------------------------
613+
614+
func TestUpdate_PatchEventOnly(t *testing.T) {
615+
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
616+
617+
stub := &httpmock.Stub{
618+
Method: "PATCH",
619+
URL: "/open-apis/calendar/v4/calendars/cal_test123/events/evt_update1",
620+
Body: map[string]interface{}{
621+
"code": 0, "msg": "ok",
622+
"data": map[string]interface{}{
623+
"event": map[string]interface{}{
624+
"event_id": "evt_update1",
625+
"summary": "Updated Meeting",
626+
"start_time": map[string]interface{}{
627+
"timestamp": "1742518800",
628+
},
629+
"end_time": map[string]interface{}{
630+
"timestamp": "1742522400",
631+
},
632+
},
633+
},
634+
},
635+
}
636+
reg.Register(stub)
637+
638+
err := mountAndRun(t, CalendarUpdate, []string{
639+
"+update",
640+
"--event-id", "evt_update1",
641+
"--calendar-id", "cal_test123",
642+
"--summary", "Updated Meeting",
643+
"--description", "Updated description",
644+
"--start", "2025-03-21T01:00:00+08:00",
645+
"--end", "2025-03-21T02:00:00+08:00",
646+
"--notify=false",
647+
"--as", "bot",
648+
}, f, stdout)
649+
650+
if err != nil {
651+
t.Fatalf("unexpected error: %v", err)
652+
}
653+
var body map[string]interface{}
654+
if err := json.Unmarshal(stub.CapturedBody, &body); err != nil {
655+
t.Fatalf("unmarshal captured patch body: %v", err)
656+
}
657+
if body["summary"] != "Updated Meeting" || body["description"] != "Updated description" {
658+
t.Fatalf("unexpected patch body: %#v", body)
659+
}
660+
if body["need_notification"] != false {
661+
t.Fatalf("need_notification = %#v, want false", body["need_notification"])
662+
}
663+
if !strings.Contains(stdout.String(), "evt_update1") {
664+
t.Fatalf("stdout should contain event id, got: %s", stdout.String())
665+
}
666+
}
667+
668+
func TestUpdate_AddAttendees(t *testing.T) {
669+
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
670+
671+
stub := &httpmock.Stub{
672+
Method: "POST",
673+
URL: "/open-apis/calendar/v4/calendars/cal_test123/events/evt_update2/attendees",
674+
Body: map[string]interface{}{"code": 0, "msg": "ok", "data": map[string]interface{}{}},
675+
}
676+
reg.Register(stub)
677+
678+
err := mountAndRun(t, CalendarUpdate, []string{
679+
"+update",
680+
"--event-id", "evt_update2",
681+
"--calendar-id", "cal_test123",
682+
"--add-attendee-ids", "ou_user1,oc_group1,omm_room1",
683+
"--as", "bot",
684+
}, f, nil)
685+
686+
if err != nil {
687+
t.Fatalf("unexpected error: %v", err)
688+
}
689+
body := decodeCalendarCapturedBody(t, stub)
690+
attendees, _ := body["attendees"].([]interface{})
691+
if !calendarBodyHasAttendee(attendees, "user", "user_id", "ou_user1") ||
692+
!calendarBodyHasAttendee(attendees, "chat", "chat_id", "oc_group1") ||
693+
!calendarBodyHasAttendee(attendees, "resource", "room_id", "omm_room1") {
694+
t.Fatalf("unexpected add attendees body: %#v", body)
695+
}
696+
}
697+
698+
func TestUpdate_RemoveAttendees(t *testing.T) {
699+
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
700+
701+
stub := &httpmock.Stub{
702+
Method: "POST",
703+
URL: "/open-apis/calendar/v4/calendars/cal_test123/events/evt_update3/attendees/batch_delete",
704+
Body: map[string]interface{}{"code": 0, "msg": "ok", "data": map[string]interface{}{}},
705+
}
706+
reg.Register(stub)
707+
708+
err := mountAndRun(t, CalendarUpdate, []string{
709+
"+update",
710+
"--event-id", "evt_update3",
711+
"--calendar-id", "cal_test123",
712+
"--remove-attendee-ids", "ou_user1,oc_group1,omm_room1",
713+
"--notify=false",
714+
"--as", "bot",
715+
}, f, nil)
716+
717+
if err != nil {
718+
t.Fatalf("unexpected error: %v", err)
719+
}
720+
body := decodeCalendarCapturedBody(t, stub)
721+
deleteIDs, _ := body["delete_ids"].([]interface{})
722+
if body["need_notification"] != false {
723+
t.Fatalf("need_notification = %#v, want false", body["need_notification"])
724+
}
725+
if !calendarBodyHasAttendee(deleteIDs, "user", "user_id", "ou_user1") ||
726+
!calendarBodyHasAttendee(deleteIDs, "chat", "chat_id", "oc_group1") ||
727+
!calendarBodyHasAttendee(deleteIDs, "resource", "room_id", "omm_room1") {
728+
t.Fatalf("unexpected remove attendees body: %#v", body)
729+
}
730+
}
731+
732+
func TestUpdate_CombinedPatchRemoveAdd(t *testing.T) {
733+
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
734+
735+
patchStub := &httpmock.Stub{
736+
Method: "PATCH",
737+
URL: "/events/evt_update4",
738+
Body: map[string]interface{}{
739+
"code": 0, "msg": "ok",
740+
"data": map[string]interface{}{"event": map[string]interface{}{"event_id": "evt_update4", "summary": "Combined"}},
741+
},
742+
}
743+
removeStub := &httpmock.Stub{
744+
Method: "POST",
745+
URL: "/events/evt_update4/attendees/batch_delete",
746+
Body: map[string]interface{}{"code": 0, "msg": "ok", "data": map[string]interface{}{}},
747+
}
748+
addStub := &httpmock.Stub{
749+
Method: "POST",
750+
URL: "/events/evt_update4/attendees",
751+
Body: map[string]interface{}{"code": 0, "msg": "ok", "data": map[string]interface{}{}},
752+
}
753+
reg.Register(patchStub)
754+
reg.Register(removeStub)
755+
reg.Register(addStub)
756+
757+
err := mountAndRun(t, CalendarUpdate, []string{
758+
"+update",
759+
"--event-id", "evt_update4",
760+
"--summary", "Combined",
761+
"--remove-attendee-ids", "ou_old",
762+
"--add-attendee-ids", "ou_new",
763+
"--as", "bot",
764+
}, f, nil)
765+
766+
if err != nil {
767+
t.Fatalf("unexpected error: %v", err)
768+
}
769+
if len(patchStub.CapturedBody) == 0 || len(removeStub.CapturedBody) == 0 || len(addStub.CapturedBody) == 0 {
770+
t.Fatalf("expected patch, remove, and add requests to be captured")
771+
}
772+
}
773+
774+
func TestUpdate_DryRun_MultiStep(t *testing.T) {
775+
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
776+
777+
err := mountAndRun(t, CalendarUpdate, []string{
778+
"+update",
779+
"--event-id", "evt_dry",
780+
"--calendar-id", "cal_test123",
781+
"--summary", "Dry",
782+
"--remove-attendee-ids", "omm_oldroom",
783+
"--add-attendee-ids", "ou_new,omm_newroom",
784+
"--dry-run",
785+
"--as", "bot",
786+
}, f, stdout)
787+
788+
if err != nil {
789+
t.Fatalf("unexpected error: %v", err)
790+
}
791+
out := stdout.String()
792+
for _, want := range []string{"PATCH", "batch_delete", "attendees", "omm_oldroom", "omm_newroom"} {
793+
if !strings.Contains(out, want) {
794+
t.Fatalf("dry-run should contain %q, got: %s", want, out)
795+
}
796+
}
797+
}
798+
799+
func TestUpdate_Validation(t *testing.T) {
800+
cases := []struct {
801+
name string
802+
args []string
803+
want string
804+
}{
805+
{
806+
name: "no fields",
807+
args: []string{"+update", "--event-id", "evt_1", "--as", "bot"},
808+
want: "nothing to update",
809+
},
810+
{
811+
name: "invalid attendee",
812+
args: []string{"+update", "--event-id", "evt_1", "--add-attendee-ids", "bad", "--as", "bot"},
813+
want: "invalid attendee id format",
814+
},
815+
{
816+
name: "duplicate add remove",
817+
args: []string{"+update", "--event-id", "evt_1", "--add-attendee-ids", "ou_same", "--remove-attendee-ids", "ou_same", "--as", "bot"},
818+
want: "appears in both",
819+
},
820+
{
821+
name: "start without end",
822+
args: []string{"+update", "--event-id", "evt_1", "--start", "2025-03-21T00:00:00+08:00", "--as", "bot"},
823+
want: "must be specified together",
824+
},
825+
{
826+
name: "end before start",
827+
args: []string{"+update", "--event-id", "evt_1", "--start", "2025-03-21T10:00:00+08:00", "--end", "2025-03-21T09:00:00+08:00", "--as", "bot"},
828+
want: "end time must be after start time",
829+
},
830+
}
831+
for _, tc := range cases {
832+
t.Run(tc.name, func(t *testing.T) {
833+
f, _, _, _ := cmdutil.TestFactory(t, defaultConfig())
834+
err := mountAndRun(t, CalendarUpdate, tc.args, f, nil)
835+
if err == nil {
836+
t.Fatal("expected validation error, got nil")
837+
}
838+
if !strings.Contains(err.Error(), tc.want) {
839+
t.Fatalf("expected error containing %q, got %v", tc.want, err)
840+
}
841+
})
842+
}
843+
}
844+
845+
func decodeCalendarCapturedBody(t *testing.T, stub *httpmock.Stub) map[string]interface{} {
846+
t.Helper()
847+
var body map[string]interface{}
848+
if err := json.Unmarshal(stub.CapturedBody, &body); err != nil {
849+
t.Fatalf("unmarshal captured body: %v\nraw=%s", err, string(stub.CapturedBody))
850+
}
851+
return body
852+
}
853+
854+
func calendarBodyHasAttendee(items []interface{}, typ, key, value string) bool {
855+
for _, item := range items {
856+
m, _ := item.(map[string]interface{})
857+
if m["type"] == typ && m[key] == value {
858+
return true
859+
}
860+
}
861+
return false
862+
}
863+
610864
// ---------------------------------------------------------------------------
611865
// CalendarAgenda tests
612866
// ---------------------------------------------------------------------------
@@ -627,6 +881,11 @@ func TestCalendarShortcuts_RequireLoginUnlessExplicitBot(t *testing.T) {
627881
shortcut: CalendarCreate,
628882
args: []string{"+create", "--summary", "Test Meeting", "--start", "2025-03-21T00:00:00+08:00", "--end", "2025-03-21T01:00:00+08:00"},
629883
},
884+
{
885+
name: "update",
886+
shortcut: CalendarUpdate,
887+
args: []string{"+update", "--event-id", "evt_1", "--summary", "Updated"},
888+
},
630889
{
631890
name: "freebusy",
632891
shortcut: CalendarFreebusy,
@@ -1710,17 +1969,17 @@ func TestResolveStartEnd_ExplicitValues(t *testing.T) {
17101969
// Shortcuts() registration test
17111970
// ---------------------------------------------------------------------------
17121971

1713-
func TestShortcuts_Returns6(t *testing.T) {
1972+
func TestShortcuts_Returns7(t *testing.T) {
17141973
shortcuts := Shortcuts()
1715-
if len(shortcuts) != 6 {
1716-
t.Fatalf("expected 6 shortcuts, got %d", len(shortcuts))
1974+
if len(shortcuts) != 7 {
1975+
t.Fatalf("expected 7 shortcuts, got %d", len(shortcuts))
17171976
}
17181977

17191978
names := map[string]bool{}
17201979
for _, s := range shortcuts {
17211980
names[s.Command] = true
17221981
}
1723-
for _, want := range []string{"+agenda", "+create", "+freebusy", "+room-find", "+rsvp", "+suggestion"} {
1982+
for _, want := range []string{"+agenda", "+create", "+update", "+freebusy", "+room-find", "+rsvp", "+suggestion"} {
17241983
if !names[want] {
17251984
t.Errorf("missing shortcut %s", want)
17261985
}

0 commit comments

Comments
 (0)