diff options
Diffstat (limited to 'internal/trash/trash_test.go')
| -rw-r--r-- | internal/trash/trash_test.go | 173 |
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)) + } +} |
