package github import ( "context" "encoding/json" "net/http" "testing" "time" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v89/github" "github.com/google/jsonschema-go/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_SearchRepositories(t *testing.T) { // Verify tool definition once serverTool := SearchRepositories(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_repositories", tool.Name) assert.NotEmpty(t, tool.Description) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Contains(t, schema.Properties, "query") assert.Contains(t, schema.Properties, "sort") assert.Contains(t, schema.Properties, "order") assert.Contains(t, schema.Properties, "page") assert.Contains(t, schema.Properties, "perPage") assert.ElementsMatch(t, schema.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]any expectError bool expectedResult *github.RepositoriesSearchResult expectedErrMsg string }{ { name: "successful repository search", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchRepositories: expectQueryParams(t, map[string]string{ "q": "golang test", "sort": "stars", "order": "desc", "page": "2", "per_page": "10", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "golang test", "sort": "stars", "order": "desc", "page": float64(2), "perPage": float64(10), }, expectError: false, expectedResult: mockSearchResult, }, { name: "repository search with default pagination", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchRepositories: expectQueryParams(t, map[string]string{ "q": "golang test", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "golang test", }, expectError: false, expectedResult: mockSearchResult, }, { name: "search fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchRepositories: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message": "Invalid query"}`)) }), }), requestArgs: map[string]any{ "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 := mustNewGHClient(t, tc.mockedClient) deps := BaseDeps{ Client: client, } handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &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 MinimalSearchRepositoriesResult 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.Repositories)) for i, repo := range returnedResult.Items { 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_SearchRepositories_IFC_InsidersMode(t *testing.T) { t.Parallel() serverTool := SearchRepositories(translations.NullTranslationHelper) type repoFixture struct { owner string name string isPrivate bool } makeRepo := func(r repoFixture) *github.Repository { return &github.Repository{ ID: github.Ptr(int64(1)), Name: github.Ptr(r.name), FullName: github.Ptr(r.owner + "/" + r.name), Private: github.Ptr(r.isPrivate), Owner: &github.User{Login: github.Ptr(r.owner)}, } } makeMockClient := func(repos []repoFixture) *http.Client { searchResult := &github.RepositoriesSearchResult{ Total: github.Ptr(len(repos)), IncompleteResults: github.Ptr(false), } for _, r := range repos { searchResult.Repositories = append(searchResult.Repositories, makeRepo(r)) } return MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchRepositories: mockResponse(t, http.StatusOK, searchResult), }) } reqParams := map[string]any{"query": "octocat"} t.Run("insiders mode disabled omits ifc label", func(t *testing.T) { deps := BaseDeps{ Client: mustNewGHClient(t, makeMockClient([]repoFixture{{owner: "octocat", name: "public-repo"}})), } handler := serverTool.Handler(deps) request := createMCPRequest(reqParams) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) assert.Nil(t, result.Meta) }) t.Run("insiders mode all public emits public untrusted", func(t *testing.T) { deps := BaseDeps{ Client: mustNewGHClient(t, makeMockClient([]repoFixture{ {owner: "octocat", name: "public-a"}, {owner: "octocat", name: "public-b"}, })), featureChecker: featureCheckerFor(FeatureFlagIFCLabels), } handler := serverTool.Handler(deps) request := createMCPRequest(reqParams) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) require.NotNil(t, result.Meta) ifcMap := unmarshalIFC(t, result.Meta["ifc"]) assert.Equal(t, "untrusted", ifcMap["integrity"]) assert.Equal(t, "public", ifcMap["confidentiality"]) }) t.Run("insiders mode mixed public and private emits private untrusted", func(t *testing.T) { deps := BaseDeps{ Client: mustNewGHClient(t, makeMockClient([]repoFixture{ {owner: "octocat", name: "private-repo", isPrivate: true}, {owner: "octocat", name: "public-repo"}, })), featureChecker: featureCheckerFor(FeatureFlagIFCLabels), } handler := serverTool.Handler(deps) request := createMCPRequest(reqParams) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) require.NotNil(t, result.Meta) ifcMap := unmarshalIFC(t, result.Meta["ifc"]) assert.Equal(t, "untrusted", ifcMap["integrity"]) assert.Equal(t, "private", ifcMap["confidentiality"]) }) t.Run("insiders mode empty results emits public untrusted", func(t *testing.T) { deps := BaseDeps{ Client: mustNewGHClient(t, makeMockClient(nil)), featureChecker: featureCheckerFor(FeatureFlagIFCLabels), } handler := serverTool.Handler(deps) request := createMCPRequest(reqParams) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) require.NotNil(t, result.Meta) ifcMap := unmarshalIFC(t, result.Meta["ifc"]) assert.Equal(t, "untrusted", ifcMap["integrity"]) assert.Equal(t, "public", ifcMap["confidentiality"]) }) } func Test_SearchRepositories_FullOutput(t *testing.T) { mockSearchResult := &github.RepositoriesSearchResult{ Total: github.Ptr(1), IncompleteResults: github.Ptr(false), Repositories: []*github.Repository{ { ID: github.Ptr(int64(12345)), Name: github.Ptr("test-repo"), FullName: github.Ptr("owner/test-repo"), HTMLURL: github.Ptr("https://github.com/owner/test-repo"), Description: github.Ptr("Test repository"), StargazersCount: github.Ptr(100), }, }, } mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchRepositories: expectQueryParams(t, map[string]string{ "q": "golang test", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }) client := mustNewGHClient(t, mockedClient) serverTool := SearchRepositories(translations.NullTranslationHelper) deps := BaseDeps{ Client: client, } handler := serverTool.Handler(deps) args := map[string]any{ "query": "golang test", "minimal_output": false, } request := createMCPRequest(args) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) textContent := getTextResult(t, result) // Unmarshal as full GitHub API response var returnedResult github.RepositoriesSearchResult err = json.Unmarshal([]byte(textContent.Text), &returnedResult) require.NoError(t, err) // Verify it's the full API response, not minimal assert.Equal(t, *mockSearchResult.Total, *returnedResult.Total) assert.Equal(t, *mockSearchResult.IncompleteResults, *returnedResult.IncompleteResults) assert.Len(t, returnedResult.Repositories, 1) assert.Equal(t, *mockSearchResult.Repositories[0].ID, *returnedResult.Repositories[0].ID) assert.Equal(t, *mockSearchResult.Repositories[0].Name, *returnedResult.Repositories[0].Name) } func Test_SearchCode(t *testing.T) { // Verify tool definition once serverTool := SearchCode(translations.NullTranslationHelper) tool := serverTool.Tool // SearchCode is the FeatureFlagFieldsParam-enabled variant; it owns the // _ff_ snapshot. The canonical search_code.snap is owned by // LegacySearchCode (see Test_LegacySearchCode_Definition). require.NoError(t, toolsnaps.Test(tool.Name+"_ff_"+FeatureFlagFieldsParam, tool)) require.Equal(t, FeatureFlagFieldsParam, serverTool.FeatureFlagEnable) assert.Equal(t, "search_code", tool.Name) assert.NotEmpty(t, tool.Description) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Contains(t, schema.Properties, "query") assert.Contains(t, schema.Properties, "sort") assert.Contains(t, schema.Properties, "order") assert.Contains(t, schema.Properties, "perPage") assert.Contains(t, schema.Properties, "page") assert.Contains(t, schema.Properties, "fields") assert.ElementsMatch(t, schema.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"), Repository: &github.Repository{ Name: github.Ptr("repo"), FullName: github.Ptr("owner/repo"), }, TextMatches: []*github.TextMatch{ { Fragment: github.Ptr("func main() { fmt.Println(\"hello\") }"), }, }, }, { Name: github.Ptr("file2.go"), Path: github.Ptr("path/to/file2.go"), SHA: github.Ptr("def456abc123"), Repository: &github.Repository{ Name: github.Ptr("repo"), FullName: github.Ptr("owner/repo"), }, }, }, } textMatchAcceptHeader := map[string]string{ "Accept": "text-match", } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedResult *github.CodeSearchResult expectedErrMsg string }{ { name: "successful code search with all parameters", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchCode: expectQueryParams(t, map[string]string{ "q": "fmt.Println language:go", "sort": "indexed", "order": "desc", "page": "1", "per_page": "30", }).withHeaders(textMatchAcceptHeader).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "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: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchCode: expectQueryParams(t, map[string]string{ "q": "fmt.Println language:go", "page": "1", "per_page": "30", }).withHeaders(textMatchAcceptHeader).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "fmt.Println language:go", }, expectError: false, expectedResult: mockSearchResult, }, { name: "search code fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchCode: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) }), }), requestArgs: map[string]any{ "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 := mustNewGHClient(t, tc.mockedClient) deps := BaseDeps{ Client: client, } handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &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) textContent := getTextResult(t, result) var returnedResult MinimalCodeSearchResult 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.CodeResults)) for i, code := range returnedResult.Items { assert.Equal(t, tc.expectedResult.CodeResults[i].GetName(), code.Name) assert.Equal(t, tc.expectedResult.CodeResults[i].GetPath(), code.Path) assert.Equal(t, tc.expectedResult.CodeResults[i].GetSHA(), code.SHA) assert.Equal(t, tc.expectedResult.CodeResults[i].Repository.GetFullName(), code.Repository) } // Verify text matches are included when present if len(tc.expectedResult.CodeResults[0].TextMatches) > 0 { require.NotEmpty(t, returnedResult.Items[0].TextMatches) assert.Equal(t, tc.expectedResult.CodeResults[0].TextMatches[0].GetFragment(), returnedResult.Items[0].TextMatches[0].GetFragment(), ) } }) } } func Test_SearchCode_FieldFiltering(t *testing.T) { mockSearchResult := &github.CodeSearchResult{ Total: github.Ptr(1), IncompleteResults: github.Ptr(false), CodeResults: []*github.CodeResult{ { Name: github.Ptr("file1.go"), Path: github.Ptr("path/to/file1.go"), SHA: github.Ptr("abc123def456"), Repository: &github.Repository{ Name: github.Ptr("repo"), FullName: github.Ptr("owner/repo"), }, TextMatches: []*github.TextMatch{ {Fragment: github.Ptr("func main() {}")}, }, }, }, } serverTool := SearchCode(translations.NullTranslationHelper) client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchCode: mockResponse(t, http.StatusOK, mockSearchResult), })) deps := BaseDeps{Client: client} handler := serverTool.Handler(deps) request := createMCPRequest(map[string]any{ "query": "fmt.Println language:go", "fields": []any{"name", "path"}, }) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) textContent := getTextResult(t, result) // The wrapper metadata is preserved while each item is reduced to the // requested fields only; the heavier repository and text_matches data is // dropped. var returned struct { TotalCount int `json:"total_count"` IncompleteResults bool `json:"incomplete_results"` Items []map[string]any `json:"items"` } require.NoError(t, json.Unmarshal([]byte(textContent.Text), &returned)) assert.Equal(t, 1, returned.TotalCount) require.Len(t, returned.Items, 1) require.Len(t, returned.Items[0], 2) assert.Contains(t, returned.Items[0], "name") assert.Contains(t, returned.Items[0], "path") assert.NotContains(t, textContent.Text, "repository") assert.NotContains(t, textContent.Text, "text_matches") } func Test_LegacySearchCode_Definition(t *testing.T) { serverTool := LegacySearchCode(translations.NullTranslationHelper) tool := serverTool.Tool // LegacySearchCode is the FeatureFlagFieldsParam-disabled variant and owns // the canonical search_code.snap (no `fields`). require.NoError(t, toolsnaps.Test(tool.Name, tool)) require.Equal(t, []string{FeatureFlagFieldsParam}, serverTool.FeatureFlagDisable) assert.Equal(t, "search_code", tool.Name) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.NotContains(t, schema.Properties, "fields") } func Test_SearchCode_FieldsTelemetry(t *testing.T) { mockSearchResult := &github.CodeSearchResult{ Total: github.Ptr(1), IncompleteResults: github.Ptr(false), CodeResults: []*github.CodeResult{ { Name: github.Ptr("file1.go"), Path: github.Ptr("path/to/file1.go"), SHA: github.Ptr("abc123def456"), Repository: &github.Repository{ Name: github.Ptr("repo"), FullName: github.Ptr("owner/repo"), }, TextMatches: []*github.TextMatch{ {Fragment: github.Ptr("func main() {}")}, }, }, }, } serverTool := SearchCode(translations.NullTranslationHelper) client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchCode: mockResponse(t, http.StatusOK, mockSearchResult), })) t.Run("filtered call records savings", func(t *testing.T) { deps, rec := depsWithRecordingMetrics(t, BaseDeps{Client: client}) handler := serverTool.Handler(deps) request := createMCPRequest(map[string]any{ "query": "fmt.Println language:go", "fields": []any{"name", "path"}, }) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) call, ok := rec.increment(metricFieldsToolCall) require.True(t, ok) assert.Equal(t, "search_code", call.tags["tool"]) assert.Equal(t, "true", call.tags["filtered"]) full, ok := rec.counter(metricFieldsBytesFull) require.True(t, ok) sent, ok := rec.counter(metricFieldsBytesSent) require.True(t, ok) assert.Greater(t, full.value, sent.value, "filtering should remove bytes") }) t.Run("unfiltered call records adoption only", func(t *testing.T) { deps, rec := depsWithRecordingMetrics(t, BaseDeps{Client: client}) handler := serverTool.Handler(deps) request := createMCPRequest(map[string]any{ "query": "fmt.Println language:go", }) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) call, ok := rec.increment(metricFieldsToolCall) require.True(t, ok) assert.Equal(t, "false", call.tags["filtered"]) _, ok = rec.counter(metricFieldsBytesFull) assert.False(t, ok, "no byte counters when not filtered") }) } func Test_SearchUsers(t *testing.T) { // Verify tool definition once serverTool := SearchUsers(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_users", tool.Name) assert.NotEmpty(t, tool.Description) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Contains(t, schema.Properties, "query") assert.Contains(t, schema.Properties, "sort") assert.Contains(t, schema.Properties, "order") assert.Contains(t, schema.Properties, "perPage") assert.Contains(t, schema.Properties, "page") assert.ElementsMatch(t, schema.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]any expectError bool expectedResult *github.UsersSearchResult expectedErrMsg string }{ { name: "successful users search with all parameters", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ 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]any{ "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: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ 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]any{ "query": "location:finland language:go", }, expectError: false, expectedResult: mockSearchResult, }, { name: "query with existing type:user filter - no duplication", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ 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]any{ "query": "type:user location:seattle followers:>100", }, expectError: false, expectedResult: mockSearchResult, }, { name: "complex query with existing type:user filter and OR operators", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ 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]any{ "query": "type:user (location:seattle OR location:california) followers:>50", }, expectError: false, expectedResult: mockSearchResult, }, { name: "search users fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchUsers: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) }), }), requestArgs: map[string]any{ "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 := mustNewGHClient(t, tc.mockedClient) deps := BaseDeps{ Client: client, } handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &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 serverTool := SearchOrgs(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_orgs", tool.Name) assert.NotEmpty(t, tool.Description) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Contains(t, schema.Properties, "query") assert.Contains(t, schema.Properties, "sort") assert.Contains(t, schema.Properties, "order") assert.Contains(t, schema.Properties, "perPage") assert.Contains(t, schema.Properties, "page") assert.ElementsMatch(t, schema.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]any expectError bool expectedResult *github.UsersSearchResult expectedErrMsg string }{ { name: "successful org search", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchUsers: expectQueryParams(t, map[string]string{ "q": "type:org github", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "github", }, expectError: false, expectedResult: mockSearchResult, }, { name: "query with existing type:org filter - no duplication", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ 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]any{ "query": "type:org location:california followers:>1000", }, expectError: false, expectedResult: mockSearchResult, }, { name: "complex query with existing type:org filter and OR operators", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ 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]any{ "query": "type:org (location:seattle OR location:california OR location:newyork) repos:>10", }, expectError: false, expectedResult: mockSearchResult, }, { name: "org search fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchUsers: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) }), }), requestArgs: map[string]any{ "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 := mustNewGHClient(t, tc.mockedClient) deps := BaseDeps{ Client: client, } handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &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) } }) } } func Test_SearchCommits(t *testing.T) { serverTool := SearchCommits(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_commits", tool.Name) assert.NotEmpty(t, tool.Description) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Contains(t, schema.Properties, "query") assert.Contains(t, schema.Properties, "sort") assert.Contains(t, schema.Properties, "order") assert.Contains(t, schema.Properties, "page") assert.Contains(t, schema.Properties, "perPage") assert.ElementsMatch(t, schema.Required, []string{"query"}) now := time.Now().Truncate(time.Second) mockSearchResult := &github.CommitsSearchResult{ Total: github.Ptr(2), IncompleteResults: github.Ptr(false), Commits: []*github.CommitResult{ { SHA: github.Ptr("abc123commit"), HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123commit"), Commit: &github.Commit{ Message: github.Ptr("Initial commit"), Author: &github.CommitAuthor{ Name: github.Ptr("Author Name"), Email: github.Ptr("author@example.com"), Date: &github.Timestamp{Time: now}, }, }, Author: &github.User{ Login: github.Ptr("author"), ID: github.Ptr(int64(1)), HTMLURL: github.Ptr("https://github.com/author"), }, Repository: &github.Repository{ FullName: github.Ptr("owner/repo"), HTMLURL: github.Ptr("https://github.com/owner/repo"), Private: github.Ptr(false), }, }, { // Commit with no resolved GitHub user for author or committer // (common when the commit email isn't linked to an account). SHA: github.Ptr("def456commit"), HTMLURL: github.Ptr("https://github.com/owner/repo/commit/def456commit"), Commit: &github.Commit{ Message: github.Ptr("Unlinked author"), }, Repository: &github.Repository{ FullName: github.Ptr("owner/repo"), }, }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedResult *github.CommitsSearchResult expectedErrMsg string }{ { name: "successful commit search", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchCommits: expectQueryParams(t, map[string]string{ "q": "fix bug in:message repo:owner/repo", "sort": "author-date", "order": "desc", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockSearchResult), ), }), requestArgs: map[string]any{ "query": "fix bug in:message repo:owner/repo", "sort": "author-date", "order": "desc", }, expectError: false, expectedResult: mockSearchResult, }, { name: "search fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetSearchCommits: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) }), }), requestArgs: map[string]any{ "query": "invalid:syntax", }, expectError: true, expectedErrMsg: "failed to search commits", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := mustNewGHClient(t, tc.mockedClient) deps := BaseDeps{ Client: client, } handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) result, err := handler(ContextWithDeps(context.Background(), deps), &request) 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) textContent := getTextResult(t, result) var returnedResult MinimalSearchCommitsResult err = json.Unmarshal([]byte(textContent.Text), &returnedResult) require.NoError(t, err) assert.Equal(t, tc.expectedResult.GetTotal(), returnedResult.TotalCount) assert.Len(t, returnedResult.Items, len(tc.expectedResult.Commits)) assert.Equal(t, *tc.expectedResult.Commits[0].SHA, returnedResult.Items[0].SHA) assert.Equal(t, *tc.expectedResult.Commits[0].Commit.Message, returnedResult.Items[0].Commit.Message) assert.Equal(t, *tc.expectedResult.Commits[0].Commit.Author.Name, returnedResult.Items[0].Commit.Author.Name) assert.Equal(t, now.Format(time.RFC3339), returnedResult.Items[0].Commit.Author.Date) assert.Equal(t, *tc.expectedResult.Commits[0].Author.Login, returnedResult.Items[0].Author.Login) // Repository info is required so callers can identify which repo // each cross-repo search result belongs to. require.NotNil(t, returnedResult.Items[0].Repository) assert.Equal(t, "owner/repo", returnedResult.Items[0].Repository.FullName) assert.Equal(t, "https://github.com/owner/repo", returnedResult.Items[0].Repository.HTMLURL) // Second commit has no resolved GitHub user for author/committer // and no commit-level author block — the handler must not panic // and must omit those fields cleanly. require.Len(t, returnedResult.Items, 2) assert.Equal(t, "def456commit", returnedResult.Items[1].SHA) assert.Nil(t, returnedResult.Items[1].Author) assert.Nil(t, returnedResult.Items[1].Committer) require.NotNil(t, returnedResult.Items[1].Commit) assert.Nil(t, returnedResult.Items[1].Commit.Author) assert.Nil(t, returnedResult.Items[1].Commit.Committer) }) } }