initial commit
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
This commit is contained in:
commit
951dd8d26b
175 changed files with 50041 additions and 0 deletions
185
internal/store/backup_test.go
Normal file
185
internal/store/backup_test.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/urpi/go-bananas/internal/domain"
|
||||
"github.com/urpi/go-bananas/internal/store"
|
||||
)
|
||||
|
||||
// openTemp opens a file-backed store in a temporary directory, which is the only
|
||||
// kind worth snapshotting: an in-memory database has nothing to preserve.
|
||||
func openTemp(t *testing.T) *store.Store {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "live.db")
|
||||
st, err := store.Open(context.Background(), store.Options{Path: path})
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = st.Close() })
|
||||
return st
|
||||
}
|
||||
|
||||
// TestSnapshotProducesAReadableDatabase is the property the backup exists for: the
|
||||
// file that comes out must be a database, not merely a file with the right size.
|
||||
func TestSnapshotProducesAReadableDatabase(t *testing.T) {
|
||||
st := openTemp(t)
|
||||
dest := filepath.Join(t.TempDir(), "snapshots", "go-bananas.db")
|
||||
|
||||
if err := st.Snapshot(context.Background(), dest); err != nil {
|
||||
t.Fatalf("Snapshot: %v", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(dest)
|
||||
if err != nil {
|
||||
t.Fatalf("stat snapshot: %v", err)
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
t.Fatal("snapshot is empty")
|
||||
}
|
||||
|
||||
// Reopening is the check that matters. A truncated or torn copy often fails
|
||||
// here rather than at the point of use, which is exactly the failure a restore
|
||||
// would hit at the worst possible moment.
|
||||
reopened, err := store.Open(context.Background(), store.Options{Path: dest, ReadOnly: true})
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot is not a readable database: %v", err)
|
||||
}
|
||||
defer func() { _ = reopened.Close() }()
|
||||
|
||||
// The schema must have come across, not just the bytes: an empty database with
|
||||
// no tables would also open cleanly.
|
||||
var count int
|
||||
if err := reopened.DB().QueryRowContext(context.Background(),
|
||||
"SELECT count(*) FROM sqlite_master WHERE type = 'table'").Scan(&count); err != nil {
|
||||
t.Fatalf("query snapshot schema: %v", err)
|
||||
}
|
||||
if count == 0 {
|
||||
t.Error("snapshot has no tables; the schema did not come across")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotRefusesToOverwrite guards the failure this feature is easiest to get
|
||||
// wrong: a snapshot that silently overwrites the previous good one turns a failed
|
||||
// run into two data-loss events instead of one.
|
||||
func TestSnapshotRefusesToOverwrite(t *testing.T) {
|
||||
st := openTemp(t)
|
||||
dest := filepath.Join(t.TempDir(), "snap.db")
|
||||
|
||||
if err := st.Snapshot(context.Background(), dest); err != nil {
|
||||
t.Fatalf("first snapshot: %v", err)
|
||||
}
|
||||
first, err := os.Stat(dest)
|
||||
if err != nil {
|
||||
t.Fatalf("stat: %v", err)
|
||||
}
|
||||
|
||||
if err := st.Snapshot(context.Background(), dest); err == nil {
|
||||
t.Fatal("expected the second snapshot to be refused")
|
||||
}
|
||||
|
||||
second, err := os.Stat(dest)
|
||||
if err != nil {
|
||||
t.Fatalf("stat: %v", err)
|
||||
}
|
||||
if !first.ModTime().Equal(second.ModTime()) {
|
||||
t.Error("the existing snapshot was modified by the refused write")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotCreatesTheParentDirectory covers the manual-invocation path, where
|
||||
// the operator names a directory that does not exist yet.
|
||||
func TestSnapshotCreatesTheParentDirectory(t *testing.T) {
|
||||
st := openTemp(t)
|
||||
base := t.TempDir()
|
||||
dest := filepath.Join(base, "a", "b", "c", "snap.db")
|
||||
|
||||
if err := st.Snapshot(context.Background(), dest); err != nil {
|
||||
t.Fatalf("Snapshot: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(dest); err != nil {
|
||||
t.Errorf("snapshot not written: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotRejectsEmptyDestination checks the argument validation rather than
|
||||
// letting SQLite interpret an empty filename as the current directory.
|
||||
func TestSnapshotRejectsEmptyDestination(t *testing.T) {
|
||||
st := openTemp(t)
|
||||
if err := st.Snapshot(context.Background(), " "); err == nil {
|
||||
t.Error("expected an error for a blank destination")
|
||||
} else if !errors.Is(err, domain.ErrInvalid) {
|
||||
t.Errorf("got %v, want an invalid-input error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotEscapesQuotesInThePath guards the SQL-literal construction. A path
|
||||
// containing a single quote must not be able to terminate the literal and append
|
||||
// statements of its own.
|
||||
func TestSnapshotEscapesQuotesInThePath(t *testing.T) {
|
||||
st := openTemp(t)
|
||||
dest := filepath.Join(t.TempDir(), "it's a snap.db")
|
||||
|
||||
if err := st.Snapshot(context.Background(), dest); err != nil {
|
||||
t.Fatalf("Snapshot: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(dest); err != nil {
|
||||
t.Errorf("snapshot not written to the quoted path: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotOfAnInMemoryDatabaseIsRefused documents that the store-level guard
|
||||
// exists, rather than leaving it to the command line.
|
||||
func TestIsPersistentPath(t *testing.T) {
|
||||
for _, path := range []string{":memory:", "file::memory:?cache=shared", "file:x?mode=memory"} {
|
||||
if store.IsPersistentPath(path) {
|
||||
t.Errorf("%q should be reported as ephemeral", path)
|
||||
}
|
||||
}
|
||||
for _, path := range []string{"/srv/data/go-bananas.db", "./data/db.sqlite"} {
|
||||
if !store.IsPersistentPath(path) {
|
||||
t.Errorf("%q should be reported as persistent", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotUnderLoadStaysConsistent is a smoke test for the reason VACUUM INTO
|
||||
// is used rather than a file copy: writing while the snapshot is taken must not
|
||||
// corrupt the result.
|
||||
func TestSnapshotUnderLoadStaysConsistent(t *testing.T) {
|
||||
st := openTemp(t)
|
||||
ctx := context.Background()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for i := 0; i < 20; i++ {
|
||||
if _, err := st.DB().ExecContext(ctx,
|
||||
`INSERT INTO settings (key, value, updated_at, updated_by) VALUES (?,?,?,?)`,
|
||||
"concurrent.key", strconv.Itoa(i), time.Now().UTC().Format(time.RFC3339Nano), "test"); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
dest := filepath.Join(t.TempDir(), "under-load.db")
|
||||
if err := st.Snapshot(ctx, dest); err != nil {
|
||||
t.Fatalf("Snapshot during writes: %v", err)
|
||||
}
|
||||
<-done
|
||||
|
||||
reopened, err := store.Open(ctx, store.Options{Path: dest, ReadOnly: true})
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot taken during writes is unreadable: %v", err)
|
||||
}
|
||||
defer func() { _ = reopened.Close() }()
|
||||
if err := reopened.DB().PingContext(ctx); err != nil {
|
||||
t.Errorf("snapshot failed an integrity check: %v", err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue