omarchy-send/internal/server/destpath_test.go
28allday beb6b9040e Support sending folders, not just single files
The sender only handled regular files: a staged directory would fail at
upload time, so users could only send individual files.

Now a staged directory is walked recursively and each file is advertised
with a name relative to the folder's parent (e.g. "Trip/day1/img.jpg"),
which is the LocalSend-compatible way to carry structure. The receiver
recreates those subdirectories under the receive dir, creating parents as
needed. The path-traversal guard is preserved: names are cleaned against a
leading "/" to collapse "..", and a containment check ensures the result
stays within the receive dir.

In the TUI send picker, "a" stages the folder currently being browsed;
staged folders are tagged in the panel and the help text is updated.

Tests: directory expansion produces relative names; an end-to-end folder
send recreates the structure on the receiver; destPath preserves subdirs,
rejects traversal, and de-duplicates within subfolders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:00:52 +01:00

64 lines
1.6 KiB
Go

package server
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestDestPathPreservesSubdirs(t *testing.T) {
dir := t.TempDir()
got, err := destPath(dir, "Trip/day1/img.jpg")
if err != nil {
t.Fatal(err)
}
want := filepath.Join(dir, "Trip", "day1", "img.jpg")
if got != want {
t.Fatalf("destPath = %q, want %q", got, want)
}
// Parent directories must already exist so the writer can create the file.
if _, err := os.Stat(filepath.Dir(got)); err != nil {
t.Fatalf("parent dir not created: %v", err)
}
}
// Traversal attempts must be neutralised — the result always stays under dir.
func TestDestPathRejectsTraversal(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{
"../escape.txt",
"../../etc/passwd",
"a/../../../oops.txt",
"/abs/path.txt",
} {
got, err := destPath(dir, name)
if err != nil {
continue // rejected outright — also acceptable
}
if got != dir && !strings.HasPrefix(got, dir+string(os.PathSeparator)) {
t.Fatalf("destPath(%q) = %q escaped %q", name, got, dir)
}
}
}
// A name colliding with an existing file gets a " (n)" suffix; subdir collisions
// are resolved within their own directory.
func TestDestPathDeDup(t *testing.T) {
dir := t.TempDir()
first, _ := destPath(dir, "sub/file.txt")
if err := os.WriteFile(first, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
second, err := destPath(dir, "sub/file.txt")
if err != nil {
t.Fatal(err)
}
if second == first {
t.Fatalf("expected a de-duplicated path, got %q twice", second)
}
want := filepath.Join(dir, "sub", "file (1).txt")
if second != want {
t.Fatalf("dedup = %q, want %q", second, want)
}
}