You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
164 lines
5.5 KiB
164 lines
5.5 KiB
package supportbundle
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestManagerCreatesReadableBoundedArchive(t *testing.T) {
|
|
directory := filepath.Join(t.TempDir(), "bundles")
|
|
manager, err := NewManager(Options{Directory: directory})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
generatedAt := time.Date(2026, 7, 23, 4, 5, 6, 0, time.UTC)
|
|
artifact, err := manager.Create(context.Background(), CreateRequest{
|
|
BundleID: "support_bundle_20260723_000001", GeneratedAt: generatedAt,
|
|
Sections: []Section{
|
|
{Name: "system-status.json", Value: map[string]any{"status": "ok"}},
|
|
{Name: "audit-events.json", Value: []string{"accepted", "succeeded"}},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if artifact.SizeBytes == 0 || len(artifact.SHA256) != 64 || len(artifact.Sections) != 2 {
|
|
t.Fatalf("artifact = %+v", artifact)
|
|
}
|
|
if artifact.Sections[0] != "audit-events.json" || artifact.Sections[1] != "system-status.json" {
|
|
t.Fatalf("sections = %v", artifact.Sections)
|
|
}
|
|
|
|
read, err := manager.Read(context.Background(), artifact.BundleID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(read.Content)
|
|
if read.SHA256 != hex.EncodeToString(digest[:]) || read.SHA256 != artifact.SHA256 {
|
|
t.Fatalf("read digest = %q, create digest = %q", read.SHA256, artifact.SHA256)
|
|
}
|
|
archive, err := zip.NewReader(bytes.NewReader(read.Content), int64(len(read.Content)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(archive.File) != 3 || archive.File[0].Name != "manifest.json" {
|
|
t.Fatalf("archive entries = %+v", archive.File)
|
|
}
|
|
manifestFile, err := archive.File[0].Open()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer manifestFile.Close()
|
|
var manifest struct {
|
|
FormatVersion int `json:"format_version"`
|
|
BundleID string `json:"bundle_id"`
|
|
Sections []string `json:"sections"`
|
|
}
|
|
if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if manifest.FormatVersion != 1 || manifest.BundleID != artifact.BundleID || len(manifest.Sections) != 2 {
|
|
t.Fatalf("manifest = %+v", manifest)
|
|
}
|
|
if runtime.GOOS != "windows" {
|
|
info, err := os.Stat(filepath.Join(directory, artifact.Filename))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("artifact mode = %o", info.Mode().Perm())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestManagerRetentionRemovesOldestArtifact(t *testing.T) {
|
|
manager, err := NewManager(Options{Directory: filepath.Join(t.TempDir(), "bundles"), MaxArtifacts: 2})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for sequence := 1; sequence <= 3; sequence++ {
|
|
bundleID := "support_bundle_20260723_00000" + string(rune('0'+sequence))
|
|
if _, err := manager.Create(context.Background(), CreateRequest{
|
|
BundleID: bundleID, GeneratedAt: time.Now(),
|
|
Sections: []Section{{Name: "status.json", Value: map[string]int{"sequence": sequence}}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := manager.Read(context.Background(), "support_bundle_20260723_000001"); err == nil {
|
|
t.Fatal("oldest support bundle was not removed")
|
|
}
|
|
for _, bundleID := range []string{"support_bundle_20260723_000002", "support_bundle_20260723_000003"} {
|
|
if _, err := manager.Read(context.Background(), bundleID); err != nil {
|
|
t.Fatalf("read retained bundle %s: %v", bundleID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestManagerRejectsUnsafeIDsSectionsAndOversizeContent(t *testing.T) {
|
|
manager, err := NewManager(Options{
|
|
Directory: filepath.Join(t.TempDir(), "bundles"), MaxBytes: 64 * 1024, MaxSection: 1024,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, bundleID := range []string{"../bundle", "support_bundle_20261340_000001", "support_bundle_20260723_00000x"} {
|
|
if _, err := manager.Read(context.Background(), bundleID); err == nil {
|
|
t.Fatalf("unsafe bundle id %q accepted", bundleID)
|
|
}
|
|
}
|
|
if _, err := manager.Create(context.Background(), CreateRequest{
|
|
BundleID: "support_bundle_20260723_000001", GeneratedAt: time.Now(),
|
|
Sections: []Section{{Name: "../secret.json", Value: "no"}},
|
|
}); err == nil {
|
|
t.Fatal("unsafe section name accepted")
|
|
}
|
|
if _, err := manager.Create(context.Background(), CreateRequest{
|
|
BundleID: "support_bundle_20260723_000002", GeneratedAt: time.Now(),
|
|
Sections: []Section{{Name: "large.json", Value: string(bytes.Repeat([]byte("x"), 2048))}},
|
|
}); err == nil {
|
|
t.Fatal("oversize section accepted")
|
|
}
|
|
}
|
|
|
|
func TestManagerRejectsAggregateSectionOverflow(t *testing.T) {
|
|
manager, err := NewManager(Options{
|
|
Directory: filepath.Join(t.TempDir(), "bundles"), MaxBytes: 64 * 1024, MaxSection: 64 * 1024,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
large := string(bytes.Repeat([]byte("x"), 40*1024))
|
|
if _, err := manager.Create(context.Background(), CreateRequest{
|
|
BundleID: "support_bundle_20260723_000001", GeneratedAt: time.Now(),
|
|
Sections: []Section{{Name: "first.json", Value: large}, {Name: "second.json", Value: large}},
|
|
}); err == nil {
|
|
t.Fatal("aggregate support bundle section overflow accepted")
|
|
}
|
|
}
|
|
|
|
func TestManagerRefusesToOverwriteArtifact(t *testing.T) {
|
|
manager, err := NewManager(Options{Directory: filepath.Join(t.TempDir(), "bundles")})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := CreateRequest{
|
|
BundleID: "support_bundle_20260723_000001", GeneratedAt: time.Now(),
|
|
Sections: []Section{{Name: "status.json", Value: map[string]bool{"ok": true}}},
|
|
}
|
|
if _, err := manager.Create(context.Background(), request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := manager.Create(context.Background(), request); err == nil {
|
|
t.Fatal("duplicate support bundle id was overwritten")
|
|
}
|
|
}
|
|
|