check / check (push) Successful in 13m41s
A first backup that failed before writing anything (a mistyped bucket, a refused credential) bound the local index to that destination, and the corrected config was then refused. A backup now checks for a different binding first, writes the destination marker, and only then records the configured destination. Purge, prune, snapshot remove and remote nuke still refuse a different binding but no longer record one: only a backup puts data in the local index, so an unbound local index holds nothing from any destination. Model: opus-5-5
183 lines
5.3 KiB
Go
183 lines
5.3 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/config"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/storage/faultstore"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// storageBindingOf returns the destination the local index is bound to,
|
|
// or "" when it is not bound.
|
|
func storageBindingOf(
|
|
ctx context.Context, t *testing.T, repos *database.Repositories,
|
|
) string {
|
|
t.Helper()
|
|
|
|
got, err := repos.LocalMeta.Get(ctx, database.LocalMetaKeyStorageURL)
|
|
require.NoError(t, err)
|
|
|
|
return got
|
|
}
|
|
|
|
// TestFailedFirstBackupLeavesIndexUnbound checks that a first backup
|
|
// the destination refuses, as a mistyped bucket would, leaves the local
|
|
// index unbound, so a backup with the corrected destination succeeds.
|
|
func TestFailedFirstBackupLeavesIndexUnbound(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
storeDir := t.TempDir()
|
|
v, repos := newFileDestinationVaultik(ctx, t, storeDir)
|
|
working := v.Storage
|
|
|
|
refusing := faultstore.New(working)
|
|
refusing.OnPut = func(string) faultstore.PutAction { return faultstore.PutFail }
|
|
refusing.OnStat = func(string) bool { return true }
|
|
v.Storage = refusing
|
|
v.Config.StorageURL = "file://" + filepath.Join(t.TempDir(), "typo")
|
|
|
|
require.ErrorIs(t, backUp(v, "first"), faultstore.ErrInjectedUpload)
|
|
assert.Empty(t, storageBindingOf(ctx, t, repos))
|
|
|
|
v.Storage = working
|
|
v.Config.StorageURL = "file://" + storeDir
|
|
|
|
require.NoError(t, backUp(v, "first"))
|
|
assert.Equal(t, "file://"+storeDir, storageBindingOf(ctx, t, repos))
|
|
}
|
|
|
|
// TestPruneLeavesIndexUnbound checks that a command other than a backup
|
|
// does not bind an unbound local index.
|
|
func TestPruneLeavesIndexUnbound(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
v, repos := newFileDestinationVaultik(ctx, t, t.TempDir())
|
|
|
|
require.NoError(t, v.Prune(&vaultik.PruneOptions{Force: true}))
|
|
assert.Empty(t, storageBindingOf(ctx, t, repos))
|
|
}
|
|
|
|
// buildBindTestVaultik returns a minimal Vaultik wired with a real
|
|
// in-memory DB and a config carrying the given StorageURL. Enough to
|
|
// exercise EnsureStorageBinding without spinning up storage or fx.
|
|
func buildBindTestVaultik(
|
|
t *testing.T, storageURL string,
|
|
) (*vaultik.TestVaultik, *database.Repositories) {
|
|
t.Helper()
|
|
|
|
db, err := database.NewTestDB()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
repos := database.NewRepositories(db)
|
|
|
|
tv := vaultik.NewForTesting(nil)
|
|
tv.Repositories = repos
|
|
tv.DB = db
|
|
tv.Config = &config.Config{StorageURL: storageURL}
|
|
tv.SetContext(context.Background())
|
|
|
|
return tv, repos
|
|
}
|
|
|
|
func TestEnsureStorageBinding_FreshDBRecordsURL(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
tv, repos := buildBindTestVaultik(t, "file:///mnt/backups/new")
|
|
|
|
require.NoError(t, tv.EnsureStorageBinding())
|
|
|
|
got, err := repos.LocalMeta.Get(context.Background(), database.LocalMetaKeyStorageURL)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "file:///mnt/backups/new", got,
|
|
"first call must record the configured URL")
|
|
}
|
|
|
|
func TestEnsureStorageBinding_MatchingURLPasses(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
tv, repos := buildBindTestVaultik(t, "s3://bucket/prefix")
|
|
|
|
require.NoError(t, repos.LocalMeta.Set(context.Background(),
|
|
database.LocalMetaKeyStorageURL, "s3://bucket/prefix"))
|
|
|
|
require.NoError(t, tv.EnsureStorageBinding())
|
|
}
|
|
|
|
func TestEnsureStorageBinding_MismatchRefuses(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
tv, repos := buildBindTestVaultik(t, "file:///mnt/backups/new")
|
|
|
|
require.NoError(t, repos.LocalMeta.Set(context.Background(),
|
|
database.LocalMetaKeyStorageURL, "file:///mnt/backups/old"))
|
|
|
|
err := tv.EnsureStorageBinding()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "file:///mnt/backups/old")
|
|
assert.Contains(t, err.Error(), "file:///mnt/backups/new")
|
|
assert.Contains(t, err.Error(), "vaultik database delete")
|
|
}
|
|
|
|
// A config with no storage_url names its destination with s3.endpoint,
|
|
// s3.bucket and s3.prefix; a change to any of them must be refused just
|
|
// as a changed storage_url is.
|
|
func TestEnsureStorageBinding_S3FieldsChangeRefuses(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
bound := config.S3Config{
|
|
Endpoint: "https://s3.example.com",
|
|
Bucket: "bucket-a",
|
|
Prefix: "backups",
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
changed config.S3Config
|
|
}{
|
|
{"bucket", config.S3Config{
|
|
Endpoint: bound.Endpoint, Bucket: "bucket-b", Prefix: bound.Prefix,
|
|
}},
|
|
{"prefix", config.S3Config{
|
|
Endpoint: bound.Endpoint, Bucket: bound.Bucket, Prefix: "other",
|
|
}},
|
|
{"endpoint", config.S3Config{
|
|
Endpoint: "https://s3.other.com", Bucket: bound.Bucket, Prefix: bound.Prefix,
|
|
}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tv, _ := buildBindTestVaultik(t, "")
|
|
|
|
tv.Config.S3 = bound
|
|
require.NoError(t, tv.EnsureStorageBinding(), "first use binds")
|
|
require.NoError(t, tv.EnsureStorageBinding(), "same destination passes")
|
|
|
|
tv.Config.S3 = tt.changed
|
|
err := tv.EnsureStorageBinding()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(),
|
|
"local index is bound to a different backup destination")
|
|
assert.Contains(t, err.Error(), "vaultik database delete")
|
|
})
|
|
}
|
|
}
|