package main // 真实挂载测试:需要 /dev/fuse、fusermount3,以及 /etc/fuse.conf 中的 user_allow_other import ( "bufio" "encoding/json" "errors" "net" "os" "path/filepath" "sort" "testing" "time" ) func needFuse(t *testing.T) { if _, err := os.Stat("/dev/fuse"); err != nil { t.Skip("没有 /dev/fuse") } } func names(t *testing.T, dir string) []string { ents, err := os.ReadDir(dir) if err != nil { t.Fatalf("读取 %s:%v", dir, err) } var out []string for _, e := range ents { out = append(out, e.Name()) } sort.Strings(out) return out } func TestMountedFilesystem(t *testing.T) { needFuse(t) base, roots := tempRoots(t) src := filepath.Join(roots[0], "data", "docs") outside := filepath.Join(base, "outside") os.MkdirAll(outside, 0o755) os.WriteFile(filepath.Join(outside, "b.txt"), []byte("机密"), 0o600) os.WriteFile(filepath.Join(src, "a.txt"), []byte("资料内容"), 0o600) os.WriteFile(filepath.Join(src, ".env"), []byte("SECRET=1"), 0o600) os.MkdirAll(filepath.Join(src, "sub", ".git"), 0o700) os.WriteFile(filepath.Join(src, "sub", ".git", "config"), []byte("x"), 0o600) os.WriteFile(filepath.Join(src, "sub", "b.txt"), []byte("子目录"), 0o600) os.Symlink("a.txt", filepath.Join(src, "link")) mntDir := filepath.Join(base, "kbmnt") os.MkdirAll(mntDir, 0o755) m := newManager(realSystem{}, roots, mntDir, filepath.Join(base, "state.json")) defer m.unmountAll() st, err := m.sync([]mountSpec{{ID: "kb_t1", Kind: "local", Local: &localSource{Path: src}}}, true) if err != nil || len(st) != 1 || st[0].State != "ok" { t.Fatalf("挂载:%+v %v", st, err) } mp := filepath.Join(mntDir, "kb_t1") // 隐藏文件不出现、按路径也访问不到 if got := names(t, mp); len(got) != 3 || got[0] != "a.txt" || got[1] != "link" || got[2] != "sub" { t.Errorf("目录列表:%v", got) } if got := names(t, filepath.Join(mp, "sub")); len(got) != 1 { t.Errorf("子目录列表:%v", got) } for _, p := range []string{".env", "sub/.git", "sub/.git/config"} { if _, err := os.Stat(filepath.Join(mp, p)); !errors.Is(err, os.ErrNotExist) { t.Errorf("%s 应该不存在:%v", p, err) } } // 读取、权限、符号链接 if b, err := os.ReadFile(filepath.Join(mp, "a.txt")); err != nil || string(b) != "资料内容" { t.Errorf("读取:%q %v", b, err) } if b, err := os.ReadFile(filepath.Join(mp, "link")); err != nil || string(b) != "资料内容" { t.Errorf("相对符号链接:%q %v", b, err) } fi, _ := os.Stat(filepath.Join(mp, "a.txt")) if fi.Mode().Perm() != 0o444 { t.Errorf("文件权限应为 0444:%v", fi.Mode()) } di, _ := os.Stat(filepath.Join(mp, "sub")) if di.Mode().Perm() != 0o555 { t.Errorf("目录权限应为 0555:%v", di.Mode()) } // 只读 if err := os.WriteFile(filepath.Join(mp, "new.txt"), []byte("x"), 0o644); err == nil { t.Error("应该不能写入") } if f, err := os.OpenFile(filepath.Join(mp, "a.txt"), os.O_WRONLY, 0); err == nil { f.Close() t.Error("应该不能以写方式打开") } // 访问过程中目录被替换为指向外部的符号链接:不会读到外部内容 if _, err := os.Stat(filepath.Join(mp, "sub")); err != nil { t.Fatal(err) } os.Rename(filepath.Join(src, "sub"), filepath.Join(src, "sub.bak")) os.Symlink(outside, filepath.Join(src, "sub")) if b, err := os.ReadFile(filepath.Join(mp, "sub", "b.txt")); err == nil { t.Errorf("不应读到源目录以外的内容:%q", b) } os.Remove(filepath.Join(src, "sub")) os.Rename(filepath.Join(src, "sub.bak"), filepath.Join(src, "sub")) // 不长期持有源目录的句柄(否则设备无法卸载) fds, _ := os.ReadDir("/proc/self/fd") for _, fd := range fds { if target, _ := os.Readlink(filepath.Join("/proc/self/fd", fd.Name())); target != "" && within(target, src) { t.Errorf("仍打开着源目录中的 %s", target) } } // 原路径上换成了另一个目录(例如设备拔出后露出挂载点下面的目录):重新检查之前不提供其中的内容 os.Rename(src, src+".orig") os.MkdirAll(src, 0o755) os.WriteFile(filepath.Join(src, "other.txt"), []byte("挂载点下面的文件"), 0o644) time.Sleep(1100 * time.Millisecond) if b, err := os.ReadFile(filepath.Join(mp, "other.txt")); err == nil { t.Errorf("不应提供另一个目录的内容:%q", b) } os.RemoveAll(src) os.Rename(src+".orig", src) m.refreshAll() time.Sleep(1100 * time.Millisecond) // 源目录不存在:状态变为 path_missing,读取返回错误;恢复后自动可用 os.Rename(src, src+".moved") m.refreshAll() if s := m.status()[0]; s.State != "path_missing" { t.Errorf("源目录移走后:%+v", s) } if _, err := os.ReadDir(mp); err == nil { t.Error("不可用时读取应该出错") } if _, err := os.Stat(mp); err != nil { t.Errorf("不可用时挂载点根目录照常存在:%v", err) } os.Rename(src+".moved", src) m.refreshAll() time.Sleep(1100 * time.Millisecond) // 等内核缓存过期 if s := m.status()[0]; s.State != "ok" { t.Errorf("恢复后:%+v", s) } if got := names(t, mp); len(got) != 3 { t.Errorf("恢复后目录列表:%v", got) } // state.json 已保存;同步空列表时卸载并删除挂载点 if b, err := os.ReadFile(filepath.Join(base, "state.json")); err != nil || !contains(string(b), "kb_t1") { t.Errorf("state.json:%s %v", b, err) } if _, err := m.sync(nil, true); err != nil { t.Fatal(err) } if _, err := os.Stat(mp); !errors.Is(err, os.ErrNotExist) { t.Errorf("卸载后挂载点应删除:%v", err) } } func TestSyncRejectsBadIDs(t *testing.T) { m := newManager(realSystem{}, allowedRoots, t.TempDir(), filepath.Join(t.TempDir(), "s.json")) for _, id := range []string{"../etc", "kb_A", "x", "kb_a/b"} { if _, err := m.sync([]mountSpec{{ID: id, Kind: "local", Local: &localSource{Path: "/mnt/x"}}}, false); err == nil { t.Errorf("%q 应该被拒绝", id) } } } func TestServerToken(t *testing.T) { _, roots := tempRoots(t) s := &server{token: "secret", mgr: newManager(realSystem{}, roots, t.TempDir(), filepath.Join(t.TempDir(), "s.json")), version: "t"} call := func(req map[string]any) map[string]any { a, b := net.Pipe() go s.serveConn(b) defer a.Close() line, _ := json.Marshal(req) a.Write(append(line, '\n')) resp, err := bufio.NewReader(a).ReadBytes('\n') if err != nil { t.Fatal(err) } var out map[string]any json.Unmarshal(resp, &out) return out } if r := call(map[string]any{"token": "wrong", "op": "ping"}); r["ok"] != false || r["error"] != "密钥不正确" { t.Errorf("错误的密钥:%v", r) } if r := call(map[string]any{"op": "status"}); r["ok"] != false { t.Errorf("没有密钥:%v", r) } if r := call(map[string]any{"token": "secret", "op": "ping"}); r["ok"] != true { t.Errorf("正确的密钥:%v", r) } if r := call(map[string]any{"token": "secret", "op": "check", "path": "/etc"}); r["ok"] != false || r["state"] != "not_allowed" { t.Errorf("检查范围外的目录:%v", r) } }