owpengram-server/internal/rpc/channels_multi_pin_android_rpc_test.go

229 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"testing"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/tg"
"go.uber.org/zap/zaptest"
appchannels "telesrv/internal/app/channels"
appdialogs "telesrv/internal/app/dialogs"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
// TestChannelMultiPinAndroidOpenAndJump 模拟 DrKLO Android 超级群多置顶消费链路:
// 打开聊天 → messages.search(filterPinned, limit=40, offset_id=0) 全量拉置顶列表;
// 点置顶栏跳最旧 pin → messages.getHistory(offset_id=pin, add_offset=-count/2, limit=count)
// AROUND 加载,响应必须包含锚点消息本身,否则客户端弹 MessageNotFound 放弃跳转;
// 本地缺对象 → channels.getMessages 精确补拉messageEmpty 会被客户端丢弃。
func TestChannelMultiPinAndroidOpenAndJump(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 61, Phone: "15550001161", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
member, err := userStore.Create(ctx, domain.User{AccessHash: 62, Phone: "15550001162", FirstName: "Member"})
if err != nil {
t.Fatalf("create member: %v", err)
}
channelStore := memory.NewChannelStore()
channelSvc := appchannels.NewService(channelStore)
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: channelSvc,
Dialogs: appdialogs.NewService(memory.NewDialogStore(), channelStore),
}, zaptest.NewLogger(t), clock.System)
created, err := channelSvc.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "MultiPin Android",
Megagroup: true,
MemberUserIDs: []int64{member.ID},
Date: 1700001000,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
channelID := created.Channel.ID
const total = 30
ids := make([]int, 0, total)
for i := 0; i < total; i++ {
sent, err := channelSvc.SendMessage(ctx, owner.ID, domain.SendChannelMessageRequest{
ChannelID: channelID,
RandomID: int64(961000 + i),
Message: "msg",
Date: 1700001001 + i,
})
if err != nil {
t.Fatalf("send %d: %v", i, err)
}
ids = append(ids, sent.Message.ID)
}
// 三条置顶早期、中间、最新Android 置顶栏循环跳转需要全部三条都可跳)。
pins := []int{ids[4], ids[14], ids[27]}
for _, id := range pins {
if _, err := channelSvc.UpdatePinnedMessage(ctx, owner.ID, domain.UpdateChannelPinnedMessageRequest{
ChannelID: channelID,
MessageID: id,
Pinned: true,
Date: 1700001100,
}); err != nil {
t.Fatalf("pin %d: %v", id, err)
}
}
memberView, err := channelSvc.GetChannel(ctx, member.ID, channelID)
if err != nil {
t.Fatalf("member get channel: %v", err)
}
peer := &tg.InputPeerChannel{ChannelID: channelID, AccessHash: memberView.Channel.AccessHash}
dispatch := func(req bin.Encoder) bin.Encoder {
t.Helper()
var b bin.Buffer
if err := req.Encode(&b); err != nil {
t.Fatalf("encode request: %v", err)
}
enc, err := r.Dispatch(WithUserID(androidClientContext(), member.ID), [8]byte{}, 0, &b)
if err != nil {
t.Fatalf("dispatch: %v", err)
}
return enc
}
// ① 打开聊天MediaDataController.loadPinnedMessages → messages.search filterPinned。
searchEnc := dispatch(&tg.MessagesSearchRequest{
Peer: peer,
Q: "",
Filter: &tg.InputMessagesFilterPinned{},
Limit: 40,
})
if box, ok := searchEnc.(*tg.MessagesMessagesBox); ok {
searchEnc = box.Messages
}
channelMessages, ok := searchEnc.(*tg.MessagesChannelMessages)
if !ok {
t.Fatalf("pinned search response = %T, want messages.channelMessages", searchEnc)
}
if len(channelMessages.Messages) != len(pins) {
t.Fatalf("pinned search messages = %d, want %d", len(channelMessages.Messages), len(pins))
}
if channelMessages.Count != len(pins) {
t.Fatalf("pinned search count = %d, want %d", channelMessages.Count, len(pins))
}
wantDesc := []int{pins[2], pins[1], pins[0]}
for i, raw := range channelMessages.Messages {
msg, ok := raw.(*tg.Message)
if !ok {
// TL_messageService / TL_messageEmpty 会被 Android loadPinnedMessages 直接跳过。
t.Fatalf("pinned search message[%d] = %T, want *tg.Message", i, raw)
}
if msg.ID != wantDesc[i] {
t.Fatalf("pinned search order[%d] = %d, want %d (id desc)", i, msg.ID, wantDesc[i])
}
if !msg.Pinned {
t.Fatalf("pinned search message %d lacks pinned flag", msg.ID)
}
}
// ② 点置顶栏跳最旧 pinscrollToMessageId → getHistory AROUND手机 count=20
const aroundCount = 20
histEnc := dispatch(&tg.MessagesGetHistoryRequest{
Peer: peer,
OffsetID: pins[0],
AddOffset: -aroundCount / 2,
Limit: aroundCount,
})
histMessages, _, _ := searchMessagesPayload(t, histEnc)
if len(histMessages) == 0 || len(histMessages) > aroundCount {
t.Fatalf("around history size = %d, want 1..%d (超出 count 时 Android 会丢最新一条)", len(histMessages), aroundCount)
}
anchorFound := false
lastID := int(^uint(0) >> 1)
for _, raw := range histMessages {
if _, isEmpty := raw.(*tg.MessageEmpty); isEmpty {
t.Fatalf("around history contains messageEmpty")
}
id := raw.GetID()
if id >= lastID {
t.Fatalf("around history not id-desc: %d then %d", lastID, id)
}
lastID = id
if id == pins[0] {
anchorFound = true
}
}
if !anchorFound {
// ChatActivity postponedScroll 在响应缺锚点时直接 MessageNotFound 放弃跳转。
t.Fatalf("around history lacks anchor %d: jump shows MessageNotFound on Android", pins[0])
}
// ③ 本地缺对象补拉MessagesStorage.loadChatInfo → channels.getMessages。
// DrKLO 发的是 pre-InputMessage 构造器 #93d7b347id:Vector<int>
// 该请求 500 会让客户端把这批 pin 按「已取消置顶」从本地缓存删除。
var legacy bin.Buffer
legacy.PutID(0x93d7b347)
if err := (&tg.InputChannel{ChannelID: channelID, AccessHash: memberView.Channel.AccessHash}).Encode(&legacy); err != nil {
t.Fatalf("encode legacy input channel: %v", err)
}
legacy.PutVectorHeader(len(pins))
for _, id := range pins {
legacy.PutInt(id)
}
getEnc, err := r.Dispatch(WithUserID(androidClientContext(), member.ID), [8]byte{}, 0, &legacy)
if err != nil {
t.Fatalf("dispatch legacy channels.getMessages#93d7b347: %v", err)
}
getMessages, _, _ := searchMessagesPayload(t, getEnc)
if len(getMessages) != len(pins) {
t.Fatalf("legacy channels.getMessages size = %d, want %d", len(getMessages), len(pins))
}
for i, raw := range getMessages {
msg, ok := raw.(*tg.Message)
if !ok {
t.Fatalf("legacy channels.getMessages[%d] = %T, want *tg.Message (messageEmpty 会被客户端丢弃)", i, raw)
}
if !msg.Pinned {
t.Fatalf("legacy channels.getMessages message %d lacks pinned flag", msg.ID)
}
}
// 新构造器TDesktop 路径)必须与 legacy 返回一致的消息集合。
getIDs := make([]tg.InputMessageClass, 0, len(pins))
for _, id := range pins {
getIDs = append(getIDs, &tg.InputMessageID{ID: id})
}
modernEnc := dispatch(&tg.ChannelsGetMessagesRequest{
Channel: &tg.InputChannel{ChannelID: channelID, AccessHash: memberView.Channel.AccessHash},
ID: getIDs,
})
modernMessages, _, _ := searchMessagesPayload(t, modernEnc)
if len(modernMessages) != len(getMessages) {
t.Fatalf("modern channels.getMessages size = %d, want %d (legacy/modern must match)", len(modernMessages), len(getMessages))
}
for i := range modernMessages {
if modernMessages[i].GetID() != getMessages[i].GetID() {
t.Fatalf("modern/legacy mismatch at %d: %d != %d", i, modernMessages[i].GetID(), getMessages[i].GetID())
}
}
// ④ chatFull 降级缓存pinned_msg_id 必须是最新置顶Android 以它判断是否重拉列表)。
fullEnc := dispatch(&tg.ChannelsGetFullChannelRequest{
Channel: &tg.InputChannel{ChannelID: channelID, AccessHash: memberView.Channel.AccessHash},
})
full, ok := fullEnc.(*tg.MessagesChatFull)
if !ok {
t.Fatalf("getFullChannel response = %T, want messages.chatFull", fullEnc)
}
channelFull, ok := full.FullChat.(*tg.ChannelFull)
if !ok {
t.Fatalf("full chat = %T, want channelFull", full.FullChat)
}
if pinnedID, _ := channelFull.GetPinnedMsgID(); pinnedID != pins[2] {
t.Fatalf("channelFull pinned_msg_id = %d, want latest pin %d", pinnedID, pins[2])
}
}