e31cc54b8e
- 门户通用聊天:选择已批准模型直接对话,审批通过后自动开通用户级运行时 API Key(加密落库,限额取批准值),聊天经受管网关统一认证/限流/配额/审计; 会话哈希链完整性 + busy 租约防并发,失败不落库。 - 扫码登录:identity_providers 扩展 wecom/dingtalk/feishu,管理端配置 (AppID/AppSecret/AgentID/回调/自动开户/默认部门),登录页自动展示; one-time state 防 CSRF,provider_uid 全局唯一防多账号绑定,平台端点 固定公网 URL 复用 public-only 拨号。 - 个人安全策略:账号安全页(登录设备管理/吊销非当前会话/登录提醒开关/ 扫码绑定解绑),登录成功发布 security.login_detected 事件按偏好落站内信 (新增 security 类别),会话索引只存令牌摘要并惰性清理。 - 迁移 000038-000041;修复 social update 参数越界/凭据回读/路由挂载缺失; 全量测试 25 包通过,前端 admin/portal 构建通过,端到端验证完成。
131 lines
6.7 KiB
Go
131 lines
6.7 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
|
|
wantPref 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"},
|
|
{name: "security.login_detected 发登录提醒且受偏好约束", eventType: "security.login_detected", values: map[string]any{"portal_user_id": "44444444-4444-4444-4444-444444444444", "ip": "203.0.113.7"}, wantKind: "portal", wantCategory: "security", wantTitle: "新设备登录提醒", wantUserID: "44444444-4444-4444-4444-444444444444", wantPref: true},
|
|
}
|
|
|
|
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 draft.NotifyPref != tc.wantPref {
|
|
t.Errorf("notify_pref = %v, want %v", draft.NotifyPref, tc.wantPref)
|
|
}
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|