The file backend listed a destination directory that does not exist as an empty store. With the volume unplugged, snapshot list reported every local snapshot as missing from the store, snapshot remove said it had removed metadata it never reached, and prune dropped every local snapshot record. List and ListStream now fail when the destination directory is missing, so those commands take their existing path for a store that cannot be listed. A missing prefix under an existing directory is still an empty listing, and a first backup still creates the directory. Three tests listed a file:// destination nothing had created; they now create it. Model: opus-5-5
156 lines
5.1 KiB
Go
156 lines
5.1 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// These tests cover https://git.eeqj.de/sneak/vaultik/issues/220: a
|
|
// file:// destination whose directory is missing, such as a USB stick
|
|
// that is not plugged in, cannot be listed. It is not an empty store, so
|
|
// no command may conclude from it that the local snapshots are gone.
|
|
|
|
// backUpToFileDestination backs up the snapshot named "first" to a
|
|
// file:// destination at storeDir, which need not exist yet. Everything
|
|
// the returned Vaultik prints after the backup goes to the returned
|
|
// buffer.
|
|
func backUpToFileDestination(
|
|
ctx context.Context, t *testing.T, storeDir string,
|
|
) (*vaultik.Vaultik, *database.Repositories, *bytes.Buffer) {
|
|
t.Helper()
|
|
|
|
osFs := afero.NewOsFs()
|
|
tempDir := t.TempDir()
|
|
dataDir := filepath.Join(tempDir, "src")
|
|
dbPath := filepath.Join(tempDir, "index.sqlite")
|
|
|
|
writeFaultSourceTree(t, osFs, dataDir)
|
|
|
|
store, err := storage.NewFileStorer(storeDir)
|
|
require.NoError(t, err)
|
|
|
|
db, err := database.New(ctx, dbPath)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
repos := database.NewRepositories(db)
|
|
cfg := changedFileConfig(dataDir, dbPath)
|
|
v := newBackupVaultik(ctx, cfg, store, repos, db, osFs)
|
|
|
|
require.NoError(t, backUp(v, "first"))
|
|
|
|
out := &bytes.Buffer{}
|
|
v.Stdout = out
|
|
v.UI = ui.NewWithColor(out, false)
|
|
|
|
return v, repos, out
|
|
}
|
|
|
|
// backUpThenUnplug backs up to a file:// destination, then moves the
|
|
// destination directory away, as unplugging the volume it lives on would.
|
|
func backUpThenUnplug(
|
|
ctx context.Context, t *testing.T,
|
|
) (*vaultik.Vaultik, *database.Repositories, *bytes.Buffer) {
|
|
t.Helper()
|
|
|
|
storeDir := filepath.Join(t.TempDir(), "usbstick")
|
|
v, repos, out := backUpToFileDestination(ctx, t, storeDir)
|
|
|
|
require.NoError(t, os.Rename(storeDir, storeDir+"-unplugged"))
|
|
|
|
return v, repos, out
|
|
}
|
|
|
|
// TestFirstBackupCreatesDestinationDirectory checks that a first backup
|
|
// to a destination directory that does not exist yet creates it, and
|
|
// that the destination can be listed afterwards.
|
|
//
|
|
//nolint:paralleltest // installs the global logger via log.Initialize
|
|
func TestFirstBackupCreatesDestinationDirectory(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
|
|
ctx := context.Background()
|
|
storeDir := filepath.Join(t.TempDir(), "volume", "backup")
|
|
v, _, out := backUpToFileDestination(ctx, t, storeDir)
|
|
|
|
require.NoError(t, v.ListSnapshots(false))
|
|
|
|
assert.NotContains(t, out.String(), "Could not list backup destination store")
|
|
assert.NotContains(t, out.String(), "not found in backup destination store")
|
|
}
|
|
|
|
// TestListSnapshotsWarnsWhenDestinationMissing checks that snapshot list
|
|
// warns and shows the local index alone, without reporting the local
|
|
// snapshot as missing from the destination.
|
|
//
|
|
//nolint:paralleltest // installs the global logger via log.Initialize
|
|
func TestListSnapshotsWarnsWhenDestinationMissing(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
|
|
ctx := context.Background()
|
|
v, repos, out := backUpThenUnplug(ctx, t)
|
|
id := localSnapshotID(ctx, t, repos, "first")
|
|
|
|
require.NoError(t, v.ListSnapshots(false))
|
|
|
|
assert.Contains(t, out.String(), "Could not list backup destination store")
|
|
assert.Contains(t, out.String(), "Showing snapshots from the local index only.")
|
|
assert.Contains(t, out.String(), id)
|
|
assert.NotContains(t, out.String(), "not found in backup destination store")
|
|
}
|
|
|
|
// TestRemoveSnapshotWarnsWhenDestinationMissing checks that snapshot
|
|
// remove warns that the metadata could not be removed from the
|
|
// destination, instead of reporting that it was.
|
|
//
|
|
//nolint:paralleltest // installs the global logger via log.Initialize
|
|
func TestRemoveSnapshotWarnsWhenDestinationMissing(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
|
|
ctx := context.Background()
|
|
v, repos, out := backUpThenUnplug(ctx, t)
|
|
|
|
result, err := v.RemoveSnapshot(localSnapshotID(ctx, t, repos, "first"),
|
|
&vaultik.RemoveOptions{Force: true})
|
|
require.NoError(t, err)
|
|
|
|
assert.False(t, result.RemoteRemoved)
|
|
assert.Contains(t, out.String(),
|
|
"Could not remove snapshot metadata from remote")
|
|
assert.NotContains(t, out.String(),
|
|
"Removed snapshot metadata from remote storage")
|
|
}
|
|
|
|
// TestPruneKeepsLocalRecordsWhenDestinationMissing checks that prune
|
|
// fails on a destination it cannot list and deletes no local snapshot
|
|
// record.
|
|
//
|
|
//nolint:paralleltest // installs the global logger via log.Initialize
|
|
func TestPruneKeepsLocalRecordsWhenDestinationMissing(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
|
|
ctx := context.Background()
|
|
v, repos, _ := backUpThenUnplug(ctx, t)
|
|
|
|
err := v.Prune(&vaultik.PruneOptions{Force: true})
|
|
require.ErrorIs(t, err, fs.ErrNotExist)
|
|
require.ErrorContains(t, err, "listing remote snapshots")
|
|
|
|
snapshots, err := repos.Snapshots.ListRecent(ctx, listRecentTestLimit)
|
|
require.NoError(t, err)
|
|
assert.Len(t, snapshots, 1, "prune must delete no local snapshot record")
|
|
}
|