package github import ( "context" "encoding/json" "net/http" "testing" "time" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v69/github" "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(mockClient, translations.NullTranslationHelper) 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, "pull_number") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number"}) // 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", "pull_number": 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", "pull_number": 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(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 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_ListPullRequests(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := listPullRequests(mockClient, translations.NullTranslationHelper) 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, "per_page") 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.WithRequestMatch( mock.GetReposPullsByOwnerByRepo, mockPRs, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "state": "all", "sort": "created", "direction": "desc", "per_page": 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(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 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(mockClient, translations.NullTranslationHelper) 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, "pull_number") 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", "pull_number"}) // 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.WithRequestMatch( mock.PutReposPullsMergeByOwnerByRepoByPullNumber, mockMergeResult, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pull_number": 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", "pull_number": 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(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.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_GetPullRequestFiles(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := getPullRequestFiles(mockClient, translations.NullTranslationHelper) 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, "pull_number") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number"}) // 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", "pull_number": float64(42), }, 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", "pull_number": 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(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 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(mockClient, translations.NullTranslationHelper) 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, "pull_number") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number"}) // 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", "pull_number": 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", "pull_number": 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", "pull_number": 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(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 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(mockClient, translations.NullTranslationHelper) 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, "pull_number") assert.Contains(t, tool.InputSchema.Properties, "expected_head_sha") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number"}) // 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, mockResponse(t, http.StatusAccepted, mockUpdateResult), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pull_number": float64(42), "expected_head_sha": "abcd1234", }, expectError: false, expectedUpdateResult: mockUpdateResult, }, { name: "branch update without expected SHA", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PutReposPullsUpdateBranchByOwnerByRepoByPullNumber, mockResponse(t, http.StatusAccepted, mockUpdateResult), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pull_number": 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", "pull_number": 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(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) assert.Contains(t, textContent.Text, "is in progress") }) } } func Test_GetPullRequestComments(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := getPullRequestComments(mockClient, translations.NullTranslationHelper) 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, "pull_number") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number"}) // 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", "pull_number": 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", "pull_number": 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(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 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(mockClient, translations.NullTranslationHelper) 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, "pull_number") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number"}) // 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", "pull_number": 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", "pull_number": 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(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 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_CreatePullRequestReview(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := createPullRequestReview(mockClient, translations.NullTranslationHelper) assert.Equal(t, "create_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, "pull_number") assert.Contains(t, tool.InputSchema.Properties, "body") assert.Contains(t, tool.InputSchema.Properties, "event") assert.Contains(t, tool.InputSchema.Properties, "commit_id") assert.Contains(t, tool.InputSchema.Properties, "comments") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number", "event"}) // Setup mock review for success case mockReview := &github.PullRequestReview{ ID: github.Ptr(int64(301)), State: github.Ptr("APPROVED"), Body: github.Ptr("Looks good!"), HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42#pullrequestreview-301"), User: &github.User{ Login: github.Ptr("reviewer"), }, CommitID: github.Ptr("abcdef123456"), SubmittedAt: &github.Timestamp{Time: time.Now()}, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedReview *github.PullRequestReview expectedErrMsg string }{ { name: "successful review creation with body only", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.PostReposPullsReviewsByOwnerByRepoByPullNumber, mockReview, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pull_number": float64(42), "body": "Looks good!", "event": "APPROVE", }, expectError: false, expectedReview: mockReview, }, { name: "successful review creation with commit_id", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.PostReposPullsReviewsByOwnerByRepoByPullNumber, mockReview, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pull_number": float64(42), "body": "Looks good!", "event": "APPROVE", "commit_id": "abcdef123456", }, expectError: false, expectedReview: mockReview, }, { name: "successful review creation with comments", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.PostReposPullsReviewsByOwnerByRepoByPullNumber, mockReview, ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pull_number": float64(42), "body": "Some issues to fix", "event": "REQUEST_CHANGES", "comments": []interface{}{ map[string]interface{}{ "path": "file1.go", "position": float64(10), "body": "This needs to be fixed", }, map[string]interface{}{ "path": "file2.go", "position": float64(20), "body": "Consider a different approach here", }, }, }, expectError: false, expectedReview: mockReview, }, { name: "invalid comment format", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PostReposPullsReviewsByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Invalid comment format"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pull_number": float64(42), "event": "REQUEST_CHANGES", "comments": []interface{}{ map[string]interface{}{ "path": "file1.go", // missing position "body": "This needs to be fixed", }, }, }, expectError: false, expectedErrMsg: "each comment must have a position", }, { name: "review creation fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.PostReposPullsReviewsByOwnerByRepoByPullNumber, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Invalid comment format"}`)) }), ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", "pull_number": float64(42), "body": "Looks good!", "event": "APPROVE", }, expectError: true, expectedErrMsg: "failed to create pull request review", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := createPullRequestReview(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) // For error messages in the result if tc.expectedErrMsg != "" { textContent := getTextResult(t, result) assert.Contains(t, textContent.Text, tc.expectedErrMsg) return } // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedReview github.PullRequestReview err = json.Unmarshal([]byte(textContent.Text), &returnedReview) require.NoError(t, err) assert.Equal(t, *tc.expectedReview.ID, *returnedReview.ID) assert.Equal(t, *tc.expectedReview.State, *returnedReview.State) assert.Equal(t, *tc.expectedReview.Body, *returnedReview.Body) assert.Equal(t, *tc.expectedReview.User.Login, *returnedReview.User.Login) assert.Equal(t, *tc.expectedReview.HTMLURL, *returnedReview.HTMLURL) }) } } func Test_CreatePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := createPullRequest(mockClient, translations.NullTranslationHelper) 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, 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(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) }) } }