Files
ai-gateway-go/internal/workbench/inbox_test.go
T
superidou 9501751792 0.10.1: 安全与业务逻辑加固、新品牌与部署加固
三轮审查修复(60+ 项),相对远端 main(b536672)的关键变更:
- 安全: 数据面 SSRF 拨号防护(防 DNS rebinding)/上游凭据剥离/登录防枚举
  与锁定态统一/可信代理(X-Forwarded-For)限流加固/会话版本失效机制/
  撤销即时传播/弱密钥拒绝启动/脱敏字节级重写(保签名契约)
- 业务逻辑: 裸 body 上传 panic/bootstrap 审计管线卡死/定价通配符优先级/
  全局工具可见性/调度器停机补跑/TOTP 挑战令牌消费顺序/熔断探针语义/
  >4MB 响应 token 计量/管理员重置密码作废会话 等
- 前端: 新 logo(语枢 AI 网关主题)/Provider 凭据异常警示/删除入口/
  后端错误消息透传/localStorage 敏感数据收敛
- 部署: CREDENTIAL_MASTER_KEY 持久化与弱值拒绝/Provider DELETE 接口/
  nginx 安全头/worker 内存限制
- 新增迁移 000029(key_hash 索引)/000030(usage_daily 币种维度)
2026-08-13 10:50:51 +08:00

126 lines
6.2 KiB
Go

package workbench
import (
"encoding/json"
"testing"
)
// TestInboxPlanMapsEvents 覆盖 inboxPlan 纯函数:每个支持的事件类型都要产出
// 预期类别 / 收件人类别 / 文案关键词,未知事件返回 nil。
func TestInboxPlanMapsEvents(t *testing.T) {
payload := func(values map[string]any) json.RawMessage {
encoded, err := json.Marshal(values)
if err != nil {
t.Fatal(err)
}
return encoded
}
cases := []struct {
name string
eventType string
values map[string]any
wantKind string // recipient_kind
wantCategory string
wantTitle string
wantUserID string
wantAll bool
wantRequest bool
}{
{name: "model_access.requested 通知全部管理员审批", eventType: "model_access.requested", values: map[string]any{"model": "gpt-5"}, wantKind: "admin", wantCategory: "approval", wantTitle: "新的模型访问申请", wantAll: true},
{name: "model_access.decided 已批准回执给申请用户", eventType: "model_access.decided", values: map[string]any{"status": "approved"}, wantKind: "portal", wantCategory: "approval", wantTitle: "模型申请已处理", wantRequest: true},
{name: "model_access.decided 已驳回文案", eventType: "model_access.decided", values: map[string]any{"status": "rejected"}, wantKind: "portal", wantCategory: "approval", wantTitle: "模型申请已处理", wantRequest: true},
{name: "marketplace.installed 发给安装用户", eventType: "marketplace.installed", values: map[string]any{"code": "report-bot", "portal_user_id": "11111111-1111-1111-1111-111111111111"}, wantKind: "portal", wantCategory: "resource", wantTitle: "资源已安装", wantUserID: "11111111-1111-1111-1111-111111111111"},
{name: "knowledge_document.ready 发给执行管理员", eventType: "knowledge_document.ready", values: map[string]any{"chunk_count": "12", "actor_id": "22222222-2222-2222-2222-222222222222"}, wantKind: "admin", wantCategory: "system", wantTitle: "知识文档已入库", wantUserID: "22222222-2222-2222-2222-222222222222"},
{name: "knowledge_document.reprocessed 发给执行管理员", eventType: "knowledge_document.reprocessed", values: map[string]any{"actor_id": "22222222-2222-2222-2222-222222222222"}, wantKind: "admin", wantCategory: "system", wantTitle: "知识文档已重新处理", wantUserID: "22222222-2222-2222-2222-222222222222"},
{name: "knowledge_document.embedding_failed 降级提示", eventType: "knowledge_document.embedding_failed", values: map[string]any{"actor_id": "22222222-2222-2222-2222-222222222222"}, wantKind: "admin", wantCategory: "system", wantTitle: "知识文档向量化失败", wantUserID: "22222222-2222-2222-2222-222222222222"},
{name: "scheduled_task.completed 发给创建者", eventType: "scheduled_task.completed", values: map[string]any{"task_code": "daily-report", "actor_id": "33333333-3333-3333-3333-333333333333"}, wantKind: "admin", wantCategory: "task_result", wantTitle: "定时任务已执行", wantUserID: "33333333-3333-3333-3333-333333333333"},
{name: "scheduled_task.failed 发给创建者", eventType: "scheduled_task.failed", values: map[string]any{"task_code": "daily-report", "error": "timeout", "actor_id": "33333333-3333-3333-3333-333333333333"}, wantKind: "admin", wantCategory: "task_result", wantTitle: "定时任务执行失败", wantUserID: "33333333-3333-3333-3333-333333333333"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
drafts := inboxPlan(tc.eventType, payload(tc.values))
if len(drafts) != 1 {
t.Fatalf("expected exactly one draft, got %d", len(drafts))
}
draft := drafts[0]
if draft.RecipientKind != tc.wantKind {
t.Errorf("recipient_kind = %q, want %q", draft.RecipientKind, tc.wantKind)
}
if draft.Category != tc.wantCategory {
t.Errorf("category = %q, want %q", draft.Category, tc.wantCategory)
}
if draft.Title != tc.wantTitle {
t.Errorf("title = %q, want %q", draft.Title, tc.wantTitle)
}
if draft.UserID != tc.wantUserID {
t.Errorf("user_id = %q, want %q", draft.UserID, tc.wantUserID)
}
if draft.AllAdmins != tc.wantAll {
t.Errorf("all_admins = %v, want %v", draft.AllAdmins, tc.wantAll)
}
if draft.RequestUser != tc.wantRequest {
t.Errorf("request_user = %v, want %v", draft.RequestUser, tc.wantRequest)
}
})
}
if drafts := inboxPlan("some.unknown.event", payload(map[string]any{})); drafts != nil {
t.Fatalf("unknown event should map to no drafts, got %+v", drafts)
}
}
// TestInboxPlanModelAccessRejectedBody 校验驳回与批准的不同正文文案。
func TestInboxPlanModelAccessRejectedBody(t *testing.T) {
values := func(status string) json.RawMessage {
encoded, _ := json.Marshal(map[string]any{"status": status})
return encoded
}
approved := inboxPlan("model_access.decided", values("approved"))
rejected := inboxPlan("model_access.decided", values("rejected"))
if !contains(approved[0].Body, "已批准") {
t.Errorf("approved body should mention 已批准, got %q", approved[0].Body)
}
if !contains(rejected[0].Body, "已驳回") {
t.Errorf("rejected body should mention 已驳回, got %q", rejected[0].Body)
}
}
func contains(haystack, needle string) bool {
for i := 0; i+len(needle) <= len(haystack); i++ {
if haystack[i:i+len(needle)] == needle {
return true
}
}
return false
}
// TestPayloadValue 校验 payloadValue 对字符串与数字两类取值的兼容(事件载荷
// 中数字可能被 JSON 解码为 float64)。
func TestPayloadValue(t *testing.T) {
payload := json.RawMessage(`{"model":"gpt-5","chunk_count":12,"active":true}`)
if got := payloadValue(payload, "model"); got != "gpt-5" {
t.Errorf("string key = %q, want gpt-5", got)
}
if got := payloadValue(payload, "chunk_count"); got != "12" {
t.Errorf("numeric key = %q, want 12", got)
}
if got := payloadValue(payload, "missing"); got != "" {
t.Errorf("missing key = %q, want empty", got)
}
}
func TestValidInboxLink(t *testing.T) {
cases := map[string]bool{
"": true, "/portal/inbox": true, "https://example.com/notice": true,
"http://example.com": true, "javascript:alert(1)": false,
"data:text/html,x": false, "//example.com/path": false, "portal/inbox": false,
}
for link, want := range cases {
if got := validInboxLink(link); got != want {
t.Errorf("validInboxLink(%q) = %v, want %v", link, got, want)
}
}
}