check / check (push) Waiting to run
The warning for a config file that others can read always said the
file contained S3 credentials, so a file:// config with none got a
false claim. When s3.access_key_id or s3.secret_access_key is set it
now says the file may contain them, because Load sees the values only
after smartconfig has replaced any ${...} reference, so a set
credential need not be in the file. Otherwise it says the file is
readable by others.
snapshot purge wrapped the listing error, which already starts with
"listing remote snapshots:", in that prefix a second time.
syncWithRemote now returns it unwrapped, as CleanupLocalSnapshots
does.
Model: opus-5-5
177 lines
5.8 KiB
Go
177 lines
5.8 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"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")
|
|
}
|
|
|
|
// TestPurgeSaysListingFailedOnceWhenDestinationMissing checks that
|
|
// snapshot purge fails on a destination it cannot list, with an error
|
|
// that says "listing remote snapshots" once.
|
|
//
|
|
//nolint:paralleltest // installs the global logger via log.Initialize
|
|
func TestPurgeSaysListingFailedOnceWhenDestinationMissing(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
|
|
ctx := context.Background()
|
|
v, _, _ := backUpThenUnplug(ctx, t)
|
|
|
|
err := v.PurgeSnapshotsWithOptions(&vaultik.SnapshotPurgeOptions{
|
|
KeepLatest: true,
|
|
Force: true,
|
|
})
|
|
require.ErrorIs(t, err, fs.ErrNotExist)
|
|
assert.Equal(t, 1, strings.Count(err.Error(), "listing remote snapshots"),
|
|
err.Error())
|
|
}
|