pkg-proxy/internal/server/browse_test.go

785 lines
21 KiB
Go
Raw Permalink Normal View History

2026-02-03 22:40:23 +00:00
package server
import (
"archive/tar"
"archive/zip"
2026-02-03 22:40:23 +00:00
"bytes"
"compress/gzip"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/git-pkgs/proxy/internal/database"
)
const testArchiveName = "test.tar.gz"
2026-02-03 22:40:23 +00:00
func TestHandleBrowseList(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
// Create a test tar.gz archive
archiveData := createTestArchive(t)
artifactsDir := filepath.Join(ts.tempDir, "artifacts")
if err := os.MkdirAll(artifactsDir, 0755); err != nil {
t.Fatalf("failed to create artifacts dir: %v", err)
}
storagePath := filepath.Join(artifactsDir, testArchiveName)
2026-02-03 22:40:23 +00:00
if err := os.WriteFile(storagePath, archiveData, 0644); err != nil {
t.Fatalf("failed to write test archive: %v", err)
}
// Storage path relative to artifacts directory
relPath := testArchiveName
2026-02-03 22:40:23 +00:00
// Setup test package and artifact
pkg := &database.Package{
PURL: "pkg:npm/test-browse",
Ecosystem: "npm",
Name: "test-browse",
}
if err := ts.db.UpsertPackage(pkg); err != nil {
t.Fatalf("failed to upsert package: %v", err)
}
ver := &database.Version{
PURL: "pkg:npm/test-browse@1.0.0",
PackagePURL: pkg.PURL,
}
if err := ts.db.UpsertVersion(ver); err != nil {
t.Fatalf("failed to upsert version: %v", err)
}
artifact := &database.Artifact{
VersionPURL: ver.PURL,
Filename: "test-browse-1.0.0.tgz",
UpstreamURL: "https://registry.npmjs.org/test-browse/-/test-browse-1.0.0.tgz",
StoragePath: sql.NullString{String: relPath, Valid: true},
}
if err := ts.db.UpsertArtifact(artifact); err != nil {
t.Fatalf("failed to upsert artifact: %v", err)
}
// Test listing root directory
req := httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
var response BrowseListResponse
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if len(response.Files) == 0 {
t.Error("expected files in response")
}
// Test listing subdirectory
req = httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0?path=lib", nil)
w = httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}
}
func TestHandleBrowseFile(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
// Create a test tar.gz archive
archiveData := createTestArchive(t)
artifactsDir := filepath.Join(ts.tempDir, "artifacts")
if err := os.MkdirAll(artifactsDir, 0755); err != nil {
t.Fatalf("failed to create artifacts dir: %v", err)
}
storagePath := filepath.Join(artifactsDir, testArchiveName)
2026-02-03 22:40:23 +00:00
if err := os.WriteFile(storagePath, archiveData, 0644); err != nil {
t.Fatalf("failed to write test archive: %v", err)
}
// Storage path relative to artifacts directory
relPath := testArchiveName
2026-02-03 22:40:23 +00:00
// Setup test package and artifact
pkg := &database.Package{
PURL: "pkg:npm/test-browse",
Ecosystem: "npm",
Name: "test-browse",
}
if err := ts.db.UpsertPackage(pkg); err != nil {
t.Fatalf("failed to upsert package: %v", err)
}
ver := &database.Version{
PURL: "pkg:npm/test-browse@1.0.0",
PackagePURL: pkg.PURL,
}
if err := ts.db.UpsertVersion(ver); err != nil {
t.Fatalf("failed to upsert version: %v", err)
}
artifact := &database.Artifact{
VersionPURL: ver.PURL,
Filename: "test-browse-1.0.0.tgz",
UpstreamURL: "https://registry.npmjs.org/test-browse/-/test-browse-1.0.0.tgz",
StoragePath: sql.NullString{String: relPath, Valid: true},
}
if err := ts.db.UpsertArtifact(artifact); err != nil {
t.Fatalf("failed to upsert artifact: %v", err)
}
// Test fetching a file
req := httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0/file/README.md", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
body := w.Body.String()
if body != "# Test Package\n" {
t.Errorf("unexpected file content: %q", body)
}
// Check content type
contentType := w.Header().Get("Content-Type")
if contentType != contentTypePlainText {
2026-02-03 22:40:23 +00:00
t.Errorf("expected text/plain content type, got %q", contentType)
}
// Test fetching non-existent file
req = httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0/file/nonexistent.txt", nil)
w = httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404 for non-existent file, got %d", w.Code)
}
}
func TestDetectContentType(t *testing.T) {
tests := []struct {
filename string
expectedCT string
2026-02-03 22:40:23 +00:00
}{
{"file.txt", contentTypePlainText},
{"file.md", contentTypePlainText},
2026-02-03 22:40:23 +00:00
{"file.json", "application/json; charset=utf-8"},
{"file.js", "application/javascript; charset=utf-8"},
{"file.go", "text/x-go; charset=utf-8"},
{"file.py", "text/x-python; charset=utf-8"},
{"file.rs", "text/x-rust; charset=utf-8"},
{"file.png", "image/png"},
{"file.jpg", "image/jpeg"},
{"README", contentTypePlainText},
{"LICENSE", contentTypePlainText},
{"Makefile", contentTypePlainText},
{".gitignore", contentTypePlainText},
2026-02-03 22:40:23 +00:00
{"file.bin", "application/octet-stream"},
}
for _, tt := range tests {
t.Run(tt.filename, func(t *testing.T) {
got := detectContentType(tt.filename)
if got != tt.expectedCT {
t.Errorf("detectContentType(%q) = %q, want %q", tt.filename, got, tt.expectedCT)
}
})
}
}
func TestIsLikelyText(t *testing.T) {
tests := []struct {
filename string
expected bool
}{
{"README", true},
{"README.md", true},
{"LICENSE", true},
{"Makefile", true},
{"Dockerfile", true},
{".gitignore", true},
{"file.bin", false},
{"data.dat", false},
}
for _, tt := range tests {
t.Run(tt.filename, func(t *testing.T) {
got := isLikelyText(tt.filename)
if got != tt.expected {
t.Errorf("isLikelyText(%q) = %v, want %v", tt.filename, got, tt.expected)
}
})
}
}
// createTestArchive creates a tar.gz archive in memory with test files
// in npm format (with package/ prefix)
func createTestArchive(t *testing.T) []byte {
t.Helper()
buf := new(bytes.Buffer)
gw := gzip.NewWriter(buf)
tw := tar.NewWriter(gw)
files := map[string]string{
"package/README.md": "# Test Package\n",
"package/package.json": `{"name": "test-browse"}`,
"package/lib/index.js": "module.exports = {};",
"package/lib/helper.js": "module.exports.help = () => {};",
"package/test/index.test.js": "// tests",
}
for path, content := range files {
header := &tar.Header{
Name: path,
Size: int64(len(content)),
Mode: 0644,
}
if err := tw.WriteHeader(header); err != nil {
t.Fatalf("failed to write tar header: %v", err)
}
if _, err := tw.Write([]byte(content)); err != nil {
t.Fatalf("failed to write tar content: %v", err)
}
}
if err := tw.Close(); err != nil {
t.Fatalf("failed to close tar writer: %v", err)
}
if err := gw.Close(); err != nil {
t.Fatalf("failed to close gzip writer: %v", err)
}
2026-02-03 22:40:23 +00:00
return buf.Bytes()
}
func TestBrowseNonCachedArtifact(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
// Setup test package without cached artifact
pkg := &database.Package{
PURL: "pkg:npm/not-cached",
Ecosystem: "npm",
Name: "not-cached",
}
if err := ts.db.UpsertPackage(pkg); err != nil {
t.Fatalf("failed to upsert package: %v", err)
}
ver := &database.Version{
PURL: "pkg:npm/not-cached@1.0.0",
PackagePURL: pkg.PURL,
}
if err := ts.db.UpsertVersion(ver); err != nil {
t.Fatalf("failed to upsert version: %v", err)
}
artifact := &database.Artifact{
VersionPURL: ver.PURL,
Filename: "not-cached-1.0.0.tgz",
UpstreamURL: "https://registry.npmjs.org/not-cached/-/not-cached-1.0.0.tgz",
// No StoragePath - not cached
}
if err := ts.db.UpsertArtifact(artifact); err != nil {
t.Fatalf("failed to upsert artifact: %v", err)
}
// Try to browse
req := httptest.NewRequest("GET", "/api/browse/npm/not-cached/1.0.0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404 for non-cached artifact, got %d", w.Code)
}
}
func TestHandleBrowseSourcePage(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
// Create a test tar.gz archive
archiveData := createTestArchive(t)
artifactsDir := filepath.Join(ts.tempDir, "artifacts")
if err := os.MkdirAll(artifactsDir, 0755); err != nil {
t.Fatalf("failed to create artifacts dir: %v", err)
}
storagePath := filepath.Join(artifactsDir, testArchiveName)
2026-02-03 22:40:23 +00:00
if err := os.WriteFile(storagePath, archiveData, 0644); err != nil {
t.Fatalf("failed to write test archive: %v", err)
}
relPath := testArchiveName
2026-02-03 22:40:23 +00:00
// Setup test package and artifact
pkg := &database.Package{
PURL: "pkg:npm/test-browse",
Ecosystem: "npm",
Name: "test-browse",
}
if err := ts.db.UpsertPackage(pkg); err != nil {
t.Fatalf("failed to upsert package: %v", err)
}
ver := &database.Version{
PURL: "pkg:npm/test-browse@1.0.0",
PackagePURL: pkg.PURL,
}
if err := ts.db.UpsertVersion(ver); err != nil {
t.Fatalf("failed to upsert version: %v", err)
}
artifact := &database.Artifact{
VersionPURL: ver.PURL,
Filename: "test-browse-1.0.0.tgz",
UpstreamURL: "https://registry.npmjs.org/test-browse/-/test-browse-1.0.0.tgz",
StoragePath: sql.NullString{String: relPath, Valid: true},
}
if err := ts.db.UpsertArtifact(artifact); err != nil {
t.Fatalf("failed to upsert artifact: %v", err)
}
// Test the browse source page loads
req := httptest.NewRequest("GET", "/package/npm/test-browse/1.0.0/browse", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
body := w.Body.String()
// Check that the page contains expected elements
expectedStrings := []string{
"Browse Source",
"test-browse",
"1.0.0",
"file-tree",
"file-content",
"loadFileTree",
"loadFile",
}
for _, expected := range expectedStrings {
if !strings.Contains(body, expected) {
t.Errorf("browse source page missing expected content: %q", expected)
}
}
// Check that the escapeHTML function is present for XSS protection
if !strings.Contains(body, "function escapeHTML(str)") {
t.Error("browse source page missing escapeHTML function for XSS protection")
}
// Check that onclick handlers use escapeHTML
if strings.Contains(body, "onclick=\"loadFileTree('${file.path}')") {
t.Error("browse source page has unescaped file.path in onclick handler")
}
if strings.Contains(body, "onclick=\"loadFile('${file.path}')") {
t.Error("browse source page has unescaped file.path in onclick handler")
}
2026-02-03 22:40:23 +00:00
// Check that ecosystem, package name, and version are set in JavaScript
if !strings.Contains(body, "const ecosystem = 'npm'") {
t.Error("browse source page missing ecosystem variable")
}
if !strings.Contains(body, "const packageName = 'test-browse'") {
t.Error("browse source page missing packageName variable")
}
if !strings.Contains(body, "const version = '1.0.0'") {
t.Error("browse source page missing version variable")
}
// Verify content type
contentType := w.Header().Get("Content-Type")
if !strings.Contains(contentType, "text/html") {
t.Errorf("expected HTML content type, got %q", contentType)
}
}
func TestHandleCompareDiff(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
// Create two test archives with different content
archive1Data := createArchiveWithContent(t, map[string]string{
"README.md": "# Version 1\n",
"main.go": "package main\n",
})
archive2Data := createArchiveWithContent(t, map[string]string{
"README.md": "# Version 2\n",
"main.go": "package main\n\nfunc main() {}\n",
"new.txt": "new file\n",
})
artifactsDir := filepath.Join(ts.tempDir, "artifacts")
if err := os.MkdirAll(artifactsDir, 0755); err != nil {
t.Fatalf("failed to create artifacts dir: %v", err)
}
2026-02-03 22:40:23 +00:00
// Write archives
if err := os.WriteFile(filepath.Join(artifactsDir, "v1.tar.gz"), archive1Data, 0644); err != nil {
t.Fatalf("failed to write v1 archive: %v", err)
}
if err := os.WriteFile(filepath.Join(artifactsDir, "v2.tar.gz"), archive2Data, 0644); err != nil {
t.Fatalf("failed to write v2 archive: %v", err)
}
2026-02-03 22:40:23 +00:00
// Setup package and versions
pkg := &database.Package{
PURL: "pkg:npm/test-compare",
Ecosystem: "npm",
Name: "test-compare",
}
if err := ts.db.UpsertPackage(pkg); err != nil {
t.Fatalf("failed to upsert package: %v", err)
}
2026-02-03 22:40:23 +00:00
ver1 := &database.Version{
PURL: "pkg:npm/test-compare@1.0.0",
PackagePURL: pkg.PURL,
}
if err := ts.db.UpsertVersion(ver1); err != nil {
t.Fatalf("failed to upsert version: %v", err)
}
2026-02-03 22:40:23 +00:00
ver2 := &database.Version{
PURL: "pkg:npm/test-compare@2.0.0",
PackagePURL: pkg.PURL,
}
if err := ts.db.UpsertVersion(ver2); err != nil {
t.Fatalf("failed to upsert version: %v", err)
}
2026-02-03 22:40:23 +00:00
artifact1 := &database.Artifact{
VersionPURL: ver1.PURL,
Filename: "test-compare-1.0.0.tgz",
UpstreamURL: "https://registry.npmjs.org/test-compare/-/test-compare-1.0.0.tgz",
StoragePath: sql.NullString{String: "v1.tar.gz", Valid: true},
}
if err := ts.db.UpsertArtifact(artifact1); err != nil {
t.Fatalf("failed to upsert artifact: %v", err)
}
2026-02-03 22:40:23 +00:00
artifact2 := &database.Artifact{
VersionPURL: ver2.PURL,
Filename: "test-compare-2.0.0.tgz",
UpstreamURL: "https://registry.npmjs.org/test-compare/-/test-compare-2.0.0.tgz",
StoragePath: sql.NullString{String: "v2.tar.gz", Valid: true},
}
if err := ts.db.UpsertArtifact(artifact2); err != nil {
t.Fatalf("failed to upsert artifact: %v", err)
}
2026-02-03 22:40:23 +00:00
// Test the compare endpoint
req := httptest.NewRequest("GET", "/api/compare/npm/test-compare/1.0.0/2.0.0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
// Parse response
var result map[string]interface{}
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
// Check that we have files
files, ok := result["files"].([]interface{})
if !ok {
t.Fatal("response should have files array")
}
if len(files) == 0 {
t.Error("should have detected file changes")
}
// Check counts exist
if _, ok := result["files_changed"]; !ok {
t.Error("response should have files_changed")
}
if _, ok := result["files_added"]; !ok {
t.Error("response should have files_added")
}
}
func createArchiveWithContent(t *testing.T, files map[string]string) []byte {
t.Helper()
buf := new(bytes.Buffer)
gw := gzip.NewWriter(buf)
tw := tar.NewWriter(gw)
// Add package/ prefix for npm-style archives
for path, content := range files {
prefixedPath := "package/" + path
header := &tar.Header{
Name: prefixedPath,
Size: int64(len(content)),
Mode: 0644,
}
if err := tw.WriteHeader(header); err != nil {
t.Fatalf("failed to write tar header: %v", err)
}
if _, err := tw.Write([]byte(content)); err != nil {
t.Fatalf("failed to write tar content: %v", err)
}
}
if err := tw.Close(); err != nil {
t.Fatalf("failed to close tar writer: %v", err)
}
if err := gw.Close(); err != nil {
t.Fatalf("failed to close gzip writer: %v", err)
}
2026-02-03 22:40:23 +00:00
return buf.Bytes()
}
func TestHandleComparePage(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
// Test valid format with ... separator
req := httptest.NewRequest("GET", "/package/npm/test/compare/1.0.0...2.0.0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
body := w.Body.String()
// Check that versions are set correctly in JavaScript
if !strings.Contains(body, "const fromVersion = '1.0.0'") {
t.Error("page should set fromVersion")
}
if !strings.Contains(body, "const toVersion = '2.0.0'") {
t.Error("page should set toVersion")
}
// Test invalid format (missing separator)
req = httptest.NewRequest("GET", "/package/npm/test/compare/invalid", nil)
w = httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for invalid format, got %d", w.Code)
}
// Test with only one dot (should fail)
req = httptest.NewRequest("GET", "/package/npm/test/compare/1.0.0.2.0.0", nil)
w = httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for invalid separator, got %d", w.Code)
}
}
func TestArchiveFilename(t *testing.T) {
tests := []struct {
input string
want string
}{
{"package.tar.gz", "package.tar.gz"},
{"d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856.zip"},
{"file.zip", "file.zip"},
{"archive.tgz", "archive.tgz"},
{"noext", "noext.zip"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := archiveFilename(tt.input)
if got != tt.want {
t.Errorf("archiveFilename(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestOpenArchiveStripsSingleRootDir(t *testing.T) {
data := createZipArchive(t, map[string]string{
"repo-abc123/README.md": "hello",
"repo-abc123/src/main.go": "package main",
"repo-abc123/go.mod": "module test",
})
reader, err := openArchive("test.zip", bytes.NewReader(data), "composer")
if err != nil {
t.Fatalf("openArchive failed: %v", err)
}
defer func() { _ = reader.Close() }()
files, err := reader.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
for _, f := range files {
if strings.HasPrefix(f.Path, "repo-abc123/") {
t.Errorf("file %q still has root prefix after stripping", f.Path)
}
}
}
func TestOpenArchiveMultipleRootDirs(t *testing.T) {
data := createZipArchive(t, map[string]string{
"src/main.go": "package main",
"docs/README.md": "hello",
})
reader, err := openArchive("test.zip", bytes.NewReader(data), "composer")
if err != nil {
t.Fatalf("openArchive failed: %v", err)
}
defer func() { _ = reader.Close() }()
files, err := reader.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
paths := make(map[string]bool)
for _, f := range files {
paths[f.Path] = true
}
if !paths["src/main.go"] {
t.Error("expected src/main.go to remain unchanged")
}
if !paths["docs/README.md"] {
t.Error("expected docs/README.md to remain unchanged")
}
}
func TestOpenArchiveFlatNoSubdirs(t *testing.T) {
data := createZipArchive(t, map[string]string{
"README.md": "hello",
"main.go": "package main",
})
reader, err := openArchive("test.zip", bytes.NewReader(data), "composer")
if err != nil {
t.Fatalf("openArchive failed: %v", err)
}
defer func() { _ = reader.Close() }()
files, err := reader.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
paths := make(map[string]bool)
for _, f := range files {
paths[f.Path] = true
}
if !paths["README.md"] {
t.Error("expected README.md at root")
}
}
func TestOpenArchiveNpmUsesPackagePrefix(t *testing.T) {
data := createTarGzArchive(t, map[string]string{
"package/README.md": "hello",
"package/index.js": "module.exports = {}",
})
reader, err := openArchive("pkg.tgz", bytes.NewReader(data), "npm")
if err != nil {
t.Fatalf("openArchive failed: %v", err)
}
defer func() { _ = reader.Close() }()
files, err := reader.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
for _, f := range files {
if strings.HasPrefix(f.Path, "package/") {
t.Errorf("file %q still has package/ prefix", f.Path)
}
}
}
func TestOpenArchiveExtensionlessFilename(t *testing.T) {
data := createZipArchive(t, map[string]string{
"repo-hash/README.md": "hello",
})
reader, err := openArchive("d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", bytes.NewReader(data), "composer")
if err != nil {
t.Fatalf("openArchive failed: %v", err)
}
defer func() { _ = reader.Close() }()
files, err := reader.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(files) == 0 {
t.Fatal("expected files in archive")
}
for _, f := range files {
if strings.HasPrefix(f.Path, "repo-hash/") {
t.Errorf("file %q still has root prefix", f.Path)
}
}
}
func createZipArchive(t *testing.T, files map[string]string) []byte {
t.Helper()
buf := new(bytes.Buffer)
w := zip.NewWriter(buf)
for name, content := range files {
f, err := w.Create(name)
if err != nil {
t.Fatalf("failed to create zip entry: %v", err)
}
if _, err := f.Write([]byte(content)); err != nil {
t.Fatalf("failed to write zip content: %v", err)
}
}
if err := w.Close(); err != nil {
t.Fatalf("failed to close zip writer: %v", err)
}
return buf.Bytes()
}
func createTarGzArchive(t *testing.T, files map[string]string) []byte {
t.Helper()
buf := new(bytes.Buffer)
gw := gzip.NewWriter(buf)
tw := tar.NewWriter(gw)
for name, content := range files {
header := &tar.Header{
Name: name,
Size: int64(len(content)),
Mode: 0644,
}
if err := tw.WriteHeader(header); err != nil {
t.Fatalf("failed to write tar header: %v", err)
}
if _, err := tw.Write([]byte(content)); err != nil {
t.Fatalf("failed to write tar content: %v", err)
}
}
if err := tw.Close(); err != nil {
t.Fatalf("failed to close tar writer: %v", err)
}
if err := gw.Close(); err != nil {
t.Fatalf("failed to close gzip writer: %v", err)
}
return buf.Bytes()
}