mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
openai_compat_test.go → webui/test/ (4 条判据)
webui/ 178 → 174 条
webui/test/ 24 → 28 条
合计 202 条不变
## 迁移碰到的连带问题
`openai_compat_test.go` 里除了自己的判据,还定义了三个**被包内其他文件
依赖**的共享件:
newOpenAITestServer(t) auth_hardening_test.go 在用
testAuthAPIKey auth_hardening / gzip / legacy_proxy(×2) 在用
seedWebUIConfig 多处
文件一移走,包内立刻 build failed:
vet: auth_hardening_test.go:373:15: undefined: newOpenAITestServer
vet: auth_hardening_test.go:377:30: undefined: testAuthAPIKey
这说明「迁一个测试文件」从来不是孤立的 —— 它可能是包内共享辅助的**宿主**。
## 处理方式:两边各留一份,不跨包共享
包内重定义 `testAuthAPIKey` 与 `newOpenAITestServer`(放 handler_test.go),
test/ 侧保留自己那份(`seedWebUIConfig` 统一收进 helpers_test.go)。
**为什么不抽到共享包**:包内版本要用包内的 `testSDK`/`seedWebUIConfig`,
test/ 版本要用那边的 helper。抽公共包等于让 test/ 反向依赖包内的未导出
辅助 —— 那正是这次迁移要摆脱的东西。两份各写 20 行,换来两侧互不牵连,
比一个"共享测试工具包"更符合这次的目的。
## 顺带修掉一处重复定义
`seedWebUIConfig` 在 secret_meta_test.go 与 helpers_test.go 各出现过一次
(上一批我加的那份没和已有的对齐)。收拢到 helpers_test.go 一份,
字段与包内版本保持一致(api_key/username/password/session_ttl_hours)。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 67 包全过,0 FAIL
webui/ + webui/test/ 174 + 28 = 202 条,与迁移前一致
150 lines
5.4 KiB
Go
150 lines
5.4 KiB
Go
package test
|
||
|
||
// 判据:**Secret 标志必须活着到达 WebUI 的 meta**。
|
||
//
|
||
// 由来:SDK 的 ConfigDef 一直有 Secret 字段,但内核侧三处转换
|
||
// (settingsImpl.RegisterDef / Defs / mapDefs)全部漏掉了它 ——
|
||
// 插件声明了 secret,前端拿到的 meta 里却没有这个标记,
|
||
// 于是敏感项被渲染成普通文本框明文回显。
|
||
//
|
||
// 这条判据钉住「声明 → meta」这一段,防止再次被漏掉。
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
|
||
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
|
||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||
"github.com/JianFeeeee/HologramAgent/internal/plugins/webui"
|
||
internalSDK "github.com/JianFeeeee/HologramAgent/internal/sdk"
|
||
)
|
||
|
||
func TestSecretFlagReachesSettingsMeta(t *testing.T) {
|
||
dir := t.TempDir()
|
||
reg := internalConfig.NewConfigRegistry(dir + "/config.db")
|
||
reg.SetCodec(internalConfig.NewTestCodec(t))
|
||
defer reg.Close()
|
||
seedWebUIConfig(reg)
|
||
|
||
// 插件用**两种写法**各声明一个敏感项
|
||
sett := internalSDK.NewSettings("secplug", reg)
|
||
sett.RegisterDef(internalSDK.ConfigDef{
|
||
Key: "by_flag", Type: "string", DisplayName: "标志式", Secret: true,
|
||
})
|
||
sett.RegisterDef(internalSDK.ConfigDef{
|
||
Key: "by_type", Type: "password", DisplayName: "类型式",
|
||
})
|
||
|
||
// 走**真实 HTTP 处理器**:NewHandler + Handler() 都是导出接口,
|
||
// 请求经完整路由分发,不直接调未导出的 handleSettings。
|
||
h := webui.NewHandler(internalSDK.New("webui", internalSDK.SDKConfig{
|
||
Settings: internalSDK.NewSettings("webui", reg),
|
||
EventBus: events.NewBus(),
|
||
}))
|
||
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings?prefix=plugin.secplug", nil)
|
||
w := httptest.NewRecorder()
|
||
serveRoutes(t, h, w, req)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("状态码 %d: %s", w.Code, w.Body.String())
|
||
}
|
||
|
||
var body struct {
|
||
Meta map[string]map[string]interface{} `json:"meta"`
|
||
}
|
||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||
t.Fatalf("解析响应失败: %v", err)
|
||
}
|
||
|
||
byFlag, ok := body.Meta["plugin.secplug.by_flag"]
|
||
if !ok {
|
||
t.Fatalf("meta 里没有 by_flag;实际键: %v", keysOfMeta(body.Meta))
|
||
}
|
||
if byFlag["secret"] != true {
|
||
t.Errorf("Secret:true 未传递到 meta(前端会明文回显): %v", byFlag)
|
||
}
|
||
byType, ok := body.Meta["plugin.secplug.by_type"]
|
||
if !ok {
|
||
t.Fatalf("meta 里没有 by_type;实际键: %v", keysOfMeta(body.Meta))
|
||
}
|
||
if byType["type"] != "password" {
|
||
t.Errorf("password 类型应保持: %v", byType)
|
||
}
|
||
}
|
||
|
||
func keysOfMeta(m map[string]map[string]interface{}) []string {
|
||
out := make([]string, 0, len(m))
|
||
for k := range m {
|
||
out = append(out, k)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// 判据:敏感项在 WebUI 列表里的默认值**必须屏蔽**(E-3),
|
||
// 单键 reveal 才能看到明文;磁盘上仍是密文。
|
||
func TestSecretPlainInUIButCipherOnDisk(t *testing.T) {
|
||
dir := t.TempDir()
|
||
reg := internalConfig.NewConfigRegistry(dir + "/config.db")
|
||
reg.SetCodec(internalConfig.NewTestCodec(t))
|
||
defer reg.Close()
|
||
seedWebUIConfig(reg)
|
||
|
||
sett := internalSDK.NewSettings("webui", reg)
|
||
sett.RegisterDef(internalSDK.ConfigDef{Key: "apikey", Type: "password"})
|
||
if err := sett.Set("apikey", "ui-visible-secret-123456"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
h := webui.NewHandler(internalSDK.New("webui", internalSDK.SDKConfig{
|
||
Settings: internalSDK.NewSettings("webui", reg),
|
||
EventBus: events.NewBus(),
|
||
}))
|
||
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings?prefix=plugin.webui", nil)
|
||
w := httptest.NewRecorder()
|
||
serveRoutes(t, h, w, req)
|
||
|
||
if strings.Contains(w.Body.String(), "ui-visible-secret-123456") {
|
||
t.Fatal("E-3:设置列表不应回显敏感明文")
|
||
}
|
||
if !strings.Contains(w.Body.String(), webui.SecretPlaceholder) {
|
||
t.Fatal("设置列表应显示敏感值占位符")
|
||
}
|
||
// 显式揭示仍可核对(单键、当前 WebUI 插件范围)。
|
||
reveal := httptest.NewRecorder()
|
||
serveRoutes(t, h, reveal, httptest.NewRequest(http.MethodGet,
|
||
"/api/v1/settings/reveal?key=plugin.webui.apikey", nil))
|
||
if reveal.Code != http.StatusOK || !strings.Contains(reveal.Body.String(), "ui-visible-secret-123456") {
|
||
t.Fatalf("显式 reveal 应返回当前插件的明文: status=%d body=%s", reveal.Code, reveal.Body.String())
|
||
}
|
||
var raw string
|
||
if err := reg.DB().QueryRow(
|
||
`SELECT value FROM config_webui WHERE key='apikey'`).Scan(&raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if strings.Contains(raw, "ui-visible-secret") {
|
||
t.Errorf("磁盘上不应有明文: %q", raw)
|
||
}
|
||
}
|
||
|
||
// serveRoutes 通过**导出的 RegisterRoutes** 建一个只含业务路由的 mux,
|
||
// 跳过 logged / gzipMW / proxyDispatch 三层中间件。
|
||
//
|
||
// 为什么不用 Handler():那三层中间件依赖完整装配(history store、settings
|
||
// 里的 webui username/password、data dir 等)。本判据测的是 settings 的
|
||
// meta 与 reveal,不是请求日志与反代 —— 用全链路会把无关依赖拖进来,
|
||
// 一个缺配置就 nil 解引用,红的不是被测对象。
|
||
//
|
||
// RegisterRoutes 是导出的,所以这仍是**黑箱**:拿到的是真实路由表,
|
||
// 只是不带外层中间件。
|
||
func serveRoutes(t *testing.T, h *webui.Handler, w http.ResponseWriter, r *http.Request) {
|
||
t.Helper()
|
||
// 带 API key 通过 requireAPI(这是生产上非浏览器客户端的正常路径,
|
||
// 见 requireWeb/requireAPI 里 validAPIKey 优先于 cookie session)。
|
||
r.Header.Set("X-API-Key", testAPIKey)
|
||
mux := http.NewServeMux()
|
||
h.RegisterRoutes(mux)
|
||
mux.ServeHTTP(w, r)
|
||
}
|