package database_test import ( "context" "strings" "testing" "time" "sneak.berlin/go/vaultik/internal/database" "sneak.berlin/go/vaultik/internal/types" ) // Chunk hashes used across the blob_chunks tests. const ( chunk1Hash = "chunk1" chunk2Hash = "chunk2" chunk3Hash = "chunk3" ) // mustCreateChunks registers the given chunk hashes (1024 bytes each). func mustCreateChunks( t *testing.T, repos *database.Repositories, hashes ...types.ChunkHash, ) { t.Helper() ctx := context.Background() for _, chunkHash := range hashes { chunk := &database.Chunk{ ChunkHash: chunkHash, Size: 1024, } err := repos.Chunks.Create(ctx, nil, chunk) if err != nil { t.Fatalf("failed to create chunk %s: %v", chunkHash, err) } } } // mustCreateBlob creates a blob row with the given hash. func mustCreateBlob( t *testing.T, repos *database.Repositories, hash types.BlobHash, ) *database.Blob { t.Helper() blob := &database.Blob{ ID: types.NewBlobID(), Hash: hash, CreatedTS: time.Now(), } err := repos.Blobs.Create(context.Background(), nil, blob) if err != nil { t.Fatalf("failed to create blob %s: %v", hash, err) } return blob } func TestBlobChunkRepository(t *testing.T) { t.Parallel() db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() repos := database.NewRepositories(db) blob := mustCreateBlob(t, repos, "blob1-hash") mustCreateChunks(t, repos, chunk1Hash, chunk2Hash, chunk3Hash) // Test Create bc1 := &database.BlobChunk{ BlobID: blob.ID, ChunkHash: types.ChunkHash(chunk1Hash), Offset: 0, Length: 1024, } err := repos.BlobChunks.Create(ctx, nil, bc1) if err != nil { t.Fatalf("failed to create blob chunk: %v", err) } // Add more chunks to the same blob bc2 := &database.BlobChunk{ BlobID: blob.ID, ChunkHash: types.ChunkHash(chunk2Hash), Offset: 1024, Length: 2048, } err = repos.BlobChunks.Create(ctx, nil, bc2) if err != nil { t.Fatalf("failed to create second blob chunk: %v", err) } bc3 := &database.BlobChunk{ BlobID: blob.ID, ChunkHash: types.ChunkHash(chunk3Hash), Offset: 3072, Length: 512, } err = repos.BlobChunks.Create(ctx, nil, bc3) if err != nil { t.Fatalf("failed to create third blob chunk: %v", err) } // Test GetByBlobID blobChunks, err := repos.BlobChunks.GetByBlobID(ctx, blob.ID.String()) if err != nil { t.Fatalf("failed to get blob chunks: %v", err) } if len(blobChunks) != 3 { t.Errorf("expected 3 chunks, got %d", len(blobChunks)) } // Verify order by offset expectedOffsets := []int64{0, 1024, 3072} for i, bc := range blobChunks { if bc.Offset != expectedOffsets[i] { t.Errorf("wrong chunk order: expected offset %d, got %d", expectedOffsets[i], bc.Offset) } } // Test duplicate insert (should fail due to primary key constraint) err = repos.BlobChunks.Create(ctx, nil, bc1) if err == nil { t.Fatal("duplicate blob_chunk insert should fail due to primary key constraint") } if !strings.Contains(err.Error(), "UNIQUE") && !strings.Contains(err.Error(), "constraint") { t.Fatalf("expected constraint error, got: %v", err) } } func TestBlobChunkRepositoryGetByChunkHash(t *testing.T) { t.Parallel() db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() repos := database.NewRepositories(db) blob := mustCreateBlob(t, repos, "blob-gbch-hash") mustCreateChunks(t, repos, chunk2Hash) bc2 := &database.BlobChunk{ BlobID: blob.ID, ChunkHash: types.ChunkHash(chunk2Hash), Offset: 1024, Length: 2048, } err := repos.BlobChunks.Create(ctx, nil, bc2) if err != nil { t.Fatalf("failed to create blob chunk: %v", err) } // Test GetByChunkHash bc, err := repos.BlobChunks.GetByChunkHash(ctx, chunk2Hash) if err != nil { t.Fatalf("failed to get blob chunk by chunk hash: %v", err) } if bc == nil { t.Fatal("expected blob chunk, got nil") } if bc.BlobID != blob.ID { t.Errorf("wrong blob ID: expected %s, got %s", blob.ID, bc.BlobID) } if bc.Offset != 1024 { t.Errorf("wrong offset: expected 1024, got %d", bc.Offset) } // Test non-existent chunk bc, err = repos.BlobChunks.GetByChunkHash(ctx, "nonexistent") if err != nil { t.Fatalf("unexpected error: %v", err) } if bc != nil { t.Error("expected nil for non-existent chunk") } } func TestBlobChunkRepositoryMultipleBlobs(t *testing.T) { t.Parallel() db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() repos := database.NewRepositories(db) blob1 := mustCreateBlob(t, repos, "blob1-hash") blob2 := mustCreateBlob(t, repos, "blob2-hash") mustCreateChunks(t, repos, chunk1Hash, chunk2Hash, chunk3Hash) // Create chunks across multiple blobs // Some chunks are shared between blobs (deduplication scenario) blobChunks := []database.BlobChunk{ {BlobID: blob1.ID, ChunkHash: chunk1Hash, Offset: 0, Length: 1024}, {BlobID: blob1.ID, ChunkHash: chunk2Hash, Offset: 1024, Length: 1024}, // chunk2 is shared between the blobs {BlobID: blob2.ID, ChunkHash: chunk2Hash, Offset: 0, Length: 1024}, {BlobID: blob2.ID, ChunkHash: chunk3Hash, Offset: 1024, Length: 1024}, } for _, bc := range blobChunks { err := repos.BlobChunks.Create(ctx, nil, &bc) if err != nil { t.Fatalf("failed to create blob chunk: %v", err) } } // Verify blob1 chunks chunks, err := repos.BlobChunks.GetByBlobID(ctx, blob1.ID.String()) if err != nil { t.Fatalf("failed to get blob1 chunks: %v", err) } if len(chunks) != 2 { t.Errorf("expected 2 chunks for blob1, got %d", len(chunks)) } // Verify blob2 chunks chunks, err = repos.BlobChunks.GetByBlobID(ctx, blob2.ID.String()) if err != nil { t.Fatalf("failed to get blob2 chunks: %v", err) } if len(chunks) != 2 { t.Errorf("expected 2 chunks for blob2, got %d", len(chunks)) } // Verify shared chunk bc, err := repos.BlobChunks.GetByChunkHash(ctx, chunk2Hash) if err != nil { t.Fatalf("failed to get shared chunk: %v", err) } if bc == nil { t.Fatal("expected shared chunk, got nil") } // GetByChunkHash returns first match, should be blob1 if bc.BlobID != blob1.ID { t.Errorf("expected %s for shared chunk, got %s", blob1.ID, bc.BlobID) } }