626 lines
23 KiB
Go
626 lines
23 KiB
Go
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 翻页 → 尾页 savedDialogs;Android 首页 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)
|
||
}
|
||
}
|
||
}
|