8000 add initial tests · github/github-mcp-server@3dddc2a · GitHub
[go: up one dir, main page]

Skip to content

Commit 3dddc2a

Browse files
committed
add initial tests
1 parent 09366fa commit 3dddc2a

14 files changed

+3203
-20
lines changed

go.mod

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,27 @@ require (
66
github.com/aws/smithy-go v1.22.3
77
github.com/google/go-github/v69 v69.2.0
88
github.com/mark3labs/mcp-go v0.11.2
9+
github.com/migueleliasweb/go-github-mock v1.1.0
910
github.com/sirupsen/logrus v1.9.3
1011
github.com/spf13/cobra v1.9.1
1112
github.com/spf13/viper v1.19.0
13+
github.com/stretchr/testify v1.9.0
1214
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
1315
)
1416

1517
require (
18+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
1619
github.com/fsnotify/fsnotify v1.7.0 // indirect
20+
github.com/google/go-github/v64 v64.0.0 // indirect
1721
github.com/google/go-querystring v1.1.0 // indirect
1822
github.com/google/uuid v1.6.0 // indirect
23+
github.com/gorilla/mux v1.8.0 // indirect
1924
github.com/hashicorp/hcl v1.0.0 // indirect
2025
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2126
github.com/magiconair/properties v1.8.7 // indirect
2227
github.com/mitchellh/mapstructure v1.5.0 // indirect
2328
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
29+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
2430
github.com/sagikazarmark/locafero v0.4.0 // indirect
2531
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
2632
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -31,7 +37,8 @@ require (
3137
go.uber.org/atomic v1.9.0 // indirect
3238
go.uber.org/multierr v1.9.0 // indirect
3339
golang.org/x/sys v0.18.0 // indirect
34-
golang.org/x/text v0.14.0 // indirect
40+
golang.org/x/text v0.19.0 // indirect
41+
golang.org/x/time v0.5.0 // indirect
3542
gopkg.in/ini.v1 v1.67.0 // indirect
3643
gopkg.in/yaml.v3 v3.0.1 // indirect
3744
)

go.sum

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyT
1212
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
1313
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
1414
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
15+
github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg=
16+
github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo=
1517
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
1618
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
1719
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
1820
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
1921
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2022
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
23+
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
24+
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
2125
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
2226
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
2327
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -30,6 +34,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
3034
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
3135
github.com/mark3labs/mcp-go v0.11.2 h1:mCxWFUTrcXOtJIn9t7F8bxAL8rpE/ZZTTnx3PU/VNdA=
3236
github.com/mark3labs/mcp-go v0.11.2/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
37+
github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE=
38+
github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc=
3339
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
3440
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
3541
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
@@ -80,8 +86,10 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqR
8086
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
8187
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
8288
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
83-
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
84-
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
89+
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
90+
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
91+
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
92+
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
8593
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
8694
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
8795
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=

pkg/github/code_scanning.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"net/http"
89

910
"github.com/google/go-github/v69/github"
1011
"github.com/mark3labs/mcp-go/mcp"
@@ -38,7 +39,7 @@ func getCodeScanningAlert(client *github.Client) (tool mcp.Tool, handler server.
3839
}
3940
defer func() { _ = resp.Body.Close() }()
4041

41-
if resp.StatusCode != 200 {
42+
if resp.StatusCode != http.StatusOK {
4243
body, err := io.ReadAll(resp.Body)
4344
if err != nil {
4445
return nil, fmt.Errorf("failed to read response body: %w", err)
@@ -90,7 +91,7 @@ func listCodeScanningAlerts(client *github.Client) (tool mcp.Tool, handler serve
9091
}
9192
defer func() { _ = resp.Body.Close() }()
9293

93-
if resp.StatusCode != 200 {
94+
if resp.StatusCode != http.StatusOK {
9495
body, err := io.ReadAll(resp.Body)
9596
if err != nil {
9697
return nil, fmt.Errorf("failed to read response body: %w", err)

pkg/github/code_scanning_test.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/google/go-github/v69/github"
10+
"github.com/migueleliasweb/go-github-mock/src/mock"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func Test_GetCodeScanningAlert(t *testing.T) {
16+
// Verify tool definition once
17+
mockClient := github.NewClient(nil)
18+
tool, _ := getCodeScanningAlert(mockClient)
19+
20+
assert.Equal(t, "get_code_scanning_alert", tool.Name)
21+
assert.NotEmpty(t, tool.Description)
22+
assert.Contains(t, tool.InputSchema.Properties, "owner")
23+
assert.Contains(t, tool.InputSchema.Properties, "repo")
24+
assert.Contains(t, tool.InputSchema.Properties, "alert_number")
25+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "alert_number"})
26+
27+
// Setup mock alert for success case
28+
mockAlert := &github.Alert{
29+
Number: github.Ptr(42),
30+
State: github.Ptr("open"),
31+
Rule: &github.Rule{ID: github.Ptr("test-rule"), Description: github.Ptr("Test Rule Description")},
32+
HTMLURL: github.Ptr("https://github.com/owner/repo/security/code-scanning/42"),
33+
}
34+
35+
tests := []struct {
36+
name string
37+
mockedClient *http.Client
38+
requestArgs map[string]interface{}
39+
expectError bool
40+
expectedAlert *github.Alert
41+
expectedErrMsg string
42+
}{
43+
{
44+
name: "successful alert fetch",
45+
mockedClient: mock.NewMockedHTTPClient(
46+
mock.WithRequestMatch(
47+
mock.GetReposCodeScanningAlertsByOwnerByRepoByAlertNumber,
48+
mockAlert,
49+
),
50+
),
51+
requestArgs: map[string]interface{}{
52+
"owner": "owner",
53+
"repo": "repo",
54+
"alert_number": float64(42),
55+
},
56+
expectError: false,
57+
expectedAlert: mockAlert,
58+
},
59+
{
60+
name: "alert fetch fails",
61+
mockedClient: mock.NewMockedHTTPClient(
62+
mock.WithRequestMatchHandler(
63+
mock.GetReposCodeScanningAlertsByOwnerByRepoByAlertNumber,
64+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65+
w.WriteHeader(http.StatusNotFound)
66+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
67+
}),
68+
),
69+
),
70+
requestArgs: map[string]interface{}{
71+
"owner": "owner",
72+
"repo": "repo",
73+
"alert_number": float64(9999),
74+
},
75+
expectError: true,
76+
expectedErrMsg: "failed to get alert",
77+
},
78+
}
79+
80+
for _, tc := range tests {
81+
t.Run(tc.name, func(t *testing.T) {
82+
// Setup client with mock
83+
client := github.NewClient(tc.mockedClient)
84+
_, handler := getCodeScanningAlert(client)
85+
86+
// Create call request
87+
request := createMCPRequest(tc.requestArgs)
88+
89+
// Call handler
90+
result, err := handler(context.Background(), request)
91+
92+
// Verify results
93+
if tc.expectError {
94+
require.Error(t, err)
95+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
96+
return
97+
}
98+
99+
require.NoError(t, err)
100+
101+
// Parse the result and get the text content if no error
102+
textContent := getTextResult(t, result)
103+
104+
// Unmarshal and verify the result
105+
var returnedAlert github.Alert
106+
err = json.Unmarshal([]byte(textContent.Text), &returnedAlert)
107+
assert.NoError(t, err)
108+
assert.Equal(t, *tc.expectedAlert.Number, *returnedAlert.Number)
109+
assert.Equal(t, *tc.expectedAlert.State, *returnedAlert.State)
110+
assert.Equal(t, *tc.expectedAlert.Rule.ID, *returnedAlert.Rule.ID)
111+
assert.Equal(t, *tc.expectedAlert.HTMLURL, *returnedAlert.HTMLURL)
112+
113+
})
114+
}
115+
}
116+
117+
func Test_ListCodeScanningAlerts(t *testing.T) {
118+
// Verify tool definition once
119+
mockClient := github.NewClient(nil)
120+
tool, _ := listCodeScanningAlerts(mockClient)
121+
122+
assert.Equal(t, "list_code_scanning_alerts", tool.Name)
123+
assert.NotEmpty(t, tool.Description)
124+
assert.Contains(t, tool.InputSchema.Properties, "owner")
125+
assert.Contains(t, tool.InputSchema.Properties, "repo")
126+
assert.Contains(t, tool.InputSchema.Properties, "ref")
127+
assert.Contains(t, tool.InputSchema.Properties, "state")
128+
assert.Contains(t, tool.InputSchema.Properties, "severity")
129+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
130+
131+
// Setup mock alerts for success case
132+
mockAlerts := []*github.Alert{
133+
{
134+
Number: github.Ptr(42),
135+
State: github.Ptr("open"),
136+
Rule: &github.Rule{ID: github.Ptr("test-rule-1"), Description: github.Ptr("Test Rule 1")},
137+
HTMLURL: github.Ptr("https://github.com/owner/repo/security/code-scanning/42"),
138+
},
139+
{
140+
Number: github.Ptr(43),
141+
State: github.Ptr("fixed"),
142+
Rule: &github.Rule{ID: github.Ptr("test-rule-2"), Description: github.Ptr("Test Rule 2")},
143+
HTMLURL: github.Ptr("https://github.com/owner/repo/security/code-scanning/43"),
144+
},
145+
}
146+
147+
tests := []struct {
148+
name string
149+
mockedClient *http.Client
150+
requestArgs map[string]interface{}
151+
expectError bool
152+
expectedAlerts []*github.Alert
153+
expectedErrMsg string
154+
}{
155+
{
156+
name: "successful alerts listing",
157+
mockedClient: mock.NewMockedHTTPClient(
158+
mock.WithRequestMatch(
159+
mock.GetReposCodeScanningAlertsByOwnerByRepo,
160+
mockAlerts,
161+
),
162+
),
163+
requestArgs: map[string]interface{}{
164+
"owner": "owner",
165+
"repo": "repo",
166+
"ref": "main",
167+
"state": "open",
168+
"severity": "high",
169+
},
170+
expectError: false,
171+
expectedAlerts: mockAlerts,
172+
},
173+
{
174+
name: "alerts listing fails",
175+
mockedClient: mock.NewMockedHTTPClient(
176+
mock.WithRequestMatchHandler(
177+
mock.GetReposCodeScanningAlertsByOwnerByRepo,
178+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
179+
w.WriteHeader(http.StatusUnauthorized)
180+
_, _ = w.Write([]byte(`{"message": "Unauthorized access"}`))
181+
}),
182+
),
183+
),
184+
requestArgs: map[string]interface{}{
185+
"owner": "owner",
186+
"repo": "repo",
187+
},
188+
expectError: true,
189+
expectedErrMsg: "failed to list alerts",
190+
},
191+
}
192+
193+
for _, tc := range tests {
194+
t.Run(tc.name, func(t *testing.T) {
195+
// Setup client with mock
196+
client := github.NewClient(tc.mockedClient)
197+
_, handler := listCodeScanningAlerts(client)
198+
199+
// Create call request
200+
request := createMCPRequest(tc.requestArgs)
201+
202+
// Call handler
203+
result, err := handler(context.Background(), request)
204+
205+
// Verify results
206+
if tc.expectError {
207+
require.Error(t, err)
208+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
209+
return
210+
}
211+
212+
require.NoError(t, err)
213+
214+
// Parse the result and get the text content if no error
215+
textContent := getTextResult(t, result)
216+
217+
// Unmarshal and verify the result
218+
var returnedAlerts []*github.Alert
219+
err = json.Unmarshal([]byte(textContent.Text), &returnedAlerts)
220+
assert.NoError(t, err)
221+
assert.Len(t, returnedAlerts, len(tc.expectedAlerts))
222+
for i, alert := range returnedAlerts {
223+
assert.Equal(t, *tc.expectedAlerts[i].Number, *alert.Number)
224+
assert.Equal(t, *tc.expectedAlerts[i].State, *alert.State)
225+
assert.Equal(t, *tc.expectedAlerts[i].Rule.ID, *alert.Rule.ID)
226+
assert.Equal(t, *tc.expectedAlerts[i].HTMLURL, *alert.HTMLURL)
227+
}
228+
})
229+
}
230+
}

pkg/github/helper_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package github
2+
3+
import (
4+
"encoding/json"
5+
"github.com/stretchr/testify/assert"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/mark3labs/mcp-go/mcp"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// mockResponse is a helper function to create a mock HTTP response handler
14+
// that returns a specified status code and marshalled body.
15+
func mockResponse(t *testing.T, code int, body interface{}) http.HandlerFunc {
16+
t.Helper()
17+
return func(w http.ResponseWriter, r *http.Request) {
18+
w.WriteHeader(code)
19+
b, err := json.Marshal(body)
20+
require.NoError(t, err)
21+
_, _ = w.Write(b)
22+
}
23+
}
24+
25+
// createMCPRequest is a helper function to create a MCP request with the given arguments.
26+
func createMCPRequest(args map[string]interface{}) mcp.CallToolRequest {
27+
return mcp.CallToolRequest{
28+
Params: struct {
29+
Name string `json:"name"`
30+
Arguments map[string]interface{} `json:"arguments,omitempty"`
31+
Meta *struct {
32+
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
33+
} `json:"_meta,omitempty"`
34+
}{
35+
Arguments: args,
36+
},
37+
}
38+
}
39+
40+
// getTextResult is a helper function that returns a text result from a tool call.
41+
func getTextResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent {
42+
t.Helper()
43+
assert.NotNil(t, result)
44+
require.Len(t, result.Content, 1)
45+
require.IsType(t, mcp.TextContent{}, result.Content[0])
46+
textContent := result.Content[0].(mcp.TextContent)
47+
assert.Equal(t, "text", textContent.Type)
48+
return textContent
49+
}

0 commit comments

Comments
 (0)
0