- Add FileCount, FileSize, RelFilePath, AbsFilePath, ModTime, Multihash types - Add UnixSeconds and UnixNanos types for timestamp handling - Add URL types (ManifestURL, FileURL, BaseURL) with safe path joining - Consolidate scanner package into mfer package - Update checker to use custom types in Result and CheckStatus - Add ModTime.Timestamp() method for protobuf conversion - Update all tests to use proper custom types
406 lines
11 KiB
Go
406 lines
11 KiB
Go
package checker
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/mfer/mfer"
|
|
)
|
|
|
|
func TestStatusString(t *testing.T) {
|
|
tests := []struct {
|
|
status Status
|
|
expected string
|
|
}{
|
|
{StatusOK, "OK"},
|
|
{StatusMissing, "MISSING"},
|
|
{StatusSizeMismatch, "SIZE_MISMATCH"},
|
|
{StatusHashMismatch, "HASH_MISMATCH"},
|
|
{StatusExtra, "EXTRA"},
|
|
{StatusError, "ERROR"},
|
|
{Status(99), "UNKNOWN"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.expected, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, tt.status.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
// createTestManifest creates a manifest file in the filesystem with the given files.
|
|
func createTestManifest(t *testing.T, fs afero.Fs, manifestPath string, files map[string][]byte) {
|
|
t.Helper()
|
|
|
|
builder := mfer.NewBuilder()
|
|
for path, content := range files {
|
|
reader := bytes.NewReader(content)
|
|
_, err := builder.AddFile(mfer.RelFilePath(path), mfer.FileSize(len(content)), mfer.ModTime(time.Now()), reader, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
require.NoError(t, builder.Build(&buf))
|
|
require.NoError(t, afero.WriteFile(fs, manifestPath, buf.Bytes(), 0644))
|
|
}
|
|
|
|
// createFilesOnDisk creates the given files on the filesystem.
|
|
func createFilesOnDisk(t *testing.T, fs afero.Fs, basePath string, files map[string][]byte) {
|
|
t.Helper()
|
|
|
|
for path, content := range files {
|
|
fullPath := basePath + "/" + path
|
|
require.NoError(t, fs.MkdirAll(basePath, 0755))
|
|
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0644))
|
|
}
|
|
}
|
|
|
|
func TestNewChecker(t *testing.T) {
|
|
t.Run("valid manifest", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
"file1.txt": []byte("hello"),
|
|
"file2.txt": []byte("world"),
|
|
}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/", fs)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, chk)
|
|
assert.Equal(t, mfer.FileCount(2), chk.FileCount())
|
|
})
|
|
|
|
t.Run("missing manifest", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
_, err := NewChecker("/nonexistent.mf", "/", fs)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("invalid manifest", func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
require.NoError(t, afero.WriteFile(fs, "/bad.mf", []byte("not a manifest"), 0644))
|
|
_, err := NewChecker("/bad.mf", "/", fs)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestCheckerFileCountAndTotalBytes(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
"small.txt": []byte("hi"),
|
|
"medium.txt": []byte("hello world"),
|
|
"large.txt": bytes.Repeat([]byte("x"), 1000),
|
|
}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/", fs)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, mfer.FileCount(3), chk.FileCount())
|
|
assert.Equal(t, mfer.FileSize(2+11+1000), chk.TotalBytes())
|
|
}
|
|
|
|
func TestCheckAllFilesOK(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
"file1.txt": []byte("content one"),
|
|
"file2.txt": []byte("content two"),
|
|
}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
createFilesOnDisk(t, fs, "/data", files)
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
var resultList []Result
|
|
for r := range results {
|
|
resultList = append(resultList, r)
|
|
}
|
|
|
|
assert.Len(t, resultList, 2)
|
|
for _, r := range resultList {
|
|
assert.Equal(t, StatusOK, r.Status, "file %s should be OK", r.Path)
|
|
}
|
|
}
|
|
|
|
func TestCheckMissingFile(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
"exists.txt": []byte("I exist"),
|
|
"missing.txt": []byte("I don't exist on disk"),
|
|
}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
// Only create one file
|
|
createFilesOnDisk(t, fs, "/data", map[string][]byte{
|
|
"exists.txt": []byte("I exist"),
|
|
})
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
var okCount, missingCount int
|
|
for r := range results {
|
|
switch r.Status {
|
|
case StatusOK:
|
|
okCount++
|
|
case StatusMissing:
|
|
missingCount++
|
|
assert.Equal(t, mfer.RelFilePath("missing.txt"), r.Path)
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, 1, okCount)
|
|
assert.Equal(t, 1, missingCount)
|
|
}
|
|
|
|
func TestCheckSizeMismatch(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
"file.txt": []byte("original content"),
|
|
}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
// Create file with different size
|
|
createFilesOnDisk(t, fs, "/data", map[string][]byte{
|
|
"file.txt": []byte("short"),
|
|
})
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
r := <-results
|
|
assert.Equal(t, StatusSizeMismatch, r.Status)
|
|
assert.Equal(t, mfer.RelFilePath("file.txt"), r.Path)
|
|
}
|
|
|
|
func TestCheckHashMismatch(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
originalContent := []byte("original content")
|
|
files := map[string][]byte{
|
|
"file.txt": originalContent,
|
|
}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
// Create file with same size but different content
|
|
differentContent := []byte("different contnt") // same length (16 bytes) but different
|
|
require.Equal(t, len(originalContent), len(differentContent), "test requires same length")
|
|
createFilesOnDisk(t, fs, "/data", map[string][]byte{
|
|
"file.txt": differentContent,
|
|
})
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
r := <-results
|
|
assert.Equal(t, StatusHashMismatch, r.Status)
|
|
assert.Equal(t, mfer.RelFilePath("file.txt"), r.Path)
|
|
}
|
|
|
|
func TestCheckWithProgress(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
"file1.txt": bytes.Repeat([]byte("a"), 100),
|
|
"file2.txt": bytes.Repeat([]byte("b"), 200),
|
|
}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
createFilesOnDisk(t, fs, "/data", files)
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
progress := make(chan CheckStatus, 10)
|
|
|
|
err = chk.Check(context.Background(), results, progress)
|
|
require.NoError(t, err)
|
|
|
|
// Drain results
|
|
for range results {
|
|
}
|
|
|
|
// Check progress was sent
|
|
var progressUpdates []CheckStatus
|
|
for p := range progress {
|
|
progressUpdates = append(progressUpdates, p)
|
|
}
|
|
|
|
assert.NotEmpty(t, progressUpdates)
|
|
// Final progress should show all files checked
|
|
final := progressUpdates[len(progressUpdates)-1]
|
|
assert.Equal(t, mfer.FileCount(2), final.TotalFiles)
|
|
assert.Equal(t, mfer.FileCount(2), final.CheckedFiles)
|
|
assert.Equal(t, mfer.FileSize(300), final.TotalBytes)
|
|
assert.Equal(t, mfer.FileSize(300), final.CheckedBytes)
|
|
assert.Equal(t, mfer.FileCount(0), final.Failures)
|
|
}
|
|
|
|
func TestCheckContextCancellation(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
// Create many files to ensure we have time to cancel
|
|
files := make(map[string][]byte)
|
|
for i := 0; i < 100; i++ {
|
|
files[string(rune('a'+i%26))+".txt"] = bytes.Repeat([]byte("x"), 1000)
|
|
}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
createFilesOnDisk(t, fs, "/data", files)
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
results := make(chan Result, 200)
|
|
err = chk.Check(ctx, results, nil)
|
|
assert.ErrorIs(t, err, context.Canceled)
|
|
}
|
|
|
|
func TestFindExtraFiles(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
// Manifest only contains file1
|
|
manifestFiles := map[string][]byte{
|
|
"file1.txt": []byte("in manifest"),
|
|
}
|
|
createTestManifest(t, fs, "/manifest.mf", manifestFiles)
|
|
|
|
// Disk has file1 and file2
|
|
createFilesOnDisk(t, fs, "/data", map[string][]byte{
|
|
"file1.txt": []byte("in manifest"),
|
|
"file2.txt": []byte("extra file"),
|
|
})
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.FindExtraFiles(context.Background(), results)
|
|
require.NoError(t, err)
|
|
|
|
var extras []Result
|
|
for r := range results {
|
|
extras = append(extras, r)
|
|
}
|
|
|
|
assert.Len(t, extras, 1)
|
|
assert.Equal(t, mfer.RelFilePath("file2.txt"), extras[0].Path)
|
|
assert.Equal(t, StatusExtra, extras[0].Status)
|
|
assert.Equal(t, "not in manifest", extras[0].Message)
|
|
}
|
|
|
|
func TestFindExtraFilesContextCancellation(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{"file.txt": []byte("data")}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
createFilesOnDisk(t, fs, "/data", files)
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.FindExtraFiles(ctx, results)
|
|
assert.ErrorIs(t, err, context.Canceled)
|
|
}
|
|
|
|
func TestCheckNilChannels(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{"file.txt": []byte("data")}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
createFilesOnDisk(t, fs, "/data", files)
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
// Should not panic with nil channels
|
|
err = chk.Check(context.Background(), nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestFindExtraFilesNilChannel(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{"file.txt": []byte("data")}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
createFilesOnDisk(t, fs, "/data", files)
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
// Should not panic with nil channel
|
|
err = chk.FindExtraFiles(context.Background(), nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCheckSubdirectories(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
"dir1/file1.txt": []byte("content1"),
|
|
"dir1/dir2/file2.txt": []byte("content2"),
|
|
"dir1/dir2/dir3/deep.txt": []byte("deep content"),
|
|
}
|
|
createTestManifest(t, fs, "/manifest.mf", files)
|
|
|
|
// Create files with full directory structure
|
|
for path, content := range files {
|
|
fullPath := "/data/" + path
|
|
require.NoError(t, fs.MkdirAll("/data/dir1/dir2/dir3", 0755))
|
|
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0644))
|
|
}
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
var okCount int
|
|
for r := range results {
|
|
assert.Equal(t, StatusOK, r.Status, "file %s should be OK", r.Path)
|
|
okCount++
|
|
}
|
|
assert.Equal(t, 3, okCount)
|
|
}
|
|
|
|
func TestCheckEmptyManifest(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
// Create manifest with no files
|
|
createTestManifest(t, fs, "/manifest.mf", map[string][]byte{})
|
|
|
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, mfer.FileCount(0), chk.FileCount())
|
|
assert.Equal(t, mfer.FileSize(0), chk.TotalBytes())
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
for range results {
|
|
count++
|
|
}
|
|
assert.Equal(t, 0, count)
|
|
}
|