owpengram-server/internal/rpc/messages_saved_dialogs_rpc_test.go

626 lines
23 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/gotd/td/bin"
"github.com/gotd/td/clock"
"github.com/gotd/td/tg"
"go.uber.org/zap/zaptest"
appcontacts "telesrv/internal/app/contacts"
appdialogs "telesrv/internal/app/dialogs"
appmessages "telesrv/internal/app/messages"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
func newSavedDialogsTestRouter(t *testing.T) (*Router, *memory.MessageStore, *memory.UserStore) {
t.Helper()
userStore := memory.NewUserStore()
dialogs := memory.NewDialogStore()
messageStore := memory.NewMessageStore(dialogs)
contactStore := memory.NewContactStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Contacts: appcontacts.NewService(contactStore, userStore),
Messages: appmessages.NewService(messageStore, dialogs),
Dialogs: appdialogs.NewService(dialogs),
}, zaptest.NewLogger(t), clock.System)
return r, messageStore, userStore
}
// savedDialogsFixture 构造收藏夹两个子会话直发笔记saved=self+
// 从 bob 私聊转发saved=bob转发更晚 top 更新)。
func savedDialogsFixture(t *testing.T) (*Router, *memory.MessageStore, domain.User, domain.User, int, int) {
t.Helper()
ctx := context.Background()
r, messageStore, userStore := newSavedDialogsTestRouter(t)
alice, _ := userStore.Create(ctx, domain.User{AccessHash: 81, Phone: "15550009301", FirstName: "Alice"})
bob, _ := userStore.Create(ctx, domain.User{AccessHash: 82, Phone: "15550009302", FirstName: "Bob"})
if _, err := r.onMessagesSendMessage(WithUserID(ctx, alice.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerSelf{},
Message: "my note",
RandomID: 93001,
}); err != nil {
t.Fatalf("send self note: %v", err)
}
if _, err := r.onMessagesSendMessage(WithUserID(ctx, bob.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
Message: "from bob",
RandomID: 93002,
}); err != nil {
t.Fatalf("bob send: %v", err)
}
bobMsgID := privateTopMessageID(t, messageStore, alice.ID, bob.ID)
if _, err := r.onMessagesForwardMessages(WithUserID(ctx, alice.ID), &tg.MessagesForwardMessagesRequest{
FromPeer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
ToPeer: &tg.InputPeerSelf{},
ID: []int{bobMsgID},
RandomID: []int64{93003},
}); err != nil {
t.Fatalf("forward to saved: %v", err)
}
noteID := savedTopMessageID(t, messageStore, alice.ID, domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID})
fwdID := savedTopMessageID(t, messageStore, alice.ID, domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID})
return r, messageStore, alice, bob, noteID, fwdID
}
func privateTopMessageID(t *testing.T, store *memory.MessageStore, ownerID, peerID int64) int {
t.Helper()
list, err := store.ListByUser(context.Background(), ownerID, domain.MessageFilter{
HasPeer: true,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: peerID},
Limit: 1,
})
if err != nil || len(list.Messages) == 0 {
t.Fatalf("list private top: %+v err %v", list.Messages, err)
}
return list.Messages[0].ID
}
func savedTopMessageID(t *testing.T, store *memory.MessageStore, ownerID int64, savedPeer domain.Peer) int {
t.Helper()
list, err := store.ListByUser(context.Background(), ownerID, domain.MessageFilter{
HasPeer: true,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: ownerID},
SavedPeer: savedPeer,
Limit: 1,
})
if err != nil || len(list.Messages) == 0 {
t.Fatalf("list saved top: %+v err %v", list.Messages, err)
}
return list.Messages[0].ID
}
func savedDialogPage(t *testing.T, res tg.MessagesSavedDialogsClass) ([]*tg.SavedDialog, []tg.MessageClass, []tg.UserClass, int, bool) {
t.Helper()
var rawDialogs []tg.SavedDialogClass
var messages []tg.MessageClass
var users []tg.UserClass
count := 0
full := false
switch v := res.(type) {
case *tg.MessagesSavedDialogs:
rawDialogs, messages, users = v.Dialogs, v.Messages, v.Users
count = len(v.Dialogs)
full = true
case *tg.MessagesSavedDialogsSlice:
rawDialogs, messages, users = v.Dialogs, v.Messages, v.Users
count = v.Count
default:
t.Fatalf("saved dialogs = %T, want savedDialogs/slice", res)
}
dialogs := make([]*tg.SavedDialog, 0, len(rawDialogs))
for _, d := range rawDialogs {
sd, ok := d.(*tg.SavedDialog)
if !ok {
t.Fatalf("dialog = %T, want *tg.SavedDialog", d)
}
dialogs = append(dialogs, sd)
}
return dialogs, messages, users, count, full
}
func savedDialogPeerID(t *testing.T, d *tg.SavedDialog) int64 {
t.Helper()
switch p := d.Peer.(type) {
case *tg.PeerUser:
return p.UserID
case *tg.PeerChannel:
return p.ChannelID
default:
t.Fatalf("saved dialog peer = %T", d.Peer)
return 0
}
}
// TestMessagesGetSavedDialogsGroupsBySource 验证直发/转发分组、排序、
// saved_peer_id 下发与 users 投影。
func TestMessagesGetSavedDialogsGroupsBySource(t *testing.T) {
ctx := context.Background()
r, _, alice, bob, noteID, fwdID := savedDialogsFixture(t)
res, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsRequest{Limit: 20})
if err != nil {
t.Fatalf("getSavedDialogs: %v", err)
}
dialogs, messages, users, _, full := savedDialogPage(t, res)
if !full || len(dialogs) != 2 {
t.Fatalf("dialogs = %d full %v, want 2 full", len(dialogs), full)
}
// 转发更晚bob 子会话在前。
if savedDialogPeerID(t, dialogs[0]) != bob.ID || dialogs[0].TopMessage != fwdID {
t.Fatalf("first dialog = peer %d top %d, want bob %d top %d", savedDialogPeerID(t, dialogs[0]), dialogs[0].TopMessage, bob.ID, fwdID)
}
if savedDialogPeerID(t, dialogs[1]) != alice.ID || dialogs[1].TopMessage != noteID {
t.Fatalf("second dialog = peer %d top %d, want self %d top %d", savedDialogPeerID(t, dialogs[1]), dialogs[1].TopMessage, alice.ID, noteID)
}
if len(messages) != 2 {
t.Fatalf("messages = %d, want 2 tops", len(messages))
}
for _, raw := range messages {
msg, ok := raw.(*tg.Message)
if !ok {
t.Fatalf("message = %T", raw)
}
saved, ok := msg.GetSavedPeerID()
if !ok {
t.Fatalf("message %d lacks saved_peer_id", msg.ID)
}
want := alice.ID
if msg.ID == fwdID {
want = bob.ID
}
if user, ok := saved.(*tg.PeerUser); !ok || user.UserID != want {
t.Fatalf("message %d saved_peer_id = %+v, want user %d", msg.ID, saved, want)
}
if msg.ID == fwdID {
fwd, ok := msg.GetFwdFrom()
if !ok {
t.Fatalf("forwarded top lacks fwd header")
}
savedFrom, ok := fwd.GetSavedFromPeer()
if !ok {
t.Fatalf("forwarded top lacks saved_from_peer")
}
if user, ok := savedFrom.(*tg.PeerUser); !ok || user.UserID != bob.ID {
t.Fatalf("saved_from_peer = %+v, want bob", savedFrom)
}
}
}
var hasSelf, hasBob bool
for _, raw := range users {
u, ok := raw.(*tg.User)
if !ok {
continue
}
if u.ID == alice.ID {
hasSelf = true
if !u.Self {
t.Fatalf("self user lacks self flag")
}
}
if u.ID == bob.ID {
hasBob = true
}
}
if !hasSelf || !hasBob {
t.Fatalf("users projection self=%v bob=%v, want both", hasSelf, hasBob)
}
}
// TestMessagesGetSavedDialogsPagination 验证分页limit 截断 → slice+count
// offset 翻页 → 尾页 savedDialogsAndroid 首页 offset_id=int32 max 等价首页。
func TestMessagesGetSavedDialogsPagination(t *testing.T) {
ctx := context.Background()
r, _, alice, bob, noteID, fwdID := savedDialogsFixture(t)
first, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsRequest{Limit: 1})
if err != nil {
t.Fatalf("first page: %v", err)
}
dialogs, _, _, count, full := savedDialogPage(t, first)
if full || count != 2 || len(dialogs) != 1 || savedDialogPeerID(t, dialogs[0]) != bob.ID {
t.Fatalf("first page = %d dialogs count %d full %v, want slice 1/2 bob first", len(dialogs), count, full)
}
second, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsRequest{
Limit: 1,
OffsetID: fwdID,
})
if err != nil {
t.Fatalf("second page: %v", err)
}
dialogs, _, _, _, full = savedDialogPage(t, second)
if !full || len(dialogs) != 1 || savedDialogPeerID(t, dialogs[0]) != alice.ID || dialogs[0].TopMessage != noteID {
t.Fatalf("second page = %+v full %v, want self note tail", dialogs, full)
}
androidFirst, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsRequest{
Limit: 20,
OffsetID: 2147483647,
})
if err != nil {
t.Fatalf("android first page: %v", err)
}
dialogs, _, _, _, full = savedDialogPage(t, androidFirst)
if !full || len(dialogs) != 2 {
t.Fatalf("android first page = %d full %v, want 2 full", len(dialogs), full)
}
}
// TestMessagesGetSavedDialogsHashNotModified 验证 Android calcHash 序列命中
// 返回 savedDialogsNotModified{count}。
func TestMessagesGetSavedDialogsHashNotModified(t *testing.T) {
ctx := context.Background()
r, _, alice, _, _, _ := savedDialogsFixture(t)
res, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsRequest{Limit: 20})
if err != nil {
t.Fatalf("getSavedDialogs: %v", err)
}
dialogs, messages, _, _, _ := savedDialogPage(t, res)
dateByID := map[int]int{}
for _, raw := range messages {
if msg, ok := raw.(*tg.Message); ok {
dateByID[msg.ID] = msg.Date
}
}
var hash uint64
for _, d := range dialogs {
pinned := int64(0)
if d.Pinned {
pinned = 1
}
hash = tdesktopHashUpdate(hash, pinned)
hash = tdesktopHashUpdate(hash, savedDialogPeerID(t, d))
hash = tdesktopHashUpdate(hash, int64(d.TopMessage))
hash = tdesktopHashUpdate(hash, int64(dateByID[d.TopMessage]))
}
cached, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsRequest{
Limit: 20,
Hash: int64(hash),
})
if err != nil {
t.Fatalf("getSavedDialogs hash: %v", err)
}
notModified, ok := cached.(*tg.MessagesSavedDialogsNotModified)
if !ok || notModified.Count != 2 {
t.Fatalf("hash hit = %#v, want notModified count 2", cached)
}
}
// TestMessagesSavedDialogPinLifecycle 验证置顶翻转/排序/排除全链路:
// updateSavedDialogPinned 推送、首页 pinned 在前、getPinnedSavedDialogs、
// exclude_pinned 口径、reorder 推 updatePinnedSavedDialogs。
func TestMessagesSavedDialogPinLifecycle(t *testing.T) {
ctx := context.Background()
r, _, alice, bob, noteID, _ := savedDialogsFixture(t)
pinSelfReq := &tg.MessagesToggleSavedDialogPinRequest{Peer: &tg.InputDialogPeer{Peer: &tg.InputPeerSelf{}}}
pinSelfReq.SetPinned(true)
ok, err := r.onMessagesToggleSavedDialogPin(WithUserID(ctx, alice.ID), pinSelfReq)
if err != nil || !ok {
t.Fatalf("pin self sublist = %v err %v", ok, err)
}
res, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsRequest{Limit: 20})
if err != nil {
t.Fatalf("getSavedDialogs: %v", err)
}
dialogs, _, _, _, _ := savedDialogPage(t, res)
if len(dialogs) != 2 || savedDialogPeerID(t, dialogs[0]) != alice.ID || !dialogs[0].Pinned {
t.Fatalf("pinned self sublist not first: %+v", dialogs)
}
if dialogs[1].Pinned {
t.Fatalf("bob sublist unexpectedly pinned")
}
pinnedRes, err := r.onMessagesGetPinnedSavedDialogs(WithUserID(ctx, alice.ID))
if err != nil {
t.Fatalf("getPinnedSavedDialogs: %v", err)
}
dialogs, _, _, _, full := savedDialogPage(t, pinnedRes)
if !full || len(dialogs) != 1 || savedDialogPeerID(t, dialogs[0]) != alice.ID || dialogs[0].TopMessage != noteID {
t.Fatalf("pinned list = %+v, want self note", dialogs)
}
excluded, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsRequest{
ExcludePinned: true,
Limit: 20,
})
if err != nil {
t.Fatalf("getSavedDialogs exclude: %v", err)
}
dialogs, _, _, count, full := savedDialogPage(t, excluded)
if !full || len(dialogs) != 1 || savedDialogPeerID(t, dialogs[0]) != bob.ID {
t.Fatalf("exclude pinned = %+v, want only bob", dialogs)
}
if count != 1 {
t.Fatalf("exclude pinned count = %d, want 1", count)
}
pinBobReq := &tg.MessagesToggleSavedDialogPinRequest{Peer: &tg.InputDialogPeer{Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash}}}
pinBobReq.SetPinned(true)
ok, err = r.onMessagesToggleSavedDialogPin(WithUserID(ctx, alice.ID), pinBobReq)
if err != nil || !ok {
t.Fatalf("pin bob sublist = %v err %v", ok, err)
}
// 新置顶插到最前。
pinnedRes, err = r.onMessagesGetPinnedSavedDialogs(WithUserID(ctx, alice.ID))
if err != nil {
t.Fatalf("getPinnedSavedDialogs: %v", err)
}
dialogs, _, _, _, _ = savedDialogPage(t, pinnedRes)
if len(dialogs) != 2 || savedDialogPeerID(t, dialogs[0]) != bob.ID {
t.Fatalf("pinned order after pin bob = %+v, want bob first", dialogs)
}
ok, err = r.onMessagesReorderPinnedSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesReorderPinnedSavedDialogsRequest{
Order: []tg.InputDialogPeerClass{
&tg.InputDialogPeer{Peer: &tg.InputPeerSelf{}},
&tg.InputDialogPeer{Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash}},
},
})
if err != nil || !ok {
t.Fatalf("reorder = %v err %v", ok, err)
}
pinnedRes, err = r.onMessagesGetPinnedSavedDialogs(WithUserID(ctx, alice.ID))
if err != nil {
t.Fatalf("getPinnedSavedDialogs after reorder: %v", err)
}
dialogs, _, _, _, _ = savedDialogPage(t, pinnedRes)
if len(dialogs) != 2 || savedDialogPeerID(t, dialogs[0]) != alice.ID || savedDialogPeerID(t, dialogs[1]) != bob.ID {
t.Fatalf("reorder result = %+v, want self then bob", dialogs)
}
ok, err = r.onMessagesToggleSavedDialogPin(WithUserID(ctx, alice.ID), &tg.MessagesToggleSavedDialogPinRequest{
Peer: &tg.InputDialogPeer{Peer: &tg.InputPeerSelf{}},
})
if err != nil || !ok {
t.Fatalf("unpin self = %v err %v", ok, err)
}
pinnedRes, err = r.onMessagesGetPinnedSavedDialogs(WithUserID(ctx, alice.ID))
if err != nil {
t.Fatalf("getPinnedSavedDialogs after unpin: %v", err)
}
dialogs, _, _, _, _ = savedDialogPage(t, pinnedRes)
if len(dialogs) != 1 || savedDialogPeerID(t, dialogs[0]) != bob.ID {
t.Fatalf("after unpin = %+v, want only bob", dialogs)
}
}
// TestMessagesGetSavedDialogsByID 验证指定查询:命中返回、未知 peer 静默缺席。
func TestMessagesGetSavedDialogsByID(t *testing.T) {
ctx := context.Background()
r, _, alice, bob, _, fwdID := savedDialogsFixture(t)
res, err := r.onMessagesGetSavedDialogsByID(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsByIDRequest{
IDs: []tg.InputPeerClass{
&tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
&tg.InputPeerUser{UserID: 999999, AccessHash: 1},
},
})
if err != nil {
t.Fatalf("getSavedDialogsByID: %v", err)
}
dialogs, _, _, _, full := savedDialogPage(t, res)
if !full || len(dialogs) != 1 || savedDialogPeerID(t, dialogs[0]) != bob.ID || dialogs[0].TopMessage != fwdID {
t.Fatalf("byID = %+v, want bob sublist only", dialogs)
}
}
// TestMessagesGetSavedHistoryFiltersBySavedPeer 验证子会话历史过滤、
// count 与 hash notModified。
func TestMessagesGetSavedHistoryFiltersBySavedPeer(t *testing.T) {
ctx := context.Background()
r, _, alice, bob, noteID, fwdID := savedDialogsFixture(t)
bobHistory, err := r.onMessagesGetSavedHistory(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedHistoryRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
Limit: 20,
})
if err != nil {
t.Fatalf("getSavedHistory bob: %v", err)
}
bobMsgs := savedHistoryMessages(t, bobHistory)
if len(bobMsgs) != 1 || bobMsgs[0].ID != fwdID {
t.Fatalf("bob saved history = %+v, want forwarded message only", bobMsgs)
}
selfHistory, err := r.onMessagesGetSavedHistory(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedHistoryRequest{
Peer: &tg.InputPeerSelf{},
Limit: 20,
})
if err != nil {
t.Fatalf("getSavedHistory self: %v", err)
}
selfMsgs := savedHistoryMessages(t, selfHistory)
if len(selfMsgs) != 1 || selfMsgs[0].ID != noteID {
t.Fatalf("self saved history = %+v, want note only", selfMsgs)
}
// max_id 边界:低于转发 id 时空。
empty, err := r.onMessagesGetSavedHistory(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedHistoryRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
MaxID: fwdID,
Limit: 20,
})
if err != nil {
t.Fatalf("getSavedHistory max_id: %v", err)
}
if msgs := savedHistoryMessages(t, empty); len(msgs) != 0 {
t.Fatalf("max_id bounded history = %+v, want empty", msgs)
}
}
func savedHistoryMessages(t *testing.T, res tg.MessagesMessagesClass) []*tg.Message {
t.Helper()
var raw []tg.MessageClass
switch v := res.(type) {
case *tg.MessagesMessages:
raw = v.Messages
case *tg.MessagesMessagesSlice:
raw = v.Messages
default:
t.Fatalf("saved history = %T", res)
}
out := make([]*tg.Message, 0, len(raw))
for _, m := range raw {
msg, ok := m.(*tg.Message)
if !ok {
t.Fatalf("message = %T", m)
}
out = append(out, msg)
}
return out
}
// TestMessagesDeleteSavedHistoryRemovesSublist 验证删除子会话:带 pts 的
// affectedHistory、子会话从列表消失、置顶清理、self-chat 历史同步消失。
func TestMessagesDeleteSavedHistoryRemovesSublist(t *testing.T) {
ctx := context.Background()
r, messageStore, alice, bob, noteID, fwdID := savedDialogsFixture(t)
pinBobReq := &tg.MessagesToggleSavedDialogPinRequest{Peer: &tg.InputDialogPeer{Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash}}}
pinBobReq.SetPinned(true)
if _, err := r.onMessagesToggleSavedDialogPin(WithUserID(ctx, alice.ID), pinBobReq); err != nil {
t.Fatalf("pin bob: %v", err)
}
affected, err := r.onMessagesDeleteSavedHistory(WithUserID(ctx, alice.ID), &tg.MessagesDeleteSavedHistoryRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
})
if err != nil {
t.Fatalf("deleteSavedHistory: %v", err)
}
if affected.Pts == 0 || affected.PtsCount != 1 || affected.Offset != 0 {
t.Fatalf("affected = %+v, want pts advance for 1 message, no continuation", affected)
}
res, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsRequest{Limit: 20})
if err != nil {
t.Fatalf("getSavedDialogs after delete: %v", err)
}
dialogs, _, _, _, _ := savedDialogPage(t, res)
if len(dialogs) != 1 || savedDialogPeerID(t, dialogs[0]) != alice.ID {
t.Fatalf("dialogs after delete = %+v, want only self note", dialogs)
}
pinnedRes, err := r.onMessagesGetPinnedSavedDialogs(WithUserID(ctx, alice.ID))
if err != nil {
t.Fatalf("getPinnedSavedDialogs after delete: %v", err)
}
dialogs, _, _, _, _ = savedDialogPage(t, pinnedRes)
if len(dialogs) != 0 {
t.Fatalf("pinned after delete = %+v, want empty (pin cleared)", dialogs)
}
list, err := messageStore.ListByUser(ctx, alice.ID, domain.MessageFilter{
HasPeer: true,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID},
Limit: 20,
})
if err != nil {
t.Fatalf("self-chat history: %v", err)
}
if len(list.Messages) != 1 || list.Messages[0].ID != noteID {
t.Fatalf("self-chat history after delete = %+v, want only note (fwd %d gone)", list.Messages, fwdID)
}
}
// TestModernForwardMessagesConstructorSavesToSelf 复现 Android「保存到收藏夹」
// 路径DrKLO 12.7.x 发晚于 Layer 225 的 messages.forwardMessages#41d41ade
// 服务端 500 时 tgnet 无限重试UI 永久转圈。compat 解码后必须等价于
// #13704a7c转发成功、收藏夹出现来源子会话。
func TestModernForwardMessagesConstructorSavesToSelf(t *testing.T) {
ctx := context.Background()
r, messageStore, userStore := newSavedDialogsTestRouter(t)
alice, _ := userStore.Create(ctx, domain.User{AccessHash: 83, Phone: "15550009401", FirstName: "Alice"})
bob, _ := userStore.Create(ctx, domain.User{AccessHash: 84, Phone: "15550009402", FirstName: "Bob"})
if _, err := r.onMessagesSendMessage(WithUserID(ctx, bob.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
Message: "save me",
RandomID: 94001,
}); err != nil {
t.Fatalf("bob send: %v", err)
}
bobMsgID := privateTopMessageID(t, messageStore, alice.ID, bob.ID)
// 按 DrKLO TL_messages_forwardMessages.serializeToStream 的线格式手工编码:
// constructor + flags + from_peer + Vector<int> id + Vector<long> random_id + to_peer。
var raw bin.Buffer
raw.PutID(0x41d41ade)
raw.PutInt(0) // flags保存到收藏夹不带任何可选字段
if err := (&tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash}).Encode(&raw); err != nil {
t.Fatalf("encode from_peer: %v", err)
}
raw.PutVectorHeader(1)
raw.PutInt(bobMsgID)
raw.PutVectorHeader(1)
raw.PutLong(94002)
if err := (&tg.InputPeerSelf{}).Encode(&raw); err != nil {
t.Fatalf("encode to_peer: %v", err)
}
enc, err := r.Dispatch(WithUserID(androidClientContext(), alice.ID), [8]byte{}, 0, &raw)
if err != nil {
t.Fatalf("dispatch messages.forwardMessages#41d41ade: %v", err)
}
// Now routed through the unified layerwire client-alias (4-byte id swap) and
// the normal gotd dispatcher, which boxes a class result as *tg.UpdatesBox
// (wire-identical to the raw UpdatesClass the dedicated handler used to return).
switch v := enc.(type) {
case tg.UpdatesClass:
case *tg.UpdatesBox:
if v.Updates == nil {
t.Fatalf("forward result box has nil Updates")
}
default:
t.Fatalf("forward result = %T, want UpdatesClass or *tg.UpdatesBox", enc)
}
res, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedDialogsRequest{Limit: 20})
if err != nil {
t.Fatalf("getSavedDialogs after android forward: %v", err)
}
dialogs, messages, _, _, _ := savedDialogPage(t, res)
if len(dialogs) != 1 || savedDialogPeerID(t, dialogs[0]) != bob.ID {
t.Fatalf("saved dialogs after android forward = %+v, want bob sublist", dialogs)
}
msg, ok := messages[0].(*tg.Message)
if !ok {
t.Fatalf("top message = %T", messages[0])
}
if saved, ok := msg.GetSavedPeerID(); !ok {
t.Fatalf("forwarded message lacks saved_peer_id")
} else if user, ok := saved.(*tg.PeerUser); !ok || user.UserID != bob.ID {
t.Fatalf("saved_peer_id = %+v, want bob", saved)
}
}
// TestSavedPeerForSelfChat 验证子会话分组键计算与 TDLib 语义对齐。
func TestSavedPeerForSelfChat(t *testing.T) {
self := int64(1000000001)
bobPeer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
cases := []struct {
name string
forward *domain.MessageForward
want domain.Peer
}{
{"direct note", nil, domain.Peer{Type: domain.PeerTypeUser, ID: self}},
{"forward with source", &domain.MessageForward{From: bobPeer, SavedFrom: bobPeer, SavedFromMsgID: 7}, bobPeer},
{"forward channel source", &domain.MessageForward{From: domain.Peer{Type: domain.PeerTypeChannel, ID: 500}, SavedFrom: domain.Peer{Type: domain.PeerTypeChannel, ID: 500}}, domain.Peer{Type: domain.PeerTypeChannel, ID: 500}},
{"hidden author without source", &domain.MessageForward{FromName: "Hidden"}, domain.Peer{Type: domain.PeerTypeUser, ID: domain.SavedHiddenAuthorUserID}},
{"visible author without source", &domain.MessageForward{From: bobPeer}, domain.Peer{Type: domain.PeerTypeUser, ID: self}},
}
for _, tc := range cases {
if got := domain.SavedPeerForSelfChat(self, tc.forward); got != tc.want {
t.Fatalf("%s: SavedPeerForSelfChat = %+v, want %+v", tc.name, got, tc.want)
}
}
}