From 07e0fc166aa63d9c33e46499e716baf735ae9609 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 10 Feb 2026 18:37:40 -0800 Subject: [PATCH] Expand test coverage: path validation, round-trip, error cases Add tests for ValidatePath, AddFile size mismatch, invalid paths, progress reporting, manifest round-trip, invalid magic, truncated input, empty input, and manifest String() method. --- mfer/builder_test.go | 153 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/mfer/builder_test.go b/mfer/builder_test.go index 5f96406..56be794 100644 --- a/mfer/builder_test.go +++ b/mfer/builder_test.go @@ -163,6 +163,159 @@ func TestSetSeedDeterministic(t *testing.T) { assert.NotEqual(t, b1.fixedUUID, b3.fixedUUID, "different seeds should produce different UUIDs") } +func TestValidatePath(t *testing.T) { + valid := []string{ + "file.txt", + "dir/file.txt", + "a/b/c/d.txt", + "file with spaces.txt", + "日本語.txt", + } + for _, p := range valid { + t.Run("valid:"+p, func(t *testing.T) { + assert.NoError(t, ValidatePath(p)) + }) + } + + invalid := []struct { + path string + desc string + }{ + {"", "empty"}, + {"/absolute", "absolute path"}, + {"has\\backslash", "backslash"}, + {"has/../traversal", "dot-dot segment"}, + {"has//double", "empty segment"}, + {"..", "just dot-dot"}, + {string([]byte{0xff, 0xfe}), "invalid UTF-8"}, + } + for _, tt := range invalid { + t.Run("invalid:"+tt.desc, func(t *testing.T) { + assert.Error(t, ValidatePath(tt.path)) + }) + } +} + +func TestBuilderAddFileSizeMismatch(t *testing.T) { + b := NewBuilder() + content := []byte("short") + reader := bytes.NewReader(content) + + // Declare wrong size + _, err := b.AddFile("test.txt", FileSize(100), ModTime(time.Now()), reader, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "size mismatch") +} + +func TestBuilderAddFileInvalidPath(t *testing.T) { + b := NewBuilder() + content := []byte("data") + reader := bytes.NewReader(content) + + _, err := b.AddFile("", FileSize(len(content)), ModTime(time.Now()), reader, nil) + assert.Error(t, err) + + reader.Reset(content) + _, err = b.AddFile("/absolute", FileSize(len(content)), ModTime(time.Now()), reader, nil) + assert.Error(t, err) +} + +func TestBuilderAddFileWithProgress(t *testing.T) { + b := NewBuilder() + content := bytes.Repeat([]byte("x"), 1000) + reader := bytes.NewReader(content) + progress := make(chan FileHashProgress, 100) + + bytesRead, err := b.AddFile("test.txt", FileSize(len(content)), ModTime(time.Now()), reader, progress) + close(progress) + require.NoError(t, err) + assert.Equal(t, FileSize(1000), bytesRead) + + var updates []FileHashProgress + for p := range progress { + updates = append(updates, p) + } + assert.NotEmpty(t, updates) + // Last update should show all bytes + assert.Equal(t, FileSize(1000), updates[len(updates)-1].BytesRead) +} + +func TestBuilderBuildRoundTrip(t *testing.T) { + // Build a manifest, deserialize it, verify all fields survive round-trip + b := NewBuilder() + now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC) + + files := []struct { + path string + content []byte + }{ + {"alpha.txt", []byte("alpha content")}, + {"beta/gamma.txt", []byte("gamma content")}, + {"beta/delta.txt", []byte("delta content")}, + } + + for _, f := range files { + reader := bytes.NewReader(f.content) + _, err := b.AddFile(RelFilePath(f.path), FileSize(len(f.content)), ModTime(now), reader, nil) + require.NoError(t, err) + } + + var buf bytes.Buffer + require.NoError(t, b.Build(&buf)) + + m, err := NewManifestFromReader(&buf) + require.NoError(t, err) + + mfiles := m.Files() + require.Len(t, mfiles, 3) + + // Verify sorted order + assert.Equal(t, "alpha.txt", mfiles[0].Path) + assert.Equal(t, "beta/delta.txt", mfiles[1].Path) + assert.Equal(t, "beta/gamma.txt", mfiles[2].Path) + + // Verify sizes + assert.Equal(t, int64(len("alpha content")), mfiles[0].Size) + + // Verify hashes are present + for _, f := range mfiles { + require.NotEmpty(t, f.Hashes, "file %s should have hashes", f.Path) + assert.NotEmpty(t, f.Hashes[0].MultiHash) + } +} + +func TestNewManifestFromReaderInvalidMagic(t *testing.T) { + _, err := NewManifestFromReader(bytes.NewReader([]byte("NOT_VALID"))) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid file format") +} + +func TestNewManifestFromReaderEmpty(t *testing.T) { + _, err := NewManifestFromReader(bytes.NewReader([]byte{})) + assert.Error(t, err) +} + +func TestNewManifestFromReaderTruncated(t *testing.T) { + // Just the magic with nothing after + _, err := NewManifestFromReader(bytes.NewReader([]byte(MAGIC))) + assert.Error(t, err) +} + +func TestManifestString(t *testing.T) { + b := NewBuilder() + content := []byte("test") + reader := bytes.NewReader(content) + _, err := b.AddFile("test.txt", FileSize(len(content)), ModTime(time.Now()), reader, nil) + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, b.Build(&buf)) + + m, err := NewManifestFromReader(&buf) + require.NoError(t, err) + assert.Contains(t, m.String(), "count=1") +} + func TestBuilderBuildEmpty(t *testing.T) { b := NewBuilder()