package github import ( "context" "encoding/base64" "encoding/json" "net/http" "net/url" "strings" "testing" "time" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v82/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_GetFileContents(t *testing.T) { // Verify tool definition once serverTool := GetFileContents(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "get_file_contents", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "path") assert.Contains(t, schema.Properties, "ref") assert.Contains(t, schema.Properties, "sha") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) // Mock response for raw content mockRawContent := []byte("# Test Repository\n\nThis is a test repository.") // Setup mock directory content for success case mockDirContent := []*github.RepositoryContent{ { Type: github.Ptr("file"), Name: github.Ptr("README.md"), Path: github.Ptr("README.md"), SHA: github.Ptr("abc123"), Size: github.Ptr(42), HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), }, { Type: github.Ptr("dir"), Name: github.Ptr("src"), Path: github.Ptr("src"), SHA: github.Ptr("def456"), HTMLURL: github.Ptr("https://github.com/owner/repo/tree/main/src"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedResult any expectedErrMsg string expectStatus int expectedMsg string // optional: expected message text to verify in result }{ { name: "successful text content fetch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, "{\"ref\": \"refs/heads/main\", \"object\": {\"sha\": \"\"}}"), GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, "{\"name\": \"repo\", \"default_branch\": \"main\"}"), GetReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) // Base64 encode the content as GitHub API does encodedContent := base64.StdEncoding.EncodeToString(mockRawContent) fileContent := &github.RepositoryContent{ Name: github.Ptr("README.md"), Path: github.Ptr("README.md"), SHA: github.Ptr("abc123"), Type: github.Ptr("file"), Content: github.Ptr(encodedContent), Size: github.Ptr(len(mockRawContent)), Encoding: github.Ptr("base64"), } contentBytes, _ := json.Marshal(fileContent) _, _ = w.Write(contentBytes) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "README.md", "ref": "refs/heads/main", }, expectError: false, expectedResult: mcp.ResourceContents{ URI: "repo://owner/repo/refs/heads/main/contents/README.md", Text: "# Test Repository\n\nThis is a test repository.", MIMEType: "text/plain; charset=utf-8", }, }, { name: "successful binary file content fetch (PNG)", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, "{\"ref\": \"refs/heads/main\", \"object\": {\"sha\": \"\"}}"), GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, "{\"name\": \"repo\", \"default_branch\": \"main\"}"), GetReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) // PNG magic bytes followed by some data pngContent := []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01") encodedContent := base64.StdEncoding.EncodeToString(pngContent) fileContent := &github.RepositoryContent{ Name: github.Ptr("test.png"), Path: github.Ptr("test.png"), SHA: github.Ptr("def456"), Type: github.Ptr("file"), Content: github.Ptr(encodedContent), Size: github.Ptr(len(pngContent)), Encoding: github.Ptr("base64"), } contentBytes, _ := json.Marshal(fileContent) _, _ = w.Write(contentBytes) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "test.png", "ref": "refs/heads/main", }, expectError: false, expectedResult: mcp.ResourceContents{ URI: "repo://owner/repo/refs/heads/main/contents/test.png", Blob: []byte(base64.StdEncoding.EncodeToString([]byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01"))), MIMEType: "image/png", }, }, { name: "successful binary file content fetch (PDF)", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, "{\"ref\": \"refs/heads/main\", \"object\": {\"sha\": \"\"}}"), GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, "{\"name\": \"repo\", \"default_branch\": \"main\"}"), GetReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) // PDF magic bytes pdfContent := []byte("%PDF-1.4 fake pdf content") encodedContent := base64.StdEncoding.EncodeToString(pdfContent) fileContent := &github.RepositoryContent{ Name: github.Ptr("document.pdf"), Path: github.Ptr("document.pdf"), SHA: github.Ptr("pdf123"), Type: github.Ptr("file"), Content: github.Ptr(encodedContent), Size: github.Ptr(len(pdfContent)), Encoding: github.Ptr("base64"), } contentBytes, _ := json.Marshal(fileContent) _, _ = w.Write(contentBytes) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "document.pdf", "ref": "refs/heads/main", }, expectError: false, expectedResult: mcp.ResourceContents{ URI: "repo://owner/repo/refs/heads/main/contents/document.pdf", Blob: []byte(base64.StdEncoding.EncodeToString([]byte("%PDF-1.4 fake pdf content"))), MIMEType: "application/pdf", }, }, { name: "successful directory content fetch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, "{\"name\": \"repo\", \"default_branch\": \"main\"}"), GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, "{\"ref\": \"refs/heads/main\", \"object\": {\"sha\": \"\"}}"), GetReposContentsByOwnerByRepoByPath: expectQueryParams(t, map[string]string{}).andThen( mockResponse(t, http.StatusOK, mockDirContent), ), GetRawReposContentsByOwnerByRepoByPath: expectQueryParams(t, map[string]string{"branch": "main"}).andThen( mockResponse(t, http.StatusNotFound, nil), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "src/", }, expectError: false, expectedResult: mockDirContent, }, { name: "successful text content fetch with leading slash in path", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, "{\"ref\": \"refs/heads/main\", \"object\": {\"sha\": \"\"}}"), GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, "{\"name\": \"repo\", \"default_branch\": \"main\"}"), GetReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) // Base64 encode the content as GitHub API does encodedContent := base64.StdEncoding.EncodeToString(mockRawContent) fileContent := &github.RepositoryContent{ Name: github.Ptr("README.md"), Path: github.Ptr("README.md"), SHA: github.Ptr("abc123"), Type: github.Ptr("file"), Content: github.Ptr(encodedContent), Size: github.Ptr(len(mockRawContent)), Encoding: github.Ptr("base64"), } contentBytes, _ := json.Marshal(fileContent) _, _ = w.Write(contentBytes) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "/README.md", "ref": "refs/heads/main", }, expectError: false, expectedResult: mcp.ResourceContents{ URI: "repo://owner/repo/refs/heads/main/contents/README.md", Text: "# Test Repository\n\nThis is a test repository.", MIMEType: "text/plain; charset=utf-8", }, }, { name: "successful text content fetch with note when ref falls back to default branch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, "{\"name\": \"repo\", \"default_branch\": \"develop\"}"), GetReposGitRefByOwnerByRepoByRef: func(w http.ResponseWriter, r *http.Request) { path := strings.ReplaceAll(r.URL.Path, "%2F", "/") switch { case strings.Contains(path, "heads/main"): w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) case strings.Contains(path, "heads/develop"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/heads/develop", "object": {"sha": "abc123def456abc123def456abc123def456abc1", "type": "commit", "url": "https://api.github.com/repos/owner/repo/git/commits/abc123def456abc123def456abc123def456abc1"}}`)) default: w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) } }, "GET /repos/{owner}/{repo}/git/refs/{ref}": func(w http.ResponseWriter, r *http.Request) { path := strings.ReplaceAll(r.URL.Path, "%2F", "/") switch { case strings.Contains(path, "heads/main"): w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) case strings.Contains(path, "heads/develop"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/heads/develop", "object": {"sha": "abc123def456abc123def456abc123def456abc1", "type": "commit", "url": "https://api.github.com/repos/owner/repo/git/commits/abc123def456abc123def456abc123def456abc1"}}`)) default: w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) } }, "GET /repos/{owner}/{repo}/git/refs/{ref:.*}": func(w http.ResponseWriter, r *http.Request) { path := strings.ReplaceAll(r.URL.Path, "%2F", "/") switch { case strings.Contains(path, "heads/main"): w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) case strings.Contains(path, "heads/develop"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/heads/develop", "object": {"sha": "abc123def456abc123def456abc123def456abc1", "type": "commit", "url": "https://api.github.com/repos/owner/repo/git/commits/abc123def456abc123def456abc123def456abc1"}}`)) default: w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) } }, "GET /repos/owner/repo/git/ref/heads/main": func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }, "GET /repos/owner/repo/git/ref/heads/develop": func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/heads/develop", "object": {"sha": "abc123def456abc123def456abc123def456abc1", "type": "commit", "url": "https://api.github.com/repos/owner/repo/git/commits/abc123def456abc123def456abc123def456abc1"}}`)) }, GetReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) // Base64 encode the content as GitHub API does encodedContent := base64.StdEncoding.EncodeToString(mockRawContent) fileContent := &github.RepositoryContent{ Name: github.Ptr("README.md"), Path: github.Ptr("README.md"), SHA: github.Ptr("abc123"), Type: github.Ptr("file"), Content: github.Ptr(encodedContent), Size: github.Ptr(len(mockRawContent)), Encoding: github.Ptr("base64"), } contentBytes, _ := json.Marshal(fileContent) _, _ = w.Write(contentBytes) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "README.md", "ref": "main", }, expectError: false, expectedResult: mcp.ResourceContents{ URI: "repo://owner/repo/sha/abc123def456abc123def456abc123def456abc1/contents/README.md", Text: "# Test Repository\n\nThis is a test repository.", MIMEType: "text/plain; charset=utf-8", }, expectedMsg: " Note: the provided ref 'main' does not exist, default branch 'refs/heads/develop' was used instead.", }, { name: "large file returns ResourceLink", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, "{\"ref\": \"refs/heads/main\", \"object\": {\"sha\": \"\"}}"), GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, "{\"name\": \"repo\", \"default_branch\": \"main\"}"), GetReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) // File larger than 1MB - Contents API returns metadata but no content fileContent := &github.RepositoryContent{ Name: github.Ptr("large-file.bin"), Path: github.Ptr("large-file.bin"), SHA: github.Ptr("largesha123"), Type: github.Ptr("file"), Size: github.Ptr(2 * 1024 * 1024), // 2MB DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/large-file.bin"), } contentBytes, _ := json.Marshal(fileContent) _, _ = w.Write(contentBytes) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "large-file.bin", "ref": "refs/heads/main", }, expectError: false, expectedResult: &mcp.ResourceLink{ URI: "repo://owner/repo/refs/heads/main/contents/large-file.bin", Name: "large-file.bin", Title: "File: large-file.bin", }, }, { name: "successful empty file content fetch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, "{\"ref\": \"refs/heads/main\", \"object\": {\"sha\": \"\"}}"), GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, "{\"name\": \"repo\", \"default_branch\": \"main\"}"), GetReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) fileContent := &github.RepositoryContent{ Name: github.Ptr(".gitkeep"), Path: github.Ptr(".gitkeep"), SHA: github.Ptr("empty123"), Type: github.Ptr("file"), Content: nil, Size: github.Ptr(0), Encoding: github.Ptr("base64"), } contentBytes, _ := json.Marshal(fileContent) _, _ = w.Write(contentBytes) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": ".gitkeep", "ref": "refs/heads/main", }, expectError: false, expectedResult: mcp.ResourceContents{ URI: "repo://owner/repo/refs/heads/main/contents/.gitkeep", Text: "", MIMEType: "text/plain", }, expectedMsg: "successfully downloaded empty file", }, { name: "content fetch fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, "{\"ref\": \"refs/heads/main\", \"object\": {\"sha\": \"\"}}"), GetReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }, GetRawReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "nonexistent.md", "ref": "refs/heads/main", }, expectError: false, expectedResult: utils.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) mockRawClient := raw.NewClient(client, &url.URL{Scheme: "https", Host: "raw.example.com", Path: "/"}) deps := BaseDeps{ Client: client, RawClient: mockRawClient, } 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) textContent := getErrorResult(t, result) assert.Contains(t, textContent.Text, tc.expectedErrMsg) return } require.NoError(t, err) // Use the correct result helper based on the expected type switch expected := tc.expectedResult.(type) { case mcp.ResourceContents: // Handle both text and blob resources resource := getResourceResult(t, result) assert.Equal(t, expected, *resource) // If expectedMsg is set, verify the message text if tc.expectedMsg != "" { require.Len(t, result.Content, 2) textContent, ok := result.Content[0].(*mcp.TextContent) require.True(t, ok, "expected Content[0] to be TextContent") assert.Contains(t, textContent.Text, tc.expectedMsg) } case []*github.RepositoryContent: // Directory content fetch returns a text result (JSON array) textContent := getTextResult(t, result) var returnedContents []*github.RepositoryContent err = json.Unmarshal([]byte(textContent.Text), &returnedContents) require.NoError(t, err, "Failed to unmarshal directory content result: %v", textContent.Text) assert.Len(t, returnedContents, len(expected)) for i, content := range returnedContents { assert.Equal(t, *expected[i].Name, *content.Name) assert.Equal(t, *expected[i].Path, *content.Path) assert.Equal(t, *expected[i].Type, *content.Type) } case *mcp.ResourceLink: // Large file returns a ResourceLink require.Len(t, result.Content, 2) resourceLink, ok := result.Content[1].(*mcp.ResourceLink) require.True(t, ok, "expected Content[1] to be ResourceLink") assert.Equal(t, expected.URI, resourceLink.URI) assert.Equal(t, expected.Name, resourceLink.Name) assert.Equal(t, expected.Title, resourceLink.Title) case mcp.TextContent: textContent := getErrorResult(t, result) require.Equal(t, textContent, expected) } }) } } func Test_ForkRepository(t *testing.T) { // Verify tool definition once serverTool := ForkRepository(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "fork_repository", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "organization") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) // Setup mock forked repo for success case mockForkedRepo := &github.Repository{ ID: github.Ptr(int64(123456)), Name: github.Ptr("repo"), FullName: github.Ptr("new-owner/repo"), Owner: &github.User{ Login: github.Ptr("new-owner"), }, HTMLURL: github.Ptr("https://github.com/new-owner/repo"), DefaultBranch: github.Ptr("main"), Fork: github.Ptr(true), ForksCount: github.Ptr(0), } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedRepo *github.Repository expectedErrMsg string }{ { name: "successful repository fork", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PostReposForksByOwnerByRepo: mockResponse(t, http.StatusAccepted, mockForkedRepo), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: false, expectedRepo: mockForkedRepo, }, { name: "repository fork fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PostReposForksByOwnerByRepo: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{"message": "Forbidden"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: true, expectedErrMsg: "failed to fork repository", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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) assert.Contains(t, textContent.Text, "Fork is in progress") }) } } func Test_CreateBranch(t *testing.T) { // Verify tool definition once serverTool := CreateBranch(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "create_branch", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "branch") assert.Contains(t, schema.Properties, "from_branch") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "branch"}) // Setup mock repository for default branch test mockRepo := &github.Repository{ DefaultBranch: github.Ptr("main"), } // Setup mock reference for from_branch tests mockSourceRef := &github.Reference{ Ref: github.Ptr("refs/heads/main"), Object: &github.GitObject{ SHA: github.Ptr("abc123def456"), }, } // Setup mock created reference mockCreatedRef := &github.Reference{ Ref: github.Ptr("refs/heads/new-feature"), Object: &github.GitObject{ SHA: github.Ptr("abc123def456"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedRef *github.Reference expectedErrMsg string }{ { name: "successful branch creation with from_branch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, mockSourceRef), "GET /repos/owner/repo/git/ref/heads/main": mockResponse(t, http.StatusOK, mockSourceRef), PostReposGitRefsByOwnerByRepo: mockResponse(t, http.StatusCreated, mockCreatedRef), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "new-feature", "from_branch": "main", }, expectError: false, expectedRef: mockCreatedRef, }, { name: "successful branch creation with default branch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, mockRepo), GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, mockSourceRef), "GET /repos/owner/repo/git/ref/heads/main": mockResponse(t, http.StatusOK, mockSourceRef), PostReposGitRefsByOwnerByRepo: expectRequestBody(t, map[string]any{ "ref": "refs/heads/new-feature", "sha": "abc123def456", }).andThen( mockResponse(t, http.StatusCreated, mockCreatedRef), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "new-feature", }, expectError: false, expectedRef: mockCreatedRef, }, { name: "fail to get repository", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposByOwnerByRepo: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Repository not found"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "nonexistent-repo", "branch": "new-feature", }, expectError: true, expectedErrMsg: "failed to get repository", }, { name: "fail to get reference", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposGitRefByOwnerByRepoByRef: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Reference not found"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "new-feature", "from_branch": "nonexistent-branch", }, expectError: true, expectedErrMsg: "failed to get reference", }, { name: "fail to create branch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, mockSourceRef), "GET /repos/owner/repo/git/ref/heads/main": mockResponse(t, http.StatusOK, mockSourceRef), PostReposGitRefsByOwnerByRepo: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Reference already exists"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "existing-branch", "from_branch": "main", }, expectError: true, expectedErrMsg: "failed to create branch", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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 returnedRef github.Reference err = json.Unmarshal([]byte(textContent.Text), &returnedRef) require.NoError(t, err) assert.Equal(t, *tc.expectedRef.Ref, *returnedRef.Ref) assert.Equal(t, *tc.expectedRef.Object.SHA, *returnedRef.Object.SHA) }) } } func Test_GetCommit(t *testing.T) { // Verify tool definition once serverTool := GetCommit(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "get_commit", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "sha") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "sha"}) mockCommit := &github.RepositoryCommit{ SHA: github.Ptr("abc123def456"), Commit: &github.Commit{ Message: github.Ptr("First commit"), Author: &github.CommitAuthor{ Name: github.Ptr("Test User"), Email: github.Ptr("test@example.com"), Date: &github.Timestamp{Time: time.Now().Add(-48 * time.Hour)}, }, }, Author: &github.User{ Login: github.Ptr("testuser"), }, HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123def456"), Stats: &github.CommitStats{ Additions: github.Ptr(10), Deletions: github.Ptr(2), Total: github.Ptr(12), }, Files: []*github.CommitFile{ { Filename: github.Ptr("file1.go"), Status: github.Ptr("modified"), Additions: github.Ptr(10), Deletions: github.Ptr(2), Changes: github.Ptr(12), Patch: github.Ptr("@@ -1,2 +1,10 @@"), }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedCommit *github.RepositoryCommit expectedErrMsg string }{ { name: "successful commit fetch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposCommitsByOwnerByRepoByRef: mockResponse(t, http.StatusOK, mockCommit), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "sha": "abc123def456", }, expectError: false, expectedCommit: mockCommit, }, { name: "commit fetch fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposCommitsByOwnerByRepoByRef: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "sha": "nonexistent-sha", }, expectError: true, expectedErrMsg: "failed to get commit", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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 returnedCommit github.RepositoryCommit err = json.Unmarshal([]byte(textContent.Text), &returnedCommit) require.NoError(t, err) assert.Equal(t, *tc.expectedCommit.SHA, *returnedCommit.SHA) assert.Equal(t, *tc.expectedCommit.Commit.Message, *returnedCommit.Commit.Message) assert.Equal(t, *tc.expectedCommit.Author.Login, *returnedCommit.Author.Login) assert.Equal(t, *tc.expectedCommit.HTMLURL, *returnedCommit.HTMLURL) }) } } func Test_ListCommits(t *testing.T) { // Verify tool definition once serverTool := ListCommits(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "list_commits", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "sha") assert.Contains(t, schema.Properties, "author") assert.Contains(t, schema.Properties, "path") assert.Contains(t, schema.Properties, "since") assert.Contains(t, schema.Properties, "until") assert.Contains(t, schema.Properties, "page") assert.Contains(t, schema.Properties, "perPage") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) // Setup mock commits for success case mockCommits := []*github.RepositoryCommit{ { SHA: github.Ptr("abc123def456"), Commit: &github.Commit{ Message: github.Ptr("First commit"), Author: &github.CommitAuthor{ Name: github.Ptr("Test User"), Email: github.Ptr("test@example.com"), Date: &github.Timestamp{Time: time.Now().Add(-48 * time.Hour)}, }, }, Author: &github.User{ Login: github.Ptr("testuser"), ID: github.Ptr(int64(12345)), HTMLURL: github.Ptr("https://github.com/testuser"), AvatarURL: github.Ptr("https://github.com/testuser.png"), }, HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123def456"), Stats: &github.CommitStats{ Additions: github.Ptr(10), Deletions: github.Ptr(5), Total: github.Ptr(15), }, Files: []*github.CommitFile{ { Filename: github.Ptr("src/main.go"), Status: github.Ptr("modified"), Additions: github.Ptr(8), Deletions: github.Ptr(3), Changes: github.Ptr(11), }, { Filename: github.Ptr("README.md"), Status: github.Ptr("added"), Additions: github.Ptr(2), Deletions: github.Ptr(2), Changes: github.Ptr(4), }, }, }, { SHA: github.Ptr("def456abc789"), Commit: &github.Commit{ Message: github.Ptr("Second commit"), Author: &github.CommitAuthor{ Name: github.Ptr("Another User"), Email: github.Ptr("another@example.com"), Date: &github.Timestamp{Time: time.Now().Add(-24 * time.Hour)}, }, }, Author: &github.User{ Login: github.Ptr("anotheruser"), ID: github.Ptr(int64(67890)), HTMLURL: github.Ptr("https://github.com/anotheruser"), AvatarURL: github.Ptr("https://github.com/anotheruser.png"), }, HTMLURL: github.Ptr("https://github.com/owner/repo/commit/def456abc789"), Stats: &github.CommitStats{ Additions: github.Ptr(20), Deletions: github.Ptr(10), Total: github.Ptr(30), }, Files: []*github.CommitFile{ { Filename: github.Ptr("src/utils.go"), Status: github.Ptr("added"), Additions: github.Ptr(20), Deletions: github.Ptr(10), Changes: github.Ptr(30), }, }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedCommits []*github.RepositoryCommit expectedErrMsg string }{ { name: "successful commits fetch with default params", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposCommitsByOwnerByRepo: mockResponse(t, http.StatusOK, mockCommits), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: false, expectedCommits: mockCommits, }, { name: "successful commits fetch with branch", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposCommitsByOwnerByRepo: expectQueryParams(t, map[string]string{ "author": "username", "sha": "main", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockCommits), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "sha": "main", "author": "username", }, expectError: false, expectedCommits: mockCommits, }, { name: "successful commits fetch with path filter", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposCommitsByOwnerByRepo: expectQueryParams(t, map[string]string{ "path": "src/main.go", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockCommits), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "src/main.go", }, expectError: false, expectedCommits: mockCommits, }, { name: "successful commits fetch with since and until", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposCommitsByOwnerByRepo: expectQueryParams(t, map[string]string{ "since": "2023-01-01T00:00:00Z", "until": "2023-12-31T23:59:59Z", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockCommits), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "since": "2023-01-01T00:00:00Z", "until": "2023-12-31T23:59:59Z", }, expectError: false, expectedCommits: mockCommits, }, { name: "successful commits fetch with path, since, and author", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposCommitsByOwnerByRepo: expectQueryParams(t, map[string]string{ "path": "projects/plugins/boost", "since": "2023-06-15T00:00:00Z", "author": "username", "page": "1", "per_page": "30", }).andThen( mockResponse(t, http.StatusOK, mockCommits), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "projects/plugins/boost", "since": "2023-06-15T00:00:00Z", "author": "username", }, expectError: false, expectedCommits: mockCommits, }, { name: "invalid since timestamp returns error", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "since": "not-a-date", }, expectError: true, expectedErrMsg: "invalid since timestamp", }, { name: "successful commits fetch with pagination", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposCommitsByOwnerByRepo: expectQueryParams(t, map[string]string{ "page": "2", "per_page": "10", }).andThen( mockResponse(t, http.StatusOK, mockCommits), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "page": float64(2), "perPage": float64(10), }, expectError: false, expectedCommits: mockCommits, }, { name: "commits fetch fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposCommitsByOwnerByRepo: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "nonexistent-repo", }, expectError: true, expectedErrMsg: "failed to list commits", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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 returnedCommits []MinimalCommit err = json.Unmarshal([]byte(textContent.Text), &returnedCommits) require.NoError(t, err) assert.Len(t, returnedCommits, len(tc.expectedCommits)) for i, commit := range returnedCommits { assert.Equal(t, tc.expectedCommits[i].GetSHA(), commit.SHA) assert.Equal(t, tc.expectedCommits[i].GetHTMLURL(), commit.HTMLURL) if tc.expectedCommits[i].Commit != nil { assert.Equal(t, tc.expectedCommits[i].Commit.GetMessage(), commit.Commit.Message) } if tc.expectedCommits[i].Author != nil { assert.Equal(t, tc.expectedCommits[i].Author.GetLogin(), commit.Author.Login) } // Files and stats are never included in list_commits assert.Nil(t, commit.Files) assert.Nil(t, commit.Stats) } }) } } func Test_CreateOrUpdateFile(t *testing.T) { // Verify tool definition once serverTool := CreateOrUpdateFile(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "create_or_update_file", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "path") assert.Contains(t, schema.Properties, "content") assert.Contains(t, schema.Properties, "message") assert.Contains(t, schema.Properties, "branch") assert.Contains(t, schema.Properties, "sha") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "path", "content", "message", "branch"}) // Setup mock file content response mockFileResponse := &github.RepositoryContentResponse{ Content: &github.RepositoryContent{ Name: github.Ptr("example.md"), Path: github.Ptr("docs/example.md"), SHA: github.Ptr("abc123def456"), Size: github.Ptr(42), HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/docs/example.md"), DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/docs/example.md"), }, Commit: github.Commit{ SHA: github.Ptr("def456abc789"), Message: github.Ptr("Add example file"), Author: &github.CommitAuthor{ Name: github.Ptr("Test User"), Email: github.Ptr("test@example.com"), Date: &github.Timestamp{Time: time.Now()}, }, HTMLURL: github.Ptr("https://github.com/owner/repo/commit/def456abc789"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedContent *github.RepositoryContentResponse expectedErrMsg string }{ { name: "successful file creation", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PutReposContentsByOwnerByRepoByPath: expectRequestBody(t, map[string]any{ "message": "Add example file", "content": "IyBFeGFtcGxlCgpUaGlzIGlzIGFuIGV4YW1wbGUgZmlsZS4=", // Base64 encoded content "branch": "main", }).andThen( mockResponse(t, http.StatusOK, mockFileResponse), ), "PUT /repos/{owner}/{repo}/contents/{path:.*}": expectRequestBody(t, map[string]any{ "message": "Add example file", "content": "IyBFeGFtcGxlCgpUaGlzIGlzIGFuIGV4YW1wbGUgZmlsZS4=", // Base64 encoded content "branch": "main", }).andThen( mockResponse(t, http.StatusOK, mockFileResponse), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "docs/example.md", "content": "# Example\n\nThis is an example file.", "message": "Add example file", "branch": "main", }, expectError: false, expectedContent: mockFileResponse, }, { name: "successful file update with SHA", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ "GET /repos/owner/repo/contents/docs/example.md": mockResponse(t, http.StatusOK, &github.RepositoryContent{ SHA: github.Ptr("abc123def456"), Type: github.Ptr("file"), }), "GET /repos/{owner}/{repo}/contents/{path:.*}": mockResponse(t, http.StatusOK, &github.RepositoryContent{ SHA: github.Ptr("abc123def456"), Type: github.Ptr("file"), }), PutReposContentsByOwnerByRepoByPath: expectRequestBody(t, map[string]any{ "message": "Update example file", "content": "IyBVcGRhdGVkIEV4YW1wbGUKClRoaXMgZmlsZSBoYXMgYmVlbiB1cGRhdGVkLg==", // Base64 encoded content "branch": "main", "sha": "abc123def456", }).andThen( mockResponse(t, http.StatusOK, mockFileResponse), ), "PUT /repos/{owner}/{repo}/contents/{path:.*}": expectRequestBody(t, map[string]any{ "message": "Update example file", "content": "IyBVcGRhdGVkIEV4YW1wbGUKClRoaXMgZmlsZSBoYXMgYmVlbiB1cGRhdGVkLg==", // Base64 encoded content "branch": "main", "sha": "abc123def456", }).andThen( mockResponse(t, http.StatusOK, mockFileResponse), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "docs/example.md", "content": "# Updated Example\n\nThis file has been updated.", "message": "Update example file", "branch": "main", "sha": "abc123def456", }, expectError: false, expectedContent: mockFileResponse, }, { name: "file creation fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PutReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Invalid request"}`)) }, "PUT /repos/{owner}/{repo}/contents/{path:.*}": func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Invalid request"}`)) }, }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "docs/example.md", "content": "#Invalid Content", "message": "Invalid request", "branch": "nonexistent-branch", }, expectError: true, expectedErrMsg: "failed to create/update file", }, { name: "sha validation - current sha matches", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ "GET /repos/owner/repo/contents/docs/example.md": mockResponse(t, http.StatusOK, &github.RepositoryContent{ SHA: github.Ptr("abc123def456"), Type: github.Ptr("file"), }), "GET /repos/{owner}/{repo}/contents/{path:.*}": mockResponse(t, http.StatusOK, &github.RepositoryContent{ SHA: github.Ptr("abc123def456"), Type: github.Ptr("file"), }), PutReposContentsByOwnerByRepoByPath: expectRequestBody(t, map[string]any{ "message": "Update example file", "content": "IyBVcGRhdGVkIEV4YW1wbGUKClRoaXMgZmlsZSBoYXMgYmVlbiB1cGRhdGVkLg==", "branch": "main", "sha": "abc123def456", }).andThen( mockResponse(t, http.StatusOK, mockFileResponse), ), "PUT /repos/{owner}/{repo}/contents/{path:.*}": expectRequestBody(t, map[string]any{ "message": "Update example file", "content": "IyBVcGRhdGVkIEV4YW1wbGUKClRoaXMgZmlsZSBoYXMgYmVlbiB1cGRhdGVkLg==", "branch": "main", "sha": "abc123def456", }).andThen( mockResponse(t, http.StatusOK, mockFileResponse), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "docs/example.md", "content": "# Updated Example\n\nThis file has been updated.", "message": "Update example file", "branch": "main", "sha": "abc123def456", }, expectError: false, expectedContent: mockFileResponse, }, { name: "sha validation - stale sha detected", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ "GET /repos/owner/repo/contents/docs/example.md": mockResponse(t, http.StatusOK, &github.RepositoryContent{ SHA: github.Ptr("newsha999888"), Type: github.Ptr("file"), }), "GET /repos/{owner}/{repo}/contents/{path:.*}": mockResponse(t, http.StatusOK, &github.RepositoryContent{ SHA: github.Ptr("newsha999888"), Type: github.Ptr("file"), }), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "docs/example.md", "content": "# Updated Example\n\nThis file has been updated.", "message": "Update example file", "branch": "main", "sha": "oldsha123456", }, expectError: true, expectedErrMsg: "SHA mismatch: provided SHA oldsha123456 is stale. Current file SHA is newsha999888", }, { name: "sha validation - file doesn't exist (404), proceed with create", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ "GET /repos/owner/repo/contents/docs/example.md": func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }, "GET /repos/{owner}/{repo}/contents/{path:.*}": func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }, PutReposContentsByOwnerByRepoByPath: expectRequestBody(t, map[string]any{ "message": "Create new file", "content": "IyBOZXcgRmlsZQoKVGhpcyBpcyBhIG5ldyBmaWxlLg==", "branch": "main", "sha": "ignoredsha", // SHA is sent but GitHub API ignores it for new files }).andThen( mockResponse(t, http.StatusCreated, mockFileResponse), ), "PUT /repos/{owner}/{repo}/contents/{path:.*}": expectRequestBody(t, map[string]any{ "message": "Create new file", "content": "IyBOZXcgRmlsZQoKVGhpcyBpcyBhIG5ldyBmaWxlLg==", "branch": "main", "sha": "ignoredsha", // SHA is sent but GitHub API ignores it for new files }).andThen( mockResponse(t, http.StatusCreated, mockFileResponse), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "docs/example.md", "content": "# New File\n\nThis is a new file.", "message": "Create new file", "branch": "main", "sha": "ignoredsha", }, expectError: false, expectedContent: mockFileResponse, }, { name: "no sha provided - file exists, rejects update", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ "GET /repos/owner/repo/contents/docs/example.md": mockResponse(t, http.StatusOK, &github.RepositoryContent{ SHA: github.Ptr("existing123"), Type: github.Ptr("file"), }), "GET /repos/{owner}/{repo}/contents/{path:.*}": mockResponse(t, http.StatusOK, &github.RepositoryContent{ SHA: github.Ptr("existing123"), Type: github.Ptr("file"), }), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "docs/example.md", "content": "# Updated\n\nUpdated without SHA.", "message": "Update without SHA", "branch": "main", }, expectError: true, expectedErrMsg: "File already exists at docs/example.md", }, { name: "no sha provided - file doesn't exist, no warning", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ "GET /repos/owner/repo/contents/docs/example.md": func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }, "GET /repos/{owner}/{repo}/contents/{path:.*}": func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }, PutReposContentsByOwnerByRepoByPath: expectRequestBody(t, map[string]any{ "message": "Create new file", "content": "IyBOZXcgRmlsZQoKQ3JlYXRlZCB3aXRob3V0IFNIQQ==", "branch": "main", }).andThen( mockResponse(t, http.StatusCreated, mockFileResponse), ), "PUT /repos/{owner}/{repo}/contents/{path:.*}": expectRequestBody(t, map[string]any{ "message": "Create new file", "content": "IyBOZXcgRmlsZQoKQ3JlYXRlZCB3aXRob3V0IFNIQQ==", "branch": "main", }).andThen( mockResponse(t, http.StatusCreated, mockFileResponse), ), }), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "docs/example.md", "content": "# New File\n\nCreated without SHA", "message": "Create new file", "branch": "main", }, expectError: false, expectedContent: mockFileResponse, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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) // If expectedErrMsg is set (but expectError is false), this is a warning case if tc.expectedErrMsg != "" { assert.Contains(t, textContent.Text, tc.expectedErrMsg) return } // Unmarshal and verify the result var returnedContent MinimalFileContentResponse err = json.Unmarshal([]byte(textContent.Text), &returnedContent) require.NoError(t, err) // Verify content assert.Equal(t, tc.expectedContent.Content.GetName(), returnedContent.Content.Name) assert.Equal(t, tc.expectedContent.Content.GetPath(), returnedContent.Content.Path) assert.Equal(t, tc.expectedContent.Content.GetSHA(), returnedContent.Content.SHA) assert.Equal(t, tc.expectedContent.Content.GetSize(), returnedContent.Content.Size) assert.Equal(t, tc.expectedContent.Content.GetHTMLURL(), returnedContent.Content.HTMLURL) // Verify commit assert.Equal(t, tc.expectedContent.Commit.GetSHA(), returnedContent.Commit.SHA) assert.Equal(t, tc.expectedContent.Commit.GetMessage(), returnedContent.Commit.Message) assert.Equal(t, tc.expectedContent.Commit.GetHTMLURL(), returnedContent.Commit.HTMLURL) // Verify commit author require.NotNil(t, returnedContent.Commit.Author) assert.Equal(t, tc.expectedContent.Commit.Author.GetName(), returnedContent.Commit.Author.Name) assert.Equal(t, tc.expectedContent.Commit.Author.GetEmail(), returnedContent.Commit.Author.Email) assert.NotEmpty(t, returnedContent.Commit.Author.Date) }) } } func Test_CreateRepository(t *testing.T) { // Verify tool definition once serverTool := CreateRepository(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "create_repository", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "name") assert.Contains(t, schema.Properties, "description") assert.Contains(t, schema.Properties, "organization") assert.Contains(t, schema.Properties, "private") assert.Contains(t, schema.Properties, "autoInit") assert.ElementsMatch(t, schema.Required, []string{"name"}) // Setup mock repository response mockRepo := &github.Repository{ Name: github.Ptr("test-repo"), Description: github.Ptr("Test repository"), Private: github.Ptr(true), HTMLURL: github.Ptr("https://github.com/testuser/test-repo"), CreatedAt: &github.Timestamp{Time: time.Now()}, Owner: &github.User{ Login: github.Ptr("testuser"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedRepo *github.Repository expectedErrMsg string }{ { name: "successful repository creation with all parameters", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( EndpointPattern("POST /user/repos"), expectRequestBody(t, map[string]any{ "name": "test-repo", "description": "Test repository", "private": true, "auto_init": true, }).andThen( mockResponse(t, http.StatusCreated, mockRepo), ), ), ), requestArgs: map[string]any{ "name": "test-repo", "description": "Test repository", "private": true, "autoInit": true, }, expectError: false, expectedRepo: mockRepo, }, { name: "successful repository creation in organization", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( EndpointPattern("POST /orgs/testorg/repos"), expectRequestBody(t, map[string]any{ "name": "test-repo", "description": "Test repository", "private": false, "auto_init": true, }).andThen( mockResponse(t, http.StatusCreated, mockRepo), ), ), ), requestArgs: map[string]any{ "name": "test-repo", "description": "Test repository", "organization": "testorg", "private": false, "autoInit": true, }, expectError: false, expectedRepo: mockRepo, }, { name: "successful repository creation with minimal parameters", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( EndpointPattern("POST /user/repos"), expectRequestBody(t, map[string]any{ "name": "test-repo", "auto_init": false, "description": "", "private": false, }).andThen( mockResponse(t, http.StatusCreated, mockRepo), ), ), ), requestArgs: map[string]any{ "name": "test-repo", }, expectError: false, expectedRepo: mockRepo, }, { name: "repository creation fails", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( EndpointPattern("POST /user/repos"), http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message": "Repository creation failed"}`)) }), ), ), requestArgs: map[string]any{ "name": "invalid-repo", }, expectError: true, expectedErrMsg: "failed to create repository", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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 minimal result var returnedRepo MinimalResponse err = json.Unmarshal([]byte(textContent.Text), &returnedRepo) assert.NoError(t, err) // Verify repository details assert.Equal(t, tc.expectedRepo.GetHTMLURL(), returnedRepo.URL) }) } } func Test_PushFiles(t *testing.T) { // Verify tool definition once serverTool := PushFiles(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "push_files", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "branch") assert.Contains(t, schema.Properties, "files") assert.Contains(t, schema.Properties, "message") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "branch", "files", "message"}) // Setup mock objects mockRef := &github.Reference{ Ref: github.Ptr("refs/heads/main"), Object: &github.GitObject{ SHA: github.Ptr("abc123"), URL: github.Ptr("https://api.github.com/repos/owner/repo/git/trees/abc123"), }, } mockCommit := &github.Commit{ SHA: github.Ptr("abc123"), Tree: &github.Tree{ SHA: github.Ptr("def456"), }, } mockTree := &github.Tree{ SHA: github.Ptr("ghi789"), } mockNewCommit := &github.Commit{ SHA: github.Ptr("jkl012"), Message: github.Ptr("Update multiple files"), HTMLURL: github.Ptr("https://github.com/owner/repo/commit/jkl012"), } mockUpdatedRef := &github.Reference{ Ref: github.Ptr("refs/heads/main"), Object: &github.GitObject{ SHA: github.Ptr("jkl012"), URL: github.Ptr("https://api.github.com/repos/owner/repo/git/trees/jkl012"), }, } // Define test cases tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedRef *github.Reference expectedErrMsg string }{ { name: "successful push of multiple files", mockedClient: NewMockedHTTPClient( // Get branch reference WithRequestMatch( GetReposGitRefByOwnerByRepoByRef, mockRef, ), // Get commit WithRequestMatch( GetReposGitCommitsByOwnerByRepoByCommitSHA, mockCommit, ), // Create tree WithRequestMatchHandler( PostReposGitTreesByOwnerByRepo, expectRequestBody(t, map[string]any{ "base_tree": "def456", "tree": []any{ map[string]any{ "path": "README.md", "mode": "100644", "type": "blob", "content": "# Updated README\n\nThis is an updated README file.", }, map[string]any{ "path": "docs/example.md", "mode": "100644", "type": "blob", "content": "# Example\n\nThis is an example file.", }, }, }).andThen( mockResponse(t, http.StatusCreated, mockTree), ), ), // Create commit WithRequestMatchHandler( PostReposGitCommitsByOwnerByRepo, expectRequestBody(t, map[string]any{ "message": "Update multiple files", "tree": "ghi789", "parents": []any{"abc123"}, }).andThen( mockResponse(t, http.StatusCreated, mockNewCommit), ), ), // Update reference WithRequestMatchHandler( PatchReposGitRefsByOwnerByRepoByRef, expectRequestBody(t, map[string]any{ "sha": "jkl012", "force": false, }).andThen( mockResponse(t, http.StatusOK, mockUpdatedRef), ), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": []any{ map[string]any{ "path": "README.md", "content": "# Updated README\n\nThis is an updated README file.", }, map[string]any{ "path": "docs/example.md", "content": "# Example\n\nThis is an example file.", }, }, "message": "Update multiple files", }, expectError: false, expectedRef: mockUpdatedRef, }, { name: "fails when files parameter is invalid", mockedClient: NewMockedHTTPClient( // No requests expected ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": "invalid-files-parameter", // Not an array "message": "Update multiple files", }, expectError: false, // This returns a tool error, not a Go error expectedErrMsg: "files parameter must be an array", }, { name: "fails when files contains object without path", mockedClient: NewMockedHTTPClient( // Get branch reference WithRequestMatch( GetReposGitRefByOwnerByRepoByRef, mockRef, ), // Get commit WithRequestMatch( GetReposGitCommitsByOwnerByRepoByCommitSHA, mockCommit, ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": []any{ map[string]any{ "content": "# Missing path", }, }, "message": "Update file", }, expectError: false, // This returns a tool error, not a Go error expectedErrMsg: "each file must have a path", }, { name: "fails when files contains object without content", mockedClient: NewMockedHTTPClient( // Get branch reference WithRequestMatch( GetReposGitRefByOwnerByRepoByRef, mockRef, ), // Get commit WithRequestMatch( GetReposGitCommitsByOwnerByRepoByCommitSHA, mockCommit, ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": []any{ map[string]any{ "path": "README.md", // Missing content }, }, "message": "Update file", }, expectError: false, // This returns a tool error, not a Go error expectedErrMsg: "each file must have content", }, { name: "fails to get branch reference", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, mockResponse(t, http.StatusNotFound, nil), ), // Mock Repositories.Get to fail when trying to create branch from default WithRequestMatchHandler( GetReposByOwnerByRepo, mockResponse(t, http.StatusNotFound, nil), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "non-existent-branch", "files": []any{ map[string]any{ "path": "README.md", "content": "# README", }, }, "message": "Update file", }, expectError: false, expectedErrMsg: "failed to create branch from default", }, { name: "fails to get base commit", mockedClient: NewMockedHTTPClient( // Get branch reference WithRequestMatch( GetReposGitRefByOwnerByRepoByRef, mockRef, ), // Fail to get commit WithRequestMatchHandler( GetReposGitCommitsByOwnerByRepoByCommitSHA, mockResponse(t, http.StatusNotFound, nil), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": []any{ map[string]any{ "path": "README.md", "content": "# README", }, }, "message": "Update file", }, expectError: true, expectedErrMsg: "failed to get base commit", }, { name: "fails to create tree", mockedClient: NewMockedHTTPClient( // Get branch reference WithRequestMatch( GetReposGitRefByOwnerByRepoByRef, mockRef, ), // Get commit WithRequestMatch( GetReposGitCommitsByOwnerByRepoByCommitSHA, mockCommit, ), // Fail to create tree WithRequestMatchHandler( PostReposGitTreesByOwnerByRepo, mockResponse(t, http.StatusInternalServerError, nil), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": []any{ map[string]any{ "path": "README.md", "content": "# README", }, }, "message": "Update file", }, expectError: true, expectedErrMsg: "failed to create tree", }, { name: "successful push to empty repository", mockedClient: NewMockedHTTPClient( // Get branch reference - first returns 409 for empty repo, second returns success after init WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, func() http.HandlerFunc { callCount := 0 return func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") callCount++ if callCount == 1 { // First call: empty repo w.WriteHeader(http.StatusConflict) response := map[string]any{ "message": "Git Repository is empty.", } _ = json.NewEncoder(w).Encode(response) } else { // Second call: return the created reference w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(mockRef) } } }(), ), // Mock Repositories.Get to return default branch for initialization WithRequestMatch( GetReposByOwnerByRepo, &github.Repository{ DefaultBranch: github.Ptr("main"), }, ), // Create initial file using Contents API WithRequestMatchHandler( PutReposContentsByOwnerByRepoByPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body map[string]any err := json.NewDecoder(r.Body).Decode(&body) require.NoError(t, err) require.Equal(t, "Initial commit", body["message"]) require.Equal(t, "main", body["branch"]) w.WriteHeader(http.StatusCreated) response := &github.RepositoryContentResponse{ Commit: github.Commit{SHA: github.Ptr("abc123")}, } b, _ := json.Marshal(response) _, _ = w.Write(b) }), ), // Get the commit after initialization WithRequestMatch( GetReposGitCommitsByOwnerByRepoByCommitSHA, mockCommit, ), // Create tree WithRequestMatch( PostReposGitTreesByOwnerByRepo, mockTree, ), // Create commit WithRequestMatch( PostReposGitCommitsByOwnerByRepo, mockNewCommit, ), // Update reference WithRequestMatch( PatchReposGitRefsByOwnerByRepoByRef, mockUpdatedRef, ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": []any{ map[string]any{ "path": "README.md", "content": "# Initial README\n\nFirst commit to empty repository.", }, }, "message": "Initial commit", }, expectError: false, expectedRef: mockUpdatedRef, }, { name: "successful push multiple files to empty repository", mockedClient: NewMockedHTTPClient( // Get branch reference - called twice: first for empty check, second after file creation WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, func() http.HandlerFunc { callCount := 0 return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { callCount++ if callCount == 1 { // First call: returns 409 Conflict for empty repo w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusConflict) response := map[string]any{ "message": "Git Repository is empty.", } _ = json.NewEncoder(w).Encode(response) } else { // Second call: returns the updated reference after first file creation w.WriteHeader(http.StatusOK) b, _ := json.Marshal(&github.Reference{ Ref: github.Ptr("refs/heads/main"), Object: &github.GitObject{SHA: github.Ptr("init456")}, }) _, _ = w.Write(b) } }) }(), ), // Mock Repositories.Get to return default branch for initialization WithRequestMatch( GetReposByOwnerByRepo, &github.Repository{ DefaultBranch: github.Ptr("main"), }, ), // Create initial empty README.md file using Contents API to initialize repo WithRequestMatchHandler( PutReposContentsByOwnerByRepoByPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body map[string]any err := json.NewDecoder(r.Body).Decode(&body) require.NoError(t, err) require.Equal(t, "Initial commit", body["message"]) require.Equal(t, "main", body["branch"]) // Verify it's an empty file expectedContent := base64.StdEncoding.EncodeToString([]byte("")) require.Equal(t, expectedContent, body["content"]) w.WriteHeader(http.StatusCreated) response := &github.RepositoryContentResponse{ Content: &github.RepositoryContent{ SHA: github.Ptr("readme123"), }, Commit: github.Commit{ SHA: github.Ptr("init456"), Tree: &github.Tree{ SHA: github.Ptr("tree456"), }, }, } b, _ := json.Marshal(response) _, _ = w.Write(b) }), ), // Get the commit to retrieve parent SHA WithRequestMatchHandler( GetReposGitCommitsByOwnerByRepoByCommitSHA, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) response := &github.Commit{ SHA: github.Ptr("init456"), Tree: &github.Tree{ SHA: github.Ptr("tree456"), }, } b, _ := json.Marshal(response) _, _ = w.Write(b) }), ), // Create tree with all user files WithRequestMatchHandler( PostReposGitTreesByOwnerByRepo, expectRequestBody(t, map[string]any{ "base_tree": "tree456", "tree": []any{ map[string]any{ "path": "README.md", "mode": "100644", "type": "blob", "content": "# Project\n\nProject README", }, map[string]any{ "path": ".gitignore", "mode": "100644", "type": "blob", "content": "node_modules/\n*.log\n", }, map[string]any{ "path": "src/main.js", "mode": "100644", "type": "blob", "content": "console.log('Hello World');\n", }, }, }).andThen( mockResponse(t, http.StatusCreated, mockTree), ), ), // Create commit with all user files WithRequestMatchHandler( PostReposGitCommitsByOwnerByRepo, expectRequestBody(t, map[string]any{ "message": "Initial project setup", "tree": "ghi789", "parents": []any{"init456"}, }).andThen( mockResponse(t, http.StatusCreated, mockNewCommit), ), ), // Update reference WithRequestMatchHandler( PatchReposGitRefsByOwnerByRepoByRef, expectRequestBody(t, map[string]any{ "sha": "jkl012", "force": false, }).andThen( mockResponse(t, http.StatusOK, mockUpdatedRef), ), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": []any{ map[string]any{ "path": "README.md", "content": "# Project\n\nProject README", }, map[string]any{ "path": ".gitignore", "content": "node_modules/\n*.log\n", }, map[string]any{ "path": "src/main.js", "content": "console.log('Hello World');\n", }, }, "message": "Initial project setup", }, expectError: false, expectedRef: mockUpdatedRef, }, { name: "fails to create initial file in empty repository", mockedClient: NewMockedHTTPClient( // Get branch reference returns 409 Conflict for empty repo WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusConflict) response := map[string]any{ "message": "Git Repository is empty.", } _ = json.NewEncoder(w).Encode(response) }), ), // Mock Repositories.Get to return default branch WithRequestMatch( GetReposByOwnerByRepo, &github.Repository{ DefaultBranch: github.Ptr("main"), }, ), // Fail to create initial file using Contents API WithRequestMatchHandler( PutReposContentsByOwnerByRepoByPath, mockResponse(t, http.StatusInternalServerError, nil), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": []any{ map[string]any{ "path": "README.md", "content": "# README", }, }, "message": "Initial commit", }, expectError: false, expectedErrMsg: "failed to initialize repository", }, { name: "fails to get reference after creating initial file in empty repository", mockedClient: NewMockedHTTPClient( // Get branch reference - called twice WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, func() http.HandlerFunc { callCount := 0 return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { callCount++ if callCount == 1 { // First call: returns 409 Conflict for empty repo w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusConflict) response := map[string]any{ "message": "Git Repository is empty.", } _ = json.NewEncoder(w).Encode(response) } else { // Second call: fails w.WriteHeader(http.StatusInternalServerError) } }) }(), ), // Mock Repositories.Get to return default branch WithRequestMatch( GetReposByOwnerByRepo, &github.Repository{ DefaultBranch: github.Ptr("main"), }, ), // Create initial file using Contents API WithRequestMatch( PutReposContentsByOwnerByRepoByPath, &github.RepositoryContentResponse{ Content: &github.RepositoryContent{SHA: github.Ptr("readme123")}, Commit: github.Commit{SHA: github.Ptr("init456")}, }, ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": []any{ map[string]any{ "path": "README.md", "content": "# README", }, }, "message": "Initial commit", }, expectError: false, expectedErrMsg: "failed to initialize repository", }, { name: "fails to get commit in empty repository with multiple files", mockedClient: NewMockedHTTPClient( // Get branch reference returns 409 Conflict for empty repo WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusConflict) response := map[string]any{ "message": "Git Repository is empty.", } _ = json.NewEncoder(w).Encode(response) }), ), // Mock Repositories.Get to return default branch WithRequestMatch( GetReposByOwnerByRepo, &github.Repository{ DefaultBranch: github.Ptr("main"), }, ), // Create initial file using Contents API WithRequestMatch( PutReposContentsByOwnerByRepoByPath, &github.RepositoryContentResponse{ Content: &github.RepositoryContent{SHA: github.Ptr("readme123")}, Commit: github.Commit{SHA: github.Ptr("init456")}, }, ), // Fail to get commit WithRequestMatchHandler( GetReposGitCommitsByOwnerByRepoByCommitSHA, mockResponse(t, http.StatusInternalServerError, nil), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "branch": "main", "files": []any{ map[string]any{ "path": "README.md", "content": "# README", }, map[string]any{ "path": "LICENSE", "content": "MIT", }, }, "message": "Initial commit", }, expectError: false, expectedErrMsg: "failed to initialize repository", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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 } if tc.expectedErrMsg != "" { require.NotNil(t, result) 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 returnedRef github.Reference err = json.Unmarshal([]byte(textContent.Text), &returnedRef) require.NoError(t, err) assert.Equal(t, *tc.expectedRef.Ref, *returnedRef.Ref) assert.Equal(t, *tc.expectedRef.Object.SHA, *returnedRef.Object.SHA) }) } } func Test_ListBranches(t *testing.T) { // Verify tool definition once serverTool := ListBranches(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "list_branches", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "page") assert.Contains(t, schema.Properties, "perPage") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) // Setup mock branches for success case mockBranches := []*github.Branch{ { Name: github.Ptr("main"), Commit: &github.RepositoryCommit{SHA: github.Ptr("abc123")}, }, { Name: github.Ptr("develop"), Commit: &github.RepositoryCommit{SHA: github.Ptr("def456")}, }, } // Test cases tests := []struct { name string args map[string]any mockResponses []MockBackendOption wantErr bool errContains string }{ { name: "success", args: map[string]any{ "owner": "owner", "repo": "repo", "page": float64(2), }, mockResponses: []MockBackendOption{ WithRequestMatch( GetReposBranchesByOwnerByRepo, mockBranches, ), }, wantErr: false, }, { name: "missing owner", args: map[string]any{ "repo": "repo", }, mockResponses: []MockBackendOption{}, wantErr: false, errContains: "missing required parameter: owner", }, { name: "missing repo", args: map[string]any{ "owner": "owner", }, mockResponses: []MockBackendOption{}, wantErr: false, errContains: "missing required parameter: repo", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock client mockClient := github.NewClient(NewMockedHTTPClient(tt.mockResponses...)) deps := BaseDeps{ Client: mockClient, } handler := serverTool.Handler(deps) // Create request request := createMCPRequest(tt.args) // Call handler result, err := handler(ContextWithDeps(context.Background(), deps), &request) if tt.wantErr { require.NoError(t, err) if tt.errContains != "" { textContent := getErrorResult(t, result) assert.Contains(t, textContent.Text, tt.errContains) } return } require.NoError(t, err) require.NotNil(t, result) if tt.errContains != "" { textContent := getTextResult(t, result) assert.Contains(t, textContent.Text, tt.errContains) return } textContent := getTextResult(t, result) require.NotEmpty(t, textContent.Text) // Verify response var branches []*github.Branch err = json.Unmarshal([]byte(textContent.Text), &branches) require.NoError(t, err) assert.Len(t, branches, 2) assert.Equal(t, "main", *branches[0].Name) assert.Equal(t, "develop", *branches[1].Name) }) } } func Test_DeleteFile(t *testing.T) { // Verify tool definition once serverTool := DeleteFile(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "delete_file", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "path") assert.Contains(t, schema.Properties, "message") assert.Contains(t, schema.Properties, "branch") // SHA is no longer required since we're using Git Data API assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "path", "message", "branch"}) // Setup mock objects for Git Data API mockRef := &github.Reference{ Ref: github.Ptr("refs/heads/main"), Object: &github.GitObject{ SHA: github.Ptr("abc123"), }, } mockCommit := &github.Commit{ SHA: github.Ptr("abc123"), Tree: &github.Tree{ SHA: github.Ptr("def456"), }, } mockTree := &github.Tree{ SHA: github.Ptr("ghi789"), } mockNewCommit := &github.Commit{ SHA: github.Ptr("jkl012"), Message: github.Ptr("Delete example file"), HTMLURL: github.Ptr("https://github.com/owner/repo/commit/jkl012"), } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedCommitSHA string expectedErrMsg string }{ { name: "successful file deletion using Git Data API", mockedClient: NewMockedHTTPClient( // Get branch reference WithRequestMatch( GetReposGitRefByOwnerByRepoByRef, mockRef, ), // Get commit WithRequestMatch( GetReposGitCommitsByOwnerByRepoByCommitSHA, mockCommit, ), // Create tree WithRequestMatchHandler( PostReposGitTreesByOwnerByRepo, expectRequestBody(t, map[string]any{ "base_tree": "def456", "tree": []any{ map[string]any{ "path": "docs/example.md", "mode": "100644", "type": "blob", "sha": nil, }, }, }).andThen( mockResponse(t, http.StatusCreated, mockTree), ), ), // Create commit WithRequestMatchHandler( PostReposGitCommitsByOwnerByRepo, expectRequestBody(t, map[string]any{ "message": "Delete example file", "tree": "ghi789", "parents": []any{"abc123"}, }).andThen( mockResponse(t, http.StatusCreated, mockNewCommit), ), ), // Update reference WithRequestMatchHandler( PatchReposGitRefsByOwnerByRepoByRef, expectRequestBody(t, map[string]any{ "sha": "jkl012", "force": false, }).andThen( mockResponse(t, http.StatusOK, &github.Reference{ Ref: github.Ptr("refs/heads/main"), Object: &github.GitObject{ SHA: github.Ptr("jkl012"), }, }), ), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "docs/example.md", "message": "Delete example file", "branch": "main", }, expectError: false, expectedCommitSHA: "jkl012", }, { name: "file deletion fails - branch not found", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Reference not found"}`)) }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "path": "docs/nonexistent.md", "message": "Delete nonexistent file", "branch": "nonexistent-branch", }, expectError: true, expectedErrMsg: "failed to get branch reference", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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.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 response map[string]any err = json.Unmarshal([]byte(textContent.Text), &response) require.NoError(t, err) // Verify the response contains the expected commit commit, ok := response["commit"].(map[string]any) require.True(t, ok) commitSHA, ok := commit["sha"].(string) require.True(t, ok) assert.Equal(t, tc.expectedCommitSHA, commitSHA) }) } } func Test_ListTags(t *testing.T) { // Verify tool definition once serverTool := ListTags(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "list_tags", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) // Setup mock tags for success case mockTags := []*github.RepositoryTag{ { Name: github.Ptr("v1.0.0"), Commit: &github.Commit{ SHA: github.Ptr("v1.0.0-tag-sha"), URL: github.Ptr("https://api.github.com/repos/owner/repo/commits/abc123"), }, ZipballURL: github.Ptr("https://github.com/owner/repo/zipball/v1.0.0"), TarballURL: github.Ptr("https://github.com/owner/repo/tarball/v1.0.0"), }, { Name: github.Ptr("v0.9.0"), Commit: &github.Commit{ SHA: github.Ptr("v0.9.0-tag-sha"), URL: github.Ptr("https://api.github.com/repos/owner/repo/commits/def456"), }, ZipballURL: github.Ptr("https://github.com/owner/repo/zipball/v0.9.0"), TarballURL: github.Ptr("https://github.com/owner/repo/tarball/v0.9.0"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedTags []*github.RepositoryTag expectedErrMsg string }{ { name: "successful tags list", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetReposTagsByOwnerByRepo, expectPath( t, "/repos/owner/repo/tags", ).andThen( mockResponse(t, http.StatusOK, mockTags), ), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: false, expectedTags: mockTags, }, { name: "list tags fails", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetReposTagsByOwnerByRepo, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"message": "Internal Server Error"}`)) }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: true, expectedErrMsg: "failed to list tags", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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) // Parse and verify the result var returnedTags []MinimalTag err = json.Unmarshal([]byte(textContent.Text), &returnedTags) require.NoError(t, err) // Verify each tag require.Equal(t, len(tc.expectedTags), len(returnedTags)) for i, expectedTag := range tc.expectedTags { assert.Equal(t, *expectedTag.Name, returnedTags[i].Name) assert.Equal(t, *expectedTag.Commit.SHA, returnedTags[i].SHA) } }) } } func Test_GetTag(t *testing.T) { // Verify tool definition once serverTool := GetTag(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "get_tag", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "tag") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "tag"}) mockTagRef := &github.Reference{ Ref: github.Ptr("refs/tags/v1.0.0"), Object: &github.GitObject{ SHA: github.Ptr("v1.0.0-tag-sha"), }, } mockTagObj := &github.Tag{ SHA: github.Ptr("v1.0.0-tag-sha"), Tag: github.Ptr("v1.0.0"), Message: github.Ptr("Release v1.0.0"), Object: &github.GitObject{ Type: github.Ptr("commit"), SHA: github.Ptr("abc123"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedTag *github.Tag expectedErrMsg string }{ { name: "successful tag retrieval", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, expectPath( t, "/repos/owner/repo/git/ref/tags/v1.0.0", ).andThen( mockResponse(t, http.StatusOK, mockTagRef), ), ), WithRequestMatchHandler( GetReposGitTagsByOwnerByRepoByTagSHA, expectPath( t, "/repos/owner/repo/git/tags/v1.0.0-tag-sha", ).andThen( mockResponse(t, http.StatusOK, mockTagObj), ), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "tag": "v1.0.0", }, expectError: false, expectedTag: mockTagObj, }, { name: "tag reference not found", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Reference does not exist"}`)) }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "tag": "v1.0.0", }, expectError: true, expectedErrMsg: "failed to get tag reference", }, { name: "tag object not found", mockedClient: NewMockedHTTPClient( WithRequestMatch( GetReposGitRefByOwnerByRepoByRef, mockTagRef, ), WithRequestMatchHandler( GetReposGitTagsByOwnerByRepoByTagSHA, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Tag object does not exist"}`)) }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "tag": "v1.0.0", }, expectError: true, expectedErrMsg: "failed to get tag object", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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) // Parse and verify the result var returnedTag github.Tag err = json.Unmarshal([]byte(textContent.Text), &returnedTag) require.NoError(t, err) assert.Equal(t, *tc.expectedTag.SHA, *returnedTag.SHA) assert.Equal(t, *tc.expectedTag.Tag, *returnedTag.Tag) assert.Equal(t, *tc.expectedTag.Message, *returnedTag.Message) assert.Equal(t, *tc.expectedTag.Object.Type, *returnedTag.Object.Type) assert.Equal(t, *tc.expectedTag.Object.SHA, *returnedTag.Object.SHA) }) } } func Test_ListReleases(t *testing.T) { serverTool := ListReleases(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "list_releases", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) mockReleases := []*github.RepositoryRelease{ { ID: github.Ptr(int64(1)), TagName: github.Ptr("v1.0.0"), Name: github.Ptr("First Release"), }, { ID: github.Ptr(int64(2)), TagName: github.Ptr("v0.9.0"), Name: github.Ptr("Beta Release"), }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedResult []*github.RepositoryRelease expectedErrMsg string }{ { name: "successful releases list", mockedClient: NewMockedHTTPClient( WithRequestMatch( GetReposReleasesByOwnerByRepo, mockReleases, ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: false, expectedResult: mockReleases, }, { name: "releases list fails", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetReposReleasesByOwnerByRepo, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: true, expectedErrMsg: "failed to list releases", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(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.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErrMsg) return } require.NoError(t, err) textContent := getTextResult(t, result) var returnedReleases []MinimalRelease err = json.Unmarshal([]byte(textContent.Text), &returnedReleases) require.NoError(t, err) assert.Len(t, returnedReleases, len(tc.expectedResult)) for i := range returnedReleases { assert.Equal(t, *tc.expectedResult[i].TagName, returnedReleases[i].TagName) } }) } } func Test_GetLatestRelease(t *testing.T) { serverTool := GetLatestRelease(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "get_latest_release", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) mockRelease := &github.RepositoryRelease{ ID: github.Ptr(int64(1)), TagName: github.Ptr("v1.0.0"), Name: github.Ptr("First Release"), } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedResult *github.RepositoryRelease expectedErrMsg string }{ { name: "successful latest release fetch", mockedClient: NewMockedHTTPClient( WithRequestMatch( GetReposReleasesLatestByOwnerByRepo, mockRelease, ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: false, expectedResult: mockRelease, }, { name: "latest release fetch fails", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetReposReleasesLatestByOwnerByRepo, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: true, expectedErrMsg: "failed to get latest release", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(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.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErrMsg) return } require.NoError(t, err) textContent := getTextResult(t, result) var returnedRelease github.RepositoryRelease err = json.Unmarshal([]byte(textContent.Text), &returnedRelease) require.NoError(t, err) assert.Equal(t, *tc.expectedResult.TagName, *returnedRelease.TagName) }) } } func Test_GetReleaseByTag(t *testing.T) { serverTool := GetReleaseByTag(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "get_release_by_tag", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.Contains(t, schema.Properties, "tag") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "tag"}) mockRelease := &github.RepositoryRelease{ ID: github.Ptr(int64(1)), TagName: github.Ptr("v1.0.0"), Name: github.Ptr("Release v1.0.0"), Body: github.Ptr("This is the first stable release."), Assets: []*github.ReleaseAsset{ { ID: github.Ptr(int64(1)), Name: github.Ptr("release-v1.0.0.tar.gz"), }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedResult *github.RepositoryRelease expectedErrMsg string }{ { name: "successful release by tag fetch", mockedClient: NewMockedHTTPClient( WithRequestMatch( GetReposReleasesTagsByOwnerByRepoByTag, mockRelease, ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "tag": "v1.0.0", }, expectError: false, expectedResult: mockRelease, }, { name: "missing owner parameter", mockedClient: NewMockedHTTPClient(), requestArgs: map[string]any{ "repo": "repo", "tag": "v1.0.0", }, expectError: false, // Returns tool error, not Go error expectedErrMsg: "missing required parameter: owner", }, { name: "missing repo parameter", mockedClient: NewMockedHTTPClient(), requestArgs: map[string]any{ "owner": "owner", "tag": "v1.0.0", }, expectError: false, // Returns tool error, not Go error expectedErrMsg: "missing required parameter: repo", }, { name: "missing tag parameter", mockedClient: NewMockedHTTPClient(), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: false, // Returns tool error, not Go error expectedErrMsg: "missing required parameter: tag", }, { name: "release by tag not found", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetReposReleasesTagsByOwnerByRepoByTag, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "tag": "v999.0.0", }, expectError: false, // API errors return tool errors, not Go errors expectedErrMsg: "failed to get release by tag: v999.0.0", }, { name: "server error", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetReposReleasesTagsByOwnerByRepoByTag, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"message": "Internal Server Error"}`)) }), ), ), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", "tag": "v1.0.0", }, expectError: false, // API errors return tool errors, not Go errors expectedErrMsg: "failed to get release by tag: v1.0.0", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(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.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErrMsg) return } require.NoError(t, err) if tc.expectedErrMsg != "" { require.True(t, result.IsError) errorContent := getErrorResult(t, result) assert.Contains(t, errorContent.Text, tc.expectedErrMsg) return } require.False(t, result.IsError) textContent := getTextResult(t, result) var returnedRelease github.RepositoryRelease err = json.Unmarshal([]byte(textContent.Text), &returnedRelease) require.NoError(t, err) assert.Equal(t, *tc.expectedResult.ID, *returnedRelease.ID) assert.Equal(t, *tc.expectedResult.TagName, *returnedRelease.TagName) assert.Equal(t, *tc.expectedResult.Name, *returnedRelease.Name) if tc.expectedResult.Body != nil { assert.Equal(t, *tc.expectedResult.Body, *returnedRelease.Body) } if len(tc.expectedResult.Assets) > 0 { require.Len(t, returnedRelease.Assets, len(tc.expectedResult.Assets)) assert.Equal(t, *tc.expectedResult.Assets[0].Name, *returnedRelease.Assets[0].Name) } }) } } func Test_looksLikeSHA(t *testing.T) { tests := []struct { name string input string expected bool }{ { name: "full 40-character SHA", input: "abc123def456abc123def456abc123def456abc1", expected: true, }, { name: "too short", input: "abc123def456abc123def45", expected: false, }, { name: "too long - 41 characters", input: "abc123def456abc123def456abc123def456abc12", expected: false, }, { name: "contains invalid character - space", input: "abc123def456abc123def456 bc123def456abc1", expected: false, }, { name: "contains invalid character - dash", input: "abc123def456abc123d-f456abc123def456abc1", expected: false, }, { name: "contains invalid character - g", input: "abc123def456gbc123def456abc123def456abc1", expected: false, }, { name: "branch name with slash", input: "feature/branch", expected: false, }, { name: "empty string", input: "", expected: false, }, { name: "all zeros SHA", input: "0000000000000000000000000000000000000000", expected: true, }, { name: "all f's SHA", input: "ffffffffffffffffffffffffffffffffffffffff", expected: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := looksLikeSHA(tc.input) assert.Equal(t, tc.expected, result) }) } } func Test_filterPaths(t *testing.T) { tests := []struct { name string tree []*github.TreeEntry path string maxResults int expected []string }{ { name: "file name", tree: []*github.TreeEntry{ {Path: github.Ptr("folder/foo.txt"), Type: github.Ptr("blob")}, {Path: github.Ptr("bar.txt"), Type: github.Ptr("blob")}, {Path: github.Ptr("nested/folder/foo.txt"), Type: github.Ptr("blob")}, {Path: github.Ptr("nested/folder/baz.txt"), Type: github.Ptr("blob")}, }, path: "foo.txt", maxResults: -1, expected: []string{"folder/foo.txt", "nested/folder/foo.txt"}, }, { name: "dir name", tree: []*github.TreeEntry{ {Path: github.Ptr("folder"), Type: github.Ptr("tree")}, {Path: github.Ptr("bar.txt"), Type: github.Ptr("blob")}, {Path: github.Ptr("nested/folder"), Type: github.Ptr("tree")}, {Path: github.Ptr("nested/folder/baz.txt"), Type: github.Ptr("blob")}, }, path: "folder/", maxResults: -1, expected: []string{"folder/", "nested/folder/"}, }, { name: "dir and file match", tree: []*github.TreeEntry{ {Path: github.Ptr("name"), Type: github.Ptr("tree")}, {Path: github.Ptr("name"), Type: github.Ptr("blob")}, }, path: "name", // No trailing slash can match both files and directories maxResults: -1, expected: []string{"name/", "name"}, }, { name: "dir only match", tree: []*github.TreeEntry{ {Path: github.Ptr("name"), Type: github.Ptr("tree")}, {Path: github.Ptr("name"), Type: github.Ptr("blob")}, }, path: "name/", // Trialing slash ensures only directories are matched maxResults: -1, expected: []string{"name/"}, }, { name: "max results limit 2", tree: []*github.TreeEntry{ {Path: github.Ptr("folder"), Type: github.Ptr("tree")}, {Path: github.Ptr("nested/folder"), Type: github.Ptr("tree")}, {Path: github.Ptr("nested/nested/folder"), Type: github.Ptr("tree")}, }, path: "folder/", maxResults: 2, expected: []string{"folder/", "nested/folder/"}, }, { name: "max results limit 1", tree: []*github.TreeEntry{ {Path: github.Ptr("folder"), Type: github.Ptr("tree")}, {Path: github.Ptr("nested/folder"), Type: github.Ptr("tree")}, {Path: github.Ptr("nested/nested/folder"), Type: github.Ptr("tree")}, }, path: "folder/", maxResults: 1, expected: []string{"folder/"}, }, { name: "max results limit 0", tree: []*github.TreeEntry{ {Path: github.Ptr("folder"), Type: github.Ptr("tree")}, {Path: github.Ptr("nested/folder"), Type: github.Ptr("tree")}, {Path: github.Ptr("nested/nested/folder"), Type: github.Ptr("tree")}, }, path: "folder/", maxResults: 0, expected: []string{}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := filterPaths(tc.tree, tc.path, tc.maxResults) assert.Equal(t, tc.expected, result) }) } } func Test_resolveGitReference(t *testing.T) { ctx := context.Background() owner := "owner" repo := "repo" tests := []struct { name string ref string sha string mockSetup func() *http.Client expectedOutput *raw.ContentOpts expectError bool errorContains string }{ { name: "sha takes precedence over ref", ref: "refs/heads/main", sha: "123sha456", mockSetup: func() *http.Client { // No API calls should be made when SHA is provided return NewMockedHTTPClient() }, expectedOutput: &raw.ContentOpts{ SHA: "123sha456", }, expectError: false, }, { name: "use default branch if ref and sha both empty", ref: "", sha: "", mockSetup: func() *http.Client { return NewMockedHTTPClient( WithRequestMatchHandler( GetReposByOwnerByRepo, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"name": "repo", "default_branch": "main"}`)) }), ), WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Contains(t, r.URL.Path, "/git/ref/heads/main") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/heads/main", "object": {"sha": "main-sha"}}`)) }), ), ) }, expectedOutput: &raw.ContentOpts{ Ref: "refs/heads/main", SHA: "main-sha", }, expectError: false, }, { name: "fully qualified ref passed through unchanged", ref: "refs/heads/feature-branch", sha: "", mockSetup: func() *http.Client { return NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Contains(t, r.URL.Path, "/git/ref/heads/feature-branch") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/heads/feature-branch", "object": {"sha": "feature-sha"}}`)) }), ), ) }, expectedOutput: &raw.ContentOpts{ Ref: "refs/heads/feature-branch", SHA: "feature-sha", }, expectError: false, }, { name: "short branch name resolves to refs/heads/", ref: "main", sha: "", mockSetup: func() *http.Client { return NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/git/ref/heads/main") { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/heads/main", "object": {"sha": "main-sha"}}`)) } else { t.Errorf("Unexpected path: %s", r.URL.Path) w.WriteHeader(http.StatusNotFound) } }), ), ) }, expectedOutput: &raw.ContentOpts{ Ref: "refs/heads/main", SHA: "main-sha", }, expectError: false, }, { name: "short tag name falls back to refs/tags/ when branch not found", ref: "v1.0.0", sha: "", mockSetup: func() *http.Client { return NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/git/ref/heads/v1.0.0"): w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) case strings.Contains(r.URL.Path, "/git/ref/tags/v1.0.0"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/tags/v1.0.0", "object": {"sha": "tag-sha"}}`)) default: t.Errorf("Unexpected path: %s", r.URL.Path) w.WriteHeader(http.StatusNotFound) } }), ), ) }, expectedOutput: &raw.ContentOpts{ Ref: "refs/tags/v1.0.0", SHA: "tag-sha", }, expectError: false, }, { name: "heads/ prefix gets refs/ prepended", ref: "heads/feature-branch", sha: "", mockSetup: func() *http.Client { return NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Contains(t, r.URL.Path, "/git/ref/heads/feature-branch") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/heads/feature-branch", "object": {"sha": "feature-sha"}}`)) }), ), ) }, expectedOutput: &raw.ContentOpts{ Ref: "refs/heads/feature-branch", SHA: "feature-sha", }, expectError: false, }, { name: "tags/ prefix gets refs/ prepended", ref: "tags/v1.0.0", sha: "", mockSetup: func() *http.Client { return NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Contains(t, r.URL.Path, "/git/ref/tags/v1.0.0") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/tags/v1.0.0", "object": {"sha": "tag-sha"}}`)) }), ), ) }, expectedOutput: &raw.ContentOpts{ Ref: "refs/tags/v1.0.0", SHA: "tag-sha", }, expectError: false, }, { name: "invalid short name that doesn't exist as branch or tag", ref: "nonexistent", sha: "", mockSetup: func() *http.Client { return NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { // Both branch and tag attempts should return 404 w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ) }, expectError: true, errorContains: "could not resolve ref \"nonexistent\" as a branch or a tag", }, { name: "fully qualified pull request ref", ref: "refs/pull/123/head", sha: "", mockSetup: func() *http.Client { return NewMockedHTTPClient( WithRequestMatchHandler( GetReposGitRefByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Contains(t, r.URL.Path, "/git/ref/pull/123/head") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ref": "refs/pull/123/head", "object": {"sha": "pr-sha"}}`)) }), ), ) }, expectedOutput: &raw.ContentOpts{ Ref: "refs/pull/123/head", SHA: "pr-sha", }, expectError: false, }, { name: "ref looks like full SHA with empty sha parameter", ref: "abc123def456abc123def456abc123def456abc1", sha: "", mockSetup: func() *http.Client { // No API calls should be made when ref looks like SHA return NewMockedHTTPClient() }, expectedOutput: &raw.ContentOpts{ SHA: "abc123def456abc123def456abc123def456abc1", }, expectError: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockSetup()) opts, _, err := resolveGitReference(ctx, client, owner, repo, tc.ref, tc.sha) if tc.expectError { require.Error(t, err) if tc.errorContains != "" { assert.Contains(t, err.Error(), tc.errorContains) } return } require.NoError(t, err) require.NotNil(t, opts) if tc.expectedOutput.SHA != "" { assert.Equal(t, tc.expectedOutput.SHA, opts.SHA) } if tc.expectedOutput.Ref != "" { assert.Equal(t, tc.expectedOutput.Ref, opts.Ref) } }) } } func Test_ListStarredRepositories(t *testing.T) { // Verify tool definition once serverTool := ListStarredRepositories(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "list_starred_repositories", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "username") assert.Contains(t, schema.Properties, "sort") assert.Contains(t, schema.Properties, "direction") assert.Contains(t, schema.Properties, "page") assert.Contains(t, schema.Properties, "perPage") assert.Empty(t, schema.Required) // All parameters are optional // Setup mock starred repositories starredAt := time.Now().Add(-24 * time.Hour) updatedAt := time.Now().Add(-2 * time.Hour) mockStarredRepos := []*github.StarredRepository{ { StarredAt: &github.Timestamp{Time: starredAt}, Repository: &github.Repository{ ID: github.Ptr(int64(12345)), Name: github.Ptr("awesome-repo"), FullName: github.Ptr("owner/awesome-repo"), Description: github.Ptr("An awesome repository"), HTMLURL: github.Ptr("https://github.com/owner/awesome-repo"), Language: github.Ptr("Go"), StargazersCount: github.Ptr(100), ForksCount: github.Ptr(25), OpenIssuesCount: github.Ptr(5), UpdatedAt: &github.Timestamp{Time: updatedAt}, Private: github.Ptr(false), Fork: github.Ptr(false), Archived: github.Ptr(false), DefaultBranch: github.Ptr("main"), }, }, { StarredAt: &github.Timestamp{Time: starredAt.Add(-12 * time.Hour)}, Repository: &github.Repository{ ID: github.Ptr(int64(67890)), Name: github.Ptr("cool-project"), FullName: github.Ptr("user/cool-project"), Description: github.Ptr("A very cool project"), HTMLURL: github.Ptr("https://github.com/user/cool-project"), Language: github.Ptr("Python"), StargazersCount: github.Ptr(500), ForksCount: github.Ptr(75), OpenIssuesCount: github.Ptr(10), UpdatedAt: &github.Timestamp{Time: updatedAt.Add(-1 * time.Hour)}, Private: github.Ptr(false), Fork: github.Ptr(true), Archived: github.Ptr(false), DefaultBranch: github.Ptr("master"), }, }, } tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedErrMsg string expectedCount int }{ { name: "successful list for authenticated user", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetUserStarred, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write(MustMarshal(mockStarredRepos)) }), ), ), requestArgs: map[string]any{}, expectError: false, expectedCount: 2, }, { name: "successful list for specific user", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetUsersStarredByUsername, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write(MustMarshal(mockStarredRepos)) }), ), ), requestArgs: map[string]any{ "username": "testuser", }, expectError: false, expectedCount: 2, }, { name: "list fails", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( GetUserStarred, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]any{}, expectError: true, expectedErrMsg: "failed to list starred repositories", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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.NotNil(t, result) textResult, ok := result.Content[0].(*mcp.TextContent) require.True(t, ok, "Expected text content") assert.Contains(t, textResult.Text, tc.expectedErrMsg) } else { require.NoError(t, err) require.NotNil(t, result) // Parse the result and get the text content textContent := getTextResult(t, result) // Unmarshal and verify the result var returnedRepos []MinimalRepository err = json.Unmarshal([]byte(textContent.Text), &returnedRepos) require.NoError(t, err) assert.Len(t, returnedRepos, tc.expectedCount) if tc.expectedCount > 0 { assert.Equal(t, "awesome-repo", returnedRepos[0].Name) assert.Equal(t, "owner/awesome-repo", returnedRepos[0].FullName) } } }) } } func Test_StarRepository(t *testing.T) { // Verify tool definition once serverTool := StarRepository(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "star_repository", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedErrMsg string }{ { name: "successful star", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( PutUserStarredByOwnerByRepo, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }), ), ), requestArgs: map[string]any{ "owner": "testowner", "repo": "testrepo", }, expectError: false, }, { name: "star fails", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( PutUserStarredByOwnerByRepo, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]any{ "owner": "testowner", "repo": "nonexistent", }, expectError: true, expectedErrMsg: "failed to star repository", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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.NotNil(t, result) textResult, ok := result.Content[0].(*mcp.TextContent) require.True(t, ok, "Expected text content") assert.Contains(t, textResult.Text, tc.expectedErrMsg) } else { require.NoError(t, err) require.NotNil(t, result) // Parse the result and get the text content textContent := getTextResult(t, result) assert.Contains(t, textContent.Text, "Successfully starred repository") } }) } } func Test_UnstarRepository(t *testing.T) { // Verify tool definition once serverTool := UnstarRepository(translations.NullTranslationHelper) tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.Equal(t, "unstar_repository", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, schema.Properties, "owner") assert.Contains(t, schema.Properties, "repo") assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedErrMsg string }{ { name: "successful unstar", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( DeleteUserStarredByOwnerByRepo, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }), ), ), requestArgs: map[string]any{ "owner": "testowner", "repo": "testrepo", }, expectError: false, }, { name: "unstar fails", mockedClient: NewMockedHTTPClient( WithRequestMatchHandler( DeleteUserStarredByOwnerByRepo, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), ), requestArgs: map[string]any{ "owner": "testowner", "repo": "nonexistent", }, expectError: true, expectedErrMsg: "failed to unstar repository", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(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.NotNil(t, result) textResult, ok := result.Content[0].(*mcp.TextContent) require.True(t, ok, "Expected text content") assert.Contains(t, textResult.Text, tc.expectedErrMsg) } else { require.NoError(t, err) require.NotNil(t, result) // Parse the result and get the text content textContent := getTextResult(t, result) assert.Contains(t, textContent.Text, "Successfully unstarred repository") } }) } }