package vaultik_test import ( "context" "io" "os" "path/filepath" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/log" "sneak.berlin/go/vaultik/internal/ui" "sneak.berlin/go/vaultik/internal/vaultik" ) // TestDeepVerifyAcceptsHealthyAndRejectsCorruptBlob backs up a real // snapshot with the on-disk storage backend, runs deep verification on // it, then flips a byte inside one stored blob and runs deep // verification again. A healthy snapshot must pass; a corrupted blob // must fail. The healthy case is the regression guard: deep // verification used to hash the encrypted blob bytes and compare them // to the blob's ID (the double SHA256 of the plaintext), so it reported // every healthy blob as corrupt. func TestDeepVerifyAcceptsHealthyAndRejectsCorruptBlob(t *testing.T) { log.Initialize(log.Config{}) t.Parallel() fs := afero.NewOsFs() tempDir := t.TempDir() dataDir := filepath.Join(tempDir, "source") storeDir := filepath.Join(tempDir, "remote") dbPath := filepath.Join(tempDir, "index.sqlite") chunkSize := int64(64 * 1024) maxBlobSize := int64(512 * 1024) // One file large enough to span several chunks within a single blob. require.NoError(t, fs.MkdirAll(dataDir, 0o755)) require.NoError(t, afero.WriteFile(fs, filepath.Join(dataDir, "data.bin"), bytesPattern("deep-", int(chunkSize*3)), 0o644)) ctx := context.Background() // runFileStorageBackup writes a real snapshot to storeDir and closes // the source index, so verification runs from remote bytes only. cfg, storer, snapshotID := runFileStorageBackup( ctx, t, fs, dataDir, storeDir, dbPath, chunkSize, maxBlobSize) newVerifier := func() *vaultik.Vaultik { v := &vaultik.Vaultik{ Config: cfg, Storage: storer, Fs: fs, Stdout: io.Discard, Stderr: io.Discard, UI: ui.NewWithColor(io.Discard, false), } v.SetContext(ctx) return v } require.NoError(t, newVerifier().RunDeepVerify(snapshotID, &vaultik.VerifyOptions{Deep: true}), "deep verify should pass on a healthy snapshot") // Flip a byte inside one blob without changing its length, so the // blob-existence and size checks still pass and verification reaches // the blob-content stage. corruptOneBlob(t, fs, filepath.Join(storeDir, "blobs")) require.Error(t, newVerifier().RunDeepVerify(snapshotID, &vaultik.VerifyOptions{Deep: true}), "deep verify should fail on a corrupted blob") } // corruptOneBlob flips a middle byte of the first blob file found under // blobsDir, leaving the file length unchanged. func corruptOneBlob(t *testing.T, fs afero.Fs, blobsDir string) { t.Helper() var blobPath string err := afero.Walk(fs, blobsDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if blobPath == "" && !info.IsDir() { blobPath = path } return nil }) require.NoError(t, err) require.NotEmpty(t, blobPath, "expected at least one blob on disk") data, err := afero.ReadFile(fs, blobPath) require.NoError(t, err) require.NotEmpty(t, data) data[len(data)/2] ^= 0xff require.NoError(t, afero.WriteFile(fs, blobPath, data, 0o644)) }