check / check (push) Waiting to run
snapshot purge, and the purge step of snapshot create --prune, logged a failed local index delete, then deleted that snapshot's metadata from the destination store anyway and exited 0, leaving a local record whose metadata was gone. It now stops at that snapshot with an error, before touching its metadata, so the command exits non-zero; snapshots already purged stay purged. This matches how the same loop already handles a failed metadata delete. Model: opus-5-5
91 lines
2.9 KiB
Go
91 lines
2.9 KiB
Go
package cli //nolint:testpackage // shares runEntry and the hermetic config helpers
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
"sneak.berlin/go/vaultik/internal/types"
|
|
)
|
|
|
|
// The `snapshot purge` command word and flags used below.
|
|
const (
|
|
cmdPurge = "purge"
|
|
flagKeepLatest = "--keep-latest"
|
|
flagForce = "--force"
|
|
)
|
|
|
|
// TestEntrySnapshotPurgeKeepsMetadataWhenLocalIndexDeleteFails makes
|
|
// removing a snapshot from the local index fail 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
|
|
// anyway and exit 0.
|
|
//
|
|
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
|
|
func TestEntrySnapshotPurgeKeepsMetadataWhenLocalIndexDeleteFails(t *testing.T) {
|
|
ctx := context.Background()
|
|
configPath := writeHermeticPruneConfig(t, false)
|
|
|
|
// writeHermeticPruneConfig puts the index and the store directory
|
|
// beside the config file.
|
|
dir := filepath.Dir(configPath)
|
|
|
|
// Two snapshots of the same name: --keep-latest purges the older.
|
|
olderStart := time.Date(2026, 5, 1, 9, 0, 0, 0, time.UTC)
|
|
newerStart := olderStart.Add(time.Hour)
|
|
olderID := "test-host_test_" + olderStart.Format(time.RFC3339)
|
|
newerID := "test-host_test_" + newerStart.Format(time.RFC3339)
|
|
|
|
db, err := database.New(ctx, filepath.Join(dir, "index.sqlite"))
|
|
require.NoError(t, err)
|
|
|
|
repos := database.NewRepositories(db)
|
|
|
|
manifestPaths := make(map[string]string)
|
|
|
|
for id, startedAt := range map[string]time.Time{
|
|
olderID: olderStart,
|
|
newerID: newerStart,
|
|
} {
|
|
completedAt := startedAt.Add(time.Minute)
|
|
snap := &database.Snapshot{
|
|
ID: types.SnapshotID(id),
|
|
Hostname: "test-host",
|
|
VaultikVersion: "test",
|
|
StartedAt: startedAt,
|
|
CompletedAt: &completedAt,
|
|
}
|
|
|
|
err = repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
|
return repos.Snapshots.Create(ctx, tx, snap)
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
manifestPaths[id] = filepath.Join(dir, "store", "metadata",
|
|
snapshot.RemoteSnapshotKey(id), "manifest.json.zst")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(manifestPaths[id]), 0o750))
|
|
require.NoError(t, os.WriteFile(manifestPaths[id], []byte("stub"), 0o600))
|
|
}
|
|
|
|
_, 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, flagKeepLatest, flagForce)
|
|
|
|
assert.Equal(t, 1, code)
|
|
assert.Contains(t, stderr, "simulated index error")
|
|
assert.FileExists(t, manifestPaths[olderID],
|
|
"a snapshot still in the local index keeps its metadata")
|
|
}
|