224 lines
6.7 KiB
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)
|
|
}
|