package backup import ( "archive/tar" "compress/gzip" "context" "os" "path/filepath" "testing" "github.com/mrleo1nid/pzmanager/internal/config" ) func TestSafeJoinRejectsTraversal(t *testing.T) { root := filepath.Join(os.TempDir(), "pzm-root") for _, name := range []string{"../evil", "../../etc/passwd", "Saves/../../evil"} { if _, err := safeJoin(root, name); err == nil { t.Errorf("путь %q принят, ожидался отказ", name) } } if _, err := safeJoin(root, "Saves/Multiplayer/map.bin"); err != nil { t.Errorf("обычный путь отклонён: %v", err) } } func TestSanitizeNote(t *testing.T) { cases := map[string]string{ "перед вайпом": "", "before wipe": "before_wipe", "v1.2/../x": "v12x", "": "", "__trim-me__": "trim-me", } for input, want := range cases { if got := sanitizeNote(input); got != want { t.Errorf("sanitizeNote(%q) = %q, ожидалось %q", input, got, want) } } } func TestNameValidation(t *testing.T) { m := New(&config.Config{}) if _, err := m.Path("../../etc/passwd"); err == nil { t.Error("имя с переходом по каталогам принято") } if _, err := m.Path("pz-20250903-181500-auto.tar.gz"); err != nil { t.Errorf("корректное имя отклонено: %v", err) } } // TestCreateAndRestore проверяет полный цикл: архивируем дерево, портим файл, // восстанавливаем и убеждаемся, что содержимое вернулось. func TestCreateAndRestore(t *testing.T) { root := t.TempDir() zomboid := filepath.Join(root, "Zomboid") savePath := filepath.Join(zomboid, "Saves", "Multiplayer", "world.bin") if err := os.MkdirAll(filepath.Dir(savePath), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(savePath, []byte("исходный мир"), 0o644); err != nil { t.Fatal(err) } cfg := &config.Config{ZomboidDir: zomboid} cfg.Backup.Dir = filepath.Join(root, "backups") m := New(cfg) archive, err := m.Create(context.Background(), func(string) {}, "test") if err != nil { t.Fatalf("Create: %v", err) } if archive.SizeBytes == 0 { t.Error("архив пустой") } if err := os.WriteFile(savePath, []byte("испорчено"), 0o644); err != nil { t.Fatal(err) } if err := m.Restore(context.Background(), func(string) {}, archive.Name); err != nil { t.Fatalf("Restore: %v", err) } restored, err := os.ReadFile(savePath) if err != nil { t.Fatal(err) } if string(restored) != "исходный мир" { t.Errorf("после восстановления содержимое = %q", restored) } } func TestPruneKeepsNewest(t *testing.T) { root := t.TempDir() cfg := &config.Config{ZomboidDir: filepath.Join(root, "Zomboid")} cfg.Backup.Dir = filepath.Join(root, "backups") m := New(cfg) if err := os.MkdirAll(cfg.Backup.Dir, 0o755); err != nil { t.Fatal(err) } names := []string{ "pz-20250101-000000.tar.gz", "pz-20250102-000000.tar.gz", "pz-20250103-000000.tar.gz", } for _, name := range names { writeEmptyArchive(t, filepath.Join(cfg.Backup.Dir, name)) } if err := m.Prune(2, func(string) {}); err != nil { t.Fatal(err) } left, err := m.List() if err != nil { t.Fatal(err) } if len(left) != 2 { t.Fatalf("осталось %d архивов, ожидалось 2: %v", len(left), left) } } // writeEmptyArchive создаёт валидный пустой tar.gz — List проверяет имя, а не // содержимое, но пусть файлы будут настоящими. func writeEmptyArchive(t *testing.T, path string) { t.Helper() f, err := os.Create(path) if err != nil { t.Fatal(err) } defer f.Close() gz := gzip.NewWriter(f) tw := tar.NewWriter(gz) if err := tw.Close(); err != nil { t.Fatal(err) } if err := gz.Close(); err != nil { t.Fatal(err) } }