Shallow snapshot verify only checked that each blob object existed and then reported "All blobs verified", overstating what it did. It now compares each blob stored size against the manifest compressed_size, using the same comparison as the deep path, and checks that the snapshot encrypted database (db.zst.age) is present. A blob of the wrong size no longer counts as verified. The final line reports only what was checked: presence and size, not contents. The README verify description and the CLI short/long text are corrected to match. Removed the now-unused resolveAndDownloadManifest helper and errBlobsMissing sentinel. Model: opus-4-8
192 lines
5.4 KiB
Go
192 lines
5.4 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"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/snapshot"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// TestShallowVerifyDetectsWrongBlobSize backs up a real snapshot, runs
|
|
// shallow verify (which passes and reports exactly the blobs it checked),
|
|
// then grows one stored blob so its size no longer matches the manifest.
|
|
// Shallow verify must then fail, count the grown blob as a size mismatch,
|
|
// and drop it from the verified count rather than continuing to report it
|
|
// as checked.
|
|
func TestShallowVerifyDetectsWrongBlobSize(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(32 * 1024)
|
|
maxBlobSize := int64(128 * 1024)
|
|
|
|
// Enough data to span several blobs, so the mismatch count and the
|
|
// dropped verified count are both meaningful.
|
|
require.NoError(t, fs.MkdirAll(dataDir, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs,
|
|
filepath.Join(dataDir, "data.bin"),
|
|
bytesPattern("shallow-", int(maxBlobSize*4)), 0o644))
|
|
|
|
ctx := context.Background()
|
|
|
|
cfg, storer, snapshotID := runFileStorageBackup(
|
|
ctx, t, fs, dataDir, storeDir, dbPath, chunkSize, maxBlobSize)
|
|
|
|
newVerifier := func(out io.Writer) *vaultik.Vaultik {
|
|
v := &vaultik.Vaultik{
|
|
Config: cfg,
|
|
Storage: storer,
|
|
Fs: fs,
|
|
Stdout: out,
|
|
Stderr: io.Discard,
|
|
UI: ui.NewWithColor(io.Discard, false),
|
|
}
|
|
v.SetContext(ctx)
|
|
|
|
return v
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
|
|
require.NoError(t,
|
|
newVerifier(&out).VerifySnapshotWithOptions(
|
|
snapshotID, &vaultik.VerifyOptions{JSON: true}),
|
|
"shallow verify should pass on a healthy snapshot")
|
|
|
|
healthy := decodeVerifyResult(t, out.Bytes())
|
|
require.Equal(t, "ok", healthy.Status)
|
|
require.Positive(t, healthy.BlobCount)
|
|
require.Equal(t, healthy.BlobCount, healthy.Verified,
|
|
"shallow verify must report exactly the blobs it checked")
|
|
require.Zero(t, healthy.Mismatched)
|
|
|
|
// Grow one stored blob so its size no longer matches the manifest.
|
|
growOneBlob(t, fs, filepath.Join(storeDir, "blobs"))
|
|
|
|
out.Reset()
|
|
err := newVerifier(&out).VerifySnapshotWithOptions(
|
|
snapshotID, &vaultik.VerifyOptions{JSON: true})
|
|
require.Error(t, err,
|
|
"shallow verify must fail when a blob's stored size differs from the manifest")
|
|
|
|
bad := decodeVerifyResult(t, out.Bytes())
|
|
require.Equal(t, "failed", bad.Status)
|
|
require.Equal(t, 1, bad.Mismatched)
|
|
require.Equal(t, healthy.BlobCount-1, bad.Verified,
|
|
"the wrong-sized blob must not be counted as verified")
|
|
}
|
|
|
|
// TestShallowVerifyDetectsMissingDatabase backs up a real snapshot,
|
|
// confirms shallow verify passes, then deletes the snapshot's encrypted
|
|
// database. Shallow verify must fail: a snapshot without its database is
|
|
// not restorable, even when every blob is present.
|
|
func TestShallowVerifyDetectsMissingDatabase(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(32 * 1024)
|
|
maxBlobSize := int64(128 * 1024)
|
|
|
|
require.NoError(t, fs.MkdirAll(dataDir, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs,
|
|
filepath.Join(dataDir, "data.bin"),
|
|
bytesPattern("shallow-db-", int(maxBlobSize*2)), 0o644))
|
|
|
|
ctx := context.Background()
|
|
|
|
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().VerifySnapshotWithOptions(
|
|
snapshotID, &vaultik.VerifyOptions{}),
|
|
"shallow verify should pass on a healthy snapshot")
|
|
|
|
// The database lives under the hashed remote key, not the human ID.
|
|
dbObject := filepath.Join(storeDir, "metadata",
|
|
snapshot.RemoteSnapshotKey(snapshotID), "db.zst.age")
|
|
require.NoError(t, os.Remove(dbObject))
|
|
|
|
require.Error(t,
|
|
newVerifier().VerifySnapshotWithOptions(
|
|
snapshotID, &vaultik.VerifyOptions{}),
|
|
"shallow verify must fail when db.zst.age is absent")
|
|
}
|
|
|
|
// growOneBlob appends bytes to the first blob file found under blobsDir,
|
|
// changing its on-disk size so it no longer matches the manifest.
|
|
func growOneBlob(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)
|
|
|
|
data = append(data, []byte("extra")...)
|
|
require.NoError(t, afero.WriteFile(fs, blobPath, data, 0o644))
|
|
}
|
|
|
|
func decodeVerifyResult(t *testing.T, b []byte) vaultik.VerifyResult {
|
|
t.Helper()
|
|
|
|
var result vaultik.VerifyResult
|
|
|
|
require.NoError(t, json.Unmarshal(b, &result))
|
|
|
|
return result
|
|
}
|