diff options
Diffstat (limited to 'internal/persistence/txn_test.go')
| -rw-r--r-- | internal/persistence/txn_test.go | 186 |
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") + } +} |
