fix(rpc): sync verification icon envelope coverage

Follow up gramsrv#27 with the remaining peer-bearing messages.Messages boundaries and route-level wire regression coverage from the telesrv canonical implementation.

Origin-PR: gramsrv#27
Origin-Head: f0bd6c9699
Origin-Merge: 7713847b2a
Canonical: f1ba406015a2d5e37802b963757a5cac8ee3eb0e

Co-authored-by: epilepticseizureee <business.egor.sg@gmail.com>
This commit is contained in:
iamxvbaba 2026-07-30 14:06:39 +08:00
parent 7713847b2a
commit 3fa0c6f1e4
12 changed files with 156 additions and 16 deletions

View file

@ -65,6 +65,53 @@ func assertFlagBitDelta(t *testing.T, label string, before, after bin.Fields, wa
}
}
func assertMessagesEnvelopeBotVerificationIcon(t *testing.T, out tg.MessagesMessagesClass, peer domain.Peer, want int64) {
t.Helper()
var users []tg.UserClass
var chats []tg.ChatClass
switch value := out.(type) {
case *tg.MessagesMessages:
users, chats = value.Users, value.Chats
case *tg.MessagesMessagesSlice:
users, chats = value.Users, value.Chats
case *tg.MessagesChannelMessages:
users, chats = value.Users, value.Chats
default:
t.Fatalf("messages envelope = %T, want peer-bearing messages.Messages", out)
}
switch peer.Type {
case domain.PeerTypeUser:
for _, item := range users {
user, ok := item.(*tg.User)
if !ok || user.ID != peer.ID {
continue
}
wire := &tg.User{}
tlRoundTrip(t, user, wire)
if icon, ok := wire.GetBotVerificationIcon(); !ok || icon != want {
t.Fatalf("user %d bot_verification_icon = %d, ok=%v, want %d", peer.ID, icon, ok, want)
}
return
}
case domain.PeerTypeChannel:
for _, item := range chats {
channel, ok := item.(*tg.Channel)
if !ok || channel.ID != peer.ID {
continue
}
wire := &tg.Channel{}
tlRoundTrip(t, channel, wire)
if icon, ok := wire.GetBotVerificationIcon(); !ok || icon != want {
t.Fatalf("channel %d bot_verification_icon = %d, ok=%v, want %d", peer.ID, icon, ok, want)
}
return
}
default:
t.Fatalf("unsupported verification peer %+v", peer)
}
t.Fatalf("messages envelope %T does not carry peer %+v", out, peer)
}
// TestBotVerificationConstructorIDs pins the two constructor ids the feature
// serialises. A drift here silently reshapes every payload below.
func TestBotVerificationConstructorIDs(t *testing.T) {

View file

@ -100,7 +100,9 @@ func (r *Router) onChannelsSearchPosts(ctx context.Context, req *tg.ChannelsSear
return nil, channelInvalidErr(err)
}
history = r.enrichChannelHistory(ctx, userID, history)
return tgChannelSearchPostsMessages(userID, history), nil
result := tgChannelSearchPostsMessages(userID, history)
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
func validateChannelSearchPostsRequest(req *tg.ChannelsSearchPostsRequest) error {

View file

@ -835,9 +835,11 @@ func TestChannelsSearchPostsReturnsPublicPostsWithSeekPaging(t *testing.T) {
viewer, _ := userStore.Create(ctx, domain.User{AccessHash: 91002, Phone: "15550091002", FirstName: "Viewer"})
channelStore := memory.NewChannelStore()
channelService := appchannels.NewService(channelStore)
verify := newFakeBotVerifications()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: channelService,
Users: appusers.NewService(userStore),
Channels: channelService,
BotVerifications: verify,
}, zaptest.NewLogger(t), clock.System)
public, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Public Search",
@ -847,6 +849,14 @@ func TestChannelsSearchPostsReturnsPublicPostsWithSeekPaging(t *testing.T) {
if err != nil {
t.Fatalf("create public channel: %v", err)
}
const searchPostsIcon = int64(8800021)
searchPostsPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: public.Channel.ID}
verify.marks[searchPostsPeer] = domain.CustomVerification{
VerifierBotID: 777000123,
Peer: searchPostsPeer,
IconDocumentID: searchPostsIcon,
Description: "Verified search result",
}
if _, err := channelService.UpdateUsername(ctx, owner.ID, domain.UpdateChannelUsernameRequest{
UserID: owner.ID,
ChannelID: public.Channel.ID,
@ -920,6 +930,7 @@ func TestChannelsSearchPostsReturnsPublicPostsWithSeekPaging(t *testing.T) {
if peer, ok := first.PeerID.(*tg.PeerChannel); !ok || peer.ChannelID != public.Channel.ID {
t.Fatalf("first result peer = %#v, want public channel %d", first.PeerID, public.Channel.ID)
}
assertMessagesEnvelopeBotVerificationIcon(t, got, searchPostsPeer, searchPostsIcon)
page2 := &tg.ChannelsSearchPostsRequest{
OffsetRate: slice.NextRate,

View file

@ -961,6 +961,16 @@ func TestTDesktopPassiveChannelStubs(t *testing.T) {
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.deletePollAnswer err = %v, want MESSAGE_ID_INVALID without poll store", err)
}
verify := newFakeBotVerifications()
r.deps.BotVerifications = verify
const unreadPollIcon = int64(8800024)
unreadPollPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}
verify.marks[unreadPollPeer] = domain.CustomVerification{
VerifierBotID: 777000123,
Peer: unreadPollPeer,
IconDocumentID: unreadPollIcon,
Description: "Verified poll peer",
}
unreadPollVotes, err := r.onMessagesGetUnreadPollVotes(ownerCtx, &tg.MessagesGetUnreadPollVotesRequest{
Peer: inputPeerChannel(channel),
Limit: 10,
@ -968,6 +978,7 @@ func TestTDesktopPassiveChannelStubs(t *testing.T) {
if err != nil {
t.Fatalf("messages.getUnreadPollVotes: %v", err)
}
assertMessagesEnvelopeBotVerificationIcon(t, unreadPollVotes, unreadPollPeer, unreadPollIcon)
if len(unreadPollVotes.(*tg.MessagesMessages).Messages) != 0 {
t.Fatalf("messages.getUnreadPollVotes = %+v, want empty messages", unreadPollVotes)
}

View file

@ -10,14 +10,35 @@ import (
"go.uber.org/zap/zaptest"
accountapp "telesrv/internal/app/account"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
func TestQuickReplyRPCSaveListAndDeleteMessage(t *testing.T) {
const userID int64 = 1000000001
const userID int64 = domain.UserIDSequenceBase
ctx := WithSessionID(WithAuthKeyID(WithUserID(context.Background(), userID), [8]byte{1}), 77)
r, updates := newChatAutomationTestRouter(t)
verify := newFakeBotVerifications()
r.deps.BotVerifications = verify
userStore := memory.NewUserStore()
self, err := userStore.Create(context.Background(), domain.User{
AccessHash: 7000001,
Phone: "15550007001",
FirstName: "Quick",
})
if err != nil || self.ID != userID {
t.Fatalf("create quick-reply user = %+v err %v, want id %d", self, err, userID)
}
r.deps.Users = appusers.NewService(userStore)
const quickReplyIcon = int64(8800023)
quickReplyPeer := domain.Peer{Type: domain.PeerTypeUser, ID: userID}
verify.marks[quickReplyPeer] = domain.CustomVerification{
VerifierBotID: 777000123,
Peer: quickReplyPeer,
IconDocumentID: quickReplyIcon,
Description: "Verified quick-reply peer",
}
got, err := r.onMessagesSendMessage(ctx, &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerSelf{},
@ -68,6 +89,17 @@ func TestQuickReplyRPCSaveListAndDeleteMessage(t *testing.T) {
t.Fatalf("quick replies = %#v", list)
}
quickReplyMessages, err := r.onMessagesGetQuickReplyMessages(ctx, &tg.MessagesGetQuickReplyMessagesRequest{
ShortcutID: shortcutID,
})
if err != nil {
t.Fatalf("onMessagesGetQuickReplyMessages: %v", err)
}
assertMessagesEnvelopeBotVerificationIcon(t, quickReplyMessages, quickReplyPeer, quickReplyIcon)
if verify.batchCalls != 0 || verify.peerCalls != 1 {
t.Fatalf("quick-reply verification reads = batch %d peer %d, want 0/1 for one peer", verify.batchCalls, verify.peerCalls)
}
deleted, err := r.onMessagesDeleteQuickReplyMessages(ctx, &tg.MessagesDeleteQuickReplyMessagesRequest{
ShortcutID: shortcutID,
ID: []int{messageID},

View file

@ -42,11 +42,13 @@ func (r *Router) onMessagesGetSavedHistory(ctx context.Context, req *tg.Messages
if req.Hash != 0 {
return &tg.MessagesMessagesNotModified{Count: 0}, nil
}
return &tg.MessagesMessages{
result := &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Chats: r.savedHistoryChats(ctx, userID, hasParent, parentPeer, req.Peer),
Users: []tg.UserClass{},
}, nil
}
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
// parent_peer = monoforum:返回该订阅者(req.Peer)在频道私信内的历史。
return r.monoforumSavedHistory(ctx, userID, mono, savedPeer, req.Limit, req.OffsetID)
@ -83,6 +85,7 @@ func (r *Router) onMessagesGetSavedHistory(ctx context.Context, req *tg.Messages
m.Chats = mergeTGChats(m.Chats, chats)
}
}
r.applyPeerReadModelsToMessages(ctx, userID, out)
return out, nil
}

View file

@ -298,11 +298,13 @@ func (r *Router) onMessagesGetReplies(ctx context.Context, req *tg.MessagesGetRe
}
return r.tgChannelHistoryMessages(ctx, userID, r.enrichChannelHistory(ctx, userID, replies)), nil
}
return &tg.MessagesMessages{
result := &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, nil
}
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
func (r *Router) onMessagesGetDiscussionMessage(ctx context.Context, req *tg.MessagesGetDiscussionMessageRequest) (*tg.MessagesDiscussionMessage, error) {
@ -596,7 +598,9 @@ func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSea
return nil, err
}
if emptyCommunitySearch {
return appendCommunitySearchChat(&tg.MessagesMessages{}, communityView), nil
result := appendCommunitySearchChat(&tg.MessagesMessages{}, communityView)
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
var private domain.MessageList
if !req.BroadcastsOnly && !req.GroupsOnly && r.deps.Messages != nil {
@ -623,7 +627,9 @@ func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSea
}
}
if req.UsersOnly || r.deps.Channels == nil {
return appendCommunitySearchChat(tgMessagesMessages(userID, r.enrichMessageList(ctx, userID, limitMessageList(private, limit))), communityView), nil
result := appendCommunitySearchChat(tgMessagesMessages(userID, r.enrichMessageList(ctx, userID, limitMessageList(private, limit))), communityView)
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
channelHistory, err := r.deps.Channels.SearchJoinedMessages(ctx, userID, domain.ChannelGlobalSearchRequest{
Query: query,

View file

@ -119,12 +119,14 @@ func (r *Router) monoforumSavedHistory(ctx context.Context, userID int64, mono d
messages = append(messages, item)
}
}
return &tg.MessagesMessagesSlice{
result := &tg.MessagesMessagesSlice{
Count: hist.Count,
Messages: messages,
Chats: r.monoforumChats(ctx, userID, mono),
Users: r.monoforumSubscriberUsers(ctx, userID, nil, hist.Messages),
}, nil
}
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
// monoforumChats 投影客户端 materialize monoforum 私信所需的频道:monoforum 自身直接投影,

View file

@ -34,9 +34,11 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) {
channelStore := memory.NewChannelStore()
channelSvc := appchannels.NewService(channelStore)
verify := newFakeBotVerifications()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: channelSvc,
Users: appusers.NewService(userStore),
Channels: channelSvc,
BotVerifications: verify,
}, zaptest.NewLogger(t), clock.System)
created, err := channelSvc.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{Title: "DM Broadcast", Broadcast: true, Date: 1000})
@ -65,6 +67,14 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) {
}
monoInput := &tg.InputPeerChannel{ChannelID: monoID, AccessHash: mono.AccessHash}
parentInput := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}
const monoforumIcon = int64(8800025)
monoforumPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: monoID}
verify.marks[monoforumPeer] = domain.CustomVerification{
VerifierBotID: 777000123,
Peer: monoforumPeer,
IconDocumentID: monoforumIcon,
Description: "Verified monoforum peer",
}
// TDesktop 点 Direct Messages 入口会先按 monoforum peer 拉普通 channel history。
// 主历史只应返回 monoforum 自身的 service messages,不能混入 saved_peer 子会话消息。
@ -83,6 +93,7 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) {
if len(mainHistory.Messages) != 1 {
t.Fatalf("main monoforum history = %d msgs, want only the creation service", len(mainHistory.Messages))
}
assertMessagesEnvelopeBotVerificationIcon(t, mainHistory, monoforumPeer, monoforumIcon)
service, ok := mainHistory.Messages[0].(*tg.MessageService)
if !ok {
t.Fatalf("main monoforum message = %T, want MessageService", mainHistory.Messages[0])
@ -167,6 +178,7 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) {
if err != nil {
t.Fatalf("getSavedHistory(monoforum): %v", err)
}
assertMessagesEnvelopeBotVerificationIcon(t, hres, monoforumPeer, monoforumIcon)
var gotMsgs []tg.MessageClass
switch m := hres.(type) {
case *tg.MessagesMessages:

View file

@ -265,12 +265,14 @@ func (r *Router) onMessagesGetUnreadPollVotes(ctx context.Context, req *tg.Messa
if topMsgID, ok := req.GetTopMsgID(); ok && (topMsgID < 0 || topMsgID > domain.MaxMessageBoxID) {
return nil, messageIDInvalidErr()
}
return &tg.MessagesMessages{
result := &tg.MessagesMessages{
Messages: []tg.MessageClass{},
Topics: []tg.ForumTopicClass{},
Chats: r.chatsForInputPeer(ctx, userID, req.Peer),
Users: []tg.UserClass{},
}, nil
}
r.applyPeerReadModelsToMessages(ctx, userID, result)
return result, nil
}
func (r *Router) onMessagesReadPollVotes(ctx context.Context, req *tg.MessagesReadPollVotesRequest) (*tg.MessagesAffectedHistory, error) {

View file

@ -141,6 +141,7 @@ func (r *Router) onMessagesGetQuickReplyMessages(ctx context.Context, req *tg.Me
}
out := tgMessagesQuickReplyMessages(list)
out.Users = r.quickReplyUsers(ctx, userID)
r.applyPeerReadModelsToMessages(ctx, userID, out)
return out, nil
}

View file

@ -447,6 +447,16 @@ func TestMessagesGetSavedDialogsByID(t *testing.T) {
func TestMessagesGetSavedHistoryFiltersBySavedPeer(t *testing.T) {
ctx := context.Background()
r, _, alice, bob, noteID, fwdID := savedDialogsFixture(t)
verify := newFakeBotVerifications()
r.deps.BotVerifications = verify
const savedHistoryIcon = int64(8800022)
bobPeer := domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID}
verify.marks[bobPeer] = domain.CustomVerification{
VerifierBotID: 777000123,
Peer: bobPeer,
IconDocumentID: savedHistoryIcon,
Description: "Verified saved peer",
}
bobHistory, err := r.onMessagesGetSavedHistory(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedHistoryRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
@ -455,6 +465,7 @@ func TestMessagesGetSavedHistoryFiltersBySavedPeer(t *testing.T) {
if err != nil {
t.Fatalf("getSavedHistory bob: %v", err)
}
assertMessagesEnvelopeBotVerificationIcon(t, bobHistory, bobPeer, savedHistoryIcon)
bobMsgs := savedHistoryMessages(t, bobHistory)
if len(bobMsgs) != 1 || bobMsgs[0].ID != fwdID {
t.Fatalf("bob saved history = %+v, want forwarded message only", bobMsgs)