package backup import ( "bytes" "encoding/hex" "errors" "os" "path/filepath" "strings" "testing" ) func keyA() []byte { return []byte("0123456789abcdef0123456789abcdef") } func keyB() []byte { return []byte("abcdef0123456789abcdef0123456789") } func writeFiles(t *testing.T, root string, files map[string]string) { t.Helper() for name, body := range files { p := filepath.Join(root, name) if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { t.Fatalf("mkdir %s: %v", filepath.Dir(p), err) } if err := os.WriteFile(p, []byte(body), 0o644); err != nil { t.Fatalf("write %s: %v", p, err) } } } func runBackup(t *testing.T, src, out string, key []byte) { t.Helper() if err := Backup(BackupOptions{ SourceDir: src, OutputPath: out, MasterKey: key, }); err != nil { t.Fatalf("Backup: %v", err) } } func TestBackupRestoreRoundTrip(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") target := t.TempDir() os.RemoveAll(target) writeFiles(t, src, map[string]string{ "cluster/master.key": "KEYMATERIAL", "_defaults/db/orca.db": "SQLITE", "_defaults/.env": "FOO=bar", "_defaults/jobs/job1.md": "job body", "cluster/peers/host1/peer.json": "{}", }) runBackup(t, src, out, keyA()) if _, err := os.Stat(out + ".sig"); err != nil { t.Fatalf("sig file missing: %v", err) } if err := Restore(RestoreOptions{ InputPath: out, TargetDir: target, MasterKey: keyA(), }); err != nil { t.Fatalf("Restore: %v", err) } for name, body := range map[string]string{ "cluster/master.key": "KEYMATERIAL", "_defaults/db/orca.db": "SQLITE", "_defaults/.env": "FOO=bar", "_defaults/jobs/job1.md": "job body", "cluster/peers/host1/peer.json": "{}", } { got, err := os.ReadFile(filepath.Join(target, name)) if err != nil { t.Errorf("restored file %s missing: %v", name, err) continue } if string(got) != body { t.Errorf("restored %s = %q, want %q", name, string(got), body) } } } func TestVerifySignatureSameKey(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") writeFiles(t, src, map[string]string{"a.txt": "hello"}) runBackup(t, src, out, keyA()) if err := VerifySignature(out, out+".sig", keyA()); err != nil { t.Fatalf("verify same key: %v", err) } } func TestVerifySignatureWrongKey(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") writeFiles(t, src, map[string]string{"a.txt": "hello"}) runBackup(t, src, out, keyA()) err := VerifySignature(out, out+".sig", keyB()) if !errors.Is(err, ErrSignatureMismatch) { t.Fatalf("verify wrong key: got %v, want ErrSignatureMismatch", err) } } func TestVerifySignatureTampered(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") writeFiles(t, src, map[string]string{"a.txt": "hello"}) runBackup(t, src, out, keyA()) body, err := os.ReadFile(out) if err != nil { t.Fatalf("read tarball: %v", err) } body[0] ^= 0xff if err := os.WriteFile(out, body, 0o644); err != nil { t.Fatalf("rewrite tampered tarball: %v", err) } err = VerifySignature(out, out+".sig", keyA()) if !errors.Is(err, ErrSignatureMismatch) { t.Fatalf("verify tampered: got %v, want ErrSignatureMismatch", err) } } func TestExclusionSocketsAndRunOrca(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") target := t.TempDir() os.RemoveAll(target) writeFiles(t, src, map[string]string{ "keep.txt": "keep me", "normal.db": "main db", "sock-excluded.sock": "sock", "side.db-wal": "wal", "side.db-shm": "shm", }) runOrcaDir := filepath.Join(src, "run", "orca") if err := os.MkdirAll(runOrcaDir, 0o755); err != nil { t.Fatalf("mkdir run/orca: %v", err) } if err := os.WriteFile(filepath.Join(runOrcaDir, "ephemeral.txt"), []byte("eph"), 0o644); err != nil { t.Fatalf("write ephemeral: %v", err) } runBackup(t, src, out, keyA()) if err := Restore(RestoreOptions{ InputPath: out, TargetDir: target, MasterKey: keyA(), }); err != nil { t.Fatalf("Restore: %v", err) } for _, excluded := range []string{ "sock-excluded.sock", "side.db-wal", "side.db-shm", "run/orca/ephemeral.txt", } { if _, err := os.Stat(filepath.Join(target, excluded)); !os.IsNotExist(err) { t.Errorf("excluded file %s should not be in restore (err=%v)", excluded, err) } } for _, kept := range []string{"keep.txt", "normal.db"} { if _, err := os.Stat(filepath.Join(target, kept)); err != nil { t.Errorf("kept file %s missing from restore: %v", kept, err) } } } func TestRestoreForceFalseRefusesNonEmpty(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") writeFiles(t, src, map[string]string{"a.txt": "hello"}) runBackup(t, src, out, keyA()) target := t.TempDir() if err := os.WriteFile(filepath.Join(target, "existing.txt"), []byte("x"), 0o644); err != nil { t.Fatalf("seed target: %v", err) } err := Restore(RestoreOptions{ InputPath: out, TargetDir: target, MasterKey: keyA(), Force: false, }) if !errors.Is(err, ErrTargetNotEmpty) { t.Fatalf("restore to non-empty: got %v, want ErrTargetNotEmpty", err) } } func TestRestoreForceTrueOverwritesNonEmpty(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") writeFiles(t, src, map[string]string{"a.txt": "new"}) runBackup(t, src, out, keyA()) target := t.TempDir() if err := os.WriteFile(filepath.Join(target, "stale.txt"), []byte("old"), 0o644); err != nil { t.Fatalf("seed target: %v", err) } err := Restore(RestoreOptions{ InputPath: out, TargetDir: target, MasterKey: keyA(), Force: true, }) if err != nil { t.Fatalf("restore force: %v", err) } got, err := os.ReadFile(filepath.Join(target, "a.txt")) if err != nil { t.Fatalf("restored a.txt missing: %v", err) } if string(got) != "new" { t.Errorf("restored a.txt = %q, want %q", string(got), "new") } } func TestEmptyBackup(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") target := t.TempDir() os.RemoveAll(target) runBackup(t, src, out, keyA()) if err := VerifySignature(out, out+".sig", keyA()); err != nil { t.Fatalf("verify empty backup: %v", err) } if err := Restore(RestoreOptions{ InputPath: out, TargetDir: target, MasterKey: keyA(), }); err != nil { t.Fatalf("restore empty backup: %v", err) } entries, err := os.ReadDir(target) if err != nil { t.Fatalf("read target: %v", err) } if len(entries) != 0 { t.Errorf("empty backup restored %d entries, want 0", len(entries)) } } func TestRestoreSignatureMismatchFailsBeforeExtract(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") writeFiles(t, src, map[string]string{"a.txt": "hello"}) runBackup(t, src, out, keyA()) target := t.TempDir() os.RemoveAll(target) err := Restore(RestoreOptions{ InputPath: out, TargetDir: target, MasterKey: keyB(), }) if !errors.Is(err, ErrSignatureMismatch) { t.Fatalf("restore wrong key: got %v, want ErrSignatureMismatch", err) } if _, err := os.Stat(target); err == nil { entries, _ := os.ReadDir(target) if len(entries) != 0 { t.Errorf("target should be empty after failed verify, got %d entries", len(entries)) } } } func TestRestoreBadSignatureContent(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") writeFiles(t, src, map[string]string{"a.txt": "hello"}) runBackup(t, src, out, keyA()) if err := os.WriteFile(out+".sig", []byte("not-hex!!"), 0o644); err != nil { t.Fatalf("write bad sig: %v", err) } err := VerifySignature(out, out+".sig", keyA()) if err == nil { t.Fatal("verify bad sig content: expected error, got nil") } if strings.Contains(err.Error(), "decode signature") { return } if errors.Is(err, ErrSignatureMismatch) { return } t.Errorf("verify bad sig content: got unexpected err %v", err) } func TestBackupSignatureFileContent(t *testing.T) { src := t.TempDir() out := filepath.Join(t.TempDir(), "b.tar.gz") writeFiles(t, src, map[string]string{"a.txt": "hello"}) runBackup(t, src, out, keyA()) sig, err := os.ReadFile(out + ".sig") if err != nil { t.Fatalf("read sig: %v", err) } if dec, err := hexDecode(string(bytes.TrimSpace(sig))); err != nil { t.Fatalf("sig not hex: %v", err) } else if len(dec) != 32 { t.Errorf("sig len = %d, want 32", len(dec)) } } func hexDecode(s string) ([]byte, error) { return hex.DecodeString(s) }