package core import ( "encoding/json" "testing" "llmsproxy/internal/lua" ) // TestChannelsPluginEndToEnd 是「同类插件」的完整验收: // 内置的 channels 插件在**真实内核**上跑通「登录 → 存凭据 → 接入为上游源」。 // // 这条链路横跨本次所有新增能力,所以它是最有信息量的一个测试: // // __llmsproxy_login 创建/轮询登录会话 // __llmsproxy_secrets 凭据进保险库(不进 state) // __llmsproxy_kernel add_source 把平台注册为上游源 // 内核权限 默认拒绝,需运维授权 // 登录主机授权 登录域不在源列表里,默认被出站白名单拒绝 func TestChannelsPluginEndToEnd(t *testing.T) { c := kernelTestCore(t) ps := c.Plugins() src, err := lua.ReadBundledPlugin("channels") if err != nil { t.Fatalf("读取内置 channels 插件: %v", err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatalf("加载 channels: %v", err) } // 1. 平台清单可用(未授权任何东西也应能列出) out, err := ps.InvokeHandler("channels", "list_platforms", &lua.HandlerRequest{ Method: "GET", Role: "admin", }) if err != nil { t.Fatalf("list_platforms: %v", err) } body, _ := out.JSON.(map[string]interface{}) plats, _ := body["platforms"].([]interface{}) if len(plats) < 3 { t.Fatalf("应至少 3 个平台, 实得 %d", len(plats)) } // 2. 发起登录 → 拿到会话与 URL out, err = ps.InvokeHandler("channels", "start_login", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"example"}`, Role: "admin", }) if err != nil { t.Fatalf("start_login: %v", err) } m := out.JSON.(map[string]interface{}) if m["error"] != nil { t.Fatalf("发起登录失败: %v", m["error"]) } if m["url"] == nil || m["url"] == "" { t.Fatal("应返回用户要打开的登录 URL") } sess := m["session"].(map[string]interface{}) id, _ := sess["id"].(string) if id == "" { t.Fatal("应返回会话 id") } // 3. 轮询到 pending out, _ = ps.InvokeHandler("channels", "poll_login", &lua.HandlerRequest{ Method: "GET", Query: map[string]string{"id": id}, Role: "admin", }) if st := out.JSON.(map[string]interface{})["state"]; st != "pending" { t.Fatalf("应为 pending, 实得 %v", st) } // 4. 未授权内核动作时,接入源必须失败且**如实报告**(不是假装成功) out, err = ps.InvokeHandler("channels", "complete_login", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"example","credential":"tok-1","account":"alice"}`, Role: "admin", }) if err != nil { t.Fatalf("complete_login: %v", err) } done := out.JSON.(map[string]interface{}) if done["source_error"] == nil { t.Fatal("未授权 add_source 时应报告 source_error") } t.Logf("未授权时的如实报告: %v", done["source_error"]) // 凭据必须已在保险库(登录本身成功了) tok, ok, _ := ps.GetSecret("channels", "channels:example") if !ok || tok != "tok-1" { t.Fatalf("凭据应已存保险库: ok=%v tok=%q", ok, tok) } // 且不得出现在 state 里 if stateHasSecret(ps, "channels", "tok-1") { t.Fatal("★ 凭据泄漏进了 plugin.state") } // 5. 运维授权后,重新接入应当真的把源注册进内核 if err := ps.SetPluginKernelPermission("channels", []string{"add_source", "list_sources", "status"}, false); err != nil { t.Fatal(err) } out, err = ps.InvokeHandler("channels", "complete_login", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"example","credential":"tok-1","account":"alice"}`, Role: "admin", }) if err != nil { t.Fatalf("complete_login(授权后): %v", err) } done = out.JSON.(map[string]interface{}) if done["source_error"] != nil { t.Fatalf("授权后仍失败: %v", done["source_error"]) } if done["source"] != "channels-example" { t.Fatalf("源名不对: %v", done["source"]) } // 内核侧必须真的有这个源 found := false for _, s := range c.Sources() { if s.Name == "channels-example" { found = true if s.BaseURL != "https://api.example.com/v1" { t.Errorf("base_url 未落库: %q", s.BaseURL) } } } if !found { t.Fatal("插件接入的源没有出现在内核源列表里") } // 平台声明的模型应可路由 models := c.Registry().ModelList() hasModel := false for _, id := range models { if id == "example-chat" { hasModel = true } } if !hasModel { t.Errorf("平台模型未进入可路由列表: %v", models) } t.Logf("已接入源 channels-example,模型可见: %v", models) } // TestChannelsPluginDeclaresItsNeeds 断言插件如实声明了它需要什么。 // // 声明是运维审批的依据:插件不声明,设置页就无法显示"申请中", // 运维只能等某个功能静默失效后才发现。 func TestChannelsPluginDeclaresItsNeeds(t *testing.T) { c := kernelTestCore(t) ps := c.Plugins() src, err := lua.ReadBundledPlugin("channels") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatal(err) } // 内核动作声明 views := ps.KernelPermissions() var declared []string for _, v := range views { if v.Plugin == "channels" { declared = v.Declared } } if len(declared) == 0 { t.Fatal("channels 应声明它需要的内核动作") } for _, want := range []string{"add_source", "status"} { found := false for _, d := range declared { if d == want { found = true } } if !found { t.Errorf("应声明 %q, 实得 %v", want, declared) } } // 登录主机声明(非源 host,需单独批准) hosts := ps.PluginAuthHosts("channels") if len(hosts) == 0 { t.Fatal("channels 应声明登录主机") } t.Logf("声明内核动作=%v 登录主机=%v", declared, hosts) } // TestChannelsPluginHandlersRequireStatus 是一个契约回归:插件的每个 handler // 都必须返回 {status,json/body},否则内核会**静默丢弃**它(实测踩过: // `return {url=…}` 得到 200 空响应,没有任何错误信息)。 func TestChannelsPluginHandlersRequireStatus(t *testing.T) { c := kernelTestCore(t) ps := c.Plugins() src, err := lua.ReadBundledPlugin("channels") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatal(err) } _ = ps.SetPluginKernelPermission("channels", []string{"add_source", "status", "list_sources"}, false) // 每个 handler 至少要在正常输入下给出非空响应 cases := []struct { fn string req *lua.HandlerRequest what string }{ {"list_platforms", &lua.HandlerRequest{Method: "GET"}, "平台列表"}, {"status_page", &lua.HandlerRequest{Method: "GET"}, "状态页"}, {"poll_login", &lua.HandlerRequest{Method: "GET", Query: map[string]string{"id": "nope"}}, "轮询(未知会话)"}, {"start_login", &lua.HandlerRequest{Method: "POST", Body: `{"platform":"example"}`}, "发起登录"}, {"start_login", &lua.HandlerRequest{Method: "POST", Body: `{"platform":"nope"}`}, "发起登录(未知平台)"}, {"complete_login", &lua.HandlerRequest{Method: "POST", Body: `{}`}, "完成登录(缺字段)"}, {"claim_daily", &lua.HandlerRequest{Method: "POST", Body: `{"platform":"example"}`}, "领额度(未登录)"}, } for _, tc := range cases { out, err := ps.InvokeHandler("channels", tc.fn, tc.req) if err != nil { t.Errorf("%s (%s) 返回错误: %v", tc.fn, tc.what, err) continue } if out.JSON == nil && out.Body == "" { t.Errorf("%s (%s) 返回空响应 —— 内核会静默丢弃,调用方看不到任何原因", tc.fn, tc.what) } } } // jsonMarshalForTest 序列化一个接口值,用于检查凭据是否出现在 state 里。 func jsonMarshalForTest(v interface{}) ([]byte, error) { return json.Marshal(v) } // stateHasSecret 检查凭据是否泄漏进 plugin.state。 func stateHasSecret(ps *lua.Plugins, plugin, secret string) bool { st := ps.State(plugin) if st == nil { return false } b, err := jsonMarshalForTest(st) if err != nil { return false } return contains(string(b), secret) }