check / check (push) Waiting to run
Every backup writes vaultik-destination.txt at the destination root. While the local index holds snapshots, a destination without it is not present: snapshot create refuses before writing, prune and snapshot purge fail, and snapshot list and snapshot remove act as for a destination that cannot be listed. remote nuke deletes it last. The file:// backend no longer creates its destination directory and checks for it on every write; that protects a directory on a removable volume, not a mount point. Fixtures standing for a written destination carry the marker; two tests check they get past it. Judgement call: the marker is rewritten on every backup. Deviation: for the complexity limit, snapshot name selection moved into snapshotNamesToCreate and the --force/--json test into confirmRemoveSnapshot, both unchanged. Model: opus-5-5
84 lines
2.4 KiB
Go
84 lines
2.4 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"filippo.io/age"
|
|
"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"
|
|
)
|
|
|
|
// TestVerifyPassesWhenBackupReusesStoredBlob backs up a file, then backs it
|
|
// up again from an empty local index, as after `database delete`, with an
|
|
// extra age recipient. The second backup builds the same blob, finds it
|
|
// already stored and skips the upload, but its own copy is larger, because
|
|
// age writes a header entry for each recipient. The second snapshot must
|
|
// record the size of the stored object, so shallow and deep verify pass.
|
|
func TestVerifyPassesWhenBackupReusesStoredBlob(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
tempDir := t.TempDir()
|
|
|
|
dataDir := filepath.Join(tempDir, "source")
|
|
storeDir := t.TempDir()
|
|
secondDBPath := filepath.Join(tempDir, "second.sqlite")
|
|
|
|
chunkSize := int64(64 * 1024)
|
|
maxBlobSize := int64(512 * 1024)
|
|
|
|
require.NoError(t, fs.MkdirAll(dataDir, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs,
|
|
filepath.Join(dataDir, "data.bin"),
|
|
bytesPattern("reuse-", int(chunkSize*3)), 0o644))
|
|
|
|
ctx := context.Background()
|
|
|
|
runFileStorageBackup(ctx, t, fs, dataDir, storeDir,
|
|
filepath.Join(tempDir, "first.sqlite"), chunkSize, maxBlobSize)
|
|
|
|
env := setupDedupBackupEnv(
|
|
ctx, t, fs, storeDir, secondDBPath, chunkSize, maxBlobSize)
|
|
|
|
defer func() { _ = env.db.Close() }()
|
|
|
|
extra, err := age.GenerateX25519Identity()
|
|
require.NoError(t, err)
|
|
|
|
env.cfg.AgeRecipients = append(env.cfg.AgeRecipients,
|
|
extra.Recipient().String())
|
|
|
|
snapshotID, result := runDedupSnapshot(ctx, t, env.sm, env.makeScanner(),
|
|
env.cfg.Hostname, "second", dataDir, secondDBPath)
|
|
require.Positive(t, result.BlobsCreated)
|
|
require.Zero(t, result.BlobsUploaded,
|
|
"the second backup should find every blob already stored")
|
|
|
|
require.NoError(t, env.db.Close())
|
|
|
|
verifier := &vaultik.Vaultik{
|
|
Config: env.cfg,
|
|
Storage: env.storer,
|
|
Fs: fs,
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
UI: ui.NewWithColor(io.Discard, false),
|
|
}
|
|
verifier.SetContext(ctx)
|
|
|
|
require.NoError(t,
|
|
verifier.VerifySnapshotWithOptions(snapshotID, &vaultik.VerifyOptions{}),
|
|
"shallow verify should pass")
|
|
require.NoError(t,
|
|
verifier.VerifySnapshotWithOptions(snapshotID,
|
|
&vaultik.VerifyOptions{Deep: true}),
|
|
"deep verify should pass")
|
|
}
|