| 1 | //go:build (linux || darwin || freebsd) && !nofuse |
| 2 | |
| 3 | package writable |
| 4 | |
| 5 | import ( |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/hanwen/go-fuse/v2/fuse" |
| 9 | dag "github.com/ipfs/boxo/ipld/merkledag" |
| 10 | fusemnt "github.com/ipfs/kubo/fuse/mount" |
| 11 | ) |
| 12 | |
| 13 | // TestNewDirNormalizesBlksize verifies that callers who don't plumb |
| 14 | // Import.UnixFSChunker through (e.g. test-only mounts) get the FUSE |
| 15 | // default so stat still advertises a usable st_blksize. |
| 16 | func TestNewDirNormalizesBlksize(t *testing.T) { |
| 17 | t.Run("zero falls back to DefaultBlksize", func(t *testing.T) { |
| 18 | cfg := &Config{DAG: dag.NewDAGService(nil)} |
| 19 | NewDir(nil, cfg) |
| 20 | if cfg.Blksize != fusemnt.DefaultBlksize { |
| 21 | t.Fatalf("Blksize = %d, want DefaultBlksize (%d)", |
| 22 | cfg.Blksize, fusemnt.DefaultBlksize) |
| 23 | } |
| 24 | }) |
| 25 | |
| 26 | t.Run("explicit value passes through", func(t *testing.T) { |
| 27 | cfg := &Config{DAG: dag.NewDAGService(nil), Blksize: 65536} |
| 28 | NewDir(nil, cfg) |
| 29 | if cfg.Blksize != 65536 { |
| 30 | t.Fatalf("Blksize = %d, want 65536", cfg.Blksize) |
| 31 | } |
| 32 | }) |
| 33 | } |
| 34 | |
| 35 | // TestSymlinkSetattrChmodNoError verifies that Setattr on a symlink |
| 36 | // with only a mode change is silently accepted. POSIX symlinks have no |
| 37 | // meaningful permission bits (access control uses the target's mode), |
| 38 | // so handlers must not return an error when the kernel forwards a |
| 39 | // chmod-on-symlink request (e.g. via BSD lchmod or fchmodat with |
| 40 | // AT_SYMLINK_NOFOLLOW). Tools like rsync depend on this contract. |
| 41 | // |
| 42 | // This is a unit test rather than an integration test because Linux |
| 43 | // usually rejects fchmodat(AT_SYMLINK_NOFOLLOW) at the VFS layer with |
| 44 | // EOPNOTSUPP and never forwards it to the FUSE filesystem, so a |
| 45 | // userspace test would not actually exercise this code path. |
| 46 | func TestSymlinkSetattrChmodNoError(t *testing.T) { |
| 47 | // MFSFile is nil: Setattr must still succeed without dereferencing |
| 48 | // it. StoreMode is true to confirm that even when persistence is |
| 49 | // enabled, mode changes on symlinks are silently dropped. |
| 50 | s := &Symlink{ |
| 51 | Target: "/some/target", |
| 52 | Cfg: &Config{StoreMode: true}, |
| 53 | } |
| 54 | |
| 55 | in := &fuse.SetAttrIn{} |
| 56 | in.Valid = fuse.FATTR_MODE |
| 57 | in.Mode = 0o600 |
| 58 | |
| 59 | out := &fuse.AttrOut{} |
| 60 | if errno := s.Setattr(t.Context(), nil, in, out); errno != 0 { |
| 61 | t.Fatalf("Symlink.Setattr returned errno %v, want 0", errno) |
| 62 | } |
| 63 | |
| 64 | // fillAttr must report the POSIX symlink mode (0o777), not the |
| 65 | // caller-supplied value, because the request is not stored. |
| 66 | if got := out.Attr.Mode & 0o777; got != 0o777 { |
| 67 | t.Fatalf("Symlink mode = 0o%o, want 0o777", got) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | // TestStatfsReportsSpace verifies that Dir.Statfs proxies the |
| 72 | // disk-space statistics of the repo's backing filesystem, and that an |
| 73 | // empty RepoPath produces zeroed (but successful) results. |
| 74 | func TestStatfsReportsSpace(t *testing.T) { |
| 75 | t.Run("matches repo filesystem", func(t *testing.T) { |
| 76 | dir := t.TempDir() |
| 77 | d := &Dir{Cfg: &Config{RepoPath: dir}} |
| 78 | out := &fuse.StatfsOut{} |
| 79 | if errno := d.Statfs(t.Context(), out); errno != 0 { |
| 80 | t.Fatalf("Statfs returned errno %v, want 0", errno) |
| 81 | } |
| 82 | |
| 83 | // Verify we got real filesystem data (non-zero) and that |
| 84 | // free blocks don't exceed total blocks. Exact comparison |
| 85 | // against a second syscall.Statfs call is racy because CI |
| 86 | // writes can change block counts between the two calls. |
| 87 | if out.Blocks == 0 { |
| 88 | t.Fatal("Blocks = 0, expected non-zero for a real filesystem") |
| 89 | } |
| 90 | if out.Bfree > out.Blocks { |
| 91 | t.Fatalf("Bfree (%d) > Blocks (%d)", out.Bfree, out.Blocks) |
| 92 | } |
| 93 | }) |
| 94 | |
| 95 | t.Run("empty repo path", func(t *testing.T) { |
| 96 | d := &Dir{Cfg: &Config{}} |
| 97 | out := &fuse.StatfsOut{} |
| 98 | if errno := d.Statfs(t.Context(), out); errno != 0 { |
| 99 | t.Fatalf("Statfs returned errno %v, want 0", errno) |
| 100 | } |
| 101 | if out.Blocks != 0 { |
| 102 | t.Fatalf("expected zeroed Blocks when RepoPath is empty, got %d", out.Blocks) |
| 103 | } |
| 104 | }) |
| 105 | } |