package github import ( "context" "encoding/json" "net/http" "testing" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v74/github" "github.com/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 mockClient := github.NewClient(nil) tool, _ := SearchRepositories(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_repositories", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "query") assert.Contains(t, tool.InputSchema.Properties, "page") assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.ElementsMatch(t, tool.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) _, handler := SearchRepositories(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedResult github.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 mockClient := github.NewClient(nil) tool, _ := SearchCode(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_code", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "query") assert.Contains(t, tool.InputSchema.Properties, "sort") assert.Contains(t, tool.InputSchema.Properties, "order") assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.Contains(t, tool.InputSchema.Properties, "page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"}) // 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{}{ "query": "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{}{ "query": "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{}{ "query": "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) _, handler := SearchCode(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedResult github.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 mockClient := github.NewClient(nil) tool, _ := SearchUsers(stubGetClientFn(mockClient), translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_users", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "query") assert.Contains(t, tool.InputSchema.Properties, "sort") assert.Contains(t, tool.InputSchema.Properties, "order") assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.Contains(t, tool.InputSchema.Properties, "page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"}) // 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"), }, { 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"), }, }, } 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": "type:user location:finland language:go", "sort": "followers", "order": "desc", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "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": "type:user location:finland language:go", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "location:finland language:go", }, expectError: false, expectedResult: mockSearchResult, }, { name: "query with existing type:user filter - no duplication", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchUsers, expectQueryParams(t, map[string]string{ "q": "type:user location:seattle followers:>100", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "type:user location:seattle followers:>100", }, expectError: false, expectedResult: mockSearchResult, }, { name: "complex query with existing type:user filter and OR operators", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchUsers, expectQueryParams(t, map[string]string{ "q": "type:user (location:seattle OR location:california) followers:>50", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "type:user (location:seattle OR location:california) followers:>50", }, 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{}{ "query": "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) _, handler := SearchUsers(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.False(t, result.IsError) // Parse the result and get the text content if no error require.NotNil(t, result) textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedResult MinimalSearchUsersResult err = json.Unmarshal([]byte(textContent.Text), &returnedResult) require.NoError(t, err) assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount) assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults) assert.Len(t, returnedResult.Items, len(tc.expectedResult.Users)) for i, user := range returnedResult.Items { 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.ProfileURL) assert.Equal(t, *tc.expectedResult.Users[i].AvatarURL, user.AvatarURL) } }) } } func Test_SearchOrgs(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := SearchOrgs(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "search_orgs", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "query") assert.Contains(t, tool.InputSchema.Properties, "sort") assert.Contains(t, tool.InputSchema.Properties, "order") assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.Contains(t, tool.InputSchema.Properties, "page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"}) // Setup mock search results mockSearchResult := &github.UsersSearchResult{ Total: github.Ptr(int(2)), IncompleteResults: github.Ptr(false), Users: []*github.User{ { Login: github.Ptr("org-1"), ID: github.Ptr(int64(111)), HTMLURL: github.Ptr("https://github.com/org-1"), AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/111?v=4"), }, { Login: github.Ptr("org-2"), ID: github.Ptr(int64(222)), HTMLURL: github.Ptr("https://github.com/org-2"), AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/222?v=4"), }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]interface{} expectError bool expectedResult *github.UsersSearchResult expectedErrMsg string }{ { name: "successful org search", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchUsers, expectQueryParams(t, map[string]string{ "q": "type:org github", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "github", }, expectError: false, expectedResult: mockSearchResult, }, { name: "query with existing type:org filter - no duplication", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchUsers, expectQueryParams(t, map[string]string{ "q": "type:org location:california followers:>1000", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "type:org location:california followers:>1000", }, expectError: false, expectedResult: mockSearchResult, }, { name: "complex query with existing type:org filter and OR operators", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( mock.GetSearchUsers, expectQueryParams(t, map[string]string{ "q": "type:org (location:seattle OR location:california OR location:newyork) repos:>10", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), ), ), requestArgs: map[string]interface{}{ "query": "type:org (location:seattle OR location:california OR location:newyork) repos:>10", }, expectError: false, expectedResult: mockSearchResult, }, { name: "org search 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{}{ "query": "invalid:query", }, expectError: true, expectedErrMsg: "failed to search orgs", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) _, handler := SearchOrgs(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(context.Background(), request) // Verify results if tc.expectError { require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) require.NotNil(t, result) textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedResult MinimalSearchUsersResult err = json.Unmarshal([]byte(textContent.Text), &returnedResult) require.NoError(t, err) assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount) assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults) assert.Len(t, returnedResult.Items, len(tc.expectedResult.Users)) for i, org := range returnedResult.Items { assert.Equal(t, *tc.expectedResult.Users[i].Login, org.Login) assert.Equal(t, *tc.expectedResult.Users[i].ID, org.ID) assert.Equal(t, *tc.expectedResult.Users[i].HTMLURL, org.ProfileURL) assert.Equal(t, *tc.expectedResult.Users[i].AvatarURL, org.AvatarURL) } }) } }