check / check (push) Waiting to run
Every backup writes vaultik-destination.txt at the destination root. While the local index holds snapshots stored there, 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. snapshot create checks before dropping incomplete snapshots, snapshot remove before looking on the destination. remote nuke deletes the marker last. The file:// backend never creates its destination directory, which protects a directory on a removable volume, not a mount point. Tests back up into directories that exist; fixtures for a written destination carry the marker. Judgement call: the marker is rewritten on every backup. Deviation: for the complexity limit, two blocks moved, unchanged, into snapshotNamesToCreate and confirmRemoveSnapshot. Model: opus-5-5
76 lines
2.7 KiB
Go
76 lines
2.7 KiB
Go
package cli //nolint:testpackage // shares runEntry and the hermetic config helpers
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
)
|
|
|
|
// Words and flags of `snapshot purge` used to build argument vectors
|
|
// below.
|
|
const (
|
|
cmdPurge = "purge"
|
|
flagOlderThan = "--older-than"
|
|
flagForce = "--force"
|
|
)
|
|
|
|
// TestEntrySnapshotPurgeKeepsMetadataWhenLocalIndexDeleteFails makes
|
|
// the local index refuse to delete an old snapshot and checks that
|
|
// `snapshot purge` exits non-zero and leaves that snapshot's metadata on
|
|
// the destination store. It used to log the error, delete the metadata
|
|
// and exit 0.
|
|
//
|
|
// `snapshot create --prune` runs the same purge, but cannot be driven
|
|
// this way: the refusal also breaks the backup's metadata export, which
|
|
// deletes the other snapshots from its copy of the local index.
|
|
//
|
|
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
|
|
func TestEntrySnapshotPurgeKeepsMetadataWhenLocalIndexDeleteFails(t *testing.T) {
|
|
configPath := writeHermeticPruneConfig(t, false)
|
|
dir := filepath.Dir(configPath)
|
|
indexPath := filepath.Join(dir, "index.sqlite")
|
|
|
|
// seedStaleSnapshotRecord writes an old snapshot to the local index
|
|
// only; the manifest planted here puts it on the destination store
|
|
// too, so the purge selects it rather than dropping it as stale.
|
|
seedStaleSnapshotRecord(t, indexPath)
|
|
|
|
// While the local index holds a snapshot, purge fails on a
|
|
// destination without the marker a backup writes.
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, "store", "vaultik-destination.txt"), nil, 0o600))
|
|
|
|
manifestDir := filepath.Join(dir, "store", "metadata",
|
|
snapshot.RemoteSnapshotKey(stalePruneSnapshotID))
|
|
require.NoError(t, os.MkdirAll(manifestDir, 0o750))
|
|
|
|
manifestPath := filepath.Join(manifestDir, "manifest.json.zst")
|
|
require.NoError(t, os.WriteFile(manifestPath, []byte("stub"), 0o600))
|
|
|
|
ctx := context.Background()
|
|
|
|
db, err := database.New(ctx, indexPath)
|
|
require.NoError(t, err)
|
|
|
|
_, err = db.Conn().ExecContext(ctx, `
|
|
CREATE TRIGGER refuse_snapshot_delete BEFORE DELETE ON snapshots
|
|
BEGIN SELECT RAISE(ABORT, 'simulated index error'); END`)
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.Close())
|
|
|
|
code, _, stderr := runEntry(t, flagConfig, configPath,
|
|
cmdSnapshot, cmdPurge, flagOlderThan, "30d", flagForce)
|
|
|
|
assert.Equal(t, 1, code)
|
|
assert.Contains(t, stderr, "simulated index error",
|
|
"the purge must fail at the local-index delete")
|
|
assert.FileExists(t, manifestPath,
|
|
"the snapshot is still in the local index, so its metadata must stay")
|
|
}
|