zlh-agent/internal/backup/backup_test.go

224 lines
6.7 KiB
Go

package backup
import (
"errors"
"os"
"path/filepath"
"testing"
"time"
"zlh-agent/internal/state"
)
func TestRestoreCreatesCheckpointBeforeDestructiveRestore(t *testing.T) {
cfg, serverRoot := setupBackupTest(t)
mustWriteFile(t, filepath.Join(serverRoot, "world", "level.dat"), "target")
target, err := Create(cfg)
if err != nil {
t.Fatalf("Create target backup: %v", err)
}
mustWriteFile(t, filepath.Join(serverRoot, "world", "level.dat"), "live")
started := false
startServerReady = func(*state.Config) error {
started = true
return nil
}
result, err := Restore(cfg, target.ID)
if err != nil {
t.Fatalf("Restore: %v", err)
}
if !started {
t.Fatalf("expected restore to restart server through readiness-aware path")
}
if result.Backup.ID != target.ID {
t.Fatalf("restored backup id = %q, want %q", result.Backup.ID, target.ID)
}
if result.Checkpoint.ID == "" || result.Checkpoint.ID == target.ID {
t.Fatalf("checkpoint id = %q, target id = %q", result.Checkpoint.ID, target.ID)
}
if result.Checkpoint.Type != BackupTypeCheckpoint {
t.Fatalf("checkpoint type = %q, want %q", result.Checkpoint.Type, BackupTypeCheckpoint)
}
if result.Checkpoint.Reason != BackupReasonPreRestore {
t.Fatalf("checkpoint reason = %q, want %q", result.Checkpoint.Reason, BackupReasonPreRestore)
}
if got := mustReadFile(t, filepath.Join(serverRoot, "world", "level.dat")); got != "target" {
t.Fatalf("restored file = %q, want target", got)
}
checkpointArchive := filepath.Join(rootDir, result.Checkpoint.Archive)
checkpointRoot := t.TempDir()
if err := restoreArchive(checkpointRoot, checkpointArchive, result.Checkpoint.Paths); err != nil {
t.Fatalf("restore checkpoint archive for inspection: %v", err)
}
if got := mustReadFile(t, filepath.Join(checkpointRoot, "world", "level.dat")); got != "live" {
t.Fatalf("checkpoint file = %q, want live", got)
}
}
func TestRestoreAbortsIfCheckpointCreationFails(t *testing.T) {
cfg, serverRoot := setupBackupTest(t)
mustWriteFile(t, filepath.Join(serverRoot, "world", "level.dat"), "target")
target, err := Create(cfg)
if err != nil {
t.Fatalf("Create target backup: %v", err)
}
mustWriteFile(t, filepath.Join(serverRoot, "world", "level.dat"), "live")
checkpointErr := errors.New("save-off failed")
getServerPID = func() (int, bool) { return 123, true }
runSaveOff = func() error { return checkpointErr }
stopped := false
stopServerAndWait = func(time.Duration) error {
stopped = true
return nil
}
started := false
startServerReady = func(*state.Config) error {
started = true
return nil
}
if _, err := Restore(cfg, target.ID); err == nil {
t.Fatalf("Restore succeeded, want checkpoint failure")
}
if stopped {
t.Fatalf("server stop was called after checkpoint failure")
}
if started {
t.Fatalf("server start was called after checkpoint failure")
}
if got := mustReadFile(t, filepath.Join(serverRoot, "world", "level.dat")); got != "live" {
t.Fatalf("live file changed after checkpoint failure: %q", got)
}
}
func TestManualBackupIsManualAndCheckpointIsListable(t *testing.T) {
cfg, serverRoot := setupBackupTest(t)
mustWriteFile(t, filepath.Join(serverRoot, "world", "level.dat"), "target")
manual, err := Create(cfg)
if err != nil {
t.Fatalf("Create manual backup: %v", err)
}
if manual.Type != BackupTypeManual {
t.Fatalf("manual type = %q, want %q", manual.Type, BackupTypeManual)
}
if manual.Reason != "" {
t.Fatalf("manual reason = %q, want empty", manual.Reason)
}
mustWriteFile(t, filepath.Join(serverRoot, "world", "level.dat"), "live")
startServerReady = func(*state.Config) error { return nil }
result, err := Restore(cfg, manual.ID)
if err != nil {
t.Fatalf("Restore: %v", err)
}
backups, err := List()
if err != nil {
t.Fatalf("List: %v", err)
}
seen := map[string]Manifest{}
for _, manifest := range backups {
seen[manifest.ID] = manifest
}
if seen[manual.ID].Type != BackupTypeManual {
t.Fatalf("listed manual type = %q, want %q", seen[manual.ID].Type, BackupTypeManual)
}
checkpoint := seen[result.Checkpoint.ID]
if checkpoint.Type != BackupTypeCheckpoint || checkpoint.Reason != BackupReasonPreRestore {
t.Fatalf("listed checkpoint = type %q reason %q", checkpoint.Type, checkpoint.Reason)
}
}
func TestRestoreArchiveRejectsUnsafeManifestPathBeforeDeleting(t *testing.T) {
serverRoot := t.TempDir()
archiveRoot := t.TempDir()
mustWriteFile(t, filepath.Join(serverRoot, "world", "level.dat"), "live")
mustWriteFile(t, filepath.Join(archiveRoot, "world", "level.dat"), "target")
manifest := Manifest{
ID: "safe",
Paths: []string{"world"},
}
archivePath := filepath.Join(t.TempDir(), "safe.tar.gz")
if err := writeArchive(archiveRoot, archivePath, &manifest); err != nil {
t.Fatalf("writeArchive: %v", err)
}
if err := restoreArchive(serverRoot, archivePath, []string{"../world"}); err == nil {
t.Fatalf("restoreArchive succeeded with unsafe path")
}
if got := mustReadFile(t, filepath.Join(serverRoot, "world", "level.dat")); got != "live" {
t.Fatalf("live file changed after unsafe path rejection: %q", got)
}
}
func setupBackupTest(t *testing.T) (*state.Config, string) {
t.Helper()
oldRootDir := rootDir
oldServerDir := serverDir
oldGetServerPID := getServerPID
oldRunSaveOff := runSaveOff
oldRunSaveOn := runSaveOn
oldStopServerAndWait := stopServerAndWait
oldStartServerReady := startServerReady
oldBackupLogPath := backupLogPath
rootDir = t.TempDir()
serverRoot := t.TempDir()
backupLogPath = filepath.Join(t.TempDir(), "backup_restore.log")
serverDir = func(state.Config) string { return serverRoot }
getServerPID = func() (int, bool) { return 0, false }
runSaveOff = func() error { return nil }
runSaveOn = func() error { return nil }
stopServerAndWait = func(time.Duration) error { return nil }
startServerReady = func(*state.Config) error { return nil }
t.Cleanup(func() {
rootDir = oldRootDir
serverDir = oldServerDir
getServerPID = oldGetServerPID
runSaveOff = oldRunSaveOff
runSaveOn = oldRunSaveOn
stopServerAndWait = oldStopServerAndWait
startServerReady = oldStartServerReady
backupLogPath = oldBackupLogPath
})
return &state.Config{
ContainerType: "game",
Game: "minecraft",
Variant: "vanilla",
Version: "1.21.4",
VMID: 5001,
World: "world",
}, serverRoot
}
func mustWriteFile(t *testing.T, path, data string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll(%s): %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
t.Fatalf("WriteFile(%s): %v", path, err)
}
}
func mustReadFile(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile(%s): %v", path, err)
}
return string(data)
}