Remediate all lint findings under the canonical golangci-lint config
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.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
//nolint:testpackage // inspects the unexported database connection
|
||||
package database
|
||||
|
||||
import (
|
||||
@@ -10,20 +11,17 @@ import (
|
||||
"sneak.berlin/go/vaultik/internal/types"
|
||||
)
|
||||
|
||||
// TestFileRepositoryEdgeCases tests edge cases for file repository
|
||||
func TestFileRepositoryEdgeCases(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
// fileEdgeCase describes one Create edge-case scenario.
|
||||
type fileEdgeCase struct {
|
||||
name string
|
||||
file *File
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
repo := NewFileRepository(db)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
file *File
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
// fileEdgeCases returns the Create edge-case table.
|
||||
func fileEdgeCases() []fileEdgeCase {
|
||||
return []fileEdgeCase{
|
||||
{
|
||||
name: "empty path",
|
||||
file: &File{
|
||||
@@ -51,6 +49,7 @@ func TestFileRepositoryEdgeCases(t *testing.T) {
|
||||
{
|
||||
name: "path with special characters",
|
||||
file: &File{
|
||||
//nolint:gosmopolitan // non-ASCII path is deliberate test data
|
||||
Path: "/test/file with spaces and 特殊文字.txt",
|
||||
MTime: time.Now(),
|
||||
Size: 1024,
|
||||
@@ -86,12 +85,26 @@ func TestFileRepositoryEdgeCases(t *testing.T) {
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
// TestFileRepositoryEdgeCases tests edge cases for file repository
|
||||
func TestFileRepositoryEdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, cleanup := setupTestDB(t)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
ctx := context.Background()
|
||||
repo := NewFileRepository(db)
|
||||
|
||||
for i, tt := range fileEdgeCases() {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Add a unique suffix to paths to avoid UNIQUE constraint violations
|
||||
if tt.file.Path != "" {
|
||||
tt.file.Path = types.FilePath(fmt.Sprintf("%s_%d_%d", tt.file.Path, i, time.Now().UnixNano()))
|
||||
tt.file.Path = types.FilePath(fmt.Sprintf("%s_%d_%d",
|
||||
tt.file.Path, i, time.Now().UnixNano()))
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, nil, tt.file)
|
||||
@@ -106,65 +119,128 @@ func TestFileRepositoryEdgeCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// testDuplicateFilePaths exercises the UPSERT behavior for duplicate paths.
|
||||
func testDuplicateFilePaths(t *testing.T, repos *Repositories) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
file1 := &File{
|
||||
Path: "/duplicate.txt",
|
||||
MTime: time.Now(),
|
||||
Size: 1024,
|
||||
Mode: 0644,
|
||||
UID: 1000,
|
||||
GID: 1000,
|
||||
}
|
||||
file2 := &File{
|
||||
Path: "/duplicate.txt", // Same path
|
||||
MTime: time.Now().Add(time.Hour),
|
||||
Size: 2048,
|
||||
Mode: 0644,
|
||||
UID: 1000,
|
||||
GID: 1000,
|
||||
}
|
||||
|
||||
err := repos.Files.Create(ctx, nil, file1)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create file1: %v", err)
|
||||
}
|
||||
|
||||
originalID := file1.ID
|
||||
|
||||
// Create with same path should update the existing record (UPSERT behavior)
|
||||
err = repos.Files.Create(ctx, nil, file2)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create file2: %v", err)
|
||||
}
|
||||
|
||||
// Verify the file was updated, not duplicated
|
||||
retrievedFile, err := repos.Files.GetByPath(ctx, "/duplicate.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve file: %v", err)
|
||||
}
|
||||
|
||||
// The file should have been updated with file2's data
|
||||
if retrievedFile.Size != 2048 {
|
||||
t.Errorf("expected size 2048, got %d", retrievedFile.Size)
|
||||
}
|
||||
|
||||
// ID might be different due to the UPSERT
|
||||
if retrievedFile.ID != file2.ID {
|
||||
t.Logf("File ID changed from %s to %s during upsert",
|
||||
originalID, retrievedFile.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// testDuplicateFileChunks exercises idempotent file-chunk mapping creation.
|
||||
func testDuplicateFileChunks(t *testing.T, repos *Repositories) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
file := &File{
|
||||
Path: "/test-dup-fc.txt",
|
||||
MTime: time.Now(),
|
||||
Size: 1024,
|
||||
Mode: 0644,
|
||||
UID: 1000,
|
||||
GID: 1000,
|
||||
}
|
||||
|
||||
err := repos.Files.Create(ctx, nil, file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
chunk := &Chunk{
|
||||
ChunkHash: types.ChunkHash("test-chunk-dup"),
|
||||
Size: 1024,
|
||||
}
|
||||
|
||||
err = repos.Chunks.Create(ctx, nil, chunk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fc := &FileChunk{
|
||||
FileID: file.ID,
|
||||
Idx: 0,
|
||||
ChunkHash: chunk.ChunkHash,
|
||||
}
|
||||
|
||||
err = repos.FileChunks.Create(ctx, nil, fc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Creating the same mapping again should be idempotent
|
||||
err = repos.FileChunks.Create(ctx, nil, fc)
|
||||
if err != nil {
|
||||
t.Error("file-chunk creation should be idempotent")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDuplicateHandling tests handling of duplicate entries
|
||||
func TestDuplicateHandling(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
ctx := context.Background()
|
||||
repos := NewRepositories(db)
|
||||
|
||||
// Test duplicate file paths - Create uses UPSERT logic
|
||||
t.Run("duplicate file paths", func(t *testing.T) {
|
||||
file1 := &File{
|
||||
Path: "/duplicate.txt",
|
||||
MTime: time.Now(),
|
||||
Size: 1024,
|
||||
Mode: 0644,
|
||||
UID: 1000,
|
||||
GID: 1000,
|
||||
}
|
||||
file2 := &File{
|
||||
Path: "/duplicate.txt", // Same path
|
||||
MTime: time.Now().Add(time.Hour),
|
||||
Size: 2048,
|
||||
Mode: 0644,
|
||||
UID: 1000,
|
||||
GID: 1000,
|
||||
}
|
||||
|
||||
err := repos.Files.Create(ctx, nil, file1)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create file1: %v", err)
|
||||
}
|
||||
|
||||
originalID := file1.ID
|
||||
|
||||
// Create with same path should update the existing record (UPSERT behavior)
|
||||
err = repos.Files.Create(ctx, nil, file2)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create file2: %v", err)
|
||||
}
|
||||
|
||||
// Verify the file was updated, not duplicated
|
||||
retrievedFile, err := repos.Files.GetByPath(ctx, "/duplicate.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve file: %v", err)
|
||||
}
|
||||
|
||||
// The file should have been updated with file2's data
|
||||
if retrievedFile.Size != 2048 {
|
||||
t.Errorf("expected size 2048, got %d", retrievedFile.Size)
|
||||
}
|
||||
|
||||
// ID might be different due to the UPSERT
|
||||
if retrievedFile.ID != file2.ID {
|
||||
t.Logf("File ID changed from %s to %s during upsert", originalID, retrievedFile.ID)
|
||||
}
|
||||
t.Parallel()
|
||||
testDuplicateFilePaths(t, repos)
|
||||
})
|
||||
|
||||
// Test duplicate chunk hashes
|
||||
t.Run("duplicate chunk hashes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
chunk := &Chunk{
|
||||
ChunkHash: types.ChunkHash("duplicate-chunk"),
|
||||
Size: 1024,
|
||||
@@ -184,59 +260,25 @@ func TestDuplicateHandling(t *testing.T) {
|
||||
|
||||
// Test duplicate file-chunk mappings
|
||||
t.Run("duplicate file-chunk mappings", func(t *testing.T) {
|
||||
file := &File{
|
||||
Path: "/test-dup-fc.txt",
|
||||
MTime: time.Now(),
|
||||
Size: 1024,
|
||||
Mode: 0644,
|
||||
UID: 1000,
|
||||
GID: 1000,
|
||||
}
|
||||
|
||||
err := repos.Files.Create(ctx, nil, file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
chunk := &Chunk{
|
||||
ChunkHash: types.ChunkHash("test-chunk-dup"),
|
||||
Size: 1024,
|
||||
}
|
||||
|
||||
err = repos.Chunks.Create(ctx, nil, chunk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fc := &FileChunk{
|
||||
FileID: file.ID,
|
||||
Idx: 0,
|
||||
ChunkHash: chunk.ChunkHash,
|
||||
}
|
||||
|
||||
err = repos.FileChunks.Create(ctx, nil, fc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Creating the same mapping again should be idempotent
|
||||
err = repos.FileChunks.Create(ctx, nil, fc)
|
||||
if err != nil {
|
||||
t.Error("file-chunk creation should be idempotent")
|
||||
}
|
||||
t.Parallel()
|
||||
testDuplicateFileChunks(t, repos)
|
||||
})
|
||||
}
|
||||
|
||||
// TestNullHandling tests handling of NULL values
|
||||
func TestNullHandling(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
ctx := context.Background()
|
||||
repos := NewRepositories(db)
|
||||
|
||||
// Test file with no link target
|
||||
t.Run("file without link target", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file := &File{
|
||||
Path: "/regular.txt",
|
||||
MTime: time.Now(),
|
||||
@@ -264,9 +306,11 @@ func TestNullHandling(t *testing.T) {
|
||||
|
||||
// Test snapshot with NULL completed_at
|
||||
t.Run("incomplete snapshot", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
snapshot := &Snapshot{
|
||||
ID: "incomplete-test",
|
||||
Hostname: "test-host",
|
||||
Hostname: internalTestHost,
|
||||
StartedAt: time.Now(),
|
||||
CompletedAt: nil, // Should remain NULL until completed
|
||||
}
|
||||
@@ -288,31 +332,86 @@ func TestNullHandling(t *testing.T) {
|
||||
|
||||
// Test blob with NULL uploaded_ts
|
||||
t.Run("blob not uploaded", func(t *testing.T) {
|
||||
blob := &Blob{
|
||||
ID: types.NewBlobID(),
|
||||
Hash: types.BlobHash("test-hash"),
|
||||
CreatedTS: time.Now(),
|
||||
UploadedTS: nil, // Not uploaded yet
|
||||
}
|
||||
|
||||
err := repos.Blobs.Create(ctx, nil, blob)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
retrieved, err := repos.Blobs.GetByID(ctx, blob.ID.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if retrieved.UploadedTS != nil {
|
||||
t.Error("expected nil UploadedTS for non-uploaded blob")
|
||||
}
|
||||
t.Parallel()
|
||||
verifyBlobNullUploadTS(ctx, t, repos)
|
||||
})
|
||||
}
|
||||
|
||||
// verifyBlobNullUploadTS checks that a blob created without an upload
|
||||
// timestamp round-trips with UploadedTS nil.
|
||||
func verifyBlobNullUploadTS(
|
||||
ctx context.Context, t *testing.T, repos *Repositories,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
blob := &Blob{
|
||||
ID: types.NewBlobID(),
|
||||
Hash: types.BlobHash("test-hash"),
|
||||
CreatedTS: time.Now(),
|
||||
UploadedTS: nil, // Not uploaded yet
|
||||
}
|
||||
|
||||
err := repos.Blobs.Create(ctx, nil, blob)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
retrieved, err := repos.Blobs.GetByID(ctx, blob.ID.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if retrieved.UploadedTS != nil {
|
||||
t.Error("expected nil UploadedTS for non-uploaded blob")
|
||||
}
|
||||
}
|
||||
|
||||
// createLargeDatasetFiles creates fileCount files and adds every other
|
||||
// one to the snapshot.
|
||||
func createLargeDatasetFiles(
|
||||
t *testing.T,
|
||||
repos *Repositories,
|
||||
snapshotID string,
|
||||
fileCount int,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
start := time.Now()
|
||||
|
||||
for i := range fileCount {
|
||||
file := &File{
|
||||
Path: types.FilePath(fmt.Sprintf("/large/file%05d.txt", i)),
|
||||
MTime: time.Now(),
|
||||
Size: int64(i * 1024),
|
||||
Mode: 0644,
|
||||
UID: uint32(1000 + (i % 10)),
|
||||
GID: uint32(1000 + (i % 10)),
|
||||
}
|
||||
|
||||
err := repos.Files.Create(ctx, nil, file)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create file %d: %v", i, err)
|
||||
}
|
||||
|
||||
// Add half to snapshot
|
||||
if i%2 == 0 {
|
||||
err = repos.Snapshots.AddFileByID(ctx, nil, snapshotID, file.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Created %d files in %v", fileCount, time.Since(start))
|
||||
}
|
||||
|
||||
// TestLargeDatasets tests operations with large amounts of data
|
||||
//
|
||||
//nolint:tparallel // subtests share one database and are order-dependent
|
||||
func TestLargeDatasets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("skipping large dataset test in short mode")
|
||||
}
|
||||
@@ -326,7 +425,7 @@ func TestLargeDatasets(t *testing.T) {
|
||||
// Create a snapshot
|
||||
snapshot := &Snapshot{
|
||||
ID: "large-dataset-test",
|
||||
Hostname: "test-host",
|
||||
Hostname: internalTestHost,
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
@@ -338,40 +437,13 @@ func TestLargeDatasets(t *testing.T) {
|
||||
// Create many files
|
||||
const fileCount = 1000
|
||||
|
||||
fileIDs := make([]types.FileID, fileCount)
|
||||
|
||||
//nolint:paralleltest // phases share one database and are order-dependent
|
||||
t.Run("create many files", func(t *testing.T) {
|
||||
start := time.Now()
|
||||
for i := range fileCount {
|
||||
file := &File{
|
||||
Path: types.FilePath(fmt.Sprintf("/large/file%05d.txt", i)),
|
||||
MTime: time.Now(),
|
||||
Size: int64(i * 1024),
|
||||
Mode: 0644,
|
||||
UID: uint32(1000 + (i % 10)),
|
||||
GID: uint32(1000 + (i % 10)),
|
||||
}
|
||||
|
||||
err := repos.Files.Create(ctx, nil, file)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create file %d: %v", i, err)
|
||||
}
|
||||
|
||||
fileIDs[i] = file.ID
|
||||
|
||||
// Add half to snapshot
|
||||
if i%2 == 0 {
|
||||
err = repos.Snapshots.AddFileByID(ctx, nil, snapshot.ID.String(), file.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Created %d files in %v", fileCount, time.Since(start))
|
||||
createLargeDatasetFiles(t, repos, snapshot.ID.String(), fileCount)
|
||||
})
|
||||
|
||||
// Test ListByPrefix performance
|
||||
//nolint:paralleltest // phases share one database and are order-dependent
|
||||
t.Run("list by prefix performance", func(t *testing.T) {
|
||||
start := time.Now()
|
||||
|
||||
@@ -388,6 +460,7 @@ func TestLargeDatasets(t *testing.T) {
|
||||
})
|
||||
|
||||
// Test orphaned cleanup performance
|
||||
//nolint:paralleltest // phases share one database and are order-dependent
|
||||
t.Run("orphaned cleanup performance", func(t *testing.T) {
|
||||
start := time.Now()
|
||||
|
||||
@@ -405,21 +478,26 @@ func TestLargeDatasets(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(files) != fileCount/2 {
|
||||
t.Errorf("expected %d files after cleanup, got %d", fileCount/2, len(files))
|
||||
t.Errorf("expected %d files after cleanup, got %d",
|
||||
fileCount/2, len(files))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestErrorPropagation tests that errors are properly propagated
|
||||
func TestErrorPropagation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
ctx := context.Background()
|
||||
repos := NewRepositories(db)
|
||||
|
||||
// Test GetByID with non-existent ID
|
||||
t.Run("GetByID non-existent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file, err := repos.Files.GetByID(ctx, types.NewFileID())
|
||||
if err != nil {
|
||||
t.Errorf("GetByID should not return error for non-existent ID, got: %v", err)
|
||||
@@ -432,9 +510,12 @@ func TestErrorPropagation(t *testing.T) {
|
||||
|
||||
// Test GetByPath with non-existent path
|
||||
t.Run("GetByPath non-existent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file, err := repos.Files.GetByPath(ctx, "/non/existent/path.txt")
|
||||
if err != nil {
|
||||
t.Errorf("GetByPath should not return error for non-existent path, got: %v", err)
|
||||
t.Errorf("GetByPath should not return error for non-existent path, got: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
if file != nil {
|
||||
@@ -444,6 +525,8 @@ func TestErrorPropagation(t *testing.T) {
|
||||
|
||||
// Test invalid foreign key reference
|
||||
t.Run("invalid foreign key", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fc := &FileChunk{
|
||||
FileID: types.NewFileID(),
|
||||
Idx: 0,
|
||||
@@ -463,8 +546,10 @@ func TestErrorPropagation(t *testing.T) {
|
||||
|
||||
// TestQueryInjection tests that the system is safe from SQL injection
|
||||
func TestQueryInjection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
ctx := context.Background()
|
||||
repos := NewRepositories(db)
|
||||
@@ -479,6 +564,8 @@ func TestQueryInjection(t *testing.T) {
|
||||
|
||||
for _, injection := range injectionTests {
|
||||
t.Run("injection attempt", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Try injection in file path
|
||||
file := &File{
|
||||
Path: types.FilePath(injection),
|
||||
@@ -495,7 +582,7 @@ func TestQueryInjection(t *testing.T) {
|
||||
// Verify tables still exist
|
||||
var count int
|
||||
|
||||
err := db.conn.QueryRow("SELECT COUNT(*) FROM files").Scan(&count)
|
||||
err := db.conn.QueryRowContext(ctx, countFilesQuery).Scan(&count)
|
||||
if err != nil {
|
||||
t.Fatal("files table was damaged by injection")
|
||||
}
|
||||
@@ -505,6 +592,8 @@ func TestQueryInjection(t *testing.T) {
|
||||
|
||||
// TestTimezoneHandling tests that times are properly handled in UTC
|
||||
func TestTimezoneHandling(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user