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(path, int64(len(content)), 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, int64(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, int64(3), chk.FileCount()) assert.Equal(t, int64(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, "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, "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, "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, int64(2), final.TotalFiles) assert.Equal(t, int64(2), final.CheckedFiles) assert.Equal(t, int64(300), final.TotalBytes) assert.Equal(t, int64(300), final.CheckedBytes) assert.Equal(t, int64(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, "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, int64(0), chk.FileCount()) assert.Equal(t, int64(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) }