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") } }