Fix every finding surfaced by the canonical .golangci.yml with golangci-lint v2.12.2 (refs #61), behavior-preserving throughout: - err113: dynamic errors replaced with package-level sentinels and %w wrapping; direct comparisons converted to errors.Is - goprintffuncname: printf-style helpers renamed with an f suffix (ui.Writer message methods, cli.ReportErrorf, database.Fatalf, vaultik stdoutf) and all call sites updated - revive: stuttering type names renamed (blob.Handler, blob.WithReader, blob.ChunkPosition, storage.URL, storage.Info), doc comments added, unused parameters blanked, package comments added - contextcheck/noctx: ctx threaded through blob.Packer (AddChunk/Flush/FinalizeBlob/PackChunks) and scanner call sites; context-aware exec and sql variants used - funlen/cyclop/gocognit/nestif/dupl: oversized or duplicated functions split into focused helpers across production and test code - paralleltest/tparallel/thelper/usetesting/testpackage: tests parallelized where safe (global log.Initialize kept in the serial phase), helpers marked, t.TempDir adopted, external test packages where only exported API is used - gosec: integer conversions clamped or justified, header timeouts added, remaining findings suppressed with per-site justifications - mnd/goconst/lll/wsl_v5/nlreturn/noinlineerr/errcheck and other mechanical findings fixed directly Remove the deprecated log.LogOptions alias (callers migrated to log.Options). make check is green.
234 lines
5.8 KiB
Go
234 lines
5.8 KiB
Go
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))
|
|
}
|
|
}
|