Files
github--github-mcp-server/pkg/github/pullrequests_test.go
William Martin 4df2d3d803
Unit Tests / build (macos-latest) (push) Has been cancelled
Unit Tests / build (ubuntu-latest) (push) Has been cancelled
Unit Tests / build (windows-latest) (push) Has been cancelled
License Check / license-check (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Lint / lint (push) Has been cancelled
WIP: schema generation
2025-04-11 22:20:59 +02:00

1711 lines
53 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
tool := GetPullRequest(translations.NullTranslationHelper)
assert.Equal(t, "get_pull_request", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "pullNumber")
assert.ElementsMatch(t, tool.Definition.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)
tool := GetPullRequest(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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
tool := UpdatePullRequest(translations.NullTranslationHelper)
assert.Equal(t, "update_pull_request", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "pullNumber")
assert.Contains(t, tool.Definition.InputSchema.Properties, "title")
assert.Contains(t, tool.Definition.InputSchema.Properties, "body")
assert.Contains(t, tool.Definition.InputSchema.Properties, "state")
assert.Contains(t, tool.Definition.InputSchema.Properties, "base")
assert.Contains(t, tool.Definition.InputSchema.Properties, "maintainer_can_modify")
assert.ElementsMatch(t, tool.Definition.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)
tool := UpdatePullRequest(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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
tool := ListPullRequests(translations.NullTranslationHelper)
assert.Equal(t, "list_pull_requests", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "state")
assert.Contains(t, tool.Definition.InputSchema.Properties, "head")
assert.Contains(t, tool.Definition.InputSchema.Properties, "base")
assert.Contains(t, tool.Definition.InputSchema.Properties, "sort")
assert.Contains(t, tool.Definition.InputSchema.Properties, "direction")
assert.Contains(t, tool.Definition.InputSchema.Properties, "perPage")
assert.Contains(t, tool.Definition.InputSchema.Properties, "page")
assert.ElementsMatch(t, tool.Definition.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)
tool := ListPullRequests(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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
tool := MergePullRequest(translations.NullTranslationHelper)
assert.Equal(t, "merge_pull_request", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "pullNumber")
assert.Contains(t, tool.Definition.InputSchema.Properties, "commit_title")
assert.Contains(t, tool.Definition.InputSchema.Properties, "commit_message")
assert.Contains(t, tool.Definition.InputSchema.Properties, "merge_method")
assert.ElementsMatch(t, tool.Definition.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)
tool := MergePullRequest(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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
tool := GetPullRequestFiles(translations.NullTranslationHelper)
assert.Equal(t, "get_pull_request_files", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "pullNumber")
assert.ElementsMatch(t, tool.Definition.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)
tool := GetPullRequestFiles(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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
tool := GetPullRequestStatus(translations.NullTranslationHelper)
assert.Equal(t, "get_pull_request_status", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "pullNumber")
assert.ElementsMatch(t, tool.Definition.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)
tool := GetPullRequestStatus(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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
tool := UpdatePullRequestBranch(translations.NullTranslationHelper)
assert.Equal(t, "update_pull_request_branch", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "pullNumber")
assert.Contains(t, tool.Definition.InputSchema.Properties, "expectedHeadSha")
assert.ElementsMatch(t, tool.Definition.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)
tool := UpdatePullRequestBranch(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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
tool := GetPullRequestComments(translations.NullTranslationHelper)
assert.Equal(t, "get_pull_request_comments", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "pullNumber")
assert.ElementsMatch(t, tool.Definition.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)
tool := GetPullRequestComments(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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
tool := GetPullRequestReviews(translations.NullTranslationHelper)
assert.Equal(t, "get_pull_request_reviews", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "pullNumber")
assert.ElementsMatch(t, tool.Definition.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)
tool := GetPullRequestReviews(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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
tool := CreatePullRequestReview(translations.NullTranslationHelper)
assert.Equal(t, "create_pull_request_review", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "pullNumber")
assert.Contains(t, tool.Definition.InputSchema.Properties, "body")
assert.Contains(t, tool.Definition.InputSchema.Properties, "event")
assert.Contains(t, tool.Definition.InputSchema.Properties, "commitId")
assert.Contains(t, tool.Definition.InputSchema.Properties, "comments")
assert.ElementsMatch(t, tool.Definition.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)
tool := CreatePullRequestReview(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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
tool := CreatePullRequest(translations.NullTranslationHelper)
assert.Equal(t, "create_pull_request", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "owner")
assert.Contains(t, tool.Definition.InputSchema.Properties, "repo")
assert.Contains(t, tool.Definition.InputSchema.Properties, "title")
assert.Contains(t, tool.Definition.InputSchema.Properties, "body")
assert.Contains(t, tool.Definition.InputSchema.Properties, "head")
assert.Contains(t, tool.Definition.InputSchema.Properties, "base")
assert.Contains(t, tool.Definition.InputSchema.Properties, "draft")
assert.Contains(t, tool.Definition.InputSchema.Properties, "maintainer_can_modify")
assert.ElementsMatch(t, tool.Definition.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)
tool := CreatePullRequest(translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := tool.Handler(stubGetClientFn(client))(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)
})
}
}