Files
HomeAgent/internal/plugins/webui/test/secret_meta_test.go
JianFeeeee 2e90c2cb5a refactor(webui): openai_compat 判据迁到 test/,包内重建共享辅助
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 条,与迁移前一致
2026-11-04 21:52:52 +08:00

150 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}