package tenant import ( "errors" "os" "path/filepath" "runtime" "testing" ) func newFS(t *testing.T) (*FS, string) { t.Helper() dir := t.TempDir() fs, err := Open(filepath.Join(dir, "user")) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = fs.Close() }) return fs, dir } func TestWriteReadStat(t *testing.T) { fs, _ := newFS(t) if err := fs.WriteFile("a/b/note.md", []byte("hello")); err != nil { t.Fatal(err) } got, err := fs.ReadFile("a/b/note.md") if err != nil { t.Fatal(err) } if string(got) != "hello" { t.Errorf("got %q", got) } fi, err := fs.Stat("a/b/note.md") if err != nil { t.Fatal(err) } if fi.Size() != 5 { t.Errorf("size %d", fi.Size()) } } func TestRemove(t *testing.T) { fs, _ := newFS(t) _ = fs.WriteFile("x.md", []byte("x")) if err := fs.Remove("x.md"); err != nil { t.Fatal(err) } if _, err := fs.ReadFile("x.md"); !errors.Is(err, ErrNotFound) { t.Errorf("expected ErrNotFound, got %v", err) } } func TestList(t *testing.T) { fs, _ := newFS(t) _ = fs.WriteFile("one.md", []byte("1")) _ = fs.WriteFile("sub/two.md", []byte("2")) root, err := fs.List(".") if err != nil { t.Fatal(err) } if len(root) != 2 { t.Errorf("root entries: %v", root) } sub, err := fs.List("sub") if err != nil { t.Fatal(err) } if len(sub) != 1 || sub[0] != "two.md" { t.Errorf("sub entries: %v", sub) } } // TestPathTraversalAttacks covers every traversal vector we care // about. They must all be blocked. func TestPathTraversalAttacks(t *testing.T) { fs, dir := newFS(t) // Plant a file outside the tenant root that we'll try to reach. outside := filepath.Join(dir, "secret.txt") if err := os.WriteFile(outside, []byte("classified"), 0o600); err != nil { t.Fatal(err) } cases := []string{ "../secret.txt", "../../secret.txt", "a/../../secret.txt", "./../secret.txt", "/etc/passwd", `\windows\system32`, "\x00", "foo\x00bar", "..", ".", "", } for _, p := range cases { t.Run(p, func(t *testing.T) { if _, err := fs.ReadFile(p); err == nil { t.Errorf("ReadFile(%q) succeeded unexpectedly", p) } if err := fs.WriteFile(p, []byte("x")); err == nil { t.Errorf("WriteFile(%q) succeeded unexpectedly", p) } if err := fs.Remove(p); err == nil { t.Errorf("Remove(%q) succeeded unexpectedly", p) } }) } } // TestSymlinkEscapeBlocked verifies that a symlink whose target is // outside the tenant root cannot be used to read or write files. // os.Root refuses to follow symlinks that escape the root. func TestSymlinkEscapeBlocked(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("symlink semantics differ on windows") } fs, dir := newFS(t) outside := filepath.Join(dir, "outside.txt") if err := os.WriteFile(outside, []byte("classified"), 0o600); err != nil { t.Fatal(err) } // Drop a symlink inside the tenant dir pointing at the outside // file. We do this via os directly because FS doesn't expose // symlink creation (and rightfully so). if err := os.Symlink(outside, filepath.Join(fs.Dir(), "escape")); err != nil { t.Fatal(err) } if _, err := fs.ReadFile("escape"); err == nil { t.Errorf("ReadFile through symlink succeeded; expected error") } } func TestRootIsolation(t *testing.T) { // Two FS instances rooted at sibling dirs cannot see each other. parent := t.TempDir() a, err := Open(filepath.Join(parent, "a")) if err != nil { t.Fatal(err) } defer a.Close() b, err := Open(filepath.Join(parent, "b")) if err != nil { t.Fatal(err) } defer b.Close() _ = a.WriteFile("note.md", []byte("a")) if _, err := b.ReadFile("note.md"); !errors.Is(err, ErrNotFound) { t.Errorf("b read a's file: %v", err) } if _, err := b.ReadFile("../a/note.md"); err == nil { t.Errorf("b reached a via traversal") } } func TestMissingFileReturnsErrNotFound(t *testing.T) { fs, _ := newFS(t) if _, err := fs.ReadFile("missing.md"); !errors.Is(err, ErrNotFound) { t.Errorf("got %v", err) } if _, err := fs.Stat("missing.md"); !errors.Is(err, ErrNotFound) { t.Errorf("got %v", err) } } func TestPermissions(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("permission semantics differ on windows") } fs, _ := newFS(t) fi, err := os.Stat(fs.Dir()) if err != nil { t.Fatal(err) } if mode := fi.Mode().Perm(); mode != 0o700 { t.Errorf("expected 0700 on tenant dir, got %o", mode) } }