package snapshot_test import ( "context" "testing" "time" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/database" "sneak.berlin/go/vaultik/internal/log" "sneak.berlin/go/vaultik/internal/snapshot" "sneak.berlin/go/vaultik/internal/types" ) // verifyChunkChange checks that after a content change the file references // the new chunk, the old chunk still exists, and the old chunk no longer // maps to the modified file. func verifyChunkChange( ctx context.Context, t *testing.T, repos *database.Repositories, oldChunkHash, newChunkHash types.ChunkHash, ) { t.Helper() // Verify the chunk hashes are different assert.NotEqual(t, oldChunkHash, newChunkHash, "Chunk hash should change when content changes") // Get chunk files from second scan chunkFiles2, err := repos.ChunkFiles.GetByFilePath(ctx, "/test.txt") require.NoError(t, err) assert.Len(t, chunkFiles2, 1) assert.Equal(t, newChunkHash, chunkFiles2[0].ChunkHash) // Verify old chunk still exists (it's still valid data) oldChunk, err := repos.Chunks.GetByHash(ctx, oldChunkHash.String()) require.NoError(t, err) assert.NotNil(t, oldChunk) // Verify new chunk exists newChunk, err := repos.Chunks.GetByHash(ctx, newChunkHash.String()) require.NoError(t, err) assert.NotNil(t, newChunk) // Verify that chunk_files for old chunk no longer references this file oldChunkFiles, err := repos.ChunkFiles.GetByChunkHash(ctx, oldChunkHash) require.NoError(t, err) for _, cf := range oldChunkFiles { file, err := repos.Files.GetByID(ctx, cf.FileID) require.NoError(t, err) assert.NotEqual(t, "/data/test.txt", file.Path, "Old chunk should not be associated with the modified file") } } // TestFileContentChange verifies that when a file's content changes, // the old chunks are properly disassociated func TestFileContentChange(t *testing.T) { // Initialize logger for tests log.Initialize(log.Config{}) t.Parallel() // Create in-memory filesystem fs := afero.NewMemMapFs() // Create initial file err := afero.WriteFile(fs, "/test.txt", []byte("Initial content"), 0644) require.NoError(t, err) // Create test database db, err := database.NewTestDB() require.NoError(t, err) defer func() { err := db.Close() if err != nil { t.Errorf("failed to close database: %v", err) } }() repos := database.NewRepositories(db) // Create scanner scanner := snapshot.NewScanner(snapshot.ScannerConfig{ FS: fs, ChunkSize: int64(1024 * 16), // 16KB chunks for testing Repositories: repos, MaxBlobSize: int64(1024 * 1024), // 1MB blobs CompressionLevel: 3, AgeRecipients: []string{testAgePublicKey}, }) // Create first snapshot ctx := context.Background() snapshotID1 := "snapshot1" createSnapshotRecord(ctx, t, repos, snapshotID1) // First scan - should create chunks for initial content result1, err := scanner.Scan(ctx, "/", snapshotID1) require.NoError(t, err) t.Logf("First scan: %d files scanned", result1.FilesScanned) // Get file chunks from first scan fileChunks1, err := repos.FileChunks.GetByPath(ctx, "/test.txt") require.NoError(t, err) assert.Len(t, fileChunks1, 1) // Small file = 1 chunk oldChunkHash := fileChunks1[0].ChunkHash // Get chunk files from first scan chunkFiles1, err := repos.ChunkFiles.GetByFilePath(ctx, "/test.txt") require.NoError(t, err) assert.Len(t, chunkFiles1, 1) // Modify the file time.Sleep(10 * time.Millisecond) // Ensure mtime changes err = afero.WriteFile(fs, "/test.txt", []byte("Modified content with different data"), 0644) require.NoError(t, err) // Create second snapshot snapshotID2 := "snapshot2" createSnapshotRecord(ctx, t, repos, snapshotID2) // Second scan - should create new chunks and remove old associations result2, err := scanner.Scan(ctx, "/", snapshotID2) require.NoError(t, err) t.Logf("Second scan: %d files scanned", result2.FilesScanned) // Get file chunks from second scan fileChunks2, err := repos.FileChunks.GetByPath(ctx, "/test.txt") require.NoError(t, err) assert.Len(t, fileChunks2, 1) // Still 1 chunk but different hash newChunkHash := fileChunks2[0].ChunkHash verifyChunkChange(ctx, t, repos, oldChunkHash, newChunkHash) } // TestMultipleFileChanges verifies handling of multiple file changes in one scan func TestMultipleFileChanges(t *testing.T) { // Initialize logger for tests log.Initialize(log.Config{}) t.Parallel() // Create in-memory filesystem fs := afero.NewMemMapFs() // Create initial files files := map[string]string{ "/file1.txt": "Content 1", "/file2.txt": "Content 2", "/file3.txt": "Content 3", } for path, content := range files { err := afero.WriteFile(fs, path, []byte(content), 0644) require.NoError(t, err) } // Create test database db, err := database.NewTestDB() require.NoError(t, err) defer func() { err := db.Close() if err != nil { t.Errorf("failed to close database: %v", err) } }() repos := database.NewRepositories(db) // Create scanner scanner := snapshot.NewScanner(snapshot.ScannerConfig{ FS: fs, ChunkSize: int64(1024 * 16), // 16KB chunks for testing Repositories: repos, MaxBlobSize: int64(1024 * 1024), // 1MB blobs CompressionLevel: 3, AgeRecipients: []string{testAgePublicKey}, }) // Create first snapshot ctx := context.Background() snapshotID1 := "snapshot1" createSnapshotRecord(ctx, t, repos, snapshotID1) // First scan result1, err := scanner.Scan(ctx, "/", snapshotID1) require.NoError(t, err) // Only regular files are counted, not directories assert.Equal(t, 3, result1.FilesScanned) // Modify two files time.Sleep(10 * time.Millisecond) // Ensure mtime changes err = afero.WriteFile(fs, "/file1.txt", []byte("Modified content 1"), 0644) require.NoError(t, err) err = afero.WriteFile(fs, "/file3.txt", []byte("Modified content 3"), 0644) require.NoError(t, err) // Create second snapshot snapshotID2 := "snapshot2" createSnapshotRecord(ctx, t, repos, snapshotID2) // Second scan result2, err := scanner.Scan(ctx, "/", snapshotID2) require.NoError(t, err) // Only regular files are counted, not directories assert.Equal(t, 3, result2.FilesScanned) // Verify each file has exactly one set of chunks for path := range files { fileChunks, err := repos.FileChunks.GetByPath(ctx, path) require.NoError(t, err) assert.Len(t, fileChunks, 1, "File %s should have exactly 1 chunk association", path) chunkFiles, err := repos.ChunkFiles.GetByFilePath(ctx, path) require.NoError(t, err) assert.Len(t, chunkFiles, 1, "File %s should have exactly 1 chunk-file association", path) } }