Files
github--github-mcp-server/pkg/github/search_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

468 lines
15 KiB
Go

package github
import (
"context"
"encoding/json"
"net/http"
"testing"
"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_SearchRepositories(t *testing.T) {
// Verify tool definition once
tool := SearchRepositories(translations.NullTranslationHelper)
assert.Equal(t, "search_repositories", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "query")
assert.Contains(t, tool.Definition.InputSchema.Properties, "page")
assert.Contains(t, tool.Definition.InputSchema.Properties, "perPage")
assert.ElementsMatch(t, tool.Definition.InputSchema.Required, []string{"query"})
// Setup mock search results
mockSearchResult := &github.RepositoriesSearchResult{
Total: github.Ptr(2),
IncompleteResults: github.Ptr(false),
Repositories: []*github.Repository{
{
ID: github.Ptr(int64(12345)),
Name: github.Ptr("repo-1"),
FullName: github.Ptr("owner/repo-1"),
HTMLURL: github.Ptr("https://github.com/owner/repo-1"),
Description: github.Ptr("Test repository 1"),
StargazersCount: github.Ptr(100),
},
{
ID: github.Ptr(int64(67890)),
Name: github.Ptr("repo-2"),
FullName: github.Ptr("owner/repo-2"),
HTMLURL: github.Ptr("https://github.com/owner/repo-2"),
Description: github.Ptr("Test repository 2"),
StargazersCount: github.Ptr(50),
},
},
}
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectedResult *github.RepositoriesSearchResult
expectedErrMsg string
}{
{
name: "successful repository search",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchRepositories,
expectQueryParams(t, map[string]string{
"q": "golang test",
"page": "2",
"per_page": "10",
}).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
),
),
requestArgs: map[string]interface{}{
"query": "golang test",
"page": float64(2),
"perPage": float64(10),
},
expectError: false,
expectedResult: mockSearchResult,
},
{
name: "repository search with default pagination",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchRepositories,
expectQueryParams(t, map[string]string{
"q": "golang test",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
),
),
requestArgs: map[string]interface{}{
"query": "golang test",
},
expectError: false,
expectedResult: mockSearchResult,
},
{
name: "search fails",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchRepositories,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message": "Invalid query"}`))
}),
),
),
requestArgs: map[string]interface{}{
"query": "invalid:query",
},
expectError: true,
expectedErrMsg: "failed to search repositories",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
tool := SearchRepositories(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.RepositoriesSearchResult
err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
require.NoError(t, err)
assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total)
assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults)
assert.Len(t, returnedResult.Repositories, len(tc.expectedResult.Repositories))
for i, repo := range returnedResult.Repositories {
assert.Equal(t, *tc.expectedResult.Repositories[i].ID, *repo.ID)
assert.Equal(t, *tc.expectedResult.Repositories[i].Name, *repo.Name)
assert.Equal(t, *tc.expectedResult.Repositories[i].FullName, *repo.FullName)
assert.Equal(t, *tc.expectedResult.Repositories[i].HTMLURL, *repo.HTMLURL)
}
})
}
}
func Test_SearchCode(t *testing.T) {
// Verify tool definition once
tool := SearchCode(translations.NullTranslationHelper)
assert.Equal(t, "search_code", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "q")
assert.Contains(t, tool.Definition.InputSchema.Properties, "sort")
assert.Contains(t, tool.Definition.InputSchema.Properties, "order")
assert.Contains(t, tool.Definition.InputSchema.Properties, "perPage")
assert.Contains(t, tool.Definition.InputSchema.Properties, "page")
assert.ElementsMatch(t, tool.Definition.InputSchema.Required, []string{"q"})
// Setup mock search results
mockSearchResult := &github.CodeSearchResult{
Total: github.Ptr(2),
IncompleteResults: github.Ptr(false),
CodeResults: []*github.CodeResult{
{
Name: github.Ptr("file1.go"),
Path: github.Ptr("path/to/file1.go"),
SHA: github.Ptr("abc123def456"),
HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/path/to/file1.go"),
Repository: &github.Repository{Name: github.Ptr("repo"), FullName: github.Ptr("owner/repo")},
},
{
Name: github.Ptr("file2.go"),
Path: github.Ptr("path/to/file2.go"),
SHA: github.Ptr("def456abc123"),
HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/path/to/file2.go"),
Repository: &github.Repository{Name: github.Ptr("repo"), FullName: github.Ptr("owner/repo")},
},
},
}
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectedResult *github.CodeSearchResult
expectedErrMsg string
}{
{
name: "successful code search with all parameters",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchCode,
expectQueryParams(t, map[string]string{
"q": "fmt.Println language:go",
"sort": "indexed",
"order": "desc",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
),
),
requestArgs: map[string]interface{}{
"q": "fmt.Println language:go",
"sort": "indexed",
"order": "desc",
"page": float64(1),
"perPage": float64(30),
},
expectError: false,
expectedResult: mockSearchResult,
},
{
name: "code search with minimal parameters",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchCode,
expectQueryParams(t, map[string]string{
"q": "fmt.Println language:go",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
),
),
requestArgs: map[string]interface{}{
"q": "fmt.Println language:go",
},
expectError: false,
expectedResult: mockSearchResult,
},
{
name: "search code fails",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchCode,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
}),
),
),
requestArgs: map[string]interface{}{
"q": "invalid:query",
},
expectError: true,
expectedErrMsg: "failed to search code",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
tool := SearchCode(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.CodeSearchResult
err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
require.NoError(t, err)
assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total)
assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults)
assert.Len(t, returnedResult.CodeResults, len(tc.expectedResult.CodeResults))
for i, code := range returnedResult.CodeResults {
assert.Equal(t, *tc.expectedResult.CodeResults[i].Name, *code.Name)
assert.Equal(t, *tc.expectedResult.CodeResults[i].Path, *code.Path)
assert.Equal(t, *tc.expectedResult.CodeResults[i].SHA, *code.SHA)
assert.Equal(t, *tc.expectedResult.CodeResults[i].HTMLURL, *code.HTMLURL)
assert.Equal(t, *tc.expectedResult.CodeResults[i].Repository.FullName, *code.Repository.FullName)
}
})
}
}
func Test_SearchUsers(t *testing.T) {
// Verify tool definition once
tool := SearchUsers(translations.NullTranslationHelper)
assert.Equal(t, "search_users", tool.Definition.Name)
assert.NotEmpty(t, tool.Definition.Description)
assert.Contains(t, tool.Definition.InputSchema.Properties, "q")
assert.Contains(t, tool.Definition.InputSchema.Properties, "sort")
assert.Contains(t, tool.Definition.InputSchema.Properties, "order")
assert.Contains(t, tool.Definition.InputSchema.Properties, "perPage")
assert.Contains(t, tool.Definition.InputSchema.Properties, "page")
assert.ElementsMatch(t, tool.Definition.InputSchema.Required, []string{"q"})
// Setup mock search results
mockSearchResult := &github.UsersSearchResult{
Total: github.Ptr(2),
IncompleteResults: github.Ptr(false),
Users: []*github.User{
{
Login: github.Ptr("user1"),
ID: github.Ptr(int64(1001)),
HTMLURL: github.Ptr("https://github.com/user1"),
AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/1001"),
Type: github.Ptr("User"),
Followers: github.Ptr(100),
Following: github.Ptr(50),
},
{
Login: github.Ptr("user2"),
ID: github.Ptr(int64(1002)),
HTMLURL: github.Ptr("https://github.com/user2"),
AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/1002"),
Type: github.Ptr("User"),
Followers: github.Ptr(200),
Following: github.Ptr(75),
},
},
}
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectedResult *github.UsersSearchResult
expectedErrMsg string
}{
{
name: "successful users search with all parameters",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchUsers,
expectQueryParams(t, map[string]string{
"q": "location:finland language:go",
"sort": "followers",
"order": "desc",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
),
),
requestArgs: map[string]interface{}{
"q": "location:finland language:go",
"sort": "followers",
"order": "desc",
"page": float64(1),
"perPage": float64(30),
},
expectError: false,
expectedResult: mockSearchResult,
},
{
name: "users search with minimal parameters",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchUsers,
expectQueryParams(t, map[string]string{
"q": "location:finland language:go",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
),
),
requestArgs: map[string]interface{}{
"q": "location:finland language:go",
},
expectError: false,
expectedResult: mockSearchResult,
},
{
name: "search users fails",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchUsers,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
}),
),
),
requestArgs: map[string]interface{}{
"q": "invalid:query",
},
expectError: true,
expectedErrMsg: "failed to search users",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
tool := SearchUsers(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
require.NotNil(t, result)
textContent := getTextResult(t, result)
// Unmarshal and verify the result
var returnedResult github.UsersSearchResult
err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
require.NoError(t, err)
assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total)
assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults)
assert.Len(t, returnedResult.Users, len(tc.expectedResult.Users))
for i, user := range returnedResult.Users {
assert.Equal(t, *tc.expectedResult.Users[i].Login, *user.Login)
assert.Equal(t, *tc.expectedResult.Users[i].ID, *user.ID)
assert.Equal(t, *tc.expectedResult.Users[i].HTMLURL, *user.HTMLURL)
assert.Equal(t, *tc.expectedResult.Users[i].AvatarURL, *user.AvatarURL)
assert.Equal(t, *tc.expectedResult.Users[i].Type, *user.Type)
assert.Equal(t, *tc.expectedResult.Users[i].Followers, *user.Followers)
}
})
}
}