clearlove2.1
1package plugin
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "clearlove/internal/config"
10 "clearlove/internal/database"
11)
12
13// setup 准备带数据库的插件测试环境(插件配置保存在 settings 表)
14func setup(t *testing.T) string {
15 t.Helper()
16 root := t.TempDir()
17 dataDir := filepath.Join(root, "data")
18 if err := os.MkdirAll(dataDir, 0o755); err != nil {
19 t.Fatal(err)
20 }
21 config.Cfg = config.Config{
22 DataDir: dataDir,
23 UploadDir: filepath.Join(root, "uploads"),
24 DBType: "sqlite",
25 SQLitePath: filepath.Join(dataDir, "clearlove.db"),
26 Installed: true,
27 Secret: "test-secret",
28 }
29 if err := database.Connect(); err != nil {
30 t.Fatalf("数据库连接失败: %v", err)
31 }
32 t.Cleanup(func() {
33 if database.DB != nil {
34 _ = database.DB.Close()
35 database.DB = nil
36 }
37 })
38 if err := database.Migrate(); err != nil {
39 t.Fatalf("数据库建表失败: %v", err)
40 }
41 return dataDir
42}
43
44// writePlugin 写入一个插件目录
45func writePlugin(t *testing.T, dataDir, name, manifest, fragment string) string {
46 t.Helper()
47 d := filepath.Join(dataDir, "plugins", name)
48 if err := os.MkdirAll(d, 0o755); err != nil {
49 t.Fatal(err)
50 }
51 if err := os.WriteFile(filepath.Join(d, "plugin.json"), []byte(manifest), 0o644); err != nil {
52 t.Fatal(err)
53 }
54 if fragment != "" {
55 if err := os.WriteFile(filepath.Join(d, "player.html"), []byte(fragment), 0o644); err != nil {
56 t.Fatal(err)
57 }
58 }
59 return d
60}
61
62// TestCallHTMLFileAndConfig 验证 html 钩子:@片段文件加载、{config:} 展开与启用开关
63func TestCallHTMLFileAndConfig(t *testing.T) {
64 dataDir := setup(t)
65 writePlugin(t, dataDir, "demo", `{
66 "name":"demo","version":"1.0.0",
67 "config":[{"key":"music","default":"/uploads/default.mp3"}],
68 "hooks":[{"hook":"footer_html","type":"html","html":"@player.html"}]
69 }`, `<audio src="{config:music}"></audio>`)
70
71 // 未启用:不注入任何内容
72 if got := CallHTML("footer_html"); got != "" {
73 t.Fatalf("未启用的插件不应注入内容, got %q", got)
74 }
75
76 SetEnabled("demo", true)
77 if got := CallHTML("footer_html"); got != `<audio src="/uploads/default.mp3"></audio>` {
78 t.Fatalf("默认配置未展开: %q", got)
79 }
80
81 p := Load()["demo"]
82 if p == nil {
83 t.Fatal("插件未被加载")
84 }
85 if err := SaveConfig(p, map[string]string{"music": "/uploads/plugins/demo/audio-abc.mp3"}); err != nil {
86 t.Fatal(err)
87 }
88 if got := CallHTML("footer_html"); !strings.Contains(got, "/uploads/plugins/demo/audio-abc.mp3") {
89 t.Fatalf("配置值未注入页面: %q", got)
90 }
91
92 // 片段文件缺失时应跳过该钩子,而不是把 "@player.html" 原样注入页面
93 if err := os.Remove(filepath.Join(dataDir, "plugins", "demo", "player.html")); err != nil {
94 t.Fatal(err)
95 }
96 if got := CallHTML("footer_html"); strings.Contains(got, "@player.html") {
97 t.Fatalf("片段缺失时不应注入占位文本: %q", got)
98 }
99}
100
101// TestHookFragmentTraversal @片段文件不允许路径穿越
102func TestHookFragmentTraversal(t *testing.T) {
103 dataDir := setup(t)
104 if err := os.WriteFile(filepath.Join(dataDir, "secret.txt"), []byte("TOP-SECRET"), 0o644); err != nil {
105 t.Fatal(err)
106 }
107 writePlugin(t, dataDir, "evil", `{
108 "name":"evil","version":"1.0.0",
109 "hooks":[{"hook":"footer_html","type":"html","html":"@../secret.txt"}]
110 }`, "")
111 SetEnabled("evil", true)
112 if got := CallHTML("footer_html"); strings.Contains(got, "TOP-SECRET") {
113 t.Fatalf("存在路径穿越读取: %q", got)
114 }
115}