owpengram-server/internal/rpc/messages_pin_rpc_test.go

416 lines
16 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 newPinTestRouter(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
}
func findPinnedMessagesUpdate(t *testing.T, updates tg.UpdatesClass) *tg.UpdatePinnedMessages {
t.Helper()
box, ok := updates.(*tg.Updates)
if !ok {
t.Fatalf("updates = %T, want *tg.Updates", updates)
}
for _, update := range box.Updates {
if pinned, ok := update.(*tg.UpdatePinnedMessages); ok {
return pinned
}
}
return nil
}
func findServiceMessageUpdate(t *testing.T, updates tg.UpdatesClass) *tg.MessageService {
t.Helper()
box, ok := updates.(*tg.Updates)
if !ok {
t.Fatalf("updates = %T, want *tg.Updates", updates)
}
for _, update := range box.Updates {
if newMsg, ok := update.(*tg.UpdateNewMessage); ok {
if svc, ok := newMsg.Message.(*tg.MessageService); ok {
return svc
}
}
}
return nil
}
func privatePinnedIDs(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},
PinnedOnly: true,
Limit: 50,
})
if err != nil {
t.Fatalf("list pinned: %v", err)
}
ids := make([]int, 0, len(list.Messages))
for _, msg := range list.Messages {
ids = append(ids, msg.ID)
}
return ids
}
// TestMessagesUpdatePinnedMessagePrivateSharedPin 验证私聊共享置顶全链路:
// 双侧 box 翻转、updatePinnedMessages 账号 pts 事件、messageActionPinMessage
// 服务消息reply_to 指向被置顶消息、filterPinned 搜索与 userFull 发现。
func TestMessagesUpdatePinnedMessagePrivateSharedPin(t *testing.T) {
ctx := context.Background()
r, messageStore, userStore := newPinTestRouter(t)
alice, _ := userStore.Create(ctx, domain.User{AccessHash: 71, Phone: "15550009201", FirstName: "Alice"})
bob, _ := userStore.Create(ctx, domain.User{AccessHash: 72, Phone: "15550009202", FirstName: "Bob"})
sent, err := r.onMessagesSendMessage(WithUserID(ctx, alice.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
Message: "pin target",
RandomID: 92001,
})
if err != nil {
t.Fatalf("send: %v", err)
}
aliceMsg := sent.(*tg.Updates).Updates[1].(*tg.UpdateNewMessage).Message.(*tg.Message)
bobList, err := messageStore.ListByUser(ctx, bob.ID, domain.MessageFilter{
HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID}, Limit: 10,
})
if err != nil || len(bobList.Messages) != 1 {
t.Fatalf("bob list = %+v err %v, want delivered message", bobList.Messages, err)
}
bobBoxID := bobList.Messages[0].ID
// 共享置顶(未设 pm_oneside = 官方"同时为对方置顶"勾选形态)。
pinRes, err := r.onMessagesUpdatePinnedMessage(WithUserID(ctx, alice.ID), &tg.MessagesUpdatePinnedMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
ID: aliceMsg.ID,
})
if err != nil {
t.Fatalf("pin: %v", err)
}
pinnedUpdate := findPinnedMessagesUpdate(t, pinRes)
if pinnedUpdate == nil || !pinnedUpdate.Pinned || pinnedUpdate.Pts == 0 || pinnedUpdate.PtsCount != 1 {
t.Fatalf("pin update = %+v, want pinned with account pts", pinnedUpdate)
}
if len(pinnedUpdate.Messages) != 1 || pinnedUpdate.Messages[0] != aliceMsg.ID {
t.Fatalf("pin update messages = %v, want [%d] (alice 视角 box id)", pinnedUpdate.Messages, aliceMsg.ID)
}
if peer, ok := pinnedUpdate.Peer.(*tg.PeerUser); !ok || peer.UserID != bob.ID {
t.Fatalf("pin update peer = %+v, want bob", pinnedUpdate.Peer)
}
svc := findServiceMessageUpdate(t, pinRes)
if svc == nil {
t.Fatalf("pin response lacks messageService, want messageActionPinMessage")
}
if _, ok := svc.Action.(*tg.MessageActionPinMessage); !ok {
t.Fatalf("service action = %T, want messageActionPinMessage", svc.Action)
}
reply, ok := svc.GetReplyTo()
if !ok {
t.Fatalf("pin service message lacks reply_to (客户端凭此渲染置顶预览)")
}
if header, ok := reply.(*tg.MessageReplyHeader); !ok || header.ReplyToMsgID != aliceMsg.ID {
t.Fatalf("service reply_to = %+v, want alice box id %d", reply, aliceMsg.ID)
}
// 双侧置顶状态 + 对端服务消息(含对端视角 reply_to 翻译)。
if ids := privatePinnedIDs(t, messageStore, alice.ID, bob.ID); len(ids) != 1 || ids[0] != aliceMsg.ID {
t.Fatalf("alice pinned = %v, want [%d]", ids, aliceMsg.ID)
}
if ids := privatePinnedIDs(t, messageStore, bob.ID, alice.ID); len(ids) != 1 || ids[0] != bobBoxID {
t.Fatalf("bob pinned = %v, want [%d] (bob 视角 box id)", ids, bobBoxID)
}
bobAfter, _ := messageStore.ListByUser(ctx, bob.ID, domain.MessageFilter{
HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID}, Limit: 10,
})
foundService := false
for _, msg := range bobAfter.Messages {
if msg.Media != nil && msg.Media.Kind == domain.MessageMediaKindService &&
msg.Media.ServiceAction != nil && msg.Media.ServiceAction.Kind == domain.MessageServiceActionPinMessage {
foundService = true
if msg.ReplyTo == nil || msg.ReplyTo.MessageID != bobBoxID {
t.Fatalf("bob service reply = %+v, want bob box id %d", msg.ReplyTo, bobBoxID)
}
}
}
if !foundService {
t.Fatalf("bob history lacks pin service message: %+v", bobAfter.Messages)
}
// 幂等:重复 pin 返回空 updates不再生成服务消息。
repeat, err := r.onMessagesUpdatePinnedMessage(WithUserID(ctx, alice.ID), &tg.MessagesUpdatePinnedMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
ID: aliceMsg.ID,
})
if err != nil {
t.Fatalf("repeat pin: %v", err)
}
if update := findPinnedMessagesUpdate(t, repeat); update != nil {
t.Fatalf("repeat pin update = %+v, want no-op", update)
}
// 搜索发现链路filterPinnedbob 视角)带总数与 message.pinned 标志。
searchReq := &tg.MessagesSearchRequest{
Peer: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
Filter: &tg.InputMessagesFilterPinned{},
Limit: 10,
}
var in bin.Buffer
if err := searchReq.Encode(&in); err != nil {
t.Fatalf("encode pinned search: %v", err)
}
enc, err := r.Dispatch(WithUserID(ctx, bob.ID), [8]byte{}, 0, &in)
if err != nil {
t.Fatalf("dispatch pinned search: %v", err)
}
pinnedMessages, _, _ := searchMessagesPayload(t, enc)
if len(pinnedMessages) != 1 {
t.Fatalf("pinned search len = %d, want 1", len(pinnedMessages))
}
if msg := pinnedMessages[0].(*tg.Message); msg.ID != bobBoxID || !msg.Pinned {
t.Fatalf("pinned search msg = id %d pinned %v, want bob box id %d pinned", msg.ID, msg.Pinned, bobBoxID)
}
// userFull 发现链路pinned_msg_id + can_pin_message。
full, err := r.onUsersGetFullUser(WithUserID(ctx, alice.ID), &tg.InputUser{UserID: bob.ID, AccessHash: bob.AccessHash})
if err != nil {
t.Fatalf("get full user: %v", err)
}
if pinnedID, ok := full.FullUser.GetPinnedMsgID(); !ok || pinnedID != aliceMsg.ID {
t.Fatalf("userFull pinned_msg_id = %d (%v), want %d", pinnedID, ok, aliceMsg.ID)
}
if !full.FullUser.CanPinMessage {
t.Fatalf("userFull can_pin_message = false, want true")
}
// unpin双侧清除事件 pinned=false。
unpinReq := &tg.MessagesUpdatePinnedMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
ID: aliceMsg.ID,
}
unpinReq.SetUnpin(true)
unpinRes, err := r.onMessagesUpdatePinnedMessage(WithUserID(ctx, alice.ID), unpinReq)
if err != nil {
t.Fatalf("unpin: %v", err)
}
unpinUpdate := findPinnedMessagesUpdate(t, unpinRes)
if unpinUpdate == nil || unpinUpdate.Pinned {
t.Fatalf("unpin update = %+v, want pinned=false", unpinUpdate)
}
if findServiceMessageUpdate(t, unpinRes) != nil {
t.Fatalf("unpin generated service message, official unpin emits none")
}
if ids := privatePinnedIDs(t, messageStore, alice.ID, bob.ID); len(ids) != 0 {
t.Fatalf("alice pinned after unpin = %v, want empty", ids)
}
if ids := privatePinnedIDs(t, messageStore, bob.ID, alice.ID); len(ids) != 0 {
t.Fatalf("bob pinned after unpin = %v, want empty (共享置顶 unpin 双侧传播)", ids)
}
}
// TestMessagesUpdatePinnedMessagePrivateOnesideAndUnpinAll 验证 pm_oneside
// 单侧置顶(不传播、无服务消息)与 unpinAllMessages 清扫。
func TestMessagesUpdatePinnedMessagePrivateOnesideAndUnpinAll(t *testing.T) {
ctx := context.Background()
r, messageStore, userStore := newPinTestRouter(t)
alice, _ := userStore.Create(ctx, domain.User{AccessHash: 73, Phone: "15550009203", FirstName: "Alice"})
bob, _ := userStore.Create(ctx, domain.User{AccessHash: 74, Phone: "15550009204", FirstName: "Bob"})
sent, err := r.onMessagesSendMessage(WithUserID(ctx, alice.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
Message: "oneside target",
RandomID: 92011,
})
if err != nil {
t.Fatalf("send: %v", err)
}
aliceMsg := sent.(*tg.Updates).Updates[1].(*tg.UpdateNewMessage).Message.(*tg.Message)
bobBefore, _ := messageStore.ListByUser(ctx, bob.ID, domain.MessageFilter{
HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID}, Limit: 10,
})
// pm_oneside = 官方私聊置顶框默认形态(未勾选"同时为对方置顶")。
onesideReq := &tg.MessagesUpdatePinnedMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
ID: aliceMsg.ID,
}
onesideReq.SetPmOneside(true)
onesideRes, err := r.onMessagesUpdatePinnedMessage(WithUserID(ctx, alice.ID), onesideReq)
if err != nil {
t.Fatalf("oneside pin: %v", err)
}
if update := findPinnedMessagesUpdate(t, onesideRes); update == nil || !update.Pinned {
t.Fatalf("oneside pin update = %+v, want pinned", update)
}
if findServiceMessageUpdate(t, onesideRes) != nil {
t.Fatalf("oneside pin generated service message, want none")
}
if ids := privatePinnedIDs(t, messageStore, alice.ID, bob.ID); len(ids) != 1 {
t.Fatalf("alice oneside pinned = %v, want 1", ids)
}
if ids := privatePinnedIDs(t, messageStore, bob.ID, alice.ID); len(ids) != 0 {
t.Fatalf("bob pinned after oneside = %v, want empty (单侧不传播)", ids)
}
bobAfter, _ := messageStore.ListByUser(ctx, bob.ID, domain.MessageFilter{
HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID}, Limit: 10,
})
if len(bobAfter.Messages) != len(bobBefore.Messages) {
t.Fatalf("bob history grew %d -> %d after oneside pin, want unchanged", len(bobBefore.Messages), len(bobAfter.Messages))
}
// 单侧→共享升级:同一消息再发不带 pm_oneside 的 pin必须向对端
// 传播并生成服务消息own 侧已置顶不构成整体 no-op
upgradeRes, err := r.onMessagesUpdatePinnedMessage(WithUserID(ctx, alice.ID), &tg.MessagesUpdatePinnedMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
ID: aliceMsg.ID,
})
if err != nil {
t.Fatalf("upgrade pin: %v", err)
}
if ids := privatePinnedIDs(t, messageStore, bob.ID, alice.ID); len(ids) != 1 {
t.Fatalf("bob pinned after upgrade = %v, want shared pin propagated", ids)
}
if findServiceMessageUpdate(t, upgradeRes) == nil {
t.Fatalf("upgrade pin lacks service message, want messageActionPinMessage")
}
// 再追加一条共享置顶,然后 unpinAll 一次清掉两条。
sent2, err := r.onMessagesSendMessage(WithUserID(ctx, alice.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
Message: "shared target",
RandomID: 92012,
})
if err != nil {
t.Fatalf("send shared: %v", err)
}
sharedMsg := sent2.(*tg.Updates).Updates[1].(*tg.UpdateNewMessage).Message.(*tg.Message)
if _, err := r.onMessagesUpdatePinnedMessage(WithUserID(ctx, alice.ID), &tg.MessagesUpdatePinnedMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
ID: sharedMsg.ID,
}); err != nil {
t.Fatalf("shared pin: %v", err)
}
if ids := privatePinnedIDs(t, messageStore, alice.ID, bob.ID); len(ids) != 2 {
t.Fatalf("alice pinned = %v, want 2 before unpinAll", ids)
}
if ids := privatePinnedIDs(t, messageStore, bob.ID, alice.ID); len(ids) != 2 {
t.Fatalf("bob pinned = %v, want 2 shared before unpinAll", ids)
}
affected, err := r.onMessagesUnpinAllMessages(WithUserID(ctx, alice.ID), &tg.MessagesUnpinAllMessagesRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
})
if err != nil {
t.Fatalf("unpinAll: %v", err)
}
if affected.Pts == 0 || affected.Offset != 0 {
t.Fatalf("unpinAll affected = %+v, want account pts and offset 0", affected)
}
if ids := privatePinnedIDs(t, messageStore, alice.ID, bob.ID); len(ids) != 0 {
t.Fatalf("alice pinned after unpinAll = %v, want empty", ids)
}
if ids := privatePinnedIDs(t, messageStore, bob.ID, alice.ID); len(ids) != 0 {
t.Fatalf("bob pinned after unpinAll = %v, want empty (共享置顶同步清除)", ids)
}
}
// TestMessagesUpdatePinnedMessageSavedMessagesAndServiceGuard 验证 Saved
// Messages 置顶(无服务消息)与服务消息禁止置顶。
func TestMessagesUpdatePinnedMessageSavedMessagesAndServiceGuard(t *testing.T) {
ctx := context.Background()
r, messageStore, userStore := newPinTestRouter(t)
alice, _ := userStore.Create(ctx, domain.User{AccessHash: 75, Phone: "15550009205", FirstName: "Alice"})
sent, err := r.onMessagesSendMessage(WithUserID(ctx, alice.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerSelf{},
Message: "saved note",
RandomID: 92021,
})
if err != nil {
t.Fatalf("send saved: %v", err)
}
savedMsg := sent.(*tg.Updates).Updates[1].(*tg.UpdateNewMessage).Message.(*tg.Message)
pinRes, err := r.onMessagesUpdatePinnedMessage(WithUserID(ctx, alice.ID), &tg.MessagesUpdatePinnedMessageRequest{
Peer: &tg.InputPeerSelf{},
ID: savedMsg.ID,
})
if err != nil {
t.Fatalf("pin saved: %v", err)
}
if update := findPinnedMessagesUpdate(t, pinRes); update == nil || !update.Pinned {
t.Fatalf("saved pin update = %+v, want pinned", update)
}
if findServiceMessageUpdate(t, pinRes) != nil {
t.Fatalf("saved messages pin generated service message, want none (无人可通知)")
}
list, _ := messageStore.ListByUser(ctx, alice.ID, domain.MessageFilter{
HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID}, Limit: 10,
})
if len(list.Messages) != 1 || !list.Messages[0].Pinned {
t.Fatalf("saved history = %+v, want single pinned message without service message", list.Messages)
}
// 服务消息不可置顶:先用共享置顶在双人聊天里生成一条服务消息再试。
bob, _ := userStore.Create(ctx, domain.User{AccessHash: 76, Phone: "15550009206", FirstName: "Bob"})
sent2, err := r.onMessagesSendMessage(WithUserID(ctx, alice.ID), &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
Message: "to pin",
RandomID: 92022,
})
if err != nil {
t.Fatalf("send: %v", err)
}
target := sent2.(*tg.Updates).Updates[1].(*tg.UpdateNewMessage).Message.(*tg.Message)
if _, err := r.onMessagesUpdatePinnedMessage(WithUserID(ctx, alice.ID), &tg.MessagesUpdatePinnedMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
ID: target.ID,
}); err != nil {
t.Fatalf("pin for service msg: %v", err)
}
aliceList, _ := messageStore.ListByUser(ctx, alice.ID, domain.MessageFilter{
HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID}, Limit: 10,
})
serviceID := 0
for _, msg := range aliceList.Messages {
if msg.Media != nil && msg.Media.Kind == domain.MessageMediaKindService {
serviceID = msg.ID
}
}
if serviceID == 0 {
t.Fatalf("no service message found in alice history: %+v", aliceList.Messages)
}
if _, err := r.onMessagesUpdatePinnedMessage(WithUserID(ctx, alice.ID), &tg.MessagesUpdatePinnedMessageRequest{
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
ID: serviceID,
}); err == nil {
t.Fatalf("pin service message succeeded, want MESSAGE_ID_INVALID")
}
}