diff options
| author | historia <[not public]> | 2026-10-06 12:57:23 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-10-06 12:57:23 -0400 |
| commit | 9666676766bf21364f4fc4d0d1d336e72f2a91b4 (patch) | |
| tree | 0cade34091d7602790dc38cd44053f0b74bbadf2 /internal/persistence/txn_containment_test.go | |
| parent | 4bc1daaf259e0c19c5fe4a12ec8bc7f6e5a735c1 (diff) | |
| download | thehouseoficarus-9666676766bf21364f4fc4d0d1d336e72f2a91b4.tar.gz | |
refactor: split casino.go, dedup helpers, remove audit docs
Diffstat (limited to 'internal/persistence/txn_containment_test.go')
| -rw-r--r-- | internal/persistence/txn_containment_test.go | 79 |
1 files changed, 79 insertions, 0 deletions
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") + } +} |
