package backup import ( "context" "crypto/rand" "encoding/hex" "net/url" "os" "os/exec" "path/filepath" "testing" "time" "github.com/jackc/pgx/v5" "textmachine/platform/internal/login" "textmachine/platform/internal/money" "textmachine/platform/internal/pgstore" ) // TestARestorePointCanActuallyBeRestored is the test this whole package exists for. // // ⚠ WITHOUT IT, A BACKUP IS A BELIEF. Everything else here proves that files were written and that // their digests match. NONE of that proves the one property an operator will one day need: that the // credit ledger comes BACK. So this test takes a real point with the real `pg_dump`, restores it // into a database that has never seen this deployment, and reads the money and the book out of the // restored copy — the same claim the runbook's restore procedure makes, executed. // // Gated on a live Postgres and on the standard tools being on PATH, like every other live test here: // a bare clone stays green and says why. func TestARestorePointCanActuallyBeRestored(t *testing.T) { admin := os.Getenv("TM_PLATFORM_TEST_DSN") if admin == "" { t.Skip("TM_PLATFORM_TEST_DSN not set: a restore cannot be demonstrated without a live Postgres") } dump, err := exec.LookPath(pgDumpName()) if err != nil { t.Skipf("%s not on PATH: a restore point cannot be taken (%v)", pgDumpName(), err) } restore, err := exec.LookPath(pgRestoreName()) if err != nil { t.Skipf("%s not on PATH: a restore cannot be demonstrated (%v)", pgRestoreName(), err) } ctx := t.Context() sourceDSN := freshDB(t, admin) store, err := pgstore.Open(ctx, sourceDSN) if err != nil { t.Fatal(err) } defer store.Close() // A deployment with something to lose: an account, credit in the ledger, and a book. now := time.Now().UTC() userID, err := store.UpsertIdentity(ctx, login.Identity{ Provider: "google", Subject: "sub-restore-probe", Email: "probe@example.org", EmailVerified: true, }, now, 0) if err != nil { t.Fatal(err) } const granted = money.MicroUSD(25_000_000) // $25 if _, err := store.Grant(ctx, userID, granted, "admin", "restore-probe-grant", "probe", now); err != nil { t.Fatal(err) } root := t.TempDir() book := aBook(t, root, "bk_restoreprobe") bookID, err := store.AddBook(ctx, pgstore.NewBook{ OwnerID: userID, Title: "Мастер Гу", SourceLang: "zh", TargetLang: "ru", ChapterCount: 3, CharacterCount: 42, Workdir: book.Workdir, Now: now, }) if err != nil { t.Fatal(err) } before, err := store.ReadAccount(ctx, userID) if err != nil { t.Fatal(err) } if before.Balance != granted { t.Fatalf("the probe deployment does not hold what it was granted: %s", before.Balance.USD()) } dest := t.TempDir() svc := &Service{ Cfg: Config{Dir: dest, Every: time.Hour, Keep: 2, PgDumpBin: dump, PgRestoreBin: restore, DSN: sourceDSN, EngineBinary: "/not/used/here"}, Store: store, Engine: &fakeEngine{}, } res, err := svc.Take(ctx) if err != nil { t.Fatalf("taking a live restore point: %v", err) } if problems, err := Verify(res.Path); err != nil || len(problems) != 0 { t.Fatalf("the live restore point does not verify: %v (%v)", problems, err) } // ⚠ THE DISK IS GONE. Everything below reads ONLY the restore point and a database that has // never seen this deployment; the source is not touched again. targetDSN := freshDBEmpty(t, admin) cmd := exec.CommandContext(ctx, restore, "--clean", "--if-exists", "--no-owner", "--dbname="+targetDSN, filepath.Join(res.Path, DumpFile)) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("pg_restore into an empty database failed: %v\n%s", err, out) } restored, err := pgstore.Open(ctx, targetDSN) if err != nil { t.Fatal(err) } defer restored.Close() after, err := restored.ReadAccount(ctx, userID) if err != nil { t.Fatalf("the account is not on the restored copy: %v", err) } if after.Balance != granted { t.Errorf("the restored balance is %s, want %s: the credit ledger did not survive", after.Balance.USD(), granted.USD()) } got, _, err := restored.GetBook(ctx, userID, bookID) if err != nil { t.Fatalf("the book is not on the restored copy: %v", err) } if got.Title != "Мастер Гу" || got.ChapterCount != 3 || got.CharacterCount != 42 { t.Errorf("the restored book is not the one that was backed up: %+v", got) } // And the book's paid work: the file the runbook tells the operator to put back is in the point, // under the name the notes give, carrying the engine's consistent copy rather than the live file. body, err := os.ReadFile(filepath.Join(res.Path, BooksDir, bookID, ProjectDBName)) if err != nil { // The point stores books by the id the STORE gave them, which is the id the restored rows // carry — so this is also the pin that says the two agree. t.Fatalf("the book's database is not in the restore point under its own id: %v", err) } if string(body) != "CONSISTENT-COPY" { t.Errorf("the stored database is not the engine's consistent copy: %q", body) } } func pgDumpName() string { if v := os.Getenv("TM_PLATFORM_TEST_PGDUMP"); v != "" { return v } return "pg_dump" } func pgRestoreName() string { if v := os.Getenv("TM_PLATFORM_TEST_PGRESTORE"); v != "" { return v } return "pg_restore" } // freshDB makes a migrated scratch database, and freshDBEmpty an unmigrated one — the restore has to // land on a database that has never seen this deployment, or the test would be proving that // `pg_restore` can overwrite a schema it already agrees with. // // ⚠ A COPY of the helper in cmd/tmplatformctl and internal/runs, deliberately and for the reason // stated there: sharing it needs a package that imports pgstore, and pgstore's own battery lives in // `package pgstore`, so that package could not exist without a cycle. func freshDB(t *testing.T, admin string) string { t.Helper() dsn := freshDBEmpty(t, admin) if err := pgstore.Migrate(context.Background(), dsn); err != nil { t.Fatalf("migrate: %v", err) } return dsn } func freshDBEmpty(t *testing.T, admin string) string { t.Helper() ctx := context.Background() var suffix [6]byte if _, err := rand.Read(suffix[:]); err != nil { t.Fatal(err) } name := "tm_backup_test_" + hex.EncodeToString(suffix[:]) conn, err := pgx.Connect(ctx, admin) if err != nil { t.Fatalf("connect: %v", err) } defer conn.Close(ctx) if _, err := conn.Exec(ctx, `create database `+pgx.Identifier{name}.Sanitize()); err != nil { t.Fatalf("create database: %v", err) } t.Cleanup(func() { c, err := pgx.Connect(context.Background(), admin) if err != nil { return } defer c.Close(context.Background()) _, _ = c.Exec(context.Background(), `drop database `+pgx.Identifier{name}.Sanitize()+` with (force)`) }) u, err := url.Parse(admin) if err != nil { t.Fatalf("TM_PLATFORM_TEST_DSN must be a URL: %v", err) } u.Path = "/" + name return u.String() }