diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 5dea9f4e9..5cd8176ea 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -160,13 +160,14 @@ func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.G } 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 113f46d89..0245d4d93 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -2,6 +2,7 @@ package github import ( "context" + "errors" "net/http" "net/url" "testing" @@ -14,6 +15,16 @@ import ( "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 ( @@ -312,3 +323,58 @@ 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) + handler := RepositoryResourceContentsHandler(stubGetClientFn(client), stubGetRawClientFn(mockRawClient)) + + request := mcp.ReadResourceRequest{ + Params: struct { + URI string `json:"uri"` + Arguments map[string]any `json:"arguments,omitempty"` + }{ + Arguments: map[string]any{ + "owner": []string{"owner"}, + "repo": []string{"repo"}, + "path": []string{"README.md"}, + }, + }, + } + + // This should not panic, even though the HTTP client returns an error + resp, err := handler(context.TODO(), request) + require.Error(t, err) + require.Nil(t, resp) + require.ErrorContains(t, err, "failed to get raw content") +} + +func Test_GetRepositoryResourceContent(t *testing.T) { + mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{}) + tmpl, _ := GetRepositoryResourceContent(nil, stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/contents{/path*}", tmpl.URITemplate.Raw()) +} + +func Test_GetRepositoryResourceBranchContent(t *testing.T) { + mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{}) + tmpl, _ := GetRepositoryResourceBranchContent(nil, stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", tmpl.URITemplate.Raw()) +} +func Test_GetRepositoryResourceCommitContent(t *testing.T) { + mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{}) + tmpl, _ := GetRepositoryResourceCommitContent(nil, stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", tmpl.URITemplate.Raw()) +} + +func Test_GetRepositoryResourceTagContent(t *testing.T) { + mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{}) + tmpl, _ := GetRepositoryResourceTagContent(nil, stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", tmpl.URITemplate.Raw()) +} +