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 rootDir = t.TempDir() serverRoot := t.TempDir() 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 }) 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) }