@@ -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\n raw=%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