package github import ( "context" "encoding/json" "net/http" "testing" "github.com/google/go-github/v69/github" "github.com/mark3labs/mcp-go/mcp" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_GetIssue(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := getIssue(mockClient) assert.Equal(t, "get_issue", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "owner") assert.Contains(t, tool.InputSchema.Properties, "repo") assert.Contains(t, tool.InputSchema.Properties, "issue_number") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number"}) // Setup mock issue for success case mockIssue := &github.Issue{ Number: github.Ptr(42), Title: github.Ptr("Test Issue"), Body: github.Ptr("This is a test issue"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"), } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedIssue *github.Issue expectedErrMsg string }{ { name: "successful issue retrieval", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposIssuesByOwnerByRepoByIssueNumber, mockIssue, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "issue_number": float64(42), }, expectError: false, expectedIssue: mockIssue, }, { name: "issue not found", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetReposIssuesByOwnerByRepoByIssueNumber, mockResponse(t, http.StatusNotFound, `{"message": "Issue not found"}`), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "issue_number": float64(999), }, expectError: true, expectedErrMsg: "failed to get issue", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := getIssue(client) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErrMsg) return } require.NoError(t, err) textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedIssue github.Issue err = json.Unmarshal([]byte(textContent.Text), &returnedIssue) require.NoError(t, err) assert.Equal(t, *tc.expectedIssue.Number, *returnedIssue.Number) assert.Equal(t, *tc.expectedIssue.Title, *returnedIssue.Title) assert.Equal(t, *tc.expectedIssue.Body, *returnedIssue.Body) }) } } func Test_AddIssueComment(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := addIssueComment(mockClient) assert.Equal(t, "add_issue_comment", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "owner") assert.Contains(t, tool.InputSchema.Properties, "repo") assert.Contains(t, tool.InputSchema.Properties, "issue_number") assert.Contains(t, tool.InputSchema.Properties, "body") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number", "body"}) // Setup mock comment for success case mockComment := &github.IssueComment{ ID: github.Ptr(int64(123)), Body: github.Ptr("This is a test comment"), User: &github.User{ Login: github.Ptr("testuser"), }, HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42#issuecomment-123"), } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedComment *github.IssueComment expectedErrMsg string }{ { name: "successful comment creation", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PostReposIssuesCommentsByOwnerByRepoByIssueNumber, mockResponse(t, http.StatusCreated, mockComment), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "issue_number": float64(42), "body": "This is a test comment", }, expectError: false, expectedComment: mockComment, }, { name: "comment creation fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PostReposIssuesCommentsByOwnerByRepoByIssueNumber, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Invalid request"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "issue_number": float64(42), "body": "", }, expectError: true, expectedErrMsg: "failed to create comment", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := addIssueComment(client) // Create call request request := mcp.CallToolRequest{ Params: struct { Name string `json:"name"` Arguments map[string]interface{} `json:"arguments,omitempty"` Meta *struct { ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"` } `json:"_meta,omitempty"` }{ Arguments: tc.requestArgs, }, } // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErrMsg) return } require.NoError(t, err) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedComment github.IssueComment err = json.Unmarshal([]byte(textContent.Text), &returnedComment) require.NoError(t, err) assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID) assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body) assert.Equal(t, *tc.expectedComment.User.Login, *returnedComment.User.Login) }) } } func Test_SearchIssues(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := searchIssues(mockClient) assert.Equal(t, "search_issues", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "q") assert.Contains(t, tool.InputSchema.Properties, "sort") assert.Contains(t, tool.InputSchema.Properties, "order") assert.Contains(t, tool.InputSchema.Properties, "per_page") assert.Contains(t, tool.InputSchema.Properties, "page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"q"}) // Setup mock search results mockSearchResult := &github.IssuesSearchResult{ Total: github.Ptr(2), IncompleteResults: github.Ptr(false), Issues: []*github.Issue{ { Number: github.Ptr(42), Title: github.Ptr("Bug: Something is broken"), Body: github.Ptr("This is a bug report"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"), Comments: github.Ptr(5), User: &github.User{ Login: github.Ptr("user1"), }, }, { Number: github.Ptr(43), Title: github.Ptr("Feature: Add new functionality"), Body: github.Ptr("This is a feature request"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/issues/43"), Comments: github.Ptr(3), User: &github.User{ Login: github.Ptr("user2"), }, }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedResult *github.IssuesSearchResult expectedErrMsg string }{ { name: "successful issues search with all parameters", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetSearchIssues, mockSearchResult, ), ), requestArgs: map[string]interface{}{ "q": "repo:owner/repo is:issue is:open", "sort": "created", "order": "desc", "page": float64(1), "per_page": float64(30), }, expectError: false, expectedResult: mockSearchResult, }, { name: "issues search with minimal parameters", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetSearchIssues, mockSearchResult, ), ), requestArgs: map[string]interface{}{ "q": "repo:owner/repo is:issue is:open", }, expectError: false, expectedResult: mockSearchResult, }, { name: "search issues fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchIssues, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) }), ), ), requestArgs: map[string]interface{}{ "q": "invalid:query", }, expectError: true, expectedErrMsg: "failed to search issues", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := searchIssues(client) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErrMsg) return } require.NoError(t, err) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedResult github.IssuesSearchResult err = json.Unmarshal([]byte(textContent.Text), &returnedResult) require.NoError(t, err) assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total) assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults) assert.Len(t, returnedResult.Issues, len(tc.expectedResult.Issues)) for i, issue := range returnedResult.Issues { assert.Equal(t, *tc.expectedResult.Issues[i].Number, *issue.Number) assert.Equal(t, *tc.expectedResult.Issues[i].Title, *issue.Title) assert.Equal(t, *tc.expectedResult.Issues[i].State, *issue.State) assert.Equal(t, *tc.expectedResult.Issues[i].HTMLURL, *issue.HTMLURL) assert.Equal(t, *tc.expectedResult.Issues[i].User.Login, *issue.User.Login) } }) } }