mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
c6357c92a1
Refs #1408
216 lines
6.6 KiB
Go
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)
|
|
}
|
|
}
|