Files
pulse/internal/updates/manager_backup_runtime_test.go
2026-04-22 09:40:49 +01:00

216 lines
6.6 KiB
Go

package updates
import (
"archive/tar"
"compress/gzip"
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
godisk "github.com/shirou/gopsutil/v4/disk"
)
func TestPruneRetainedUpdateBackupsKeepsNewestAndClearsHistory(t *testing.T) {
dataDir := t.TempDir()
t.Setenv("PULSE_DATA_DIR", dataDir)
history, err := NewUpdateHistory(t.TempDir())
if err != nil {
t.Fatalf("NewUpdateHistory error: %v", err)
}
manager := &Manager{history: history}
base := time.Date(2026, time.April, 22, 8, 0, 0, 0, time.UTC)
backupPaths := make([]string, 0, 4)
for i := 0; i < 4; i++ {
backupDir := filepath.Join(dataDir, "backup-"+base.Add(time.Duration(i)*time.Minute).Format("20060102-150405"))
if err := os.MkdirAll(backupDir, 0755); err != nil {
t.Fatalf("mkdir backup %d: %v", i, err)
}
modTime := base.Add(time.Duration(i) * time.Minute)
if err := os.Chtimes(backupDir, modTime, modTime); err != nil {
t.Fatalf("chtimes backup %d: %v", i, err)
}
backupPaths = append(backupPaths, backupDir)
}
oldEventID, err := history.CreateEntry(context.Background(), UpdateHistoryEntry{
Action: "update",
Status: StatusSuccess,
VersionFrom: "v6.0.0-rc.1",
VersionTo: "v6.0.0-rc.2",
BackupPath: backupPaths[0],
})
if err != nil {
t.Fatalf("CreateEntry old error: %v", err)
}
newEventID, err := history.CreateEntry(context.Background(), UpdateHistoryEntry{
Action: "update",
Status: StatusSuccess,
VersionFrom: "v6.0.0-rc.2",
VersionTo: "v6.0.0-rc.3",
BackupPath: backupPaths[3],
})
if err != nil {
t.Fatalf("CreateEntry new error: %v", err)
}
if err := manager.pruneRetainedUpdateBackups(context.Background()); err != nil {
t.Fatalf("pruneRetainedUpdateBackups error: %v", err)
}
if _, err := os.Stat(backupPaths[0]); !os.IsNotExist(err) {
t.Fatalf("expected oldest backup to be pruned, stat err: %v", err)
}
for _, path := range backupPaths[1:] {
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected retained backup %q, got %v", path, err)
}
}
oldEntry, err := history.GetEntry(oldEventID)
if err != nil {
t.Fatalf("GetEntry old error: %v", err)
}
if oldEntry.BackupPath != "" {
t.Fatalf("expected pruned backup path to be cleared, got %q", oldEntry.BackupPath)
}
if !strings.Contains(oldEntry.Notes, "Rollback backup pruned by retention") {
t.Fatalf("expected retention note on old entry, got %q", oldEntry.Notes)
}
newEntry, err := history.GetEntry(newEventID)
if err != nil {
t.Fatalf("GetEntry new error: %v", err)
}
if filepath.Clean(newEntry.BackupPath) != filepath.Clean(backupPaths[3]) {
t.Fatalf("expected newest backup path to remain, got %q", newEntry.BackupPath)
}
}
func TestCreateBackupFallsBackToTmpWhenRuntimeDataDirIsTooFull(t *testing.T) {
dataDir := t.TempDir()
installDir := t.TempDir()
t.Setenv("PULSE_DATA_DIR", dataDir)
t.Setenv("PULSE_INSTALL_DIR", installDir)
if err := os.MkdirAll(filepath.Join(installDir, "data"), 0755); err != nil {
t.Fatalf("mkdir data: %v", err)
}
if err := os.MkdirAll(filepath.Join(installDir, "config"), 0755); err != nil {
t.Fatalf("mkdir config: %v", err)
}
if err := os.WriteFile(filepath.Join(installDir, "data", "payload.bin"), []byte(strings.Repeat("a", 1024)), 0600); err != nil {
t.Fatalf("write data payload: %v", err)
}
if err := os.WriteFile(filepath.Join(installDir, "config", "config.json"), []byte(`{"ok":true}`), 0600); err != nil {
t.Fatalf("write config payload: %v", err)
}
originalDiskUsage := updateDiskUsage
updateDiskUsage = func(_ context.Context, path string) (*godisk.UsageStat, error) {
cleaned := filepath.Clean(path)
switch cleaned {
case filepath.Clean(dataDir):
return &godisk.UsageStat{Free: 1}, nil
case "/tmp":
return &godisk.UsageStat{Free: 1 << 30}, nil
default:
return &godisk.UsageStat{Free: 1 << 30}, nil
}
}
t.Cleanup(func() { updateDiskUsage = originalDiskUsage })
manager := &Manager{}
backupDir, err := manager.createBackup(context.Background())
if err != nil {
t.Fatalf("createBackup error: %v", err)
}
t.Cleanup(func() { _ = os.RemoveAll(backupDir) })
if !strings.HasPrefix(filepath.Clean(backupDir), filepath.Clean("/tmp/pulse-backup-")) {
t.Fatalf("expected /tmp fallback backup path, got %q", backupDir)
}
if _, err := os.Stat(filepath.Join(backupDir, "data", "payload.bin")); err != nil {
t.Fatalf("expected backup data payload: %v", err)
}
}
func TestCreateBackupFailsWhenNoManagedLocationHasSpace(t *testing.T) {
dataDir := t.TempDir()
installDir := t.TempDir()
t.Setenv("PULSE_DATA_DIR", dataDir)
t.Setenv("PULSE_INSTALL_DIR", installDir)
if err := os.MkdirAll(filepath.Join(installDir, "data"), 0755); err != nil {
t.Fatalf("mkdir data: %v", err)
}
if err := os.WriteFile(filepath.Join(installDir, "data", "payload.bin"), []byte(strings.Repeat("a", 2048)), 0600); err != nil {
t.Fatalf("write data payload: %v", err)
}
originalDiskUsage := updateDiskUsage
updateDiskUsage = func(_ context.Context, path string) (*godisk.UsageStat, error) {
return &godisk.UsageStat{Free: 1}, nil
}
t.Cleanup(func() { updateDiskUsage = originalDiskUsage })
manager := &Manager{}
_, err := manager.createBackup(context.Background())
if err == nil || !strings.Contains(err.Error(), "need") {
t.Fatalf("expected insufficient space error, got %v", err)
}
}
func TestEstimateTarballExtractBytesSumsRegularFiles(t *testing.T) {
tarballPath := filepath.Join(t.TempDir(), "update.tar.gz")
file, err := os.Create(tarballPath)
if err != nil {
t.Fatalf("create tarball: %v", err)
}
gzWriter := gzip.NewWriter(file)
tarWriter := tar.NewWriter(gzWriter)
entries := []struct {
Name string
Body string
}{
{Name: "bin/pulse", Body: "abcdef"},
{Name: "VERSION", Body: "v6.0.0-rc.3"},
}
for _, entry := range entries {
if err := tarWriter.WriteHeader(&tar.Header{
Name: entry.Name,
Mode: 0600,
Size: int64(len(entry.Body)),
}); err != nil {
t.Fatalf("write header %s: %v", entry.Name, err)
}
if _, err := tarWriter.Write([]byte(entry.Body)); err != nil {
t.Fatalf("write body %s: %v", entry.Name, err)
}
}
if err := tarWriter.Close(); err != nil {
t.Fatalf("close tar writer: %v", err)
}
if err := gzWriter.Close(); err != nil {
t.Fatalf("close gzip writer: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("close tarball file: %v", err)
}
extractBytes, err := estimateTarballExtractBytes(tarballPath)
if err != nil {
t.Fatalf("estimateTarballExtractBytes error: %v", err)
}
expected := int64(len(entries[0].Body) + len(entries[1].Body))
if extractBytes != expected {
t.Fatalf("estimateTarballExtractBytes = %d, want %d", extractBytes, expected)
}
}