package database_test import ( "context" "testing" "time" "sneak.berlin/go/vaultik/internal/database" "sneak.berlin/go/vaultik/internal/types" ) const chunk4Hash = "chunk4" // verifyChunkFilePair asserts that the chunk-file rows cover both test // files at their expected offsets. func verifyChunkFilePair( t *testing.T, chunkFiles []*database.ChunkFile, file1ID, file2ID types.FileID, ) { t.Helper() foundFile1 := false foundFile2 := false for _, cf := range chunkFiles { if cf.FileID == file1ID && cf.FileOffset == 0 { foundFile1 = true } if cf.FileID == file2ID && cf.FileOffset == 2048 { foundFile2 = true } } if !foundFile1 || !foundFile2 { t.Error("not all expected files found") } } // createChunkFileTestFiles creates the two files used by the chunk-file // repository tests. func createChunkFileTestFiles( t *testing.T, fileRepo *database.FileRepository, ) (*database.File, *database.File) { t.Helper() testTime := time.Now().Truncate(time.Second) file1 := &database.File{ Path: testFilePath1, MTime: testTime, Size: 1024, Mode: 0644, UID: 1000, GID: 1000, LinkTarget: "", } file2 := &database.File{ Path: testFilePath2, MTime: testTime, Size: 1024, Mode: 0644, UID: 1000, GID: 1000, LinkTarget: "", } mustCreateFile(t, fileRepo, file1) mustCreateFile(t, fileRepo, file2) return file1, file2 } func TestChunkFileRepository(t *testing.T) { t.Parallel() db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() repo := database.NewChunkFileRepository(db) fileRepo := database.NewFileRepository(db) repos := database.NewRepositories(db) file1, file2 := createChunkFileTestFiles(t, fileRepo) mustCreateChunks(t, repos, chunk1Hash) // Test Create cf1 := &database.ChunkFile{ ChunkHash: types.ChunkHash(chunk1Hash), FileID: file1.ID, FileOffset: 0, Length: 1024, } err := repo.Create(ctx, nil, cf1) if err != nil { t.Fatalf("failed to create chunk file: %v", err) } // Add same chunk in different file (deduplication scenario) cf2 := &database.ChunkFile{ ChunkHash: types.ChunkHash(chunk1Hash), FileID: file2.ID, FileOffset: 2048, Length: 1024, } err = repo.Create(ctx, nil, cf2) if err != nil { t.Fatalf("failed to create second chunk file: %v", err) } // Test GetByChunkHash chunkFiles, err := repo.GetByChunkHash(ctx, chunk1Hash) if err != nil { t.Fatalf("failed to get chunk files: %v", err) } if len(chunkFiles) != 2 { t.Errorf("expected 2 files for chunk, got %d", len(chunkFiles)) } // Verify both files are returned verifyChunkFilePair(t, chunkFiles, file1.ID, file2.ID) // Test GetByFileID chunkFiles, err = repo.GetByFileID(ctx, file1.ID) if err != nil { t.Fatalf("failed to get chunks by file ID: %v", err) } if len(chunkFiles) != 1 { t.Errorf("expected 1 chunk for file, got %d", len(chunkFiles)) } if chunkFiles[0].ChunkHash != types.ChunkHash(chunk1Hash) { t.Errorf("wrong chunk hash: expected chunk1, got %s", chunkFiles[0].ChunkHash) } // Test duplicate insert (should be idempotent) err = repo.Create(ctx, nil, cf1) if err != nil { t.Fatalf("failed to create duplicate chunk file: %v", err) } } func TestChunkFileRepositoryComplexDeduplication(t *testing.T) { t.Parallel() db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() repo := database.NewChunkFileRepository(db) fileRepo := database.NewFileRepository(db) repos := database.NewRepositories(db) // Create test files testTime := time.Now().Truncate(time.Second) file1 := &database.File{ Path: testFilePath1, MTime: testTime, Size: 3072, Mode: 0644, UID: 1000, GID: 1000, } file2 := &database.File{ Path: testFilePath2, MTime: testTime, Size: 3072, Mode: 0644, UID: 1000, GID: 1000, } file3 := &database.File{ Path: "/file3.txt", MTime: testTime, Size: 2048, Mode: 0644, UID: 1000, GID: 1000, } mustCreateFile(t, fileRepo, file1) mustCreateFile(t, fileRepo, file2) mustCreateFile(t, fileRepo, file3) mustCreateChunks(t, repos, chunk1Hash, chunk2Hash, chunk3Hash, chunk4Hash) // Simulate a scenario where multiple files share chunks // File1: chunk1, chunk2, chunk3 // File2: chunk2, chunk3, chunk4 // File3: chunk1, chunk4 chunkFiles := []database.ChunkFile{ // File1 {ChunkHash: chunk1Hash, FileID: file1.ID, FileOffset: 0, Length: 1024}, {ChunkHash: chunk2Hash, FileID: file1.ID, FileOffset: 1024, Length: 1024}, {ChunkHash: chunk3Hash, FileID: file1.ID, FileOffset: 2048, Length: 1024}, // File2 {ChunkHash: chunk2Hash, FileID: file2.ID, FileOffset: 0, Length: 1024}, {ChunkHash: chunk3Hash, FileID: file2.ID, FileOffset: 1024, Length: 1024}, {ChunkHash: chunk4Hash, FileID: file2.ID, FileOffset: 2048, Length: 1024}, // File3 {ChunkHash: chunk1Hash, FileID: file3.ID, FileOffset: 0, Length: 1024}, {ChunkHash: chunk4Hash, FileID: file3.ID, FileOffset: 1024, Length: 1024}, } for _, cf := range chunkFiles { err := repo.Create(ctx, nil, &cf) if err != nil { t.Fatalf("failed to create chunk file: %v", err) } } // Test chunk1 (used by file1 and file3) files, err := repo.GetByChunkHash(ctx, chunk1Hash) if err != nil { t.Fatalf("failed to get files for chunk1: %v", err) } if len(files) != 2 { t.Errorf("expected 2 files for chunk1, got %d", len(files)) } // Test chunk2 (used by file1 and file2) files, err = repo.GetByChunkHash(ctx, chunk2Hash) if err != nil { t.Fatalf("failed to get files for chunk2: %v", err) } if len(files) != 2 { t.Errorf("expected 2 files for chunk2, got %d", len(files)) } // Test file2 chunks file2Chunks, err := repo.GetByFileID(ctx, file2.ID) if err != nil { t.Fatalf("failed to get chunks for file2: %v", err) } if len(file2Chunks) != 3 { t.Errorf("expected 3 chunks for file2, got %d", len(file2Chunks)) } }