package qq_obv11 import ( "context" "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) func TestToUnifiedPrivateTextAndFace(t *testing.T) { t.Parallel() a := NewAdapter(Config{}) raw := []byte(`{"post_type":"message","message_type":"private","time":1700000000,"self_id":100,"message_id":5,"user_id":200,"message":[{"type":"text","data":{"text":"hi "}},{"type":"face","data":{"id":1}}],"sender":{"user_id":200,"nickname":"Alice","role":"owner"}}`) ev, err := parseEvent(raw) if err != nil { t.Fatalf("parseEvent error: %v", err) } msg := a.toUnified(ev, raw) if msg.Platform != platformName { t.Errorf("Platform = %q, want %q", msg.Platform, platformName) } if msg.ChannelType != adapter.ChannelDirect { t.Errorf("ChannelType = %q, want %q", msg.ChannelType, adapter.ChannelDirect) } if msg.ChannelID != "200" || msg.SenderID != "200" { t.Errorf("ChannelID/SenderID = %q/%q, want 200/200", msg.ChannelID, msg.SenderID) } if msg.SenderName != "Alice" { t.Errorf("SenderName = %q, want Alice", msg.SenderName) } if msg.Content != "hi [表情]" { t.Errorf("Content = %q, want %q", msg.Content, "hi [表情]") } if msg.ContentType != adapter.ContentText { t.Errorf("ContentType = %q, want text", msg.ContentType) } if msg.MessageID != "5" { t.Errorf("MessageID = %q, want 5", msg.MessageID) } if !msg.Timestamp.Equal(time.Unix(1700000000, 0)) { t.Errorf("Timestamp = %v, want %v", msg.Timestamp, time.Unix(1700000000, 0)) } // 私聊帧里的 role=owner 是**伪造**的主人标记:适配器只把它当群内/平台身份,判主 // 由白名单决定(裁决 06.4.2),所以这里必须是 RoleMember、IsOwner()=false。 if msg.Sender.Role != adapter.RoleMember || msg.Sender.IsOwner() { t.Errorf("Sender.Role = %q (IsOwner=%v), want member (伪造的 role=owner 不得判主)", msg.Sender.Role, msg.Sender.IsOwner()) } } func TestToUnifiedGroupWithMediaAndMention(t *testing.T) { t.Parallel() a := NewAdapter(Config{Mode: "server"}) raw := []byte(`{"post_type":"message","message_type":"group","time":1700000001,"self_id":100,"message_id":9,"user_id":42,"group_id":123,"group_name":"Team","message":[{"type":"at","data":{"qq":"300"}},{"type":"text","data":{"text":" look "}},{"type":"image","data":{"file":"p.jpg","url":"http://cdn/p.jpg"}},{"type":"record","data":{"file":"v.amr"}}],"sender":{"user_id":42,"nickname":"Bob","card":"Bobby","role":"member"}}`) ev, err := parseEvent(raw) if err != nil { t.Fatalf("parseEvent error: %v", err) } msg := a.toUnified(ev, raw) if !msg.IsGroup() { t.Fatal("expected group message") } if msg.ChannelType != adapter.ChannelGroup || msg.ChannelID != "123" || msg.GroupID != "123" { t.Errorf("group fields = %q/%q/%q, want group/123/123", msg.ChannelType, msg.ChannelID, msg.GroupID) } if msg.GroupName != "Team" { t.Errorf("GroupName = %q, want Team", msg.GroupName) } if msg.SenderName != "Bobby" { t.Errorf("SenderName = %q, want Bobby (card)", msg.SenderName) } if msg.Sender.Role != adapter.RoleMember { t.Errorf("Sender.Role = %q, want member", msg.Sender.Role) } if len(msg.Mentions) != 1 || msg.Mentions[0].UserID != "300" { t.Errorf("Mentions = %+v, want one @300", msg.Mentions) } if len(msg.Media) != 2 { t.Fatalf("Media len = %d, want 2", len(msg.Media)) } if msg.Media[0].Kind != adapter.MediaImage || msg.Media[0].URL != "http://cdn/p.jpg" || msg.Media[0].Name != "p.jpg" { t.Errorf("Media[0] = %+v, want image p.jpg", msg.Media[0]) } if msg.Media[1].Kind != adapter.MediaVoice { t.Errorf("Media[1].Kind = %q, want voice", msg.Media[1].Kind) } if msg.Content != "@300 look [图片][语音]" { t.Errorf("Content = %q, want %q", msg.Content, "@300 look [图片][语音]") } } func TestToUnifiedCQString(t *testing.T) { t.Parallel() a := NewAdapter(Config{}) raw := []byte(`{"post_type":"message","message_type":"private","time":1700000002,"self_id":100,"message_id":7,"user_id":200,"message":"[CQ:at,qq=300]hello[CQ:image,file=a.jpg,url=http://cdn/a.jpg]","sender":{"user_id":200,"nickname":"Alice","role":""}}`) ev, err := parseEvent(raw) if err != nil { t.Fatalf("parseEvent error: %v", err) } msg := a.toUnified(ev, raw) if msg.Content != "@300hello[图片]" { t.Errorf("Content = %q, want %q", msg.Content, "@300hello[图片]") } if len(msg.Mentions) != 1 || msg.Mentions[0].UserID != "300" { t.Errorf("Mentions = %+v, want @300", msg.Mentions) } if len(msg.Media) != 1 || msg.Media[0].Kind != adapter.MediaImage || msg.Media[0].URL != "http://cdn/a.jpg" { t.Errorf("Media = %+v, want image a.jpg", msg.Media) } if msg.Sender.Role != adapter.RoleUnknown { t.Errorf("Sender.Role = %q, want unknown for empty role", msg.Sender.Role) } } func TestContentTypePureImage(t *testing.T) { t.Parallel() a := NewAdapter(Config{}) raw := []byte(`{"post_type":"message","message_type":"private","time":1700000003,"self_id":100,"message_id":8,"user_id":200,"message":[{"type":"image","data":{"file":"a.jpg"}}],"sender":{"user_id":200,"nickname":"Alice"}}`) ev, err := parseEvent(raw) if err != nil { t.Fatalf("parseEvent error: %v", err) } msg := a.toUnified(ev, raw) if msg.ContentType != adapter.ContentImage { t.Errorf("ContentType = %q, want image", msg.ContentType) } if len(msg.Media) != 1 || msg.Media[0].Kind != adapter.MediaImage { t.Errorf("Media = %+v, want one image", msg.Media) } } func TestOutboundSegmentsTextAndImage(t *testing.T) { t.Parallel() segs, err := outboundSegments(adapter.UnifiedMessage{ Content: "pong", Media: []adapter.Media{{Kind: adapter.MediaImage, URL: "http://cdn/x.jpg"}}, }) if err != nil { t.Fatalf("outboundSegments error: %v", err) } if len(segs) != 2 { t.Fatalf("segs len = %d, want 2", len(segs)) } if segs[0].Type != "text" || segs[0].Data["text"] != "pong" { t.Errorf("segs[0] = %+v, want text pong", segs[0]) } if segs[1].Type != "image" || segs[1].Data["file"] != "http://cdn/x.jpg" { t.Errorf("segs[1] = %+v, want image", segs[1]) } } func TestOutboundSegmentsEmptyErrors(t *testing.T) { t.Parallel() _, err := outboundSegments(adapter.UnifiedMessage{}) if err == nil { t.Error("empty message: expected error, got nil") } } func TestResolveTargetGroupAndPrivate(t *testing.T) { t.Parallel() tests := []struct { name string msg adapter.UnifiedMessage wantType string wantUser int64 wantGroup int64 wantErr bool }{ { name: "group", msg: adapter.UnifiedMessage{ChannelType: adapter.ChannelGroup, GroupID: "123", Content: "x"}, wantType: "group", wantGroup: 123, }, { name: "private by channel id", msg: adapter.UnifiedMessage{ChannelType: adapter.ChannelDirect, ChannelID: "200", Content: "x"}, wantType: "private", wantUser: 200, }, { name: "private by sender", msg: adapter.UnifiedMessage{ChannelType: adapter.ChannelDirect, SenderID: "300", Content: "x"}, wantType: "private", wantUser: 300, }, { name: "group missing target", msg: adapter.UnifiedMessage{ChannelType: adapter.ChannelGroup, Content: "x"}, wantErr: true, }, { name: "private missing target", msg: adapter.UnifiedMessage{ChannelType: adapter.ChannelDirect, Content: "x"}, wantErr: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() gotType, gotUser, gotGroup, err := resolveTarget(tt.msg) if (err != nil) != tt.wantErr { t.Fatalf("resolveTarget error = %v, wantErr %v", err, tt.wantErr) } if err != nil { return } if gotType != tt.wantType || gotUser != tt.wantUser || gotGroup != tt.wantGroup { t.Errorf("resolveTarget = %q/%d/%d, want %q/%d/%d", gotType, gotUser, gotGroup, tt.wantType, tt.wantUser, tt.wantGroup) } }) } } func TestRoleFromOneBot(t *testing.T) { t.Parallel() // 群主(owner)与群管理员(admin)都必须是 RoleMember(裁决 06.4.2):平台角色字段 // 不是主人车道凭据,主人只认白名单;未知角色值保守归入 RoleUnknown。 tests := []struct { raw string want adapter.Role }{ {"owner", adapter.RoleMember}, {"admin", adapter.RoleMember}, {"member", adapter.RoleMember}, {"", adapter.RoleUnknown}, {"bogus", adapter.RoleUnknown}, } for _, tt := range tests { tt := tt t.Run(tt.raw, func(t *testing.T) { t.Parallel() if got := roleFromOneBot(tt.raw); got != tt.want { t.Errorf("roleFromOneBot(%q) = %q, want %q", tt.raw, got, tt.want) } }) } } func TestBuildSendReq(t *testing.T) { t.Parallel() segs := []obSegment{{Type: "text", Data: map[string]any{"text": "hi"}}} payload, err := buildSendReq("group", 0, 123, segs, "echo_1") if err != nil { t.Fatalf("buildSendReq error: %v", err) } if string(payload) == "" { t.Fatal("empty payload") } } func TestListChannelsCachesGroups(t *testing.T) { t.Parallel() a := NewAdapter(Config{Mode: "server"}) a.rememberGroup("123", "Team") a.rememberGroup("456", "Gamer") channels, err := a.ListChannels(context.Background()) if err != nil { t.Fatalf("ListChannels error: %v", err) } if len(channels) != 2 { t.Fatalf("channels len = %d, want 2", len(channels)) } byID := map[string]adapter.ChannelInfo{} for _, c := range channels { byID[c.ChannelID] = c } if c := byID["123"]; c.GroupName != "Team" || c.ChannelType != adapter.ChannelGroup || c.GroupID != "123" { t.Errorf("channel 123 = %+v", c) } }