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.
230 lines
6.6 KiB
Go
230 lines
6.6 KiB
Go
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)
|
|
}
|
|
}
|