package archive import ( "bytes" "database/sql" "os" "path/filepath" "testing" "git.wntrmute.dev/mc/mcdsl/db" ) // setupServiceDir creates a realistic /srv// directory. func setupServiceDir(t *testing.T, database *sql.DB) string { t.Helper() dir := t.TempDir() // Config file. writeFile(t, filepath.Join(dir, "service.toml"), "listen_addr = \":8443\"\n") // Certs directory. certsDir := filepath.Join(dir, "certs") if err := os.Mkdir(certsDir, 0700); err != nil { t.Fatalf("mkdir certs: %v", err) } writeFile(t, filepath.Join(certsDir, "cert.pem"), "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----\n") writeFile(t, filepath.Join(certsDir, "key.pem"), "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----\n") // Live database file (should be excluded). writeFile(t, filepath.Join(dir, "service.db"), "live-db-data") writeFile(t, filepath.Join(dir, "service.db-wal"), "wal-data") writeFile(t, filepath.Join(dir, "service.db-shm"), "shm-data") // Backups directory (should be excluded). backupsDir := filepath.Join(dir, "backups") if err := os.Mkdir(backupsDir, 0700); err != nil { t.Fatalf("mkdir backups: %v", err) } writeFile(t, filepath.Join(backupsDir, "old-backup.db"), "backup-data") return dir } func writeFile(t *testing.T, path, content string) { t.Helper() if err := os.WriteFile(path, []byte(content), 0600); err != nil { t.Fatalf("write %s: %v", path, err) } } func openTestDB(t *testing.T) (*sql.DB, string) { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "real.db") database, err := db.Open(path) if err != nil { t.Fatalf("db.Open: %v", err) } t.Cleanup(func() { _ = database.Close() }) // Create some data to verify snapshot integrity. if _, err := database.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, val TEXT)"); err != nil { t.Fatalf("create table: %v", err) } if _, err := database.Exec("INSERT INTO test (val) VALUES ('snapshot-data')"); err != nil { t.Fatalf("insert: %v", err) } return database, path } func TestSnapshotAndRestore(t *testing.T) { database, dbPath := openTestDB(t) serviceDir := setupServiceDir(t, database) // Snapshot. var buf bytes.Buffer err := Snapshot(SnapshotOptions{ ServiceDir: serviceDir, DBPath: dbPath, DB: database, }, &buf) if err != nil { t.Fatalf("Snapshot: %v", err) } if buf.Len() == 0 { t.Fatal("archive is empty") } // Restore to a new directory. restoreDir := t.TempDir() if err := Restore(&buf, restoreDir); err != nil { t.Fatalf("Restore: %v", err) } // Verify config file was restored. content, err := os.ReadFile(filepath.Join(restoreDir, "service.toml")) //nolint:gosec // test code if err != nil { t.Fatalf("read config: %v", err) } if string(content) != "listen_addr = \":8443\"\n" { t.Fatalf("config content = %q", string(content)) } // Verify certs were restored. if _, err := os.Stat(filepath.Join(restoreDir, "certs", "cert.pem")); err != nil { t.Fatalf("cert.pem missing: %v", err) } if _, err := os.Stat(filepath.Join(restoreDir, "certs", "key.pem")); err != nil { t.Fatalf("key.pem missing: %v", err) } // Verify live DB files were excluded. if _, err := os.Stat(filepath.Join(restoreDir, "service.db-wal")); !os.IsNotExist(err) { t.Fatal("service.db-wal should not be in archive") } if _, err := os.Stat(filepath.Join(restoreDir, "service.db-shm")); !os.IsNotExist(err) { t.Fatal("service.db-shm should not be in archive") } // Verify backups were excluded. if _, err := os.Stat(filepath.Join(restoreDir, "backups")); !os.IsNotExist(err) { t.Fatal("backups/ should not be in archive") } // Verify the VACUUM INTO snapshot was injected as the DB. dbFile := filepath.Join(restoreDir, filepath.Base(dbPath)) restoredDB, err := sql.Open("sqlite", dbFile) if err != nil { t.Fatalf("open restored db: %v", err) } defer func() { _ = restoredDB.Close() }() var val string if err := restoredDB.QueryRow("SELECT val FROM test").Scan(&val); err != nil { t.Fatalf("query restored db: %v", err) } if val != "snapshot-data" { t.Fatalf("val = %q, want %q", val, "snapshot-data") } } func TestSnapshotWithoutDB(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "config.toml"), "test = true\n") var buf bytes.Buffer err := Snapshot(SnapshotOptions{ ServiceDir: dir, }, &buf) if err != nil { t.Fatalf("Snapshot: %v", err) } // Restore and verify. restoreDir := t.TempDir() if err := Restore(&buf, restoreDir); err != nil { t.Fatalf("Restore: %v", err) } content, err := os.ReadFile(filepath.Join(restoreDir, "config.toml")) //nolint:gosec // test code if err != nil { t.Fatalf("read: %v", err) } if string(content) != "test = true\n" { t.Fatalf("content = %q", string(content)) } } func TestSnapshotExcludesLiveDB(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "service.db"), "live") writeFile(t, filepath.Join(dir, "service.db-wal"), "wal") writeFile(t, filepath.Join(dir, "other.txt"), "keep") var buf bytes.Buffer err := Snapshot(SnapshotOptions{ ServiceDir: dir, }, &buf) if err != nil { t.Fatalf("Snapshot: %v", err) } restoreDir := t.TempDir() if err := Restore(&buf, restoreDir); err != nil { t.Fatalf("Restore: %v", err) } // other.txt should be present. if _, err := os.Stat(filepath.Join(restoreDir, "other.txt")); err != nil { t.Fatalf("other.txt missing: %v", err) } // DB files should not. if _, err := os.Stat(filepath.Join(restoreDir, "service.db")); !os.IsNotExist(err) { t.Fatal("service.db should be excluded") } if _, err := os.Stat(filepath.Join(restoreDir, "service.db-wal")); !os.IsNotExist(err) { t.Fatal("service.db-wal should be excluded") } } func TestSnapshotCustomExcludes(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "keep.txt"), "keep") writeFile(t, filepath.Join(dir, "skip.log"), "skip") var buf bytes.Buffer err := Snapshot(SnapshotOptions{ ServiceDir: dir, ExcludePatterns: []string{"*.log"}, }, &buf) if err != nil { t.Fatalf("Snapshot: %v", err) } restoreDir := t.TempDir() if err := Restore(&buf, restoreDir); err != nil { t.Fatalf("Restore: %v", err) } if _, err := os.Stat(filepath.Join(restoreDir, "keep.txt")); err != nil { t.Fatal("keep.txt should be present") } if _, err := os.Stat(filepath.Join(restoreDir, "skip.log")); !os.IsNotExist(err) { t.Fatal("skip.log should be excluded") } } func TestRestoreCreatesDestDir(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "file.txt"), "data") var buf bytes.Buffer if err := Snapshot(SnapshotOptions{ServiceDir: dir}, &buf); err != nil { t.Fatalf("Snapshot: %v", err) } destDir := filepath.Join(t.TempDir(), "new", "nested", "dir") if err := Restore(&buf, destDir); err != nil { t.Fatalf("Restore: %v", err) } content, err := os.ReadFile(filepath.Join(destDir, "file.txt")) //nolint:gosec // test code if err != nil { t.Fatalf("read: %v", err) } if string(content) != "data" { t.Fatalf("content = %q", string(content)) } }