master
go 188 lines 5.27 KB
Raw
1 //go:build (linux || darwin || freebsd) && !nofuse
2
3 // Unit tests for the /ipns FUSE mount.
4 // Generic writable operations are exercised by the shared suite in
5 // fusetest.RunWritableSuite. This file contains the mount factory
6 // and IPNS-specific tests only.
7
8 package ipns
9
10 import (
11 "bytes"
12 "context"
13 "os"
14 "testing"
15
16 "github.com/hanwen/go-fuse/v2/fs"
17 "github.com/hanwen/go-fuse/v2/fuse"
18 "github.com/stretchr/testify/require"
19
20 "github.com/ipfs/kubo/config"
21 "github.com/ipfs/kubo/core"
22 coreapi "github.com/ipfs/kubo/core/coreapi"
23 iface "github.com/ipfs/kubo/core/coreiface"
24 "github.com/ipfs/kubo/fuse/fusetest"
25 fusemnt "github.com/ipfs/kubo/fuse/mount"
26 "github.com/ipfs/kubo/fuse/writable"
27 )
28
29 type mountWrap struct {
30 Dir string
31 Root *Root
32 server *fuse.Server
33 closed bool
34 }
35
36 func (m *mountWrap) Close() {
37 if m.closed {
38 return
39 }
40 m.closed = true
41 if m.server != nil {
42 _ = m.server.Unmount()
43 }
44 _ = m.Root.Close()
45 }
46
47 // fakeMount is a minimal mount.Mount that reports itself as active.
48 // This simulates the real daemon path where node.Mounts.Ipns is set
49 // after the FUSE filesystem is mounted, ensuring that checkPublishAllowed
50 // is actually exercised during tests (see issue #2168).
51 type fakeMount struct{}
52
53 func (fakeMount) MountPoint() string { return "/fake/ipns" }
54 func (fakeMount) Unmount() error { return nil }
55 func (fakeMount) IsActive() bool { return true }
56
57 func setupIpnsTest(t *testing.T, nd *core.IpfsNode, cfgs ...config.Mounts) (*core.IpfsNode, *mountWrap) {
58 t.Helper()
59 fusetest.SkipUnlessFUSE(t)
60
61 var cfg config.Mounts
62 if len(cfgs) > 0 {
63 cfg = cfgs[0]
64 }
65
66 var err error
67 if nd == nil {
68 nd, err = core.NewNode(context.Background(), &core.BuildCfg{})
69 require.NoError(t, err)
70
71 err = InitializeKeyspace(nd, nd.PrivateKey)
72 require.NoError(t, err)
73 }
74
75 coreAPI, err := coreapi.NewCoreAPI(nd)
76 require.NoError(t, err)
77
78 key, err := coreAPI.Key().Self(nd.Context())
79 require.NoError(t, err)
80
81 root, err := CreateRoot(nd.Context(), coreAPI, map[string]iface.Key{"local": key}, "", "", nd.Repo.Path(), cfg, config.Import{})
82 require.NoError(t, err)
83
84 mntDir := t.TempDir()
85 server, err := fs.Mount(mntDir, root, &fs.Options{
86 NullPermissions: true,
87 UID: uint32(os.Getuid()),
88 GID: uint32(os.Getgid()),
89 EntryTimeout: &mutableCacheTime,
90 AttrTimeout: &mutableCacheTime,
91 MountOptions: fuse.MountOptions{
92 FsName: "kubo-test",
93 MaxReadAhead: fusemnt.MaxReadAhead,
94 ExtraCapabilities: fusemnt.WritableMountCapabilities,
95 },
96 })
97 fusetest.MountError(t, err)
98
99 mnt := &mountWrap{Dir: mntDir, Root: root, server: server}
100 t.Cleanup(mnt.Close)
101
102 nd.Mounts.Ipns = fakeMount{}
103 return nd, mnt
104 }
105
106 // newIpnsMount is the factory for the shared writable suite. It creates
107 // an IPNS mount and returns the writable /local directory path.
108 func newIpnsMount(t *testing.T, cfg writable.Config) string {
109 t.Helper()
110 mountsCfg := config.Mounts{}
111 if cfg.StoreMtime {
112 mountsCfg.StoreMtime = config.True
113 }
114 if cfg.StoreMode {
115 mountsCfg.StoreMode = config.True
116 }
117 _, mnt := setupIpnsTest(t, nil, mountsCfg)
118 return mnt.Dir + "/local"
119 }
120
121 func TestWritableSuite(t *testing.T) {
122 fusetest.RunWritableSuite(t, newIpnsMount)
123 }
124
125 // TestIpnsLocalLink verifies that /ipns/local is a symlink to the
126 // node's own peer ID directory.
127 func TestIpnsLocalLink(t *testing.T) {
128 nd, mnt := setupIpnsTest(t, nil)
129
130 target, err := os.Readlink(mnt.Dir + "/local")
131 require.NoError(t, err)
132 require.Equal(t, nd.Identity.String(), target)
133 }
134
135 // TestNamespaceRootMode verifies that the /ipns root has execute-only
136 // mode (not listable, only traversable).
137 func TestNamespaceRootMode(t *testing.T) {
138 _, mnt := setupIpnsTest(t, nil)
139
140 info, err := os.Stat(mnt.Dir)
141 require.NoError(t, err)
142 require.Equal(t, os.FileMode(0o111), info.Mode().Perm())
143 }
144
145 // TestFilePersistence verifies that file data survives unmount and remount.
146 func TestFilePersistence(t *testing.T) {
147 nd, mnt := setupIpnsTest(t, nil)
148
149 data := fusetest.RandBytes(4000)
150 require.NoError(t, os.WriteFile(mnt.Dir+"/local/persist", data, 0o644))
151 mnt.Close()
152
153 _, mnt = setupIpnsTest(t, nd)
154 got, err := os.ReadFile(mnt.Dir + "/local/persist")
155 require.NoError(t, err)
156 require.True(t, bytes.Equal(data, got))
157 }
158
159 // TestMultipleDirs verifies nested directories persist across remount.
160 func TestMultipleDirs(t *testing.T) {
161 nd, mnt := setupIpnsTest(t, nil)
162
163 require.NoError(t, os.Mkdir(mnt.Dir+"/local/test1", 0o755))
164 data1 := fusetest.WriteFileOrFail(t, 4000, mnt.Dir+"/local/test1/file1")
165 require.NoError(t, os.Mkdir(mnt.Dir+"/local/test1/dir2", 0o755))
166 data2 := fusetest.WriteFileOrFail(t, 5000, mnt.Dir+"/local/test1/dir2/file2")
167
168 mnt.Close()
169 _, mnt = setupIpnsTest(t, nd)
170
171 fusetest.CheckExists(t, mnt.Dir+"/local/test1")
172 fusetest.VerifyFile(t, mnt.Dir+"/local/test1/file1", data1)
173 fusetest.VerifyFile(t, mnt.Dir+"/local/test1/dir2/file2", data2)
174 }
175
176 // TestStatfs verifies that statfs on the /ipns mount reports the disk
177 // space of the repo's backing filesystem. macOS Finder refuses to copy
178 // files onto a volume that reports zero free space.
179 func TestStatfs(t *testing.T) {
180 _, mnt := setupIpnsTest(t, nil)
181
182 // The in-memory test repo returns "" for Path(), so point RepoPath
183 // at a real directory to exercise the syscall path.
184 repoDir := t.TempDir()
185 mnt.Root.RepoPath = repoDir
186
187 fusetest.AssertStatfsNonZero(t, mnt.Dir)
188 }