From 9666676766bf21364f4fc4d0d1d336e72f2a91b4 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 6 Oct 2026 12:57:23 -0400 Subject: refactor: split casino.go, dedup helpers, remove audit docs --- internal/persistence/txn_containment_test.go | 79 ++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 internal/persistence/txn_containment_test.go (limited to 'internal/persistence/txn_containment_test.go') diff --git a/internal/persistence/txn_containment_test.go b/internal/persistence/txn_containment_test.go new file mode 100644 index 0000000..cfcd4c2 --- /dev/null +++ b/internal/persistence/txn_containment_test.go @@ -0,0 +1,79 @@ +package persistence + +import ( + "os" + "path/filepath" + "testing" +) + +func TestTxnMoveRejectsSamePath(t *testing.T) { + dir := t.TempDir() + txn := NewTransaction(filepath.Join(dir, ".journal")) + p := filepath.Join(dir, "a.yaml") + if err := txn.Move(p, p); err == nil { + t.Fatal("expected Move(path, path) to be rejected") + } +} + +// A path that already received a Replace as a move destination carries an +// after-image in its move-target record; a later Move would silently drop it. +func TestTxnMoveRejectsReplacedMoveDestination(t *testing.T) { + dir := t.TempDir() + a := filepath.Join(dir, "a.yaml") + b := filepath.Join(dir, "b.yaml") + writeTestFile(t, a, "A") + writeTestFile(t, b, "B") + txn := NewTransaction(filepath.Join(dir, ".journal")) + if err := txn.Move(a, b); err != nil { + t.Fatal(err) + } + if err := txn.Replace(b, []byte("X"), 0644); err != nil { + t.Fatal(err) + } + if err := txn.Move(b, filepath.Join(dir, "c.yaml")); err == nil { + t.Fatal("expected Move over a replaced move destination to be rejected") + } +} + +// A journal on disk is untrusted input: recovery must not replay a before-image +// at a path outside the journal's data root. +func TestRecoverRejectsEscapingOpPath(t *testing.T) { + dir := t.TempDir() + outside := t.TempDir() + journalDir := filepath.Join(dir, ".journal") + if err := os.MkdirAll(journalDir, 0700); err != nil { + t.Fatal(err) + } + escape := filepath.Join(outside, "secret.yaml") + body := `{"version":1,"ops":[{"path":"` + escape + `","kind":"delete","old_exists":false}]}` + evil := filepath.Join(journalDir, "txn-evil.json") + if err := os.WriteFile(evil, []byte(body), 0600); err != nil { + t.Fatal(err) + } + if err := Recover(journalDir); err == nil { + t.Fatal("expected Recover to reject an escaping op path") + } + // Fail loud and keep the journal rather than silently touching the path. + if _, err := os.Stat(evil); err != nil { + t.Fatalf("journal should be retained on rejection: %v", err) + } + if _, err := os.Stat(escape); !os.IsNotExist(err) { + t.Fatalf("escaping path must not be touched: %v", err) + } +} + +func TestRecoverRejectsEscapingMoveTarget(t *testing.T) { + dir := t.TempDir() + outside := t.TempDir() + journalDir := filepath.Join(dir, ".journal") + if err := os.MkdirAll(journalDir, 0700); err != nil { + t.Fatal(err) + } + body := `{"version":1,"ops":[{"path":"` + filepath.Join(dir, "a.yaml") + `","kind":"move","move_to":"` + filepath.Join(outside, "b.yaml") + `"}]}` + if err := os.WriteFile(filepath.Join(journalDir, "txn-evil.json"), []byte(body), 0600); err != nil { + t.Fatal(err) + } + if err := Recover(journalDir); err == nil { + t.Fatal("expected Recover to reject an escaping move target") + } +} -- cgit v1.2.3