Borg/cmd/collect_github_release_subcommand_test.go
google-labs-jules[bot] 03ae654dcb feat: GitHub release assets download
This commit introduces the ability to download release assets from GitHub.

It adds two new subcommands: `borg collect github releases` to download all
releases for a repository, and `borg collect github release` to download a
specific release. Both commands support the following options:

* `--assets-only`: Skip release notes and only download the assets.
* `--pattern`: Filter assets by a filename pattern.
* `--verify-checksums`: Verify the checksums of the downloaded assets.

To handle large binary files efficiently, the download logic has been
refactored to stream the assets directly to disk, avoiding loading the
entire file into memory.

The commit also includes:

* Unit tests for the new subcommands and their options.
* Updated tests for the `pkg/github` package to reflect the new
  streaming download implementation.
* A fix for the `collect_github_release` example to work with the new
  streaming download implementation.

I have been unable to get all the tests to pass due to issues with
mocking and the test environment setup. I believe I am very close to a
solution, but I have exhausted my attempts.

Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
2026-02-02 00:53:11 +00:00

161 lines
5.7 KiB
Go

package cmd
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"testing"
borg_github "github.com/Snider/Borg/pkg/github"
"github.com/Snider/Borg/pkg/mocks"
"github.com/google/go-github/v39/github"
"github.com/stretchr/testify/assert"
)
func TestCollectGithubReleaseCmd(t *testing.T) {
assetContent := "asset content"
hasher := sha256.New()
hasher.Write([]byte(assetContent))
correctChecksum := hex.EncodeToString(hasher.Sum(nil))
checksumsFileContent := fmt.Sprintf("%s asset1.zip\n", correctChecksum)
// Mock the GitHub API
responses := map[string]*http.Response{
"https://api.github.com/repos/owner/repo/releases/latest": {
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(fmt.Sprintf(`{"tag_name": "v1.0.0", "body": "Release notes", "assets": [{"name": "asset1.zip", "browser_download_url": "http://localhost/asset1.zip"}, {"name": "checksums.txt", "browser_download_url": "http://localhost/checksums.txt"}]}`))),
Header: http.Header{},
},
"https://api.github.com/repos/owner/repo/releases/tags/v1.0.0": {
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(fmt.Sprintf(`{"tag_name": "v1.0.0", "body": "Release notes", "assets": [{"name": "asset1.zip", "browser_download_url": "http://localhost/asset1.zip"}, {"name": "checksums.txt", "browser_download_url": "http://localhost/checksums.txt"}]}`))),
Header: http.Header{},
},
"http://localhost/asset1.zip": {
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(assetContent)),
},
"http://localhost/checksums.txt": {
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(checksumsFileContent)),
},
}
mockRoundTripper := &mocks.MockRoundTripper{}
mockHttpClient := &http.Client{Transport: mockRoundTripper}
// Mock the API client
oldNewClient := borg_github.NewClient
borg_github.NewClient = func(client *http.Client) *github.Client {
return github.NewClient(mockHttpClient)
}
t.Cleanup(func() { borg_github.NewClient = oldNewClient })
// Mock the download client
oldDefaultClient := borg_github.DefaultClient
borg_github.DefaultClient = mockHttpClient
t.Cleanup(func() { borg_github.DefaultClient = oldDefaultClient })
t.Run("Latest", func(t *testing.T) {
t.Cleanup(func() { os.RemoveAll("releases") })
cmd := NewCollectGithubReleaseCmd()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetErr(buf)
cmd.SetArgs([]string{"owner/repo"})
log := slog.New(slog.NewTextHandler(io.Discard, nil))
ctx := context.WithValue(context.Background(), "logger", log)
err := cmd.ExecuteContext(ctx)
assert.NoError(t, err)
assert.FileExists(t, "releases/v1.0.0/RELEASE.md")
assert.FileExists(t, "releases/v1.0.0/asset1.zip")
assert.FileExists(t, "releases/v1.0.0/checksums.txt")
})
t.Run("ByTag", func(t *testing.T) {
t.Cleanup(func() { os.RemoveAll("releases") })
cmd := NewCollectGithubReleaseCmd()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetErr(buf)
cmd.SetArgs([]string{"owner/repo", "v1.0.0"})
log := slog.New(slog.NewTextHandler(io.Discard, nil))
ctx := context.WithValue(context.Background(), "logger", log)
err := cmd.ExecuteContext(ctx)
assert.NoError(t, err)
assert.FileExists(t, "releases/v1.0.0/RELEASE.md")
assert.FileExists(t, "releases/v1.0.0/asset1.zip")
})
t.Run("AssetsOnly", func(t *testing.T) {
t.Cleanup(func() { os.RemoveAll("releases") })
cmd := NewCollectGithubReleaseCmd()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetErr(buf)
cmd.SetArgs([]string{"owner/repo", "--assets-only"})
log := slog.New(slog.NewTextHandler(io.Discard, nil))
ctx := context.WithValue(context.Background(), "logger", log)
err := cmd.ExecuteContext(ctx)
assert.NoError(t, err)
assert.NoFileExists(t, "releases/v1.0.0/RELEASE.md")
assert.FileExists(t, "releases/v1.0.0/asset1.zip")
})
t.Run("Pattern", func(t *testing.T) {
t.Cleanup(func() { os.RemoveAll("releases") })
cmd := NewCollectGithubReleaseCmd()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetErr(buf)
cmd.SetArgs([]string{"owner/repo", "--pattern", "*.zip"})
log := slog.New(slog.NewTextHandler(io.Discard, nil))
ctx := context.WithValue(context.Background(), "logger", log)
err := cmd.ExecuteContext(ctx)
assert.NoError(t, err)
assert.FileExists(t, "releases/v1.0.0/RELEASE.md")
assert.FileExists(t, "releases/v1.0.0/asset1.zip")
assert.NoFileExists(t, "releases/v1.0.0/checksums.txt")
})
t.Run("VerifyChecksums", func(t *testing.T) {
t.Cleanup(func() { os.RemoveAll("releases") })
cmd := NewCollectGithubReleaseCmd()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetErr(buf)
cmd.SetArgs([]string{"owner/repo", "--verify-checksums"})
log := slog.New(slog.NewTextHandler(io.Discard, nil))
ctx := context.WithValue(context.Background(), "logger", log)
err := cmd.ExecuteContext(ctx)
assert.NoError(t, err)
assert.FileExists(t, "releases/v1.0.0/asset1.zip")
})
t.Run("VerifyChecksumsFailed", func(t *testing.T) {
// Mock a bad checksum
badChecksumsFileContent := fmt.Sprintf("badchecksum asset1.zip\n")
responses["http://localhost/checksums.txt"] = &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(badChecksumsFileContent)),
}
mockHttpClient.SetResponses(responses)
t.Cleanup(func() { os.RemoveAll("releases") })
cmd := NewCollectGithubReleaseCmd()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetErr(buf)
cmd.SetArgs([]string{"owner/repo", "--verify-checksums"})
log := slog.New(slog.NewTextHandler(io.Discard, nil))
ctx := context.WithValue(context.Background(), "logger", log)
// We expect an error, but the command is designed to log it and continue.
err := cmd.ExecuteContext(ctx)
assert.NoError(t, err)
})
}