package github import ( "context" "encoding/json" "net/http" "testing" "time" "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v74/github" "github.com/shurcooL/githubv4" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_GetPullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request", 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, "pullNumber") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock PR for success case mockPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), Head: &github.PullRequestBranch{ SHA: github.Ptr("abcd1234"), Ref: github.Ptr("feature-branch"), }, Base: &github.PullRequestBranch{ Ref: github.Ptr("main"), }, Body: github.Ptr("This is a test PR"), User: &github.User{ Login: github.Ptr("testuser"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedPR *github.PullRequest expectedErrMsg string }{ { name: "successful PR fetch", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposPullsByOwnerByRepoByPullNumber, mockPR, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: false, expectedPR: mockPR, }, { name: "PR fetch fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetReposPullsByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(999), }, expectError: true, expectedErrMsg: "failed to get pull request", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := GetPullRequest(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedPR github.PullRequest err = json.Unmarshal([]byte(textContent.Text), &returnedPR) require.NoError(t, err) assert.Equal(t, *tc.expectedPR.Number, *returnedPR.Number) assert.Equal(t, *tc.expectedPR.Title, *returnedPR.Title) assert.Equal(t, *tc.expectedPR.State, *returnedPR.State) assert.Equal(t, *tc.expectedPR.HTMLURL, *returnedPR.HTMLURL) }) } } func Test_UpdatePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := UpdatePullRequest(stubGetClientFn(mockClient), stubGetGQLClientFn(githubv4.NewClient(nil)), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "update_pull_request", 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, "pullNumber") assert.Contains(t, tool.InputSchema.Properties, "draft") assert.Contains(t, tool.InputSchema.Properties, "title") assert.Contains(t, tool.InputSchema.Properties, "body") assert.Contains(t, tool.InputSchema.Properties, "state") assert.Contains(t, tool.InputSchema.Properties, "base") assert.Contains(t, tool.InputSchema.Properties, "maintainer_can_modify") assert.Contains(t, tool.InputSchema.Properties, "reviewers") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock PR for success case mockUpdatedPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Updated Test PR Title"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), Body: github.Ptr("Updated test PR body."), MaintainerCanModify: github.Ptr(false), Draft: github.Ptr(false), Base: &github.PullRequestBranch{ Ref: github.Ptr("develop"), }, } mockClosedPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR"), State: github.Ptr("closed"), // State updated } // Mock PR for when there are no updates but we still need a response mockPRWithReviewers := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR"), State: github.Ptr("open"), RequestedReviewers: []*github.User{ {Login: github.Ptr("reviewer1")}, {Login: github.Ptr("reviewer2")}, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedPR *github.PullRequest expectedErrMsg string }{ { name: "successful PR update (title, body, base, maintainer_can_modify)", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PatchReposPullsByOwnerByRepoByPullNumber, // Expect the flat string based on previous test failure output and API docs expectRequestBody(t, map[string]interface{}{ "title": "Updated Test PR Title", "body": "Updated test PR body.", "base": "develop", "maintainer_can_modify": false, }).andThen( mockResponse(t, http.StatusOK, mockUpdatedPR), ), ), mock.WithRequestMatch( mock.GetReposPullsByOwnerByRepoByPullNumber, mockUpdatedPR, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "title": "Updated Test PR Title", "body": "Updated test PR body.", "base": "develop", "maintainer_can_modify": false, }, expectError: false, expectedPR: mockUpdatedPR, }, { name: "successful PR update (state)", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PatchReposPullsByOwnerByRepoByPullNumber, expectRequestBody(t, map[string]interface{}{ "state": "closed", }).andThen( mockResponse(t, http.StatusOK, mockClosedPR), ), ), mock.WithRequestMatch( mock.GetReposPullsByOwnerByRepoByPullNumber, mockClosedPR, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "state": "closed", }, expectError: false, expectedPR: mockClosedPR, }, { name: "successful PR update with reviewers", mockedClient: mock.NewMockedHTTPClient( // Mock for RequestReviewers call, returning the PR with reviewers mock.WithRequestMatch( mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber, mockPRWithReviewers, ), mock.WithRequestMatch( mock.GetReposPullsByOwnerByRepoByPullNumber, mockPRWithReviewers, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "reviewers": []interface{}{"reviewer1", "reviewer2"}, }, expectError: false, expectedPR: mockPRWithReviewers, }, { name: "successful PR update (title only)", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PatchReposPullsByOwnerByRepoByPullNumber, expectRequestBody(t, map[string]interface{}{ "title": "Updated Test PR Title", }).andThen( mockResponse(t, http.StatusOK, mockUpdatedPR), ), ), mock.WithRequestMatch( mock.GetReposPullsByOwnerByRepoByPullNumber, mockUpdatedPR, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "title": "Updated Test PR Title", }, expectError: false, expectedPR: mockUpdatedPR, }, { name: "no update parameters provided", mockedClient: mock.NewMockedHTTPClient(), // No API call expected requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), // No update fields }, expectError: false, // Error is returned in the result, not as Go error expectedErrMsg: "No update parameters provided", }, { name: "PR update fails (API error)", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PatchReposPullsByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "title": "Invalid Title Causing Error", }, expectError: true, expectedErrMsg: "failed to update pull request", }, { name: "request reviewers fails", mockedClient: mock.NewMockedHTTPClient( // Then reviewer request fails mock.WithRequestMatchHandler( mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Invalid reviewers"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "reviewers": []interface{}{"invalid-user"}, }, expectError: true, expectedErrMsg: "failed to request reviewers", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := UpdatePullRequest(stubGetClientFn(client), stubGetGQLClientFn(githubv4.NewClient(nil)), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError || tc.expectedErrMsg != "" { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) if tc.expectedErrMsg != "" { assert.Contains(t, errorContent.Text, tc.expectedErrMsg) } return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content textContent := getTextResult(t, result) // Unmarshal and verify the successful result var returnedPR github.PullRequest err = json.Unmarshal([]byte(textContent.Text), &returnedPR) require.NoError(t, err) assert.Equal(t, *tc.expectedPR.Number, *returnedPR.Number) if tc.expectedPR.Title != nil { assert.Equal(t, *tc.expectedPR.Title, *returnedPR.Title) } if tc.expectedPR.Body != nil { assert.Equal(t, *tc.expectedPR.Body, *returnedPR.Body) } if tc.expectedPR.State != nil { assert.Equal(t, *tc.expectedPR.State, *returnedPR.State) } if tc.expectedPR.Base != nil && tc.expectedPR.Base.Ref != nil { assert.NotNil(t, returnedPR.Base) assert.Equal(t, *tc.expectedPR.Base.Ref, *returnedPR.Base.Ref) } if tc.expectedPR.MaintainerCanModify != nil { assert.Equal(t, *tc.expectedPR.MaintainerCanModify, *returnedPR.MaintainerCanModify) } // Check reviewers if they exist in the expected PR if len(tc.expectedPR.RequestedReviewers) > 0 { assert.NotNil(t, returnedPR.RequestedReviewers) assert.Equal(t, len(tc.expectedPR.RequestedReviewers), len(returnedPR.RequestedReviewers)) // Create maps of reviewer logins for easy comparison expectedReviewers := make(map[string]bool) for _, reviewer := range tc.expectedPR.RequestedReviewers { expectedReviewers[*reviewer.Login] = true } actualReviewers := make(map[string]bool) for _, reviewer := range returnedPR.RequestedReviewers { actualReviewers[*reviewer.Login] = true } // Compare the maps assert.Equal(t, expectedReviewers, actualReviewers) } }) } } func Test_UpdatePullRequest_Draft(t *testing.T) { // Setup mock PR for success case mockUpdatedPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR Title"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), Body: github.Ptr("Test PR body."), MaintainerCanModify: github.Ptr(false), Draft: github.Ptr(false), // Updated to ready for review Base: &github.PullRequestBranch{ Ref: github.Ptr("main"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedPR *github.PullRequest expectedErrMsg string }{ { name: "successful draft update to ready for review", mockedClient: githubv4mock.NewMockedHTTPClient( githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` }{}, map[string]any{ "owner": githubv4.String("owner"), "repo": githubv4.String("repo"), "prNum": githubv4.Int(42), }, githubv4mock.DataResponse(map[string]any{ "repository": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDOA0xdyM50BPaO", "isDraft": true, // Current state is draft }, }, }), ), githubv4mock.NewMutationMatcher( struct { MarkPullRequestReadyForReview struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } } `graphql:"markPullRequestReadyForReview(input: $input)"` }{}, githubv4.MarkPullRequestReadyForReviewInput{ PullRequestID: "PR_kwDOA0xdyM50BPaO", }, nil, githubv4mock.DataResponse(map[string]any{ "markPullRequestReadyForReview": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDOA0xdyM50BPaO", "isDraft": false, }, }, }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "draft": false, }, expectError: false, expectedPR: mockUpdatedPR, }, { name: "successful convert pull request to draft", mockedClient: githubv4mock.NewMockedHTTPClient( githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` }{}, map[string]any{ "owner": githubv4.String("owner"), "repo": githubv4.String("repo"), "prNum": githubv4.Int(42), }, githubv4mock.DataResponse(map[string]any{ "repository": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDOA0xdyM50BPaO", "isDraft": false, // Current state is draft }, }, }), ), githubv4mock.NewMutationMatcher( struct { ConvertPullRequestToDraft struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } } `graphql:"convertPullRequestToDraft(input: $input)"` }{}, githubv4.ConvertPullRequestToDraftInput{ PullRequestID: "PR_kwDOA0xdyM50BPaO", }, nil, githubv4mock.DataResponse(map[string]any{ "convertPullRequestToDraft": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDOA0xdyM50BPaO", "isDraft": true, }, }, }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "draft": true, }, expectError: false, expectedPR: mockUpdatedPR, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // For draft-only tests, we need to mock both GraphQL and the final REST GET call restClient := github.NewClient(mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposPullsByOwnerByRepoByPullNumber, mockUpdatedPR, ), )) gqlClient := githubv4.NewClient(tc.mockedClient) _, handler := UpdatePullRequest(stubGetClientFn(restClient), stubGetGQLClientFn(gqlClient), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) result, err := handler(context.Background(), request) if tc.expectError || tc.expectedErrMsg != "" { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) if tc.expectedErrMsg != "" { assert.Contains(t, errorContent.Text, tc.expectedErrMsg) } return } require.NoError(t, err) require.False(t, result.IsError) textContent := getTextResult(t, result) // Unmarshal and verify the successful result var returnedPR github.PullRequest err = json.Unmarshal([]byte(textContent.Text), &returnedPR) require.NoError(t, err) assert.Equal(t, *tc.expectedPR.Number, *returnedPR.Number) }) } } func Test_ListPullRequests(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := ListPullRequests(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_pull_requests", 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, "state") assert.Contains(t, tool.InputSchema.Properties, "head") assert.Contains(t, tool.InputSchema.Properties, "base") assert.Contains(t, tool.InputSchema.Properties, "sort") assert.Contains(t, tool.InputSchema.Properties, "direction") assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.Contains(t, tool.InputSchema.Properties, "page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) // Setup mock PRs for success case mockPRs := []*github.PullRequest{ { Number: github.Ptr(42), Title: github.Ptr("First PR"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), }, { Number: github.Ptr(43), Title: github.Ptr("Second PR"), State: github.Ptr("closed"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/43"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedPRs []*github.PullRequest expectedErrMsg string }{ { name: "successful PRs listing", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetReposPullsByOwnerByRepo, expectQueryParams(t, map[string]string{ "state": "all", "sort": "created", "direction": "desc", "per_page": "30", "page": "1", }).andThen( mockResponse(t, http.StatusOK, mockPRs), ), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "state": "all", "sort": "created", "direction": "desc", "perPage": float64(30), "page": float64(1), }, expectError: false, expectedPRs: mockPRs, }, { name: "PRs listing fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetReposPullsByOwnerByRepo, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message": "Invalid request"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "state": "invalid", }, expectError: true, expectedErrMsg: "failed to list pull requests", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := ListPullRequests(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedPRs []*github.PullRequest err = json.Unmarshal([]byte(textContent.Text), &returnedPRs) require.NoError(t, err) assert.Len(t, returnedPRs, 2) assert.Equal(t, *tc.expectedPRs[0].Number, *returnedPRs[0].Number) assert.Equal(t, *tc.expectedPRs[0].Title, *returnedPRs[0].Title) assert.Equal(t, *tc.expectedPRs[0].State, *returnedPRs[0].State) assert.Equal(t, *tc.expectedPRs[1].Number, *returnedPRs[1].Number) assert.Equal(t, *tc.expectedPRs[1].Title, *returnedPRs[1].Title) assert.Equal(t, *tc.expectedPRs[1].State, *returnedPRs[1].State) }) } } func Test_MergePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := MergePullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "merge_pull_request", 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, "pullNumber") assert.Contains(t, tool.InputSchema.Properties, "commit_title") assert.Contains(t, tool.InputSchema.Properties, "commit_message") assert.Contains(t, tool.InputSchema.Properties, "merge_method") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock merge result for success case mockMergeResult := &github.PullRequestMergeResult{ Merged: github.Ptr(true), Message: github.Ptr("Pull Request successfully merged"), SHA: github.Ptr("abcd1234efgh5678"), } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedMergeResult *github.PullRequestMergeResult expectedErrMsg string }{ { name: "successful merge", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PutReposPullsMergeByOwnerByRepoByPullNumber, expectRequestBody(t, map[string]interface{}{ "commit_title": "Merge PR #42", "commit_message": "Merging awesome feature", "merge_method": "squash", }).andThen( mockResponse(t, http.StatusOK, mockMergeResult), ), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "commit_title": "Merge PR #42", "commit_message": "Merging awesome feature", "merge_method": "squash", }, expectError: false, expectedMergeResult: mockMergeResult, }, { name: "merge fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PutReposPullsMergeByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusMethodNotAllowed) _, _ = w.Write([]byte(`{"message": "Pull request cannot be merged"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: true, expectedErrMsg: "failed to merge pull request", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := MergePullRequest(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedResult github.PullRequestMergeResult err = json.Unmarshal([]byte(textContent.Text), &returnedResult) require.NoError(t, err) assert.Equal(t, *tc.expectedMergeResult.Merged, *returnedResult.Merged) assert.Equal(t, *tc.expectedMergeResult.Message, *returnedResult.Message) assert.Equal(t, *tc.expectedMergeResult.SHA, *returnedResult.SHA) }) } } func Test_SearchPullRequests(t *testing.T) { mockClient := github.NewClient(nil) tool, _ := SearchPullRequests(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_pull_requests", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "query") assert.Contains(t, tool.InputSchema.Properties, "owner") assert.Contains(t, tool.InputSchema.Properties, "repo") assert.Contains(t, tool.InputSchema.Properties, "sort") assert.Contains(t, tool.InputSchema.Properties, "order") assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.Contains(t, tool.InputSchema.Properties, "page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"}) mockSearchResult := &github.IssuesSearchResult{ Total: github.Ptr(2), IncompleteResults: github.Ptr(false), Issues: []*github.Issue{ { Number: github.Ptr(42), Title: github.Ptr("Test PR 1"), Body: github.Ptr("Updated tests."), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/1"), Comments: github.Ptr(5), User: &github.User{ Login: github.Ptr("user1"), }, }, { Number: github.Ptr(43), Title: github.Ptr("Test PR 2"), Body: github.Ptr("Updated build scripts."), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/2"), 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 pull request search with all parameters", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchIssues, expectQueryParams( t, map[string]string{ "q": "is:pr repo:owner/repo is:open", "sort": "created", "order": "desc", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "repo:owner/repo is:open", "sort": "created", "order": "desc", "page": float64(1), "perPage": float64(30), }, expectError: false, expectedResult: mockSearchResult, }, { name: "pull request search with owner and repo parameters", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchIssues, expectQueryParams( t, map[string]string{ "q": "repo:test-owner/test-repo is:pr draft:false", "sort": "updated", "order": "asc", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "draft:false", "owner": "test-owner", "repo": "test-repo", "sort": "updated", "order": "asc", }, expectError: false, expectedResult: mockSearchResult, }, { name: "pull request search with only owner parameter (should ignore it)", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchIssues, expectQueryParams( t, map[string]string{ "q": "is:pr feature", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "feature", "owner": "test-owner", }, expectError: false, expectedResult: mockSearchResult, }, { name: "pull request search with only repo parameter (should ignore it)", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchIssues, expectQueryParams( t, map[string]string{ "q": "is:pr review-required", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "review-required", "repo": "test-repo", }, expectError: false, expectedResult: mockSearchResult, }, { name: "pull request search with minimal parameters", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetSearchIssues, mockSearchResult, ), ), requestArgs: map[string]interface{}{ "query": "is:pr repo:owner/repo is:open", }, expectError: false, expectedResult: mockSearchResult, }, { name: "query with existing is:pr filter - no duplication", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchIssues, expectQueryParams( t, map[string]string{ "q": "is:pr repo:github/github-mcp-server is:open draft:false", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "is:pr repo:github/github-mcp-server is:open draft:false", }, expectError: false, expectedResult: mockSearchResult, }, { name: "query with existing repo: filter and conflicting owner/repo params - uses query filter", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchIssues, expectQueryParams( t, map[string]string{ "q": "is:pr repo:github/github-mcp-server author:octocat", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "repo:github/github-mcp-server author:octocat", "owner": "different-owner", "repo": "different-repo", }, expectError: false, expectedResult: mockSearchResult, }, { name: "complex query with existing is:pr filter and OR operators", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchIssues, expectQueryParams( t, map[string]string{ "q": "is:pr repo:github/github-mcp-server (label:bug OR label:enhancement OR label:feature)", "page": "1", "per_page": "30", }, ).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "is:pr repo:github/github-mcp-server (label:bug OR label:enhancement OR label:feature)", }, expectError: false, expectedResult: mockSearchResult, }, { name: "search pull requests fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchIssues, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) }), ), ), requestArgs: map[string]interface{}{ "query": "invalid:query", }, expectError: true, expectedErrMsg: "failed to search pull requests", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := SearchPullRequests(stubGetClientFn(client), translations.NullTranslationHelper) // 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) } }) } } func Test_GetPullRequestFiles(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequestFiles(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request_files", 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, "pullNumber") assert.Contains(t, tool.InputSchema.Properties, "page") assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock PR files for success case mockFiles := []*github.CommitFile{ { Filename: github.Ptr("file1.go"), Status: github.Ptr("modified"), Additions: github.Ptr(10), Deletions: github.Ptr(5), Changes: github.Ptr(15), Patch: github.Ptr("@@ -1,5 +1,10 @@"), }, { Filename: github.Ptr("file2.go"), Status: github.Ptr("added"), Additions: github.Ptr(20), Deletions: github.Ptr(0), Changes: github.Ptr(20), Patch: github.Ptr("@@ -0,0 +1,20 @@"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedFiles []*github.CommitFile expectedErrMsg string }{ { name: "successful files fetch", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposPullsFilesByOwnerByRepoByPullNumber, mockFiles, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: false, expectedFiles: mockFiles, }, { name: "successful files fetch with pagination", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposPullsFilesByOwnerByRepoByPullNumber, mockFiles, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "page": float64(2), "perPage": float64(10), }, expectError: false, expectedFiles: mockFiles, }, { name: "files fetch fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetReposPullsFilesByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(999), }, expectError: true, expectedErrMsg: "failed to get pull request files", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := GetPullRequestFiles(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedFiles []*github.CommitFile err = json.Unmarshal([]byte(textContent.Text), &returnedFiles) require.NoError(t, err) assert.Len(t, returnedFiles, len(tc.expectedFiles)) for i, file := range returnedFiles { assert.Equal(t, *tc.expectedFiles[i].Filename, *file.Filename) assert.Equal(t, *tc.expectedFiles[i].Status, *file.Status) assert.Equal(t, *tc.expectedFiles[i].Additions, *file.Additions) assert.Equal(t, *tc.expectedFiles[i].Deletions, *file.Deletions) } }) } } func Test_GetPullRequestStatus(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequestStatus(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request_status", 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, "pullNumber") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock PR for successful PR fetch mockPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), Head: &github.PullRequestBranch{ SHA: github.Ptr("abcd1234"), Ref: github.Ptr("feature-branch"), }, } // Setup mock status for success case mockStatus := &github.CombinedStatus{ State: github.Ptr("success"), TotalCount: github.Ptr(3), Statuses: []*github.RepoStatus{ { State: github.Ptr("success"), Context: github.Ptr("continuous-integration/travis-ci"), Description: github.Ptr("Build succeeded"), TargetURL: github.Ptr("https://travis-ci.org/owner/repo/builds/123"), }, { State: github.Ptr("success"), Context: github.Ptr("codecov/patch"), Description: github.Ptr("Coverage increased"), TargetURL: github.Ptr("https://codecov.io/gh/owner/repo/pull/42"), }, { State: github.Ptr("success"), Context: github.Ptr("lint/golangci-lint"), Description: github.Ptr("No issues found"), TargetURL: github.Ptr("https://golangci.com/r/owner/repo/pull/42"), }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedStatus *github.CombinedStatus expectedErrMsg string }{ { name: "successful status fetch", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposPullsByOwnerByRepoByPullNumber, mockPR, ), mock.WithRequestMatch( mock.GetReposCommitsStatusByOwnerByRepoByRef, mockStatus, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: false, expectedStatus: mockStatus, }, { name: "PR fetch fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetReposPullsByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(999), }, expectError: true, expectedErrMsg: "failed to get pull request", }, { name: "status fetch fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposPullsByOwnerByRepoByPullNumber, mockPR, ), mock.WithRequestMatchHandler( mock.GetReposCommitsStatusesByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: true, expectedErrMsg: "failed to get combined status", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := GetPullRequestStatus(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedStatus github.CombinedStatus err = json.Unmarshal([]byte(textContent.Text), &returnedStatus) require.NoError(t, err) assert.Equal(t, *tc.expectedStatus.State, *returnedStatus.State) assert.Equal(t, *tc.expectedStatus.TotalCount, *returnedStatus.TotalCount) assert.Len(t, returnedStatus.Statuses, len(tc.expectedStatus.Statuses)) for i, status := range returnedStatus.Statuses { assert.Equal(t, *tc.expectedStatus.Statuses[i].State, *status.State) assert.Equal(t, *tc.expectedStatus.Statuses[i].Context, *status.Context) assert.Equal(t, *tc.expectedStatus.Statuses[i].Description, *status.Description) } }) } } func Test_UpdatePullRequestBranch(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := UpdatePullRequestBranch(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "update_pull_request_branch", 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, "pullNumber") assert.Contains(t, tool.InputSchema.Properties, "expectedHeadSha") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock update result for success case mockUpdateResult := &github.PullRequestBranchUpdateResponse{ Message: github.Ptr("Branch was updated successfully"), URL: github.Ptr("https://api.github.com/repos/owner/repo/pulls/42"), } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedUpdateResult *github.PullRequestBranchUpdateResponse expectedErrMsg string }{ { name: "successful branch update", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PutReposPullsUpdateBranchByOwnerByRepoByPullNumber, expectRequestBody(t, map[string]interface{}{ "expected_head_sha": "abcd1234", }).andThen( mockResponse(t, http.StatusAccepted, mockUpdateResult), ), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "expectedHeadSha": "abcd1234", }, expectError: false, expectedUpdateResult: mockUpdateResult, }, { name: "branch update without expected SHA", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PutReposPullsUpdateBranchByOwnerByRepoByPullNumber, expectRequestBody(t, map[string]interface{}{}).andThen( mockResponse(t, http.StatusAccepted, mockUpdateResult), ), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: false, expectedUpdateResult: mockUpdateResult, }, { name: "branch update fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PutReposPullsUpdateBranchByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusConflict) _, _ = w.Write([]byte(`{"message": "Merge conflict"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: true, expectedErrMsg: "failed to update pull request branch", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := UpdatePullRequestBranch(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) assert.Contains(t, textContent.Text, "is in progress") }) } } func Test_GetPullRequestComments(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequestComments(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request_comments", 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, "pullNumber") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock PR comments for success case mockComments := []*github.PullRequestComment{ { ID: github.Ptr(int64(101)), Body: github.Ptr("This looks good"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42#discussion_r101"), User: &github.User{ Login: github.Ptr("reviewer1"), }, Path: github.Ptr("file1.go"), Position: github.Ptr(5), CommitID: github.Ptr("abcdef123456"), CreatedAt: &github.Timestamp{Time: time.Now().Add(-24 * time.Hour)}, UpdatedAt: &github.Timestamp{Time: time.Now().Add(-24 * time.Hour)}, }, { ID: github.Ptr(int64(102)), Body: github.Ptr("Please fix this"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42#discussion_r102"), User: &github.User{ Login: github.Ptr("reviewer2"), }, Path: github.Ptr("file2.go"), Position: github.Ptr(10), CommitID: github.Ptr("abcdef123456"), CreatedAt: &github.Timestamp{Time: time.Now().Add(-12 * time.Hour)}, UpdatedAt: &github.Timestamp{Time: time.Now().Add(-12 * time.Hour)}, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedComments []*github.PullRequestComment expectedErrMsg string }{ { name: "successful comments fetch", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposPullsCommentsByOwnerByRepoByPullNumber, mockComments, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: false, expectedComments: mockComments, }, { name: "comments fetch fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetReposPullsCommentsByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(999), }, expectError: true, expectedErrMsg: "failed to get pull request comments", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := GetPullRequestComments(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedComments []*github.PullRequestComment err = json.Unmarshal([]byte(textContent.Text), &returnedComments) require.NoError(t, err) assert.Len(t, returnedComments, len(tc.expectedComments)) for i, comment := range returnedComments { assert.Equal(t, *tc.expectedComments[i].ID, *comment.ID) assert.Equal(t, *tc.expectedComments[i].Body, *comment.Body) assert.Equal(t, *tc.expectedComments[i].User.Login, *comment.User.Login) assert.Equal(t, *tc.expectedComments[i].Path, *comment.Path) assert.Equal(t, *tc.expectedComments[i].HTMLURL, *comment.HTMLURL) } }) } } func Test_GetPullRequestReviews(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequestReviews(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request_reviews", 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, "pullNumber") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock PR reviews for success case mockReviews := []*github.PullRequestReview{ { ID: github.Ptr(int64(201)), State: github.Ptr("APPROVED"), Body: github.Ptr("LGTM"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42#pullrequestreview-201"), User: &github.User{ Login: github.Ptr("approver"), }, CommitID: github.Ptr("abcdef123456"), SubmittedAt: &github.Timestamp{Time: time.Now().Add(-24 * time.Hour)}, }, { ID: github.Ptr(int64(202)), State: github.Ptr("CHANGES_REQUESTED"), Body: github.Ptr("Please address the following issues"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42#pullrequestreview-202"), User: &github.User{ Login: github.Ptr("reviewer"), }, CommitID: github.Ptr("abcdef123456"), SubmittedAt: &github.Timestamp{Time: time.Now().Add(-12 * time.Hour)}, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedReviews []*github.PullRequestReview expectedErrMsg string }{ { name: "successful reviews fetch", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposPullsReviewsByOwnerByRepoByPullNumber, mockReviews, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, expectError: false, expectedReviews: mockReviews, }, { name: "reviews fetch fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetReposPullsReviewsByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pullNumber": float64(999), }, expectError: true, expectedErrMsg: "failed to get pull request reviews", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := GetPullRequestReviews(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedReviews []*github.PullRequestReview err = json.Unmarshal([]byte(textContent.Text), &returnedReviews) require.NoError(t, err) assert.Len(t, returnedReviews, len(tc.expectedReviews)) for i, review := range returnedReviews { assert.Equal(t, *tc.expectedReviews[i].ID, *review.ID) assert.Equal(t, *tc.expectedReviews[i].State, *review.State) assert.Equal(t, *tc.expectedReviews[i].Body, *review.Body) assert.Equal(t, *tc.expectedReviews[i].User.Login, *review.User.Login) assert.Equal(t, *tc.expectedReviews[i].HTMLURL, *review.HTMLURL) } }) } } func Test_CreatePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := CreatePullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "create_pull_request", 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, "title") assert.Contains(t, tool.InputSchema.Properties, "body") assert.Contains(t, tool.InputSchema.Properties, "head") assert.Contains(t, tool.InputSchema.Properties, "base") assert.Contains(t, tool.InputSchema.Properties, "draft") assert.Contains(t, tool.InputSchema.Properties, "maintainer_can_modify") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "title", "head", "base"}) // Setup mock PR for success case mockPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), Head: &github.PullRequestBranch{ SHA: github.Ptr("abcd1234"), Ref: github.Ptr("feature-branch"), }, Base: &github.PullRequestBranch{ SHA: github.Ptr("efgh5678"), Ref: github.Ptr("main"), }, Body: github.Ptr("This is a test PR"), Draft: github.Ptr(false), MaintainerCanModify: github.Ptr(true), User: &github.User{ Login: github.Ptr("testuser"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedPR *github.PullRequest expectedErrMsg string }{ { name: "successful PR creation", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PostReposPullsByOwnerByRepo, expectRequestBody(t, map[string]interface{}{ "title": "Test PR", "body": "This is a test PR", "head": "feature-branch", "base": "main", "draft": false, "maintainer_can_modify": true, }).andThen( mockResponse(t, http.StatusCreated, mockPR), ), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "title": "Test PR", "body": "This is a test PR", "head": "feature-branch", "base": "main", "draft": false, "maintainer_can_modify": true, }, expectError: false, expectedPR: mockPR, }, { name: "missing required parameter", mockedClient: mock.NewMockedHTTPClient(), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", // missing title, head, base }, expectError: true, expectedErrMsg: "missing required parameter: title", }, { name: "PR creation fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PostReposPullsByOwnerByRepo, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message":"Validation failed","errors":[{"resource":"PullRequest","code":"invalid"}]}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "title": "Test PR", "head": "feature-branch", "base": "main", }, expectError: true, expectedErrMsg: "failed to create pull request", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := CreatePullRequest(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { if err != nil { assert.Contains(t, err.Error(), tc.expectedErrMsg) return } // If no error returned but in the result textContent := getTextResult(t, result) assert.Contains(t, textContent.Text, 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 returnedPR github.PullRequest err = json.Unmarshal([]byte(textContent.Text), &returnedPR) require.NoError(t, err) assert.Equal(t, *tc.expectedPR.Number, *returnedPR.Number) assert.Equal(t, *tc.expectedPR.Title, *returnedPR.Title) assert.Equal(t, *tc.expectedPR.State, *returnedPR.State) assert.Equal(t, *tc.expectedPR.HTMLURL, *returnedPR.HTMLURL) assert.Equal(t, *tc.expectedPR.Head.SHA, *returnedPR.Head.SHA) assert.Equal(t, *tc.expectedPR.Base.Ref, *returnedPR.Base.Ref) assert.Equal(t, *tc.expectedPR.Body, *returnedPR.Body) assert.Equal(t, *tc.expectedPR.User.Login, *returnedPR.User.Login) }) } } func TestCreateAndSubmitPullRequestReview(t *testing.T) { t.Parallel() // Verify tool definition once mockClient := githubv4.NewClient(nil) tool, _ := CreateAndSubmitPullRequestReview(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "create_and_submit_pull_request_review", 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, "pullNumber") assert.Contains(t, tool.InputSchema.Properties, "body") assert.Contains(t, tool.InputSchema.Properties, "event") assert.Contains(t, tool.InputSchema.Properties, "commitID") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber", "body", "event"}) tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectToolError bool expectedToolErrMsg string }{ { name: "successful review creation", mockedClient: githubv4mock.NewMockedHTTPClient( githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { ID githubv4.ID } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` }{}, map[string]any{ "owner": githubv4.String("owner"), "repo": githubv4.String("repo"), "prNum": githubv4.Int(42), }, githubv4mock.DataResponse( map[string]any{ "repository": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDODKw3uc6WYN1T", }, }, }, ), ), githubv4mock.NewMutationMatcher( struct { AddPullRequestReview struct { PullRequestReview struct { ID githubv4.ID } } `graphql:"addPullRequestReview(input: $input)"` }{}, githubv4.AddPullRequestReviewInput{ PullRequestID: githubv4.ID("PR_kwDODKw3uc6WYN1T"), Body: githubv4.NewString("This is a test review"), Event: githubv4mock.Ptr(githubv4.PullRequestReviewEventComment), CommitOID: githubv4.NewGitObjectID("abcd1234"), }, nil, githubv4mock.DataResponse(map[string]any{}), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "body": "This is a test review", "event": "COMMENT", "commitID": "abcd1234", }, expectToolError: false, }, { name: "failure to get pull request", mockedClient: githubv4mock.NewMockedHTTPClient( githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { ID githubv4.ID } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` }{}, map[string]any{ "owner": githubv4.String("owner"), "repo": githubv4.String("repo"), "prNum": githubv4.Int(42), }, githubv4mock.ErrorResponse("expected test failure"), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "body": "This is a test review", "event": "COMMENT", "commitID": "abcd1234", }, expectToolError: true, expectedToolErrMsg: "expected test failure", }, { name: "failure to submit review", mockedClient: githubv4mock.NewMockedHTTPClient( githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { ID githubv4.ID } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` }{}, map[string]any{ "owner": githubv4.String("owner"), "repo": githubv4.String("repo"), "prNum": githubv4.Int(42), }, githubv4mock.DataResponse( map[string]any{ "repository": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDODKw3uc6WYN1T", }, }, }, ), ), githubv4mock.NewMutationMatcher( struct { AddPullRequestReview struct { PullRequestReview struct { ID githubv4.ID } } `graphql:"addPullRequestReview(input: $input)"` }{}, githubv4.AddPullRequestReviewInput{ PullRequestID: githubv4.ID("PR_kwDODKw3uc6WYN1T"), Body: githubv4.NewString("This is a test review"), Event: githubv4mock.Ptr(githubv4.PullRequestReviewEventComment), CommitOID: githubv4.NewGitObjectID("abcd1234"), }, nil, githubv4mock.ErrorResponse("expected test failure"), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "body": "This is a test review", "event": "COMMENT", "commitID": "abcd1234", }, expectToolError: true, expectedToolErrMsg: "expected test failure", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Setup client with mock client := githubv4.NewClient(tc.mockedClient) _, handler := CreateAndSubmitPullRequestReview(stubGetGQLClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) require.NoError(t, err) textContent := getTextResult(t, result) if tc.expectToolError { require.True(t, result.IsError) assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) return } // Parse the result and get the text content if no error require.Equal(t, textContent.Text, "pull request review submitted successfully") }) } } func Test_RequestCopilotReview(t *testing.T) { t.Parallel() mockClient := github.NewClient(nil) tool, _ := RequestCopilotReview(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "request_copilot_review", 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, "pullNumber") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock PR for success case mockPR := &github.PullRequest{ Number: github.Ptr(42), Title: github.Ptr("Test PR"), State: github.Ptr("open"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), Head: &github.PullRequestBranch{ SHA: github.Ptr("abcd1234"), Ref: github.Ptr("feature-branch"), }, Base: &github.PullRequestBranch{ Ref: github.Ptr("main"), }, Body: github.Ptr("This is a test PR"), User: &github.User{ Login: github.Ptr("testuser"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedErrMsg string }{ { name: "successful request", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber, expect(t, expectations{ path: "/repos/owner/repo/pulls/1/requested_reviewers", requestBody: map[string]any{ "reviewers": []any{"copilot-pull-request-reviewer[bot]"}, }, }).andThen( mockResponse(t, http.StatusCreated, mockPR), ), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(1), }, expectError: false, }, { name: "request fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(999), }, expectError: true, expectedErrMsg: "failed to request copilot review", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() client := github.NewClient(tc.mockedClient) _, handler := RequestCopilotReview(stubGetClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) result, err := handler(context.Background(), request) if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) assert.NotNil(t, result) assert.Len(t, result.Content, 1) textContent := getTextResult(t, result) require.Equal(t, "", textContent.Text) }) } } func TestCreatePendingPullRequestReview(t *testing.T) { t.Parallel() // Verify tool definition once mockClient := githubv4.NewClient(nil) tool, _ := CreatePendingPullRequestReview(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "create_pending_pull_request_review", 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, "pullNumber") assert.Contains(t, tool.InputSchema.Properties, "commitID") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectToolError bool expectedToolErrMsg string }{ { name: "successful review creation", mockedClient: githubv4mock.NewMockedHTTPClient( githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { ID githubv4.ID } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` }{}, map[string]any{ "owner": githubv4.String("owner"), "repo": githubv4.String("repo"), "prNum": githubv4.Int(42), }, githubv4mock.DataResponse( map[string]any{ "repository": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDODKw3uc6WYN1T", }, }, }, ), ), githubv4mock.NewMutationMatcher( struct { AddPullRequestReview struct { PullRequestReview struct { ID githubv4.ID } } `graphql:"addPullRequestReview(input: $input)"` }{}, githubv4.AddPullRequestReviewInput{ PullRequestID: githubv4.ID("PR_kwDODKw3uc6WYN1T"), CommitOID: githubv4.NewGitObjectID("abcd1234"), }, nil, githubv4mock.DataResponse(map[string]any{}), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "commitID": "abcd1234", }, expectToolError: false, }, { name: "failure to get pull request", mockedClient: githubv4mock.NewMockedHTTPClient( githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { ID githubv4.ID } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` }{}, map[string]any{ "owner": githubv4.String("owner"), "repo": githubv4.String("repo"), "prNum": githubv4.Int(42), }, githubv4mock.ErrorResponse("expected test failure"), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "commitID": "abcd1234", }, expectToolError: true, expectedToolErrMsg: "expected test failure", }, { name: "failure to create pending review", mockedClient: githubv4mock.NewMockedHTTPClient( githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { ID githubv4.ID } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` }{}, map[string]any{ "owner": githubv4.String("owner"), "repo": githubv4.String("repo"), "prNum": githubv4.Int(42), }, githubv4mock.DataResponse( map[string]any{ "repository": map[string]any{ "pullRequest": map[string]any{ "id": "PR_kwDODKw3uc6WYN1T", }, }, }, ), ), githubv4mock.NewMutationMatcher( struct { AddPullRequestReview struct { PullRequestReview struct { ID githubv4.ID } } `graphql:"addPullRequestReview(input: $input)"` }{}, githubv4.AddPullRequestReviewInput{ PullRequestID: githubv4.ID("PR_kwDODKw3uc6WYN1T"), CommitOID: githubv4.NewGitObjectID("abcd1234"), }, nil, githubv4mock.ErrorResponse("expected test failure"), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "commitID": "abcd1234", }, expectToolError: true, expectedToolErrMsg: "expected test failure", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Setup client with mock client := githubv4.NewClient(tc.mockedClient) _, handler := CreatePendingPullRequestReview(stubGetGQLClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) require.NoError(t, err) textContent := getTextResult(t, result) if tc.expectToolError { require.True(t, result.IsError) assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) return } // Parse the result and get the text content if no error require.Equal(t, textContent.Text, "pending pull request created") }) } } func TestAddPullRequestReviewCommentToPendingReview(t *testing.T) { t.Parallel() // Verify tool definition once mockClient := githubv4.NewClient(nil) tool, _ := AddCommentToPendingReview(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "add_comment_to_pending_review", 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, "pullNumber") assert.Contains(t, tool.InputSchema.Properties, "path") assert.Contains(t, tool.InputSchema.Properties, "body") assert.Contains(t, tool.InputSchema.Properties, "subjectType") assert.Contains(t, tool.InputSchema.Properties, "line") assert.Contains(t, tool.InputSchema.Properties, "side") assert.Contains(t, tool.InputSchema.Properties, "startLine") assert.Contains(t, tool.InputSchema.Properties, "startSide") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber", "path", "body", "subjectType"}) tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectToolError bool expectedToolErrMsg string }{ { name: "successful line comment addition", requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "path": "file.go", "body": "This is a test comment", "subjectType": "LINE", "line": float64(10), "side": "RIGHT", "startLine": float64(5), "startSide": "RIGHT", }, mockedClient: githubv4mock.NewMockedHTTPClient( viewerQuery("williammartin"), getLatestPendingReviewQuery(getLatestPendingReviewQueryParams{ author: "williammartin", owner: "owner", repo: "repo", prNum: 42, reviews: []getLatestPendingReviewQueryReview{ { id: "PR_kwDODKw3uc6WYN1T", state: "PENDING", url: "https://github.com/owner/repo/pull/42", }, }, }), githubv4mock.NewMutationMatcher( struct { AddPullRequestReviewThread struct { Thread struct { ID githubv4.String // We don't need this, but a selector is required or GQL complains. } } `graphql:"addPullRequestReviewThread(input: $input)"` }{}, githubv4.AddPullRequestReviewThreadInput{ Path: githubv4.String("file.go"), Body: githubv4.String("This is a test comment"), SubjectType: githubv4mock.Ptr(githubv4.PullRequestReviewThreadSubjectTypeLine), Line: githubv4.NewInt(10), Side: githubv4mock.Ptr(githubv4.DiffSideRight), StartLine: githubv4.NewInt(5), StartSide: githubv4mock.Ptr(githubv4.DiffSideRight), PullRequestReviewID: githubv4.NewID("PR_kwDODKw3uc6WYN1T"), }, nil, githubv4mock.DataResponse(map[string]any{}), ), ), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Setup client with mock client := githubv4.NewClient(tc.mockedClient) _, handler := AddCommentToPendingReview(stubGetGQLClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) require.NoError(t, err) textContent := getTextResult(t, result) if tc.expectToolError { require.True(t, result.IsError) assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) return } // Parse the result and get the text content if no error require.Equal(t, textContent.Text, "pull request review comment successfully added to pending review") }) } } func TestSubmitPendingPullRequestReview(t *testing.T) { t.Parallel() // Verify tool definition once mockClient := githubv4.NewClient(nil) tool, _ := SubmitPendingPullRequestReview(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "submit_pending_pull_request_review", 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, "pullNumber") assert.Contains(t, tool.InputSchema.Properties, "event") assert.Contains(t, tool.InputSchema.Properties, "body") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber", "event"}) tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectToolError bool expectedToolErrMsg string }{ { name: "successful review submission", requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), "event": "COMMENT", "body": "This is a test review", }, mockedClient: githubv4mock.NewMockedHTTPClient( viewerQuery("williammartin"), getLatestPendingReviewQuery(getLatestPendingReviewQueryParams{ author: "williammartin", owner: "owner", repo: "repo", prNum: 42, reviews: []getLatestPendingReviewQueryReview{ { id: "PR_kwDODKw3uc6WYN1T", state: "PENDING", url: "https://github.com/owner/repo/pull/42", }, }, }), githubv4mock.NewMutationMatcher( struct { SubmitPullRequestReview struct { PullRequestReview struct { ID githubv4.ID } } `graphql:"submitPullRequestReview(input: $input)"` }{}, githubv4.SubmitPullRequestReviewInput{ PullRequestReviewID: githubv4.NewID("PR_kwDODKw3uc6WYN1T"), Event: githubv4.PullRequestReviewEventComment, Body: githubv4.NewString("This is a test review"), }, nil, githubv4mock.DataResponse(map[string]any{}), ), ), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Setup client with mock client := githubv4.NewClient(tc.mockedClient) _, handler := SubmitPendingPullRequestReview(stubGetGQLClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) require.NoError(t, err) textContent := getTextResult(t, result) if tc.expectToolError { require.True(t, result.IsError) assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) return } // Parse the result and get the text content if no error require.Equal(t, "pending pull request review successfully submitted", textContent.Text) }) } } func TestDeletePendingPullRequestReview(t *testing.T) { t.Parallel() // Verify tool definition once mockClient := githubv4.NewClient(nil) tool, _ := DeletePendingPullRequestReview(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "delete_pending_pull_request_review", 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, "pullNumber") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) tests := []struct { name string requestArgs map[string]any mockedClient *http.Client expectToolError bool expectedToolErrMsg string }{ { name: "successful review deletion", requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, mockedClient: githubv4mock.NewMockedHTTPClient( viewerQuery("williammartin"), getLatestPendingReviewQuery(getLatestPendingReviewQueryParams{ author: "williammartin", owner: "owner", repo: "repo", prNum: 42, reviews: []getLatestPendingReviewQueryReview{ { id: "PR_kwDODKw3uc6WYN1T", state: "PENDING", url: "https://github.com/owner/repo/pull/42", }, }, }), githubv4mock.NewMutationMatcher( struct { DeletePullRequestReview struct { PullRequestReview struct { ID githubv4.ID } } `graphql:"deletePullRequestReview(input: $input)"` }{}, githubv4.DeletePullRequestReviewInput{ PullRequestReviewID: githubv4.NewID("PR_kwDODKw3uc6WYN1T"), }, nil, githubv4mock.DataResponse(map[string]any{}), ), ), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Setup client with mock client := githubv4.NewClient(tc.mockedClient) _, handler := DeletePendingPullRequestReview(stubGetGQLClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) require.NoError(t, err) textContent := getTextResult(t, result) if tc.expectToolError { require.True(t, result.IsError) assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) return } // Parse the result and get the text content if no error require.Equal(t, "pending pull request review successfully deleted", textContent.Text) }) } } func TestGetPullRequestDiff(t *testing.T) { t.Parallel() // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequestDiff(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request_diff", 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, "pullNumber") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) stubbedDiff := `diff --git a/README.md b/README.md index 5d6e7b2..8a4f5c3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # Hello-World Hello World project for GitHub +## New Section + +This is a new section added in the pull request.` tests := []struct { name string requestArgs map[string]any mockedClient *http.Client expectToolError bool expectedToolErrMsg string }{ { name: "successful diff retrieval", requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "pullNumber": float64(42), }, mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetReposPullsByOwnerByRepoByPullNumber, // Should also expect Accept header to be application/vnd.github.v3.diff expectPath(t, "/repos/owner/repo/pulls/42").andThen( mockResponse(t, http.StatusOK, stubbedDiff), ), ), ), expectToolError: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := GetPullRequestDiff(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) require.NoError(t, err) textContent := getTextResult(t, result) if tc.expectToolError { require.True(t, result.IsError) assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) return } // Parse the result and get the text content if no error require.Equal(t, stubbedDiff, textContent.Text) }) } } func viewerQuery(login string) githubv4mock.Matcher { return githubv4mock.NewQueryMatcher( struct { Viewer struct { Login githubv4.String } `graphql:"viewer"` }{}, map[string]any{}, githubv4mock.DataResponse(map[string]any{ "viewer": map[string]any{ "login": login, }, }), ) } type getLatestPendingReviewQueryReview struct { id string state string url string } type getLatestPendingReviewQueryParams struct { author string owner string repo string prNum int32 reviews []getLatestPendingReviewQueryReview } func getLatestPendingReviewQuery(p getLatestPendingReviewQueryParams) githubv4mock.Matcher { return githubv4mock.NewQueryMatcher( struct { Repository struct { PullRequest struct { Reviews struct { Nodes []struct { ID githubv4.ID State githubv4.PullRequestReviewState URL githubv4.URI } } `graphql:"reviews(first: 1, author: $author)"` } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $name)"` }{}, map[string]any{ "author": githubv4.String(p.author), "owner": githubv4.String(p.owner), "name": githubv4.String(p.repo), "prNum": githubv4.Int(p.prNum), }, githubv4mock.DataResponse( map[string]any{ "repository": map[string]any{ "pullRequest": map[string]any{ "reviews": map[string]any{ "nodes": []any{ map[string]any{ "id": p.reviews[0].id, "state": p.reviews[0].state, "url": p.reviews[0].url, }, }, }, }, }, }, ), ) }