aboutsummaryrefslogtreecommitdiff
path: root/internal/persistence/txn_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/persistence/txn_test.go')
-rw-r--r--internal/persistence/txn_test.go186
1 files changed, 186 insertions, 0 deletions
diff --git a/internal/persistence/txn_test.go b/internal/persistence/txn_test.go
new file mode 100644
index 0000000..10a809d
--- /dev/null
+++ b/internal/persistence/txn_test.go
@@ -0,0 +1,186 @@
+package persistence
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func writeTestFile(t *testing.T, path, content string) {
+ t.Helper()
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func readTestFile(t *testing.T, path string) string {
+ t.Helper()
+ data, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return string(data)
+}
+
+func TestTxnCommitAppliesAllOps(t *testing.T) {
+ dir := t.TempDir()
+ journalDir := filepath.Join(dir, ".journal")
+ writeTestFile(t, filepath.Join(dir, "a.yaml"), "old-a")
+ writeTestFile(t, filepath.Join(dir, "b.yaml"), "old-b")
+ writeTestFile(t, filepath.Join(dir, "c.yaml"), "old-c")
+
+ txn := NewTransaction(journalDir)
+ if err := txn.Replace(filepath.Join(dir, "a.yaml"), []byte("new-a"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ if err := txn.Delete(filepath.Join(dir, "b.yaml")); err != nil {
+ t.Fatal(err)
+ }
+ if err := txn.Move(filepath.Join(dir, "c.yaml"), filepath.Join(dir, "d.yaml")); err != nil {
+ t.Fatal(err)
+ }
+ if err := txn.Replace(filepath.Join(dir, "created.yaml"), []byte("created"), 0600); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := txn.Commit(); err != nil {
+ t.Fatalf("commit: %v", err)
+ }
+ if got := readTestFile(t, filepath.Join(dir, "a.yaml")); got != "new-a" {
+ t.Fatalf("replace failed: %q", got)
+ }
+ if _, err := os.Stat(filepath.Join(dir, "b.yaml")); !os.IsNotExist(err) {
+ t.Fatalf("delete failed: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(dir, "c.yaml")); !os.IsNotExist(err) {
+ t.Fatalf("move left source behind: %v", err)
+ }
+ if got := readTestFile(t, filepath.Join(dir, "d.yaml")); got != "old-c" {
+ t.Fatalf("move lost content: %q", got)
+ }
+ if got := readTestFile(t, filepath.Join(dir, "created.yaml")); got != "created" {
+ t.Fatalf("create failed: %q", got)
+ }
+ if entries, _ := ListJournalFiles(journalDir); len(entries) != 0 {
+ t.Fatalf("journal not cleaned: %v", entries)
+ }
+}
+
+func TestTxnRollbackOnFailure(t *testing.T) {
+ dir := t.TempDir()
+ journalDir := filepath.Join(dir, ".journal")
+ writeTestFile(t, filepath.Join(dir, "a.yaml"), "old-a")
+ // A file inside a read-only subdirectory makes the delete fail.
+ sub := filepath.Join(dir, "sub")
+ writeTestFile(t, filepath.Join(sub, "gone.yaml"), "old-gone")
+ if err := os.Chmod(sub, 0500); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { os.Chmod(sub, 0700) })
+
+ txn := NewTransaction(journalDir)
+ if err := txn.Replace(filepath.Join(dir, "a.yaml"), []byte("new-a"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ if err := txn.Delete(filepath.Join(sub, "gone.yaml")); err != nil {
+ t.Fatal(err)
+ }
+ if err := txn.Commit(); err == nil {
+ t.Fatal("expected commit to fail")
+ }
+
+ if got := readTestFile(t, filepath.Join(dir, "a.yaml")); got != "old-a" {
+ t.Fatalf("rollback did not restore a.yaml: %q", got)
+ }
+ if got := readTestFile(t, filepath.Join(sub, "gone.yaml")); got != "old-gone" {
+ t.Fatalf("rollback did not restore deleted file: %q", got)
+ }
+ if entries, _ := ListJournalFiles(journalDir); len(entries) != 0 {
+ t.Fatalf("journal not cleaned after rollback: %v", entries)
+ }
+}
+
+func TestRecoverRollsBackLeftoverJournal(t *testing.T) {
+ // Simulate a crash: journal exists (it says a.yaml becomes new-a and
+ // c.yaml is deleted) but the operations never ran.
+ dir := t.TempDir()
+ journalDir := filepath.Join(dir, ".journal")
+ writeTestFile(t, filepath.Join(dir, "a.yaml"), "old-a")
+ writeTestFile(t, filepath.Join(dir, "c.yaml"), "old-c")
+
+ txn := NewTransaction(journalDir)
+ if err := txn.Replace(filepath.Join(dir, "a.yaml"), []byte("new-a"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ if err := txn.Delete(filepath.Join(dir, "c.yaml")); err != nil {
+ t.Fatal(err)
+ }
+ journal, err := txn.MarshalJournal()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.MkdirAll(journalDir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := WriteFile(filepath.Join(journalDir, "txn-crash.json"), journal, 0600); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Recover(journalDir); err != nil {
+ t.Fatalf("recover: %v", err)
+ }
+ if got := readTestFile(t, filepath.Join(dir, "a.yaml")); got != "old-a" {
+ t.Fatalf("recovery did not roll back a.yaml: %q", got)
+ }
+ if got := readTestFile(t, filepath.Join(dir, "c.yaml")); got != "old-c" {
+ t.Fatalf("recovery resurrected a delete: %q", got)
+ }
+ if entries, _ := ListJournalFiles(journalDir); len(entries) != 0 {
+ t.Fatalf("recovered journal not removed: %v", entries)
+ }
+}
+
+func TestRecoverDropsCorruptJournal(t *testing.T) {
+ dir := t.TempDir()
+ journalDir := filepath.Join(dir, ".journal")
+ if err := os.MkdirAll(journalDir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(journalDir, "txn-bad.json"), []byte("{not json"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := Recover(journalDir); err != nil {
+ t.Fatalf("recover corrupt: %v", err)
+ }
+ if entries, _ := ListJournalFiles(journalDir); len(entries) != 0 {
+ t.Fatalf("corrupt journal not removed: %v", entries)
+ }
+}
+
+func TestTxnMoveOverwritesDestination(t *testing.T) {
+ dir := t.TempDir()
+ journalDir := filepath.Join(dir, ".journal")
+ writeTestFile(t, filepath.Join(dir, "src.yaml"), "src-content")
+ writeTestFile(t, filepath.Join(dir, "dst.yaml"), "dst-content")
+
+ txn := NewTransaction(journalDir)
+ if err := txn.Move(filepath.Join(dir, "src.yaml"), filepath.Join(dir, "dst.yaml")); err != nil {
+ t.Fatal(err)
+ }
+ if err := txn.Commit(); err != nil {
+ t.Fatalf("commit: %v", err)
+ }
+ if got := readTestFile(t, filepath.Join(dir, "dst.yaml")); got != "src-content" {
+ t.Fatalf("move did not overwrite destination: %q", got)
+ }
+ if _, err := os.Stat(filepath.Join(dir, "src.yaml")); !os.IsNotExist(err) {
+ t.Fatalf("source not removed: %v", err)
+ }
+ if !strings.HasSuffix(readTestFile(t, filepath.Join(dir, "dst.yaml")), "content") {
+ t.Fatal("unreachable")
+ }
+}