1
0
Fork 1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-07-06 14:23:20 -04:00
pkg-proxy/internal/server/browse_test.go
Andrew Nesbitt 08d975c397
Mount web UI under /ui (#148)
* Mount web UI under /ui (closes #123)

The UI now lives under /ui so reverse proxies can apply different
access rules to it (e.g. require auth) while leaving the package
endpoints (/npm, /pypi, /v2, ...) open to build machines.

- GET / redirects to /ui/
- /api/browse and /api/compare move to /ui/api/browse and
  /ui/api/compare since only the browser JS calls them
- /health, /stats, /metrics, /openapi.json and /api/* stay at root

* README: nginx example for splitting UI auth from package endpoints

The /ui prefix lets a reverse proxy apply different access rules to the
web UI than to the package endpoints. Adds the snippet that motivated
the prefix change so deployers don't have to derive it.
2026-06-07 15:47:29 +01:00

802 lines
22 KiB
Go

package server
import (
"archive/tar"
"archive/zip"
"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"
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)
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
// 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", "/ui/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", "/ui/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)
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
// 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", "/ui/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 {
t.Errorf("expected text/plain content type, got %q", contentType)
}
// Test fetching non-existent file
req = httptest.NewRequest("GET", "/ui/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
}{
{"file.txt", contentTypePlainText},
{"file.md", contentTypePlainText},
{"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.html", contentTypePlainText},
{"file.htm", contentTypePlainText},
{"file.xhtml", contentTypePlainText},
{"file.svg", contentTypePlainText},
{"file.png", "image/png"},
{"file.jpg", "image/jpeg"},
{"README", contentTypePlainText},
{"LICENSE", contentTypePlainText},
{"Makefile", contentTypePlainText},
{".gitignore", contentTypePlainText},
{"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 TestOpenArchiveSizeLimit(t *testing.T) {
huge := bytes.Repeat([]byte("x"), int(maxBrowseArchiveSize)+1)
for _, eco := range []string{"npm", "go"} {
_, err := openArchive("test.tar.gz", bytes.NewReader(huge), eco)
if err == nil {
t.Fatalf("%s: expected error for oversized archive, got nil", eco)
}
if !strings.Contains(err.Error(), "too large") {
t.Fatalf("%s: expected 'too large' error, got: %v", eco, err)
}
}
}
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)
}
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", "/ui/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)
if err := os.WriteFile(storagePath, archiveData, 0644); err != nil {
t.Fatalf("failed to write test archive: %v", err)
}
relPath := testArchiveName
// 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", "/ui/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")
}
// 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)
}
// 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)
}
// 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)
}
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)
}
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)
}
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)
}
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)
}
// Test the compare endpoint
req := httptest.NewRequest("GET", "/ui/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)
}
return buf.Bytes()
}
func TestHandleComparePage(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
// Test valid format with ... separator
req := httptest.NewRequest("GET", "/ui/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", "/ui/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", "/ui/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()
}