aboutsummaryrefslogtreecommitdiff
path: root/internal/trash/trash_test.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 20:14:47 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 20:14:47 +0200
commit3f8679be9373ee7508d512dfdfc1dda0839c7f90 (patch)
treeec02eb075f6c4e90f21baa2fe674e86a2f7f6a62 /internal/trash/trash_test.go
parent24a84671ace373ae331fa83a1ff484990f4dff0e (diff)
downloadkrino-3f8679be9373ee7508d512dfdfc1dda0839c7f90.tar.gz
krino-3f8679be9373ee7508d512dfdfc1dda0839c7f90.zip
krino: acting — trash, journal, apply, lock, review, undo
Diffstat (limited to 'internal/trash/trash_test.go')
-rw-r--r--internal/trash/trash_test.go173
1 files changed, 173 insertions, 0 deletions
diff --git a/internal/trash/trash_test.go b/internal/trash/trash_test.go
new file mode 100644
index 0000000..24f4ea8
--- /dev/null
+++ b/internal/trash/trash_test.go
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package trash
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// sandbox points XDG_DATA_HOME at a temporary tree, so the real Trash is
+// never touched.
+func sandbox(t *testing.T) string {
+ t.Helper()
+ h := t.TempDir()
+ t.Setenv("HOME", h)
+ t.Setenv("XDG_DATA_HOME", filepath.Join(h, "share"))
+ t.Setenv("XDG_CONFIG_HOME", "")
+ t.Setenv("XDG_STATE_HOME", "")
+ t.Setenv("XDG_CACHE_HOME", "")
+ return h
+}
+
+func write(t *testing.T, path, content string) {
+ t.Helper()
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPutWritesBothParts(t *testing.T) {
+ h := sandbox(t)
+ src := filepath.Join(h, "dl", "old report.pdf")
+ write(t, src, "pdf")
+
+ entry, err := Put(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if entry != "old report.pdf" {
+ t.Errorf("entry = %q, want the base name", entry)
+ }
+ if _, err := os.Stat(src); !os.IsNotExist(err) {
+ t.Error("the original is still in place")
+ }
+ if b, err := os.ReadFile(filepath.Join(Dir(), "files", entry)); err != nil || string(b) != "pdf" {
+ t.Errorf("trashed content = %q, %v", b, err)
+ }
+ info, err := os.ReadFile(filepath.Join(Dir(), "info", entry+".trashinfo"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(info)
+ if !strings.HasPrefix(got, "[Trash Info]\n") {
+ t.Errorf("trashinfo lacks its header:\n%s", got)
+ }
+ if !strings.Contains(got, "Path="+strings.ReplaceAll(src, " ", "%20")+"\n") {
+ t.Errorf("Path is not the absolute percent-encoded original:\n%s", got)
+ }
+ // DeletionDate is local time with no offset: 19 characters, no Z, no +.
+ for _, line := range strings.Split(got, "\n") {
+ if v, ok := strings.CutPrefix(line, "DeletionDate="); ok {
+ if len(v) != 19 || strings.ContainsAny(v, "Z+") {
+ t.Errorf("DeletionDate = %q; want local time like 2026-04-23T16:04:23", v)
+ }
+ }
+ }
+}
+
+func TestPutSuffixesOnCollision(t *testing.T) {
+ h := sandbox(t)
+ first := filepath.Join(h, "a", "x.pdf")
+ second := filepath.Join(h, "b", "x.pdf")
+ write(t, first, "one")
+ write(t, second, "two")
+
+ if _, err := Put(first); err != nil {
+ t.Fatal(err)
+ }
+ entry, err := Put(second)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if entry != "x_1.pdf" {
+ t.Fatalf("second entry = %q, want x_1.pdf", entry)
+ }
+ if b, _ := os.ReadFile(filepath.Join(Dir(), "files", "x.pdf")); string(b) != "one" {
+ t.Error("the first entry was overwritten")
+ }
+ if b, _ := os.ReadFile(filepath.Join(Dir(), "files", "x_1.pdf")); string(b) != "two" {
+ t.Error("the second entry holds the wrong content")
+ }
+ info, err := os.ReadFile(filepath.Join(Dir(), "info", "x_1.pdf.trashinfo"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ wantPath := "Path=" + strings.ReplaceAll(second, " ", "%20") + "\n"
+ if !strings.Contains(string(info), wantPath) {
+ t.Errorf("x_1.pdf.trashinfo does not point at its own original %q:\n%s", second, info)
+ }
+}
+
+func TestRestoreRoundTrips(t *testing.T) {
+ h := sandbox(t)
+ src := filepath.Join(h, "dl", "zażółć gęślą.pdf")
+ write(t, src, "polish")
+
+ entry, err := Put(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ restored, err := Restore(entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if restored != src {
+ t.Errorf("restored to %q, want %q", restored, src)
+ }
+ if b, err := os.ReadFile(src); err != nil || string(b) != "polish" {
+ t.Errorf("content after restore = %q, %v", b, err)
+ }
+ if _, err := os.Stat(filepath.Join(Dir(), "info", entry+".trashinfo")); !os.IsNotExist(err) {
+ t.Error("the .trashinfo was left behind")
+ }
+}
+
+func TestRestoreRefusesWhenTargetExists(t *testing.T) {
+ h := sandbox(t)
+ src := filepath.Join(h, "dl", "x.pdf")
+ write(t, src, "one")
+ entry, err := Put(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ write(t, src, "something new")
+ if _, err := Restore(entry); err == nil {
+ t.Fatal("Restore overwrote a file that had taken the original path")
+ }
+ if b, _ := os.ReadFile(src); string(b) != "something new" {
+ t.Error("the file at the original path was modified")
+ }
+ if _, err := os.Stat(filepath.Join(Dir(), "files", entry)); err != nil {
+ t.Error("the trash entry was consumed by a refused restore")
+ }
+}
+
+// TestPutRefusesOtherFilesystem needs a second filesystem. /dev/shm is one on
+// Linux; the test skips where there is none.
+func TestPutRefusesOtherFilesystem(t *testing.T) {
+ sandbox(t)
+ other, err := os.MkdirTemp("/dev/shm", "krino-trash-")
+ if err != nil {
+ t.Skip("no second filesystem available:", err)
+ }
+ defer os.RemoveAll(other)
+ src := filepath.Join(other, "x.pdf")
+ write(t, src, "elsewhere")
+
+ if _, err := Put(src); !errors.Is(err, ErrOtherFilesystem) {
+ t.Fatalf("Put across filesystems: err = %v, want ErrOtherFilesystem", err)
+ }
+ if b, err := os.ReadFile(src); err != nil || string(b) != "elsewhere" {
+ t.Errorf("the file was disturbed by a refused Put: %q, %v", b, err)
+ }
+ if entries, _ := os.ReadDir(filepath.Join(Dir(), "info")); len(entries) != 0 {
+ t.Errorf("a refused Put left %d orphaned info files", len(entries))
+ }
+}