188 lines
5.1 KiB
Go
188 lines
5.1 KiB
Go
package backup
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func seedDir(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("hello"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(dir, "sub"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "sub", "b.key"), []byte("secret-bytes"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return dir
|
|
}
|
|
|
|
func TestWriteRestoreServerRoundTripPlain(t *testing.T) {
|
|
src := seedDir(t)
|
|
var buf bytes.Buffer
|
|
if err := WriteServer(&buf, src, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dst := filepath.Join(t.TempDir(), "restored")
|
|
if err := RestoreServer(&buf, dst, "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := os.ReadFile(filepath.Join(dst, "sub", "b.key"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != "secret-bytes" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestWriteRestoreServerRoundTripEncrypted(t *testing.T) {
|
|
src := seedDir(t)
|
|
var buf bytes.Buffer
|
|
if err := WriteServer(&buf, src, "correct horse"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dst := filepath.Join(t.TempDir(), "restored")
|
|
if err := RestoreServer(bytes.NewReader(buf.Bytes()), dst, "correct horse", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := os.ReadFile(filepath.Join(dst, "a.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != "hello" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
|
|
// Wrong passphrase must fail, not silently produce garbage.
|
|
if err := RestoreServer(bytes.NewReader(buf.Bytes()), filepath.Join(t.TempDir(), "wrong"), "wrong passphrase", false); err == nil {
|
|
t.Fatal("expected an error restoring with the wrong passphrase")
|
|
}
|
|
}
|
|
|
|
func TestRestoreServerRefusesNonEmptyTargetWithoutForce(t *testing.T) {
|
|
src := seedDir(t)
|
|
var buf bytes.Buffer
|
|
if err := WriteServer(&buf, src, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dst := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dst, "existing.txt"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := RestoreServer(&buf, dst, "", false); err == nil {
|
|
t.Fatal("expected restore into a non-empty directory to be refused without force")
|
|
}
|
|
}
|
|
|
|
func TestWriteServerWithOverrideSubstitutesFileContent(t *testing.T) {
|
|
src := seedDir(t)
|
|
override := filepath.Join(t.TempDir(), "snapshot.txt")
|
|
if err := os.WriteFile(override, []byte("snapshot-content"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := WriteServerWithOverride(&buf, src, "a.txt", override, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dst := filepath.Join(t.TempDir(), "restored")
|
|
if err := RestoreServer(&buf, dst, "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := os.ReadFile(filepath.Join(dst, "a.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != "snapshot-content" {
|
|
t.Fatalf("got %q, want the override file's content substituted in place of the live a.txt", got)
|
|
}
|
|
// The untouched sibling file should still come through unmodified.
|
|
if got, err := os.ReadFile(filepath.Join(dst, "sub", "b.key")); err != nil || string(got) != "secret-bytes" {
|
|
t.Fatalf("sub/b.key = %q, %v; expected untouched", got, err)
|
|
}
|
|
}
|
|
|
|
func TestWriteServerConsistentSnapshotsLiveDatabase(t *testing.T) {
|
|
src := seedDir(t)
|
|
dbPath := filepath.Join(src, "app.db")
|
|
sqlDB, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer sqlDB.Close()
|
|
if _, err := sqlDB.Exec(`CREATE TABLE t (v TEXT)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := sqlDB.Exec(`INSERT INTO t (v) VALUES ('before-backup')`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := WriteServerConsistent(sqlDB, &buf, src, dbPath, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// A write after the snapshot was taken must not appear in the archive — proves
|
|
// VACUUM INTO's point-in-time semantics are actually being used, not just a raw
|
|
// copy of the (possibly since-changed) live file.
|
|
if _, err := sqlDB.Exec(`INSERT INTO t (v) VALUES ('after-backup')`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dst := filepath.Join(t.TempDir(), "restored")
|
|
if err := RestoreServer(&buf, dst, "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restoredDB, err := sql.Open("sqlite", filepath.Join(dst, "app.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer restoredDB.Close()
|
|
var rows []string
|
|
res, err := restoredDB.Query(`SELECT v FROM t ORDER BY v`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer res.Close()
|
|
for res.Next() {
|
|
var v string
|
|
res.Scan(&v)
|
|
rows = append(rows, v)
|
|
}
|
|
if len(rows) != 1 || rows[0] != "before-backup" {
|
|
t.Fatalf("got %v, want exactly [\"before-backup\"] (the post-snapshot insert must not appear)", rows)
|
|
}
|
|
}
|
|
|
|
func TestRestoreServerForceOverwritesNonEmptyTarget(t *testing.T) {
|
|
src := seedDir(t)
|
|
var buf bytes.Buffer
|
|
if err := WriteServer(&buf, src, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dst := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dst, "existing.txt"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := RestoreServer(&buf, dst, "", true); err != nil {
|
|
t.Fatalf("expected force restore to succeed, got: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dst, "a.txt")); err != nil {
|
|
t.Fatalf("expected restored file to exist: %v", err)
|
|
}
|
|
}
|