package backup import ( "errors" "os" "path/filepath" "strings" "testing" "github.com/alexei/tinyforge/internal/store" ) // newTestEngine spins up an isolated store + engine pair for tests. // Each test gets its own tempdir so backup files do not collide. func newTestEngine(t *testing.T) (*Engine, *store.Store, string) { t.Helper() dir := t.TempDir() dbPath := filepath.Join(dir, "tinyforge.db") st, err := store.New(dbPath) if err != nil { t.Fatalf("store.New: %v", err) } t.Cleanup(func() { _ = st.Close() }) eng, err := New(st, dbPath, dir) if err != nil { t.Fatalf("backup.New: %v", err) } return eng, st, dbPath } func TestPrepareRestore_RejectsTinyFile(t *testing.T) { eng, st, _ := newTestEngine(t) // Plant a backup row with a tiny file masquerading as a backup. tinyPath := filepath.Join(eng.BackupDir(), "tinyforge-manual-junk.db") if err := os.WriteFile(tinyPath, []byte("hi"), 0o600); err != nil { t.Fatalf("write tiny: %v", err) } bk, err := st.CreateBackup(store.Backup{ Filename: "tinyforge-manual-junk.db", SizeBytes: 2, BackupType: "manual", }) if err != nil { t.Fatalf("CreateBackup row: %v", err) } if _, err := eng.PrepareRestore(bk.ID); err == nil { t.Fatal("expected PrepareRestore to reject tiny file, got nil") } else if !strings.Contains(err.Error(), "suspiciously small") { t.Errorf("error = %v, want 'suspiciously small'", err) } } func TestPrepareRestore_RejectsNonSQLite(t *testing.T) { eng, st, _ := newTestEngine(t) // 200 bytes of non-SQLite garbage: passes the size check, fails // the header magic check. garbagePath := filepath.Join(eng.BackupDir(), "tinyforge-manual-bogus.db") junk := make([]byte, 200) for i := range junk { junk[i] = byte('x') } if err := os.WriteFile(garbagePath, junk, 0o600); err != nil { t.Fatalf("write junk: %v", err) } bk, err := st.CreateBackup(store.Backup{ Filename: "tinyforge-manual-bogus.db", SizeBytes: int64(len(junk)), BackupType: "manual", }) if err != nil { t.Fatalf("CreateBackup row: %v", err) } if _, err := eng.PrepareRestore(bk.ID); err == nil { t.Fatal("expected PrepareRestore to reject non-SQLite blob, got nil") } else if !strings.Contains(err.Error(), "header") { t.Errorf("error = %v, want header mismatch", err) } } func TestPrepareRestore_AcceptsValidVacuumInto(t *testing.T) { eng, _, _ := newTestEngine(t) // A fresh CreateBackup from the engine itself is, by construction, // a valid SQLite database — VACUUM INTO produces a clean copy. bk, err := eng.CreateBackup("manual") if err != nil { t.Fatalf("CreateBackup: %v", err) } path, err := eng.PrepareRestore(bk.ID) if err != nil { t.Fatalf("PrepareRestore on valid backup: %v", err) } if path == "" { t.Errorf("PrepareRestore returned empty path") } } func TestPrepareRestore_UnknownID(t *testing.T) { eng, _, _ := newTestEngine(t) _, err := eng.PrepareRestore("nonexistent-id") if err == nil { t.Fatal("expected error for unknown id, got nil") } if errors.Is(err, store.ErrNotFound) { // fine — wrapped through RestorePath } }