Borg/pkg/datanode/datanode_test.go
google-labs-jules[bot] bd14b14483 Improve test coverage for datanode and tim packages, and fix cmd tests
- Added unit tests for `ToTar` and `FromTar` in `pkg/datanode`, including a round-trip test and invalid input handling.
- Added unit tests for `Walk` options (`MaxDepth`, `Filter`, `SkipErrors`) in `pkg/datanode`.
- Added security tests for `pkg/tim` to verify protection against path traversal (Zip Slip) attacks and handling of invalid inputs.
- Fixed `cmd` package tests execution by adding `TestHelperProcess` to `cmd/run_test.go` to support mocked command execution.
- Increased coverage for `pkg/datanode` to 84.2%, `pkg/tim` to 74.2%, and `cmd` to 44.1%.
2025-11-23 18:58:32 +00:00

590 lines
14 KiB
Go

package datanode
import (
"archive/tar"
"bytes"
"errors"
"io"
"io/fs"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
)
func TestNew_Good(t *testing.T) {
dn := New()
if dn == nil {
t.Fatal("New() returned nil")
}
if dn.files == nil {
t.Error("New() did not initialize the files map")
}
}
func TestAddData_Good(t *testing.T) {
dn := New()
path := "foo.txt"
data := []byte("foo")
dn.AddData(path, data)
file, ok := dn.files[path]
if !ok {
t.Fatalf("file %q not found in datanode", path)
}
if string(file.content) != string(data) {
t.Errorf("expected data %q, got %q", data, file.content)
}
info, err := file.Stat()
if err != nil {
t.Fatalf("file.Stat() failed: %v", err)
}
if info.Name() != "foo.txt" {
t.Errorf("expected name foo.txt, got %s", info.Name())
}
}
func TestAddData_Ugly(t *testing.T) {
t.Run("Overwrite", func(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("foo.txt", []byte("bar"))
file, _ := dn.files["foo.txt"]
if string(file.content) != "bar" {
t.Errorf("expected data to be overwritten to 'bar', got %q", file.content)
}
})
t.Run("Weird Path", func(t *testing.T) {
dn := New()
// path.Clean treats "a/../b/./c.txt" as "b/c.txt" but our implementation is simpler
// and doesn't handle `..`. Let's test what it does handle.
path := "./b/./c.txt"
dn.AddData(path, []byte("c"))
if _, ok := dn.files["./b/./c.txt"]; !ok {
t.Errorf("expected path to be stored as is")
}
})
}
func TestOpen_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
file, err := dn.Open("foo.txt")
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer file.Close()
}
func TestOpen_Bad(t *testing.T) {
dn := New()
_, err := dn.Open("nonexistent.txt")
if err == nil {
t.Fatal("expected error opening nonexistent file, got nil")
}
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("expected fs.ErrNotExist, got %v", err)
}
}
func TestOpen_Ugly(t *testing.T) {
dn := New()
dn.AddData("bar/baz.txt", []byte("baz"))
file, err := dn.Open("bar") // Opening a directory
if err != nil {
t.Fatalf("expected no error when opening a directory, got %v", err)
}
defer file.Close()
// Reading from a directory should fail
_, err = file.Read(make([]byte, 1))
if err == nil {
t.Fatal("expected error reading from a directory, got nil")
}
var pathErr *fs.PathError
if !errors.As(err, &pathErr) || pathErr.Err != fs.ErrInvalid {
t.Errorf("expected fs.ErrInvalid when reading a directory, got %v", err)
}
}
func TestStat_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("bar/baz.txt", []byte("baz"))
// Test file
info, err := dn.Stat("bar/baz.txt")
if err != nil {
t.Fatalf("Stat failed: %v", err)
}
if info.Name() != "baz.txt" {
t.Errorf("expected name baz.txt, got %s", info.Name())
}
if info.Size() != 3 {
t.Errorf("expected size 3, got %d", info.Size())
}
if info.IsDir() {
t.Error("expected baz.txt to not be a directory")
}
// Test directory
dirInfo, err := dn.Stat("bar")
if err != nil {
t.Fatalf("Stat directory failed: %v", err)
}
if !dirInfo.IsDir() {
t.Error("expected 'bar' to be a directory")
}
if dirInfo.Name() != "bar" {
t.Errorf("expected dir name 'bar', got %s", dirInfo.Name())
}
}
func TestStat_Bad(t *testing.T) {
dn := New()
_, err := dn.Stat("nonexistent")
if err == nil {
t.Fatal("expected error stating nonexistent file, got nil")
}
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("expected fs.ErrNotExist, got %v", err)
}
}
func TestStat_Ugly(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
// Test root
info, err := dn.Stat(".")
if err != nil {
t.Fatalf("Stat('.') failed: %v", err)
}
if !info.IsDir() {
t.Error("expected '.' to be a directory")
}
if info.Name() != "." {
t.Errorf("expected name '.', got %s", info.Name())
}
}
func TestExists_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("bar/baz.txt", []byte("baz"))
exists, err := dn.Exists("foo.txt")
if err != nil || !exists {
t.Errorf("expected foo.txt to exist, err: %v", err)
}
exists, err = dn.Exists("bar")
if err != nil || !exists {
t.Errorf("expected 'bar' directory to exist, err: %v", err)
}
}
func TestExists_Bad(t *testing.T) {
dn := New()
exists, err := dn.Exists("nonexistent")
if err != nil {
t.Errorf("unexpected error for nonexistent file: %v", err)
}
if exists {
t.Error("expected 'nonexistent' to not exist")
}
}
func TestExists_Ugly(t *testing.T) {
dn := New()
dn.AddData("dummy.txt", []byte("dummy"))
// Test root
exists, err := dn.Exists(".")
if err != nil || !exists {
t.Error("expected root '.' to exist")
}
// Test empty path
exists, err = dn.Exists("")
if err != nil {
// our stat treats "" as "."
if !strings.Contains(err.Error(), "exists") {
t.Errorf("unexpected error for empty path: %v", err)
}
}
if !exists {
t.Error("expected empty path '' to exist (as root)")
}
}
func TestReadDir_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("bar/baz.txt", []byte("baz"))
dn.AddData("bar/qux.txt", []byte("qux"))
// Read root
entries, err := dn.ReadDir(".")
if err != nil {
t.Fatalf("ReadDir failed: %v", err)
}
expectedRootEntries := []string{"bar", "foo.txt"}
entryNames := toSortedNames(entries)
if !reflect.DeepEqual(entryNames, expectedRootEntries) {
t.Errorf("expected entries %v, got %v", expectedRootEntries, entryNames)
}
// Read subdirectory
barEntries, err := dn.ReadDir("bar")
if err != nil {
t.Fatalf("ReadDir('bar') failed: %v", err)
}
expectedBarEntries := []string{"baz.txt", "qux.txt"}
barEntryNames := toSortedNames(barEntries)
if !reflect.DeepEqual(barEntryNames, expectedBarEntries) {
t.Errorf("expected entries %v, got %v", expectedBarEntries, barEntryNames)
}
}
func TestReadDir_Bad(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
// Read nonexistent dir
entries, err := dn.ReadDir("nonexistent")
if err != nil {
t.Fatalf("expected no error reading nonexistent dir, got %v", err)
}
if len(entries) != 0 {
t.Errorf("expected 0 entries for nonexistent dir, got %d", len(entries))
}
// Read file
_, err = dn.ReadDir("foo.txt")
if err == nil {
t.Fatal("expected error reading a file")
}
var pathErr *fs.PathError
if !errors.As(err, &pathErr) || pathErr.Err != fs.ErrInvalid {
t.Errorf("expected fs.ErrInvalid when reading a file, got %v", err)
}
}
func TestReadDir_Ugly(t *testing.T) {
dn := New()
dn.AddData("bar/baz.txt", []byte("baz"))
dn.AddData("empty_dir/", nil)
// Read dir with another dir but no files
entries, err := dn.ReadDir(".")
if err != nil {
t.Fatalf("ReadDir failed: %v", err)
}
expected := []string{"bar"} // empty_dir/ is ignored by AddData
names := toSortedNames(entries)
if !reflect.DeepEqual(names, expected) {
t.Errorf("expected %v, got %v", expected, names)
}
}
func TestWalk_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("bar/baz.txt", []byte("baz"))
dn.AddData("bar/qux.txt", []byte("qux"))
var paths []string
dn.Walk(".", func(path string, d fs.DirEntry, err error) error {
paths = append(paths, path)
return nil
})
expectedPaths := []string{".", "bar", "bar/baz.txt", "bar/qux.txt", "foo.txt"}
sort.Strings(paths)
if !reflect.DeepEqual(paths, expectedPaths) {
t.Errorf("Walk expected paths %v, got %v", expectedPaths, paths)
}
}
func TestWalk_Bad(t *testing.T) {
dn := New()
// Walk non-existent path. fs.WalkDir will call the func with the error.
var called bool
err := dn.Walk("nonexistent", func(path string, d fs.DirEntry, err error) error {
called = true
if err == nil {
t.Error("expected error for nonexistent path")
}
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("unexpected error message: %v", err)
}
return err // propagate error
})
if !called {
t.Fatal("walk function was not called for nonexistent root")
}
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("expected Walk to return fs.ErrNotExist, got %v", err)
}
}
func TestWalk_Ugly(t *testing.T) {
dn := New()
dn.AddData("a/b.txt", []byte("b"))
dn.AddData("a/c.txt", []byte("c"))
// Test stopping walk
walkErr := errors.New("stop walking")
var paths []string
err := dn.Walk(".", func(path string, d fs.DirEntry, err error) error {
if path == "a/b.txt" {
return walkErr
}
paths = append(paths, path)
return nil
})
if err != walkErr {
t.Errorf("expected walk to return the callback error, got %v", err)
}
}
func TestWalk_Options(t *testing.T) {
dn := New()
dn.AddData("root.txt", []byte("root"))
dn.AddData("a/a1.txt", []byte("a1"))
dn.AddData("a/b/b1.txt", []byte("b1"))
dn.AddData("c/c1.txt", []byte("c1"))
t.Run("MaxDepth", func(t *testing.T) {
var paths []string
err := dn.Walk(".", func(path string, d fs.DirEntry, err error) error {
paths = append(paths, path)
return nil
}, WalkOptions{MaxDepth: 1})
if err != nil {
t.Fatalf("Walk failed: %v", err)
}
expected := []string{".", "a", "c", "root.txt"}
sort.Strings(paths)
if !reflect.DeepEqual(paths, expected) {
t.Errorf("expected paths %v, got %v", expected, paths)
}
})
t.Run("Filter", func(t *testing.T) {
var paths []string
err := dn.Walk(".", func(path string, d fs.DirEntry, err error) error {
paths = append(paths, path)
return nil
}, WalkOptions{Filter: func(path string, d fs.DirEntry) bool {
return !strings.HasPrefix(path, "a")
}})
if err != nil {
t.Fatalf("Walk failed: %v", err)
}
expected := []string{".", "c", "c/c1.txt", "root.txt"}
sort.Strings(paths)
if !reflect.DeepEqual(paths, expected) {
t.Errorf("expected paths %v, got %v", expected, paths)
}
})
t.Run("SkipErrors", func(t *testing.T) {
// Mock a walk failure by passing a non-existent root with SkipErrors.
// Normally, WalkDir calls fn with an error for the root if it doesn't exist.
var called bool
err := dn.Walk("nonexistent", func(path string, d fs.DirEntry, err error) error {
called = true
return err
}, WalkOptions{SkipErrors: true})
if err != nil {
t.Errorf("expected no error with SkipErrors, got %v", err)
}
if called {
t.Error("callback should NOT be called if error is skipped internally")
}
})
}
func TestCopyFile_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
tmpfile := filepath.Join(t.TempDir(), "test.txt")
err := dn.CopyFile("foo.txt", tmpfile, 0644)
if err != nil {
t.Fatalf("CopyFile failed: %v", err)
}
content, err := os.ReadFile(tmpfile)
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
if string(content) != "foo" {
t.Errorf("expected foo, got %s", string(content))
}
}
func TestCopyFile_Bad(t *testing.T) {
dn := New()
tmpfile := filepath.Join(t.TempDir(), "test.txt")
// Source does not exist
err := dn.CopyFile("nonexistent.txt", tmpfile, 0644)
if err == nil {
t.Fatal("expected error for nonexistent source file")
}
// Destination is not writable
dn.AddData("foo.txt", []byte("foo"))
err = dn.CopyFile("foo.txt", "/nonexistent_dir/test.txt", 0644)
if err == nil {
t.Fatal("expected error for unwritable destination")
}
}
func TestCopyFile_Ugly(t *testing.T) {
dn := New()
dn.AddData("bar/baz.txt", []byte("baz"))
tmpfile := filepath.Join(t.TempDir(), "test.txt")
// Attempting to copy a directory
err := dn.CopyFile("bar", tmpfile, 0644)
if err == nil {
t.Fatal("expected error when trying to copy a directory")
}
}
func TestToTar_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("bar/baz.txt", []byte("baz"))
tarball, err := dn.ToTar()
if err != nil {
t.Fatalf("ToTar failed: %v", err)
}
if len(tarball) == 0 {
t.Fatal("expected non-empty tarball")
}
// Verify tar content
tr := tar.NewReader(bytes.NewReader(tarball))
files := make(map[string]string)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("tar.Next failed: %v", err)
}
content, err := io.ReadAll(tr)
if err != nil {
t.Fatalf("read tar content failed: %v", err)
}
files[header.Name] = string(content)
}
if files["foo.txt"] != "foo" {
t.Errorf("expected foo.txt content 'foo', got %q", files["foo.txt"])
}
if files["bar/baz.txt"] != "baz" {
t.Errorf("expected bar/baz.txt content 'baz', got %q", files["bar/baz.txt"])
}
}
func TestFromTar_Good(t *testing.T) {
// Create a tarball
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
files := []struct{ Name, Body string }{
{"foo.txt", "foo"},
{"bar/baz.txt", "baz"},
}
for _, file := range files {
hdr := &tar.Header{
Name: file.Name,
Mode: 0600,
Size: int64(len(file.Body)),
Typeflag: tar.TypeReg,
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatalf("WriteHeader failed: %v", err)
}
if _, err := tw.Write([]byte(file.Body)); err != nil {
t.Fatalf("Write failed: %v", err)
}
}
if err := tw.Close(); err != nil {
t.Fatalf("Close failed: %v", err)
}
dn, err := FromTar(buf.Bytes())
if err != nil {
t.Fatalf("FromTar failed: %v", err)
}
// Verify DataNode content
exists, _ := dn.Exists("foo.txt")
if !exists {
t.Error("foo.txt missing")
}
exists, _ = dn.Exists("bar/baz.txt")
if !exists {
t.Error("bar/baz.txt missing")
}
}
func TestTarRoundTrip_Good(t *testing.T) {
dn1 := New()
dn1.AddData("a.txt", []byte("a"))
dn1.AddData("b/c.txt", []byte("c"))
tarball, err := dn1.ToTar()
if err != nil {
t.Fatalf("ToTar failed: %v", err)
}
dn2, err := FromTar(tarball)
if err != nil {
t.Fatalf("FromTar failed: %v", err)
}
// Verify dn2 matches dn1
exists, _ := dn2.Exists("a.txt")
if !exists {
t.Error("a.txt missing in dn2")
}
exists, _ = dn2.Exists("b/c.txt")
if !exists {
t.Error("b/c.txt missing in dn2")
}
}
func TestFromTar_Bad(t *testing.T) {
// Pass invalid data (truncated header)
// A valid tar header is 512 bytes.
truncated := make([]byte, 100)
_, err := FromTar(truncated)
if err == nil {
t.Error("expected error for truncated tar header, got nil")
} else if err != io.EOF && err != io.ErrUnexpectedEOF {
// Verify it's some sort of read error or EOF related
// Depending on implementation details of archive/tar
}
}
func toSortedNames(entries []fs.DirEntry) []string {
var names []string
for _, e := range entries {
names = append(names, e.Name())
}
sort.Strings(names)
return names
}