416 lines
16 KiB
Go
416 lines
16 KiB
Go
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"
|
||
|
||
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)
|
||
}
|
||
|
||
// 搜索发现链路:filterPinned(bob 视角)带总数与 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")
|
||
}
|
||
}
|