aboutsummaryrefslogtreecommitdiff
path: root/internal/apply/swap_test.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 20:12:35 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 20:12:35 +0200
commit792e6e5416a51fb05f7171aa7ef4f9a284a8cf68 (patch)
treefe306d8517a2f4e6f6d9f4e171973ddee1171dea /internal/apply/swap_test.go
parentce9f7b10cf5025bdc2dfc08144f7efe011410a67 (diff)
downloadkrino-792e6e5416a51fb05f7171aa7ef4f9a284a8cf68.tar.gz
krino-792e6e5416a51fb05f7171aa7ef4f9a284a8cf68.zip
plan 8: apply refuses a source swapped for a symlink or another file
Diffstat (limited to 'internal/apply/swap_test.go')
-rw-r--r--internal/apply/swap_test.go101
1 files changed, 101 insertions, 0 deletions
diff --git a/internal/apply/swap_test.go b/internal/apply/swap_test.go
new file mode 100644
index 0000000..fe7542e
--- /dev/null
+++ b/internal/apply/swap_test.go
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package apply
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "krino/internal/plan"
+ "krino/internal/scan"
+)
+
+// planned writes body to root/rel and returns the chain a plan would build
+// for it, identity included, with steps.
+func planned(t *testing.T, root, rel, body string, steps ...plan.Step) plan.Chain {
+ t.Helper()
+ p := filepath.Join(root, rel)
+ if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ info, err := os.Lstat(p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return plan.Chain{File: scan.NewFile(p, rel, info), Steps: steps}
+}
+
+// TestChainRefusesSourceSwappedForSymlink: a file replaced by a symlink
+// between plan and apply - even to a file of the same size and modification
+// time - is not acted on, so a copy never reads through the link.
+func TestChainRefusesSourceSwappedForSymlink(t *testing.T) {
+ root := t.TempDir()
+ p := filepath.Join(root, "a.txt")
+ out := filepath.Join(root, "Out", "a.txt")
+ c := planned(t, root, "a.txt", "12345", plan.Step{Kind: plan.Copy, Src: p, Dst: out})
+ target := filepath.Join(root, "secret.txt")
+ if err := os.WriteFile(target, []byte("54321"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chtimes(target, c.File.ModTime, c.File.ModTime); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Remove(p); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(target, p); err != nil {
+ t.Fatal(err)
+ }
+ res := Chain(c)
+ if res[0].Status != "failed" || !strings.HasPrefix(res[0].Detail, "changed since plan") {
+ t.Fatalf("step = %s %q, want failed: changed since plan", res[0].Status, res[0].Detail)
+ }
+ if _, err := os.Lstat(out); !os.IsNotExist(err) {
+ t.Errorf("the copy was made through the symlink: %v", err)
+ }
+}
+
+// TestChainRefusesSourceReplacedByAnotherFile: another file renamed into
+// the planned path, with the same size and modification time, has another
+// inode: it is not the file that was planned.
+func TestChainRefusesSourceReplacedByAnotherFile(t *testing.T) {
+ root := t.TempDir()
+ p := filepath.Join(root, "a.txt")
+ c := planned(t, root, "a.txt", "12345", plan.Step{Kind: plan.Move, Src: p, Dst: filepath.Join(root, "Out", "a.txt")})
+ other := filepath.Join(root, "other.txt")
+ if err := os.WriteFile(other, []byte("54321"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chtimes(other, c.File.ModTime, c.File.ModTime); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Rename(other, p); err != nil {
+ t.Fatal(err)
+ }
+ res := Chain(c)
+ if res[0].Status != "failed" || !strings.HasPrefix(res[0].Detail, "changed since plan") {
+ t.Fatalf("step = %s %q, want failed: changed since plan", res[0].Status, res[0].Detail)
+ }
+ if b, err := os.ReadFile(p); err != nil || string(b) != "54321" {
+ t.Errorf("the replacement was moved: %q, %v", b, err)
+ }
+}
+
+// TestChainFollowsItsOwnFile: the file's identity check does not stop a
+// chain that renames and then moves the planned file itself.
+func TestChainFollowsItsOwnFile(t *testing.T) {
+ root := t.TempDir()
+ p := filepath.Join(root, "a.txt")
+ b := filepath.Join(root, "b.txt")
+ c := planned(t, root, "a.txt", "12345",
+ plan.Step{Kind: plan.Rename, Src: p, Dst: b},
+ plan.Step{Kind: plan.Move, Src: b, Dst: filepath.Join(root, "Out", "b.txt")},
+ )
+ for i, r := range Chain(c) {
+ if r.Status != "ok" {
+ t.Errorf("step %d: %s %q", i+1, r.Status, r.Detail)
+ }
+ }
+}