check / check (push) Waiting to run
snapshot purge, and the purge step of snapshot create --prune, logged a failure to delete a snapshot from the local index, deleted that snapshot's metadata from the destination store anyway, and exited 0. The purge now stops at that snapshot with an error before touching its remote metadata, so the command exits non-zero. Stopping matches what the same loop already does when the remote delete fails; snapshots purged before the failure stay purged. The test drives snapshot purge only: the trigger that makes the local index refuse the delete also breaks the backup's metadata export, so snapshot create --prune cannot be tested that way. Model: opus-5-5
69 lines
2.4 KiB
Go
69 lines
2.4 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)
|
|
|
|
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, _, _ := runEntry(t, flagConfig, configPath,
|
|
cmdSnapshot, cmdPurge, flagOlderThan, "30d", flagForce)
|
|
|
|
assert.Equal(t, 1, code)
|
|
assert.FileExists(t, manifestPath,
|
|
"the snapshot is still in the local index, so its metadata must stay")
|
|
}
|