diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 28ce63b46..beabb8a26 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -191,13 +191,14 @@ func RepositoryResourceContentsHandler(resourceURITemplate *uritemplate.Template } resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts) + if err != nil { + return nil, fmt.Errorf("failed to get raw content: %w", err) + } defer func() { _ = resp.Body.Close() }() // If the raw content is not found, we will fall back to the GitHub API (in case it is a directory) switch { - case err != nil: - return nil, fmt.Errorf("failed to get raw content: %w", err) case resp.StatusCode == http.StatusOK: ext := filepath.Ext(path) mimeType := resp.Header.Get("Content-Type") diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index a3b3ca754..ea06f093e 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -2,16 +2,27 @@ package github import ( "context" + "errors" "net/http" "net/url" "testing" "github.com/github/github-mcp-server/pkg/raw" + "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v79/github" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/require" ) +// errorTransport is a http.RoundTripper that always returns an error. +type errorTransport struct { + err error +} + +func (t *errorTransport) RoundTrip(*http.Request) (*http.Response, error) { + return nil, t.err +} + type resourceResponseType int const ( @@ -272,3 +283,52 @@ func Test_repositoryResourceContents(t *testing.T) { }) } } + +// Test_repositoryResourceContentsHandler_NetworkError tests that a network error +// during raw content fetch does not cause a panic (nil response body dereference). +func Test_repositoryResourceContentsHandler_NetworkError(t *testing.T) { + base, _ := url.Parse("https://raw.example.com/") + networkErr := errors.New("network error: connection refused") + + httpClient := &http.Client{Transport: &errorTransport{err: networkErr}} + client := github.NewClient(httpClient) + mockRawClient := raw.NewClient(client, base) + + deps := BaseDeps{ + Client: client, + RawClient: mockRawClient, + } + ctx := ContextWithDeps(context.Background(), deps) + handler := RepositoryResourceContentsHandler(repositoryResourceContentURITemplate) + + request := &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{ + URI: "repo://owner/repo/contents/README.md", + }, + } + + // This should not panic, even though the HTTP client returns an error + resp, err := handler(ctx, request) + require.Error(t, err) + require.Nil(t, resp) + require.ErrorContains(t, err, "failed to get raw content") +} + +func Test_GetRepositoryResourceContent(t *testing.T) { + tmpl := GetRepositoryResourceContent(translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/contents{/path*}", tmpl.Template.URITemplate) +} + +func Test_GetRepositoryResourceBranchContent(t *testing.T) { + tmpl := GetRepositoryResourceBranchContent(translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", tmpl.Template.URITemplate) +} +func Test_GetRepositoryResourceCommitContent(t *testing.T) { + tmpl := GetRepositoryResourceCommitContent(translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", tmpl.Template.URITemplate) +} + +func Test_GetRepositoryResourceTagContent(t *testing.T) { + tmpl := GetRepositoryResourceTagContent(translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", tmpl.Template.URITemplate) +}