6c05b4009a
* Add add_pull_request_review_comment tool for PR review comments Adds the ability to add review comments to pull requests with support for line, multi-line, and file-level comments, as well as replying to existing comments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add reply_to_pull_request_review_comment tool Adds a new tool to reply to existing pull request review comments using the GitHub API's comment reply endpoint. This allows for threaded discussions on pull request reviews. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Update README with new PR review comment tools * rebase * use new getClient function inadd and reply pr review tools * Unify PR review comment tools into a single consolidated tool The separate AddPullRequestReviewComment and ReplyToPullRequestReviewComment tools have been merged into a single tool that handles both creating new comments and replying to existing ones. This approach simplifies the API and provides a more consistent interface for users. - Made commit_id and path optional when using in_reply_to for replies - Updated the tests to verify both comment and reply functionality - Removed the separate ReplyToPullRequestReviewComment tool - Fixed test expectations to match how errors are returned 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Update README to reflect the unified PR review comment tool --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Javier Uruen Val <juruen@github.com>
1919 lines
59 KiB
Go
1919 lines
59 KiB
Go
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(stubGetClientFn(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, "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.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_UpdatePullRequest(t *testing.T) {
|
|
// Verify tool definition once
|
|
mockClient := github.NewClient(nil)
|
|
tool, _ := UpdatePullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
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, "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.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),
|
|
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
|
|
}
|
|
|
|
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),
|
|
),
|
|
),
|
|
),
|
|
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),
|
|
),
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pullNumber": float64(42),
|
|
"state": "closed",
|
|
},
|
|
expectError: false,
|
|
expectedPR: mockClosedPR,
|
|
},
|
|
{
|
|
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",
|
|
},
|
|
}
|
|
|
|
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), 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
|
|
textContent := getTextResult(t, result)
|
|
|
|
// Check for expected error message within the result text
|
|
if tc.expectedErrMsg != "" {
|
|
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
|
|
return
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_ListPullRequests(t *testing.T) {
|
|
// Verify tool definition once
|
|
mockClient := github.NewClient(nil)
|
|
tool, _ := ListPullRequests(stubGetClientFn(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, "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.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(stubGetClientFn(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, "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.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(stubGetClientFn(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, "pullNumber")
|
|
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: "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.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(stubGetClientFn(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, "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.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(stubGetClientFn(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, "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.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(stubGetClientFn(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, "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.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(stubGetClientFn(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, "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.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(stubGetClientFn(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, "pullNumber")
|
|
assert.Contains(t, tool.InputSchema.Properties, "body")
|
|
assert.Contains(t, tool.InputSchema.Properties, "event")
|
|
assert.Contains(t, tool.InputSchema.Properties, "commitId")
|
|
assert.Contains(t, tool.InputSchema.Properties, "comments")
|
|
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber", "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.WithRequestMatchHandler(
|
|
mock.PostReposPullsReviewsByOwnerByRepoByPullNumber,
|
|
expectRequestBody(t, map[string]interface{}{
|
|
"body": "Looks good!",
|
|
"event": "APPROVE",
|
|
}).andThen(
|
|
mockResponse(t, http.StatusOK, mockReview),
|
|
),
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pullNumber": float64(42),
|
|
"body": "Looks good!",
|
|
"event": "APPROVE",
|
|
},
|
|
expectError: false,
|
|
expectedReview: mockReview,
|
|
},
|
|
{
|
|
name: "successful review creation with commitId",
|
|
mockedClient: mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatchHandler(
|
|
mock.PostReposPullsReviewsByOwnerByRepoByPullNumber,
|
|
expectRequestBody(t, map[string]interface{}{
|
|
"body": "Looks good!",
|
|
"event": "APPROVE",
|
|
"commit_id": "abcdef123456",
|
|
}).andThen(
|
|
mockResponse(t, http.StatusOK, mockReview),
|
|
),
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pullNumber": float64(42),
|
|
"body": "Looks good!",
|
|
"event": "APPROVE",
|
|
"commitId": "abcdef123456",
|
|
},
|
|
expectError: false,
|
|
expectedReview: mockReview,
|
|
},
|
|
{
|
|
name: "successful review creation with comments",
|
|
mockedClient: mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatchHandler(
|
|
mock.PostReposPullsReviewsByOwnerByRepoByPullNumber,
|
|
expectRequestBody(t, map[string]interface{}{
|
|
"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",
|
|
},
|
|
},
|
|
}).andThen(
|
|
mockResponse(t, http.StatusOK, mockReview),
|
|
),
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pullNumber": 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",
|
|
"pullNumber": 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 either position or line",
|
|
},
|
|
{
|
|
name: "successful review creation with line parameter",
|
|
mockedClient: mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatchHandler(
|
|
mock.PostReposPullsReviewsByOwnerByRepoByPullNumber,
|
|
expectRequestBody(t, map[string]interface{}{
|
|
"body": "Code review comments",
|
|
"event": "COMMENT",
|
|
"comments": []interface{}{
|
|
map[string]interface{}{
|
|
"path": "main.go",
|
|
"line": float64(42),
|
|
"body": "Consider adding a comment here",
|
|
},
|
|
},
|
|
}).andThen(
|
|
mockResponse(t, http.StatusOK, mockReview),
|
|
),
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pullNumber": float64(42),
|
|
"body": "Code review comments",
|
|
"event": "COMMENT",
|
|
"comments": []interface{}{
|
|
map[string]interface{}{
|
|
"path": "main.go",
|
|
"line": float64(42),
|
|
"body": "Consider adding a comment here",
|
|
},
|
|
},
|
|
},
|
|
expectError: false,
|
|
expectedReview: mockReview,
|
|
},
|
|
{
|
|
name: "successful review creation with multi-line comment",
|
|
mockedClient: mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatchHandler(
|
|
mock.PostReposPullsReviewsByOwnerByRepoByPullNumber,
|
|
expectRequestBody(t, map[string]interface{}{
|
|
"body": "Multi-line comment review",
|
|
"event": "COMMENT",
|
|
"comments": []interface{}{
|
|
map[string]interface{}{
|
|
"path": "main.go",
|
|
"start_line": float64(10),
|
|
"line": float64(15),
|
|
"side": "RIGHT",
|
|
"body": "This entire block needs refactoring",
|
|
},
|
|
},
|
|
}).andThen(
|
|
mockResponse(t, http.StatusOK, mockReview),
|
|
),
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pullNumber": float64(42),
|
|
"body": "Multi-line comment review",
|
|
"event": "COMMENT",
|
|
"comments": []interface{}{
|
|
map[string]interface{}{
|
|
"path": "main.go",
|
|
"start_line": float64(10),
|
|
"line": float64(15),
|
|
"side": "RIGHT",
|
|
"body": "This entire block needs refactoring",
|
|
},
|
|
},
|
|
},
|
|
expectError: false,
|
|
expectedReview: mockReview,
|
|
},
|
|
{
|
|
name: "invalid multi-line comment - missing line parameter",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pullNumber": float64(42),
|
|
"event": "COMMENT",
|
|
"comments": []interface{}{
|
|
map[string]interface{}{
|
|
"path": "main.go",
|
|
"start_line": float64(10),
|
|
// missing line parameter
|
|
"body": "Invalid multi-line comment",
|
|
},
|
|
},
|
|
},
|
|
expectError: false,
|
|
expectedErrMsg: "each comment must have either position or line", // Updated error message
|
|
},
|
|
{
|
|
name: "invalid comment - mixing position with line parameters",
|
|
mockedClient: mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatch(
|
|
mock.PostReposPullsReviewsByOwnerByRepoByPullNumber,
|
|
mockReview,
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pullNumber": float64(42),
|
|
"event": "COMMENT",
|
|
"comments": []interface{}{
|
|
map[string]interface{}{
|
|
"path": "main.go",
|
|
"position": float64(5),
|
|
"line": float64(42),
|
|
"body": "Invalid parameter combination",
|
|
},
|
|
},
|
|
},
|
|
expectError: false,
|
|
expectedErrMsg: "position cannot be combined with line, side, start_line, or start_side",
|
|
},
|
|
{
|
|
name: "invalid multi-line comment - missing side parameter",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pullNumber": float64(42),
|
|
"event": "COMMENT",
|
|
"comments": []interface{}{
|
|
map[string]interface{}{
|
|
"path": "main.go",
|
|
"start_line": float64(10),
|
|
"line": float64(15),
|
|
"start_side": "LEFT",
|
|
// missing side parameter
|
|
"body": "Invalid multi-line comment",
|
|
},
|
|
},
|
|
},
|
|
expectError: false,
|
|
expectedErrMsg: "if start_side is provided, side must also be provided",
|
|
},
|
|
{
|
|
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",
|
|
"pullNumber": 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(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)
|
|
|
|
// 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(stubGetClientFn(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,
|
|
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 Test_AddPullRequestReviewComment(t *testing.T) {
|
|
mockClient := github.NewClient(nil)
|
|
tool, _ := AddPullRequestReviewComment(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
assert.Equal(t, "add_pull_request_review_comment", tool.Name)
|
|
assert.NotEmpty(t, tool.Description)
|
|
assert.Contains(t, tool.InputSchema.Properties, "owner")
|
|
assert.Contains(t, tool.InputSchema.Properties, "repo")
|
|
assert.Contains(t, tool.InputSchema.Properties, "pull_number")
|
|
assert.Contains(t, tool.InputSchema.Properties, "body")
|
|
assert.Contains(t, tool.InputSchema.Properties, "commit_id")
|
|
assert.Contains(t, tool.InputSchema.Properties, "path")
|
|
// Since we've updated commit_id and path to be optional when using in_reply_to
|
|
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number", "body"})
|
|
|
|
mockComment := &github.PullRequestComment{
|
|
ID: github.Ptr(int64(123)),
|
|
Body: github.Ptr("Great stuff!"),
|
|
Path: github.Ptr("file1.txt"),
|
|
Line: github.Ptr(2),
|
|
Side: github.Ptr("RIGHT"),
|
|
}
|
|
|
|
mockReply := &github.PullRequestComment{
|
|
ID: github.Ptr(int64(456)),
|
|
Body: github.Ptr("Good point, will fix!"),
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
mockedClient *http.Client
|
|
requestArgs map[string]interface{}
|
|
expectError bool
|
|
expectedComment *github.PullRequestComment
|
|
expectedErrMsg string
|
|
}{
|
|
{
|
|
name: "successful line comment creation",
|
|
mockedClient: mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatchHandler(
|
|
mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
|
|
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusCreated)
|
|
err := json.NewEncoder(w).Encode(mockComment)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pull_number": float64(1),
|
|
"body": "Great stuff!",
|
|
"commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
"path": "file1.txt",
|
|
"line": float64(2),
|
|
"side": "RIGHT",
|
|
},
|
|
expectError: false,
|
|
expectedComment: mockComment,
|
|
},
|
|
{
|
|
name: "successful reply using in_reply_to",
|
|
mockedClient: mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatchHandler(
|
|
mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
|
|
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusCreated)
|
|
err := json.NewEncoder(w).Encode(mockReply)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pull_number": float64(1),
|
|
"body": "Good point, will fix!",
|
|
"in_reply_to": float64(123),
|
|
},
|
|
expectError: false,
|
|
expectedComment: mockReply,
|
|
},
|
|
{
|
|
name: "comment creation fails",
|
|
mockedClient: mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatchHandler(
|
|
mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
|
|
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
|
|
}),
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pull_number": float64(1),
|
|
"body": "Great stuff!",
|
|
"commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
|
"path": "file1.txt",
|
|
"line": float64(2),
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "failed to create pull request comment",
|
|
},
|
|
{
|
|
name: "reply creation fails",
|
|
mockedClient: mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatchHandler(
|
|
mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
|
|
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"message": "Comment not found"}`))
|
|
}),
|
|
),
|
|
),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pull_number": float64(1),
|
|
"body": "Good point, will fix!",
|
|
"in_reply_to": float64(999),
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "failed to reply to pull request comment",
|
|
},
|
|
{
|
|
name: "missing required parameters for comment",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]interface{}{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"pull_number": float64(1),
|
|
"body": "Great stuff!",
|
|
// missing commit_id and path
|
|
},
|
|
expectError: false,
|
|
expectedErrMsg: "missing required parameter: commit_id",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
mockClient := github.NewClient(tc.mockedClient)
|
|
|
|
_, handler := AddPullRequestReviewComment(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
request := createMCPRequest(tc.requestArgs)
|
|
|
|
result, err := handler(context.Background(), request)
|
|
|
|
if tc.expectError {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tc.expectedErrMsg)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
require.Len(t, result.Content, 1)
|
|
|
|
textContent := getTextResult(t, result)
|
|
if tc.expectedErrMsg != "" {
|
|
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
|
|
return
|
|
}
|
|
|
|
var returnedComment github.PullRequestComment
|
|
err = json.Unmarshal([]byte(getTextResult(t, result).Text), &returnedComment)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID)
|
|
assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body)
|
|
|
|
// Only check Path, Line, and Side if they exist in the expected comment
|
|
if tc.expectedComment.Path != nil {
|
|
assert.Equal(t, *tc.expectedComment.Path, *returnedComment.Path)
|
|
}
|
|
if tc.expectedComment.Line != nil {
|
|
assert.Equal(t, *tc.expectedComment.Line, *returnedComment.Line)
|
|
}
|
|
if tc.expectedComment.Side != nil {
|
|
assert.Equal(t, *tc.expectedComment.Side, *returnedComment.Side)
|
|
}
|
|
})
|
|
}
|
|
}
|