@@ -693,6 +693,204 @@ func Test_ListIssues(t *testing.T) {
693693 }
694694}
695695
696+ func Test_UpdateIssue (t * testing.T ) {
697+ // Verify tool definition
698+ mockClient := github .NewClient (nil )
699+ tool , _ := updateIssue (mockClient , translations .NullTranslationHelper )
700+
701+ assert .Equal (t , "update_issue" , tool .Name )
702+ assert .NotEmpty (t , tool .Description )
703+ assert .Contains (t , tool .InputSchema .Properties , "owner" )
704+ assert .Contains (t , tool .InputSchema .Properties , "repo" )
705+ assert .Contains (t , tool .InputSchema .Properties , "issue_number" )
706+ assert .Contains (t , tool .InputSchema .Properties , "title" )
707+ assert .Contains (t , tool .InputSchema .Properties , "body" )
708+ assert .Contains (t , tool .InputSchema .Properties , "state" )
709+ assert .Contains (t , tool .InputSchema .Properties , "labels" )
710+ assert .Contains (t , tool .InputSchema .Properties , "assignees" )
711+ assert .Contains (t , tool .InputSchema .Properties , "milestone" )
712+ assert .ElementsMatch (t , tool .InputSchema .Required , []string {"owner" , "repo" , "issue_number" })
713+
714+ // Setup mock issue for success case
715+ mockIssue := & github.Issue {
716+ Number : github .Ptr (123 ),
717+ Title : github .Ptr ("Updated Issue Title" ),
718+ Body : github .Ptr ("Updated issue description" ),
719+ State : github .Ptr ("closed" ),
720+ HTMLURL : github .Ptr ("https://github.com/owner/repo/issues/123" ),
721+ Assignees : []* github.User {{Login : github .Ptr ("assignee1" )}, {Login : github .Ptr ("assignee2" )}},
722+ Labels : []* github.Label {{Name : github .Ptr ("bug" )}, {Name : github .Ptr ("priority" )}},
723+ Milestone : & github.Milestone {Number : github .Ptr (5 )},
724+ }
725+
726+ tests := []struct {
727+ name string
728+ mockedClient * http.Client
729+ requestArgs map [string ]interface {}
730+ expectError bool
731+ expectedIssue * github.Issue
732+ expectedErrMsg string
733+ }{
734+ {
735+ name : "update issue with all fields" ,
736+ mockedClient : mock .NewMockedHTTPClient (
737+ mock .WithRequestMatchHandler (
738+ mock .PatchReposIssuesByOwnerByRepoByIssueNumber ,
739+ mockResponse (t , http .StatusOK , mockIssue ),
740+ ),
741+ ),
742+ requestArgs : map [string ]interface {}{
743+ "owner" : "owner" ,
744+ "repo" : "repo" ,
745+ "issue_number" : float64 (123 ),
746+ "title" : "Updated Issue Title" ,
747+ "body" : "Updated issue description" ,
748+ "state" : "closed" ,
749+ "labels" : "bug,priority" ,
750+ "assignees" : "assignee1,assignee2" ,
751+ "milestone" : float64 (5 ),
752+ },
753+ expectError : false ,
754+ expectedIssue : mockIssue ,
755+ },
756+ {
757+ name : "update issue with minimal fields" ,
758+ mockedClient : mock .NewMockedHTTPClient (
759+ mock .WithRequestMatchHandler (
760+ mock .PatchReposIssuesByOwnerByRepoByIssueNumber ,
761+ mockResponse (t , http .StatusOK , & github.Issue {
762+ Number : github .Ptr (123 ),
763+ Title : github .Ptr ("Only Title Updated" ),
764+ HTMLURL : github .Ptr ("https://github.com/owner/repo/issues/123" ),
765+ State : github .Ptr ("open" ),
766+ }),
767+ ),
768+ ),
769+ requestArgs : map [string ]interface {}{
770+ "owner" : "owner" ,
771+ "repo" : "repo" ,
772+ "issue_number" : float64 (123 ),
773+ "title" : "Only Title Updated" ,
774+ },
775+ expectError : false ,
776+ expectedIssue : & github.Issue {
777+ Number : github .Ptr (123 ),
778+ Title : github .Ptr ("Only Title Updated" ),
779+ HTMLURL : github .Ptr ("https://github.com/owner/repo/issues/123" ),
780+ State : github .Ptr ("open" ),
781+ },
782+ },
783+ {
784+ name : "update issue fails with not found" ,
785+ mockedClient : mock .NewMockedHTTPClient (
786+ mock .WithRequestMatchHandler (
787+ mock .PatchReposIssuesByOwnerByRepoByIssueNumber ,
788+ http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
789+ w .WriteHeader (http .StatusNotFound )
790+ _ , _ = w .Write ([]byte (`{"message": "Issue not found"}` ))
791+ }),
792+ ),
793+ ),
794+ requestArgs : map [string ]interface {}{
795+ "owner" : "owner" ,
796+ "repo" : "repo" ,
797+ "issue_number" : float64 (999 ),
798+ "title" : "This issue doesn't exist" ,
799+ },
800+ expectError : true ,
801+ expectedErrMsg : "failed to update issue" ,
802+ },
803+ {
804+ name : "update issue fails with validation error" ,
805+ mockedClient : mock .NewMockedHTTPClient (
806+ mock .WithRequestMatchHandler (
807+ mock .PatchReposIssuesByOwnerByRepoByIssueNumber ,
808+ http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
809+ w .WriteHeader (http .StatusUnprocessableEntity )
810+ _ , _ = w .Write ([]byte (`{"message": "Invalid state value"}` ))
811+ }),
812+ ),
813+ ),
814+ requestArgs : map [string ]interface {}{
815+ "owner" : "owner" ,
816+ "repo" : "repo" ,
817+ "issue_number" : float64 (123 ),
818+ "state" : "invalid_state" ,
819+ },
820+ expectError : true ,
821+ expectedErrMsg : "failed to update issue" ,
822+ },
823+ }
824+
825+ for _ , tc := range tests {
826+ t .Run (tc .name , func (t * testing.T ) {
827+ // Setup client with mock
828+ client := github .NewClient (tc .mockedClient )
829+ _ , handler := updateIssue (client , translations .NullTranslationHelper )
830+
831+ // Create call request
832+ request := createMCPRequest (tc .requestArgs )
833+
834+ // Call handler
835+ result , err := handler (context .Background (), request )
836+
837+ // Verify results
838+ if tc .expectError {
839+ if err != nil {
840+ assert .Contains (t , err .Error (), tc .expectedErrMsg )
841+ } else {
842+ // For errors returned as part of the result, not as an error
843+ require .NotNil (t , result )
844+ textContent := getTextResult (t , result )
845+ assert .Contains (t , textContent .Text , tc .expectedErrMsg )
846+ }
847+ return
848+ }
849+
850+ require .NoError (t , err )
851+
852+ // Parse the result and get the text content if no error
853+ textContent := getTextResult (t , result )
854+
855+ // Unmarshal and verify the result
856+ var returnedIssue github.Issue
857+ err = json .Unmarshal ([]byte (textContent .Text ), & returnedIssue )
858+ require .NoError (t , err )
859+
860+ assert .Equal (t , * tc .expectedIssue .Number , * returnedIssue .Number )
861+ assert .Equal (t , * tc .expectedIssue .Title , * returnedIssue .Title )
862+ assert .Equal (t , * tc .expectedIssue .State , * returnedIssue .State )
863+ assert .Equal (t , * tc .expectedIssue .HTMLURL , * returnedIssue .HTMLURL )
864+
865+ if tc .expectedIssue .Body != nil {
866+ assert .Equal (t , * tc .expectedIssue .Body , * returnedIssue .Body )
867+ }
868+
869+ // Check assignees if expected
870+ if tc .expectedIssue .Assignees != nil && len (tc .expectedIssue .Assignees ) > 0 {
871+ assert .Len (t , returnedIssue .Assignees , len (tc .expectedIssue .Assignees ))
872+ for i , assignee := range returnedIssue .Assignees {
873+ assert .Equal (t , * tc .expectedIssue .Assignees [i ].Login , * assignee .Login )
874+ }
875+ }
876+
877+ // Check labels if expected
878+ if tc .expectedIssue .Labels != nil && len (tc .expectedIssue .Labels ) > 0 {
879+ assert .Len (t , returnedIssue .Labels , len (tc .expectedIssue .Labels ))
880+ for i , label := range returnedIssue .Labels {
881+ assert .Equal (t , * tc .expectedIssue .Labels [i ].Name , * label .Name )
882+ }
883+ }
884+
885+ // Check milestone if expected
886+ if tc .expectedIssue .Milestone != nil {
887+ assert .NotNil (t , returnedIssue .Milestone )
888+ assert .Equal (t , * tc .expectedIssue .Milestone .Number , * returnedIssue .Milestone .Number )
889+ }
890+ })
891+ }
892+ }
893+
696894func Test_ParseISOTimestamp (t * testing.T ) {
697895 tests := []struct {
698896 name string
0 commit comments