package plugin import ( "os" "path/filepath" "strings" "testing" "clearlove/internal/config" "clearlove/internal/database" ) // setup 准备带数据库的插件测试环境(插件配置保存在 settings 表) func setup(t *testing.T) string { t.Helper() root := t.TempDir() dataDir := filepath.Join(root, "data") if err := os.MkdirAll(dataDir, 0o755); err != nil { t.Fatal(err) } config.Cfg = config.Config{ DataDir: dataDir, UploadDir: filepath.Join(root, "uploads"), DBType: "sqlite", SQLitePath: filepath.Join(dataDir, "clearlove.db"), Installed: true, Secret: "test-secret", } if err := database.Connect(); err != nil { t.Fatalf("数据库连接失败: %v", err) } t.Cleanup(func() { if database.DB != nil { _ = database.DB.Close() database.DB = nil } }) if err := database.Migrate(); err != nil { t.Fatalf("数据库建表失败: %v", err) } return dataDir } // writePlugin 写入一个插件目录 func writePlugin(t *testing.T, dataDir, name, manifest, fragment string) string { t.Helper() d := filepath.Join(dataDir, "plugins", name) if err := os.MkdirAll(d, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(d, "plugin.json"), []byte(manifest), 0o644); err != nil { t.Fatal(err) } if fragment != "" { if err := os.WriteFile(filepath.Join(d, "player.html"), []byte(fragment), 0o644); err != nil { t.Fatal(err) } } return d } // TestCallHTMLFileAndConfig 验证 html 钩子:@片段文件加载、{config:} 展开与启用开关 func TestCallHTMLFileAndConfig(t *testing.T) { dataDir := setup(t) writePlugin(t, dataDir, "demo", `{ "name":"demo","version":"1.0.0", "config":[{"key":"music","default":"/uploads/default.mp3"}], "hooks":[{"hook":"footer_html","type":"html","html":"@player.html"}] }`, ``) // 未启用:不注入任何内容 if got := CallHTML("footer_html"); got != "" { t.Fatalf("未启用的插件不应注入内容, got %q", got) } SetEnabled("demo", true) if got := CallHTML("footer_html"); got != `` { t.Fatalf("默认配置未展开: %q", got) } p := Load()["demo"] if p == nil { t.Fatal("插件未被加载") } if err := SaveConfig(p, map[string]string{"music": "/uploads/plugins/demo/audio-abc.mp3"}); err != nil { t.Fatal(err) } if got := CallHTML("footer_html"); !strings.Contains(got, "/uploads/plugins/demo/audio-abc.mp3") { t.Fatalf("配置值未注入页面: %q", got) } // 片段文件缺失时应跳过该钩子,而不是把 "@player.html" 原样注入页面 if err := os.Remove(filepath.Join(dataDir, "plugins", "demo", "player.html")); err != nil { t.Fatal(err) } if got := CallHTML("footer_html"); strings.Contains(got, "@player.html") { t.Fatalf("片段缺失时不应注入占位文本: %q", got) } } // TestHookFragmentTraversal @片段文件不允许路径穿越 func TestHookFragmentTraversal(t *testing.T) { dataDir := setup(t) if err := os.WriteFile(filepath.Join(dataDir, "secret.txt"), []byte("TOP-SECRET"), 0o644); err != nil { t.Fatal(err) } writePlugin(t, dataDir, "evil", `{ "name":"evil","version":"1.0.0", "hooks":[{"hook":"footer_html","type":"html","html":"@../secret.txt"}] }`, "") SetEnabled("evil", true) if got := CallHTML("footer_html"); strings.Contains(got, "TOP-SECRET") { t.Fatalf("存在路径穿越读取: %q", got) } }