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.
294 lines
6.1 KiB
Go
294 lines
6.1 KiB
Go
package database_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/types"
|
|
)
|
|
|
|
// errIntentionalRollback forces a transaction rollback in tests.
|
|
var errIntentionalRollback = errors.New("intentional rollback")
|
|
|
|
// createTxTestData returns a transaction body that creates a file with
|
|
// two chunks packed into one blob.
|
|
func createTxTestData(
|
|
repos *database.Repositories,
|
|
) func(context.Context, *sql.Tx) error {
|
|
return func(ctx context.Context, tx *sql.Tx) error {
|
|
file := &database.File{
|
|
Path: testTxFile,
|
|
MTime: time.Now().Truncate(time.Second),
|
|
Size: 1024,
|
|
Mode: 0644,
|
|
UID: 1000,
|
|
GID: 1000,
|
|
}
|
|
|
|
err := repos.Files.Create(ctx, tx, file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = createTxFileChunks(ctx, tx, repos, file.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return createTxBlob(ctx, tx, repos)
|
|
}
|
|
}
|
|
|
|
// createTxFileChunks creates the two test chunks and maps them to the file.
|
|
func createTxFileChunks(
|
|
ctx context.Context, tx *sql.Tx,
|
|
repos *database.Repositories, fileID types.FileID,
|
|
) error {
|
|
// Create chunks
|
|
chunk1 := &database.Chunk{
|
|
ChunkHash: types.ChunkHash("tx_chunk1"),
|
|
Size: 512,
|
|
}
|
|
|
|
err := repos.Chunks.Create(ctx, tx, chunk1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
chunk2 := &database.Chunk{
|
|
ChunkHash: types.ChunkHash("tx_chunk2"),
|
|
Size: 512,
|
|
}
|
|
|
|
err = repos.Chunks.Create(ctx, tx, chunk2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Map chunks to file
|
|
fc1 := &database.FileChunk{
|
|
FileID: fileID,
|
|
Idx: 0,
|
|
ChunkHash: chunk1.ChunkHash,
|
|
}
|
|
|
|
err = repos.FileChunks.Create(ctx, tx, fc1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fc2 := &database.FileChunk{
|
|
FileID: fileID,
|
|
Idx: 1,
|
|
ChunkHash: chunk2.ChunkHash,
|
|
}
|
|
|
|
return repos.FileChunks.Create(ctx, tx, fc2)
|
|
}
|
|
|
|
// createTxBlob creates the test blob and maps both chunks into it.
|
|
func createTxBlob(
|
|
ctx context.Context, tx *sql.Tx, repos *database.Repositories,
|
|
) error {
|
|
blob := &database.Blob{
|
|
ID: types.NewBlobID(),
|
|
Hash: types.BlobHash("tx_blob1"),
|
|
CreatedTS: time.Now().Truncate(time.Second),
|
|
}
|
|
|
|
err := repos.Blobs.Create(ctx, tx, blob)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Map chunks to blob
|
|
bc1 := &database.BlobChunk{
|
|
BlobID: blob.ID,
|
|
ChunkHash: types.ChunkHash("tx_chunk1"),
|
|
Offset: 0,
|
|
Length: 512,
|
|
}
|
|
|
|
err = repos.BlobChunks.Create(ctx, tx, bc1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bc2 := &database.BlobChunk{
|
|
BlobID: blob.ID,
|
|
ChunkHash: types.ChunkHash("tx_chunk2"),
|
|
Offset: 512,
|
|
Length: 512,
|
|
}
|
|
|
|
return repos.BlobChunks.Create(ctx, tx, bc2)
|
|
}
|
|
|
|
func TestRepositoriesTransaction(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
repos := database.NewRepositories(db)
|
|
|
|
err := repos.WithTx(ctx, createTxTestData(repos))
|
|
if err != nil {
|
|
t.Fatalf("transaction failed: %v", err)
|
|
}
|
|
|
|
// Verify all data was committed
|
|
file, err := repos.Files.GetByPath(ctx, testTxFile)
|
|
if err != nil {
|
|
t.Fatalf("failed to get file: %v", err)
|
|
}
|
|
|
|
if file == nil {
|
|
t.Error("expected file after transaction")
|
|
}
|
|
|
|
chunks, err := repos.FileChunks.GetByFile(ctx, testTxFile)
|
|
if err != nil {
|
|
t.Fatalf("failed to get file chunks: %v", err)
|
|
}
|
|
|
|
if len(chunks) != 2 {
|
|
t.Errorf("expected 2 file chunks, got %d", len(chunks))
|
|
}
|
|
|
|
blob, err := repos.Blobs.GetByHash(ctx, "tx_blob1")
|
|
if err != nil {
|
|
t.Fatalf("failed to get blob: %v", err)
|
|
}
|
|
|
|
if blob == nil {
|
|
t.Error("expected blob after transaction")
|
|
}
|
|
}
|
|
|
|
func TestRepositoriesTransactionRollback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
repos := database.NewRepositories(db)
|
|
|
|
// Test transaction rollback
|
|
err := repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
|
// Create a file
|
|
file := &database.File{
|
|
Path: "/test/rollback_file.txt",
|
|
MTime: time.Now().Truncate(time.Second),
|
|
Size: 1024,
|
|
Mode: 0644,
|
|
UID: 1000,
|
|
GID: 1000,
|
|
}
|
|
|
|
err := repos.Files.Create(ctx, tx, file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a chunk
|
|
chunk := &database.Chunk{
|
|
ChunkHash: types.ChunkHash("rollback_chunk"),
|
|
Size: 1024,
|
|
}
|
|
|
|
err = repos.Chunks.Create(ctx, tx, chunk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Return error to trigger rollback
|
|
return errIntentionalRollback
|
|
})
|
|
if !errors.Is(err, errIntentionalRollback) {
|
|
t.Fatalf("expected rollback error, got: %v", err)
|
|
}
|
|
|
|
// Verify nothing was committed
|
|
file, err := repos.Files.GetByPath(ctx, "/test/rollback_file.txt")
|
|
if err != nil {
|
|
t.Fatalf("error checking for file: %v", err)
|
|
}
|
|
|
|
if file != nil {
|
|
t.Error("file should not exist after rollback")
|
|
}
|
|
|
|
chunk, err := repos.Chunks.GetByHash(ctx, "rollback_chunk")
|
|
if err != nil {
|
|
t.Fatalf("error checking for chunk: %v", err)
|
|
}
|
|
|
|
if chunk != nil {
|
|
t.Error("chunk should not exist after rollback")
|
|
}
|
|
}
|
|
|
|
func TestRepositoriesReadTransaction(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
repos := database.NewRepositories(db)
|
|
|
|
// First, create some data
|
|
file := &database.File{
|
|
Path: "/test/read_file.txt",
|
|
MTime: time.Now().Truncate(time.Second),
|
|
Size: 1024,
|
|
Mode: 0644,
|
|
UID: 1000,
|
|
GID: 1000,
|
|
}
|
|
|
|
err := repos.Files.Create(ctx, nil, file)
|
|
if err != nil {
|
|
t.Fatalf("failed to create file: %v", err)
|
|
}
|
|
|
|
// Test read-only transaction
|
|
var retrievedFile *database.File
|
|
|
|
err = repos.WithReadTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
|
var err error
|
|
|
|
retrievedFile, err = repos.Files.GetByPathTx(ctx, tx, "/test/read_file.txt")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Try to write in read-only transaction (should fail)
|
|
_ = repos.Files.Create(ctx, tx, &database.File{
|
|
Path: "/test/should_fail.txt",
|
|
MTime: time.Now(),
|
|
Size: 0,
|
|
Mode: 0644,
|
|
UID: 1000,
|
|
GID: 1000,
|
|
})
|
|
// SQLite might not enforce read-only at this level, but we test the pattern
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("read transaction failed: %v", err)
|
|
}
|
|
|
|
if retrievedFile == nil {
|
|
t.Error("expected to retrieve file in read transaction")
|
|
}
|
|
}
|