@@ -5,11 +5,14 @@ import (
55 "encoding/json"
66 "net/http"
77 "testing"
8+ "time"
89
10+ "github.com/github/github-mcp-server/internal/githubv4mock"
911 "github.com/github/github-mcp-server/internal/toolsnaps"
1012 "github.com/github/github-mcp-server/pkg/translations"
1113 "github.com/google/go-github/v87/github"
1214 "github.com/google/jsonschema-go/jsonschema"
15+ "github.com/shurcooL/githubv4"
1316 "github.com/stretchr/testify/assert"
1417 "github.com/stretchr/testify/require"
1518)
@@ -46,13 +49,25 @@ func Test_UIGet(t *testing.T) {
4649 {Name : github .Ptr ("feature" ), Protected : github .Ptr (false )},
4750 }
4851
52+ dueDate := time .Date (2026 , 1 , 31 , 0 , 0 , 0 , 0 , time .UTC )
53+ mockMilestones := []* github.Milestone {
54+ {Number : github .Ptr (1 ), Title : github .Ptr ("with due date" ), DueOn : & github.Timestamp {Time : dueDate }},
55+ {Number : github .Ptr (2 ), Title : github .Ptr ("no due date" )},
56+ }
57+
58+ mockIssueTypes := []* github.IssueType {
59+ {Name : github .Ptr ("Bug" )},
60+ {Name : github .Ptr ("Feature" )},
61+ }
62+
4963 tests := []struct {
50- name string
51- mockedClient * http.Client
52- requestArgs map [string ]any
53- expectError bool
54- expectedErrMsg string
55- validateResult func (t * testing.T , response map [string ]any )
64+ name string
65+ mockedClient * http.Client
66+ mockedGQLClient * http.Client
67+ requestArgs map [string ]any
68+ expectError bool
69+ expectedErrMsg string
70+ validateResult func (t * testing.T , responseText string )
5671 }{
5772 {
5873 name : "successful assignees fetch" ,
@@ -65,7 +80,9 @@ func Test_UIGet(t *testing.T) {
6580 "repo" : "repo" ,
6681 },
6782 expectError : false ,
68- validateResult : func (t * testing.T , response map [string ]any ) {
83+ validateResult : func (t * testing.T , responseText string ) {
84+ var response map [string ]any
85+ require .NoError (t , json .Unmarshal ([]byte (responseText ), & response ))
6986 assert .Contains (t , response , "assignees" )
7087 assert .Contains (t , response , "totalCount" )
7188 },
@@ -81,11 +98,128 @@ func Test_UIGet(t *testing.T) {
8198 "repo" : "repo" ,
8299 },
83100 expectError : false ,
84- validateResult : func (t * testing.T , response map [string ]any ) {
101+ validateResult : func (t * testing.T , responseText string ) {
102+ var response map [string ]any
103+ require .NoError (t , json .Unmarshal ([]byte (responseText ), & response ))
85104 assert .Contains (t , response , "branches" )
86105 assert .Contains (t , response , "totalCount" )
87106 },
88107 },
108+ {
109+ name : "successful milestones fetch" ,
110+ mockedClient : MockHTTPClientWithHandlers (map [string ]http.HandlerFunc {
111+ "GET /repos/owner/repo/milestones" : mockResponse (t , http .StatusOK , mockMilestones ),
112+ }),
113+ requestArgs : map [string ]any {
114+ "method" : "milestones" ,
115+ "owner" : "owner" ,
116+ "repo" : "repo" ,
117+ },
118+ expectError : false ,
119+ validateResult : func (t * testing.T , responseText string ) {
120+ var response map [string ]any
121+ require .NoError (t , json .Unmarshal ([]byte (responseText ), & response ))
122+ milestones , ok := response ["milestones" ].([]any )
123+ require .True (t , ok , "milestones should be a list" )
124+ require .Len (t , milestones , 2 )
125+ first := milestones [0 ].(map [string ]any )
126+ assert .Equal (t , "2026-01-31" , first ["due_on" ], "milestone with a due date should be formatted" )
127+ second := milestones [1 ].(map [string ]any )
128+ assert .Equal (t , "" , second ["due_on" ], "milestone without a due date should be empty, not zero time" )
129+ },
130+ },
131+ {
132+ name : "successful issue_types fetch" ,
133+ mockedClient : MockHTTPClientWithHandlers (map [string ]http.HandlerFunc {
134+ "GET /orgs/owner/issue-types" : mockResponse (t , http .StatusOK , mockIssueTypes ),
135+ }),
136+ requestArgs : map [string ]any {
137+ "method" : "issue_types" ,
138+ "owner" : "owner" ,
139+ },
140+ expectError : false ,
141+ validateResult : func (t * testing.T , responseText string ) {
142+ var issueTypes []map [string ]any
143+ require .NoError (t , json .Unmarshal ([]byte (responseText ), & issueTypes ))
144+ require .Len (t , issueTypes , 2 )
145+ assert .Equal (t , "Bug" , issueTypes [0 ]["name" ])
146+ },
147+ },
148+ {
149+ name : "issue_types API error returns response context" ,
150+ mockedClient : MockHTTPClientWithHandlers (map [string ]http.HandlerFunc {
151+ "GET /orgs/owner/issue-types" : mockResponse (t , http .StatusForbidden , map [string ]string {"message" : "Forbidden" }),
152+ }),
153+ requestArgs : map [string ]any {
154+ "method" : "issue_types" ,
155+ "owner" : "owner" ,
156+ },
157+ expectError : true ,
158+ expectedErrMsg : "failed to list issue types" ,
159+ },
160+ {
161+ name : "successful labels fetch" ,
162+ mockedGQLClient : githubv4mock .NewMockedHTTPClient (
163+ githubv4mock .NewQueryMatcher (
164+ struct {
165+ Repository struct {
166+ Labels struct {
167+ Nodes []struct {
168+ ID githubv4.ID
169+ Name githubv4.String
170+ Color githubv4.String
171+ Description githubv4.String
172+ }
173+ TotalCount githubv4.Int
174+ PageInfo struct {
175+ HasNextPage githubv4.Boolean
176+ EndCursor githubv4.String
177+ }
178+ } `graphql:"labels(first: 100, after: $cursor)"`
179+ } `graphql:"repository(owner: $owner, name: $repo)"`
180+ }{},
181+ map [string ]any {
182+ "owner" : githubv4 .String ("owner" ),
183+ "repo" : githubv4 .String ("repo" ),
184+ "cursor" : (* githubv4 .String )(nil ),
185+ },
186+ githubv4mock .DataResponse (map [string ]any {
187+ "repository" : map [string ]any {
188+ "labels" : map [string ]any {
189+ "nodes" : []any {
190+ map [string ]any {
191+ "id" : githubv4 .ID ("label-1" ),
192+ "name" : githubv4 .String ("bug" ),
193+ "color" : githubv4 .String ("d73a4a" ),
194+ "description" : githubv4 .String ("Something isn't working" ),
195+ },
196+ },
197+ "totalCount" : githubv4 .Int (1 ),
198+ "pageInfo" : map [string ]any {
199+ "hasNextPage" : githubv4 .Boolean (false ),
200+ "endCursor" : githubv4 .String ("" ),
201+ },
202+ },
203+ },
204+ }),
205+ ),
206+ ),
207+ requestArgs : map [string ]any {
208+ "method" : "labels" ,
209+ "owner" : "owner" ,
210+ "repo" : "repo" ,
211+ },
212+ expectError : false ,
213+ validateResult : func (t * testing.T , responseText string ) {
214+ var response map [string ]any
215+ require .NoError (t , json .Unmarshal ([]byte (responseText ), & response ))
216+ labels , ok := response ["labels" ].([]any )
217+ require .True (t , ok , "labels should be a list" )
218+ require .Len (t , labels , 1 )
219+ assert .Equal (t , "bug" , labels [0 ].(map [string ]any )["name" ])
220+ assert .Equal (t , float64 (1 ), response ["totalCount" ])
221+ },
222+ },
89223 {
90224 name : "missing method parameter" ,
91225 mockedClient : MockHTTPClientWithHandlers (map [string ]http.HandlerFunc {}),
@@ -131,11 +265,15 @@ func Test_UIGet(t *testing.T) {
131265
132266 for _ , tc := range tests {
133267 t .Run (tc .name , func (t * testing.T ) {
134- // Setup client with mock
135- client , err := github .NewClient (github .WithHTTPClient (tc .mockedClient ))
136- require .NoError (t , err )
137- deps := BaseDeps {
138- Client : client ,
268+ // Setup deps with REST and/or GraphQL mocks
269+ deps := BaseDeps {}
270+ if tc .mockedClient != nil {
271+ client , err := github .NewClient (github .WithHTTPClient (tc .mockedClient ))
272+ require .NoError (t , err )
273+ deps .Client = client
274+ }
275+ if tc .mockedGQLClient != nil {
276+ deps .GQLClient = githubv4 .NewClient (tc .mockedGQLClient )
139277 }
140278 handler := serverTool .Handler (deps )
141279
@@ -163,12 +301,8 @@ func Test_UIGet(t *testing.T) {
163301 require .False (t , result .IsError )
164302 textContent := getTextResult (t , result )
165303
166- var response map [string ]any
167- err = json .Unmarshal ([]byte (textContent .Text ), & response )
168- require .NoError (t , err )
169-
170304 if tc .validateResult != nil {
171- tc .validateResult (t , response )
305+ tc .validateResult (t , textContent . Text )
172306 }
173307 })
174308 }
0 commit comments