owpengram-server/internal/rpc/channels_passive_stubs_rpc_test.go
iamxvbaba 3fa0c6f1e4 fix(rpc): sync verification icon envelope coverage
Follow up gramsrv#27 with the remaining peer-bearing messages.Messages boundaries and route-level wire regression coverage from the telesrv canonical implementation.

Origin-PR: gramsrv#27
Origin-Head: f0bd6c9699
Origin-Merge: 7713847b2a
Canonical: f1ba406015a2d5e37802b963757a5cac8ee3eb0e

Co-authored-by: epilepticseizureee <business.egor.sg@gmail.com>
2026-07-30 14:06:39 +08:00

1368 lines
62 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 (
"github.com/iamxvbaba/td/tg"
"strings"
apptelemetry "telesrv/internal/app/clienttelemetry"
appmessages "telesrv/internal/app/messages"
appmoderation "telesrv/internal/app/moderation"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
"testing"
"time"
)
func TestTDesktopPassiveChannelStubs(t *testing.T) {
f := newRPCChannelFixture(t)
r := f.router
moderationReports := memory.NewModerationReportStore()
telemetryEvents := memory.NewClientTelemetryStore()
r.deps.ClientTelemetry = apptelemetry.NewService(telemetryEvents)
r.deps.Moderation = appmoderation.NewService(
moderationReports,
appmoderation.WithMessageReaders(nil, r.deps.Channels),
appmoderation.WithPeerReaders(r.deps.Users, r.deps.Channels),
)
owner := f.user(41, "15550002101", "Owner")
friend := f.user(42, "15550002102", "Friend")
invited := f.user(43, "15550002103", "Invited")
ownerCtx := f.userCtx(owner)
friendCtx := f.userCtx(friend)
channel := f.createLegacyMegagroup(owner, "RPC Passive Group", friend)
sendAs, err := r.onChannelsGetSendAs(ownerCtx, &tg.ChannelsGetSendAsRequest{
Peer: inputPeerChannel(channel),
})
if err != nil {
t.Fatalf("get send as: %v", err)
}
if len(sendAs.Peers) != 2 || len(sendAs.Users) != 1 || len(sendAs.Chats) != 1 {
t.Fatalf("send as = %+v, want self + current channel peers with current channel context", sendAs)
}
if peer, ok := sendAs.Peers[0].Peer.(*tg.PeerUser); !ok || peer.UserID != owner.ID {
t.Fatalf("send as peer = %#v, want owner user", sendAs.Peers[0].Peer)
}
if peer, ok := sendAs.Peers[1].Peer.(*tg.PeerChannel); !ok || peer.ChannelID != channel.ID {
t.Fatalf("send as peer[1] = %#v, want current channel", sendAs.Peers[1].Peer)
}
friendSendAs, err := r.onChannelsGetSendAs(friendCtx, &tg.ChannelsGetSendAsRequest{
Peer: inputPeerChannel(channel),
})
if err != nil {
t.Fatalf("friend get send as: %v", err)
}
if len(friendSendAs.Peers) != 1 {
t.Fatalf("friend send as peers = %+v, want only self", friendSendAs.Peers)
}
if ok, err := r.onMessagesSaveDefaultSendAs(ownerCtx, &tg.MessagesSaveDefaultSendAsRequest{
Peer: inputPeerChannel(channel),
SendAs: &tg.InputPeerSelf{},
}); err != nil || !ok {
t.Fatalf("save default send as self = ok %v err %v, want true", ok, err)
}
if ok, err := r.onMessagesSaveDefaultSendAs(ownerCtx, &tg.MessagesSaveDefaultSendAsRequest{
Peer: inputPeerChannel(channel),
SendAs: inputPeerChannel(channel),
}); err != nil || !ok {
t.Fatalf("save default send as channel = ok %v err %v, want true", ok, err)
}
fullWithDefault, err := r.onChannelsGetFullChannel(ownerCtx, inputChannel(channel))
if err != nil {
t.Fatalf("get full channel with default send as: %v", err)
}
channelFull := fullWithDefault.FullChat.(*tg.ChannelFull)
defaultSendAs, ok := channelFull.GetDefaultSendAs()
if !ok {
t.Fatalf("full channel default_send_as missing")
}
if peer, ok := defaultSendAs.(*tg.PeerChannel); !ok || peer.ChannelID != channel.ID {
t.Fatalf("full channel default_send_as = %#v, want current channel %d", defaultSendAs, channel.ID)
}
sentWithDefault, err := r.onMessagesSendMessage(ownerCtx, &tg.MessagesSendMessageRequest{
Peer: inputPeerChannel(channel),
Message: "default send as fallback",
RandomID: 80102,
})
if err != nil {
t.Fatalf("send message with default send as: %v", err)
}
defaultMsg := sentWithDefault.(*tg.Updates).Updates[1].(*tg.UpdateNewChannelMessage).Message.(*tg.Message)
if from, ok := defaultMsg.FromID.(*tg.PeerChannel); !ok || from.ChannelID != channel.ID {
t.Fatalf("default send_as message from = %#v, want channel %d", defaultMsg.FromID, channel.ID)
}
forwardedWithDefault, err := r.onMessagesForwardMessages(ownerCtx, &tg.MessagesForwardMessagesRequest{
FromPeer: inputPeerChannel(channel),
ToPeer: inputPeerChannel(channel),
ID: []int{defaultMsg.ID},
RandomID: []int64{80103},
})
if err != nil {
t.Fatalf("forward with default send as: %v", err)
}
defaultForward := forwardedWithDefault.(*tg.Updates).Updates[1].(*tg.UpdateNewChannelMessage).Message.(*tg.Message)
if from, ok := defaultForward.FromID.(*tg.PeerChannel); !ok || from.ChannelID != channel.ID {
t.Fatalf("default forward send_as from = %#v, want channel %d", defaultForward.FromID, channel.ID)
}
if ok, err := r.onMessagesSaveDefaultSendAs(ownerCtx, &tg.MessagesSaveDefaultSendAsRequest{
Peer: inputPeerChannel(channel),
SendAs: &tg.InputPeerSelf{},
}); err != nil || !ok {
t.Fatalf("clear default send as self = ok %v err %v, want true", ok, err)
}
fullAfterClear, err := r.onChannelsGetFullChannel(ownerCtx, inputChannel(channel))
if err != nil {
t.Fatalf("get full channel after clearing default send as: %v", err)
}
if _, ok := fullAfterClear.FullChat.(*tg.ChannelFull).GetDefaultSendAs(); ok {
t.Fatalf("full channel default_send_as still set after saving self")
}
if _, err := r.onMessagesSaveDefaultSendAs(ownerCtx, &tg.MessagesSaveDefaultSendAsRequest{
Peer: inputPeerChannel(channel),
SendAs: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash},
}); err == nil || !strings.Contains(err.Error(), "SEND_AS_PEER_INVALID") {
t.Fatalf("save default send as friend err = %v, want SEND_AS_PEER_INVALID", err)
}
sponsored, err := r.onMessagesGetSponsoredMessages(ownerCtx, &tg.MessagesGetSponsoredMessagesRequest{
Peer: inputPeerChannel(channel),
})
if err != nil {
t.Fatalf("get sponsored messages: %v", err)
}
if _, ok := sponsored.(*tg.MessagesSponsoredMessagesEmpty); !ok {
t.Fatalf("sponsored = %T, want empty", sponsored)
}
historyTTL, err := r.onMessagesGetDefaultHistoryTTL(ownerCtx)
if err != nil {
t.Fatalf("get default history ttl: %v", err)
}
if historyTTL.Period != 0 {
t.Fatalf("default history ttl = %+v, want disabled", historyTTL)
}
accountTTL, err := r.onAccountGetAccountTTL(ownerCtx)
if err != nil {
t.Fatalf("get account ttl: %v", err)
}
if accountTTL.Days <= 0 {
t.Fatalf("account ttl = %+v, want positive days", accountTTL)
}
preview, err := r.onMessagesGetWebPagePreview(ownerCtx, &tg.MessagesGetWebPagePreviewRequest{
Message: "https://example.com",
})
if err != nil {
t.Fatalf("messages.getWebPagePreview: %v", err)
}
if _, ok := preview.Media.(*tg.MessageMediaEmpty); !ok || len(preview.Chats) != 0 || len(preview.Users) != 0 {
t.Fatalf("messages.getWebPagePreview = %+v, want empty media preview", preview)
}
if _, err := r.onMessagesGetWebPagePreview(ownerCtx, &tg.MessagesGetWebPagePreviewRequest{}); err == nil || !strings.Contains(err.Error(), "MESSAGE_EMPTY") {
t.Fatalf("messages.getWebPagePreview empty err = %v, want MESSAGE_EMPTY", err)
}
if media, err := r.onMessagesUploadMedia(ownerCtx, &tg.MessagesUploadMediaRequest{
Peer: inputPeerChannel(channel),
Media: &tg.InputMediaEmpty{},
}); err != nil {
t.Fatalf("messages.uploadMedia empty: %v", err)
} else if _, ok := media.(*tg.MessageMediaEmpty); !ok {
t.Fatalf("messages.uploadMedia empty = %#v, want messageMediaEmpty", media)
}
if _, err := r.onMessagesUploadMedia(ownerCtx, &tg.MessagesUploadMediaRequest{
Peer: inputPeerChannel(channel),
Media: &tg.InputMediaUploadedPhoto{},
}); err == nil || !strings.Contains(err.Error(), "MEDIA_INVALID") {
t.Fatalf("messages.uploadMedia unsupported err = %v, want MEDIA_INVALID", err)
}
if updates, err := r.onMessagesSendMedia(ownerCtx, &tg.MessagesSendMediaRequest{
Peer: inputPeerChannel(channel),
Media: &tg.InputMediaWebPage{URL: "https://example.com"},
Message: "https://example.com",
RandomID: 91001,
}); err != nil {
t.Fatalf("messages.sendMedia webpage-as-text: %v", err)
} else if len(updates.(*tg.Updates).Updates) == 0 {
t.Fatalf("messages.sendMedia webpage-as-text = %+v, want channel message updates", updates)
}
if _, err := r.onMessagesSendMedia(ownerCtx, &tg.MessagesSendMediaRequest{
Peer: inputPeerChannel(channel),
Media: &tg.InputMediaUploadedPhoto{},
Message: "photo",
RandomID: 91002,
}); err == nil || !strings.Contains(err.Error(), "MEDIA_INVALID") {
t.Fatalf("messages.sendMedia unsupported err = %v, want MEDIA_INVALID", err)
}
if _, err := r.onMessagesSendMultiMedia(ownerCtx, &tg.MessagesSendMultiMediaRequest{
Peer: inputPeerChannel(channel),
MultiMedia: []tg.InputSingleMedia{{
Media: &tg.InputMediaUploadedPhoto{},
RandomID: 91003,
Message: "photo",
}},
}); err == nil || !strings.Contains(err.Error(), "MEDIA_INVALID") {
t.Fatalf("messages.sendMultiMedia unsupported err = %v, want MEDIA_INVALID", err)
}
savedHistoryReq := &tg.MessagesGetSavedHistoryRequest{
Peer: &tg.InputPeerSelf{},
Limit: 20,
}
savedHistoryReq.SetParentPeer(inputPeerChannel(channel))
savedHistory, err := r.onMessagesGetSavedHistory(ownerCtx, savedHistoryReq)
if err != nil {
t.Fatalf("messages.getSavedHistory: %v", err)
}
if len(savedHistory.(*tg.MessagesMessages).Messages) != 0 || len(savedHistory.(*tg.MessagesMessages).Chats) != 1 {
t.Fatalf("messages.getSavedHistory = %+v, want empty with parent channel context", savedHistory)
}
badSavedParent := &tg.MessagesGetSavedHistoryRequest{
Peer: &tg.InputPeerSelf{},
Limit: 20,
}
badSavedParent.SetParentPeer(inputPeerChannelWithHash(channel, channel.AccessHash+1))
if _, err := r.onMessagesGetSavedHistory(ownerCtx, badSavedParent); err == nil || !strings.Contains(err.Error(), "PARENT_PEER_INVALID") {
t.Fatalf("messages.getSavedHistory bad parent err = %v, want PARENT_PEER_INVALID", err)
}
if _, err := r.onMessagesGetSavedHistory(ownerCtx, &tg.MessagesGetSavedHistoryRequest{
Peer: inputPeerChannelWithHash(channel, channel.AccessHash+1),
Limit: 20,
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("messages.getSavedHistory bad peer err = %v, want CHANNEL_PRIVATE", err)
}
if _, err := r.onMessagesGetSavedHistory(ownerCtx, &tg.MessagesGetSavedHistoryRequest{
Peer: &tg.InputPeerSelf{},
Limit: maxSearchResultsLimit + 1,
}); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("messages.getSavedHistory limit err = %v, want LIMIT_INVALID", err)
}
if ok, err := r.onMessagesReadSavedHistory(ownerCtx, &tg.MessagesReadSavedHistoryRequest{
ParentPeer: inputPeerChannel(channel),
Peer: &tg.InputPeerSelf{},
MaxID: 0,
}); err != nil || !ok {
t.Fatalf("messages.readSavedHistory = ok %v err %v, want true", ok, err)
}
if _, err := r.onMessagesReadSavedHistory(ownerCtx, &tg.MessagesReadSavedHistoryRequest{
ParentPeer: &tg.InputPeerSelf{},
Peer: &tg.InputPeerSelf{},
}); err == nil || !strings.Contains(err.Error(), "PARENT_PEER_INVALID") {
t.Fatalf("messages.readSavedHistory bad parent err = %v, want PARENT_PEER_INVALID", err)
}
if _, err := r.onMessagesReadSavedHistory(ownerCtx, &tg.MessagesReadSavedHistoryRequest{
ParentPeer: inputPeerChannel(channel),
Peer: &tg.InputPeerSelf{},
MaxID: domain.MaxMessageBoxID + 1,
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.readSavedHistory max_id err = %v, want MESSAGE_ID_INVALID", err)
}
deleteSavedReq := &tg.MessagesDeleteSavedHistoryRequest{
Peer: &tg.InputPeerSelf{},
}
deleteSavedReq.SetParentPeer(inputPeerChannel(channel))
deletedSaved, err := r.onMessagesDeleteSavedHistory(ownerCtx, deleteSavedReq)
if err != nil {
t.Fatalf("messages.deleteSavedHistory: %v", err)
}
if deletedSaved.Offset != 0 || deletedSaved.PtsCount != 0 {
t.Fatalf("messages.deleteSavedHistory = %+v, want empty affected history", deletedSaved)
}
badDeleteDate := &tg.MessagesDeleteSavedHistoryRequest{
Peer: &tg.InputPeerSelf{},
}
badDeleteDate.SetMinDate(-1)
if _, err := r.onMessagesDeleteSavedHistory(ownerCtx, badDeleteDate); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("messages.deleteSavedHistory min_date err = %v, want LIMIT_INVALID", err)
}
scheduledHistory, err := r.onMessagesGetScheduledHistory(ownerCtx, &tg.MessagesGetScheduledHistoryRequest{
Peer: inputPeerChannel(channel),
})
if err != nil {
t.Fatalf("messages.getScheduledHistory: %v", err)
}
if len(scheduledHistory.(*tg.MessagesMessages).Messages) != 0 || len(scheduledHistory.(*tg.MessagesMessages).Chats) != 1 {
t.Fatalf("messages.getScheduledHistory = %+v, want empty with channel context", scheduledHistory)
}
if _, err := r.onMessagesGetScheduledHistory(ownerCtx, &tg.MessagesGetScheduledHistoryRequest{
Peer: inputPeerChannelWithHash(channel, channel.AccessHash+1),
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("messages.getScheduledHistory bad hash err = %v, want CHANNEL_PRIVATE", err)
}
scheduled, err := r.onMessagesGetScheduledMessages(ownerCtx, &tg.MessagesGetScheduledMessagesRequest{
Peer: inputPeerChannel(channel),
ID: []int{1},
})
if err != nil {
t.Fatalf("messages.getScheduledMessages: %v", err)
}
if len(scheduled.(*tg.MessagesMessages).Messages) != 0 || len(scheduled.(*tg.MessagesMessages).Chats) != 1 {
t.Fatalf("messages.getScheduledMessages = %+v, want empty with channel context", scheduled)
}
if _, err := r.onMessagesGetScheduledMessages(ownerCtx, &tg.MessagesGetScheduledMessagesRequest{
Peer: inputPeerChannel(channel),
ID: make([]int, maxGetMessagesIDs+1),
}); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("messages.getScheduledMessages id cap err = %v, want LIMIT_INVALID", err)
}
deletedScheduled, err := r.onMessagesDeleteScheduledMessages(ownerCtx, &tg.MessagesDeleteScheduledMessagesRequest{
Peer: inputPeerChannel(channel),
ID: []int{7, 8},
})
if err != nil {
t.Fatalf("messages.deleteScheduledMessages: %v", err)
}
if got := deletedScheduled.(*tg.Updates).Updates; len(got) != 1 {
t.Fatalf("messages.deleteScheduledMessages updates = %+v, want one update", got)
} else if update, ok := got[0].(*tg.UpdateDeleteScheduledMessages); !ok || len(update.Messages) != 2 || update.Messages[0] != 7 {
t.Fatalf("messages.deleteScheduledMessages update = %#v, want requested ids", got[0])
}
if _, err := r.onMessagesSendScheduledMessages(ownerCtx, &tg.MessagesSendScheduledMessagesRequest{
Peer: inputPeerChannel(channel),
ID: []int{7},
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.sendScheduledMessages err = %v, want MESSAGE_ID_INVALID", err)
}
if _, err := r.onMessagesCreateForumTopic(ownerCtx, &tg.MessagesCreateForumTopicRequest{
Peer: inputPeerChannel(channel),
Title: "Topic",
RandomID: 91004,
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_FORUM_MISSING") {
t.Fatalf("messages.createForumTopic err = %v, want CHANNEL_FORUM_MISSING", err)
}
if _, err := r.onMessagesCreateForumTopic(ownerCtx, &tg.MessagesCreateForumTopicRequest{
Peer: inputPeerChannel(channel),
RandomID: 91005,
}); err == nil || !strings.Contains(err.Error(), "TOPIC_TITLE_EMPTY") {
t.Fatalf("messages.createForumTopic empty title err = %v, want TOPIC_TITLE_EMPTY", err)
}
if _, err := r.onMessagesEditForumTopic(ownerCtx, &tg.MessagesEditForumTopicRequest{
Peer: inputPeerChannel(channel),
TopicID: 1,
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_FORUM_MISSING") {
t.Fatalf("messages.editForumTopic err = %v, want CHANNEL_FORUM_MISSING", err)
}
if _, err := r.onMessagesUpdatePinnedForumTopic(ownerCtx, &tg.MessagesUpdatePinnedForumTopicRequest{
Peer: inputPeerChannel(channel),
TopicID: 1,
Pinned: true,
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_FORUM_MISSING") {
t.Fatalf("messages.updatePinnedForumTopic err = %v, want CHANNEL_FORUM_MISSING", err)
}
if _, err := r.onMessagesReorderPinnedForumTopics(ownerCtx, &tg.MessagesReorderPinnedForumTopicsRequest{
Peer: inputPeerChannel(channel),
Order: []int{1, 2},
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_FORUM_MISSING") {
t.Fatalf("messages.reorderPinnedForumTopics err = %v, want CHANNEL_FORUM_MISSING", err)
}
if _, err := r.onMessagesDeleteTopicHistory(ownerCtx, &tg.MessagesDeleteTopicHistoryRequest{
Peer: inputPeerChannel(channel),
TopMsgID: 1,
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_FORUM_MISSING") {
t.Fatalf("messages.deleteTopicHistory err = %v, want CHANNEL_FORUM_MISSING", err)
}
chats, err := r.onMessagesGetChats(ownerCtx, []int64{channel.ID})
if err != nil {
t.Fatalf("messages.getChats legacy wrapper: %v", err)
}
if len(chats.(*tg.MessagesChats).Chats) != 1 {
t.Fatalf("messages.getChats = %+v, want current megagroup channel", chats)
}
migrated, err := r.onMessagesMigrateChat(ownerCtx, channel.ID)
if err != nil {
t.Fatalf("messages.migrateChat legacy mapping: %v", err)
}
migratedUpdates := migrated.(*tg.Updates)
if len(migratedUpdates.Chats) != 1 || len(migratedUpdates.Updates) != 1 {
t.Fatalf("messages.migrateChat = %+v, want updateChannel with current megagroup", migratedUpdates)
}
if _, err := r.onMessagesMigrateChat(friendCtx, channel.ID); err == nil {
t.Fatalf("messages.migrateChat by non-admin = nil err, want CHAT_ADMIN_REQUIRED")
}
full, err := r.onMessagesGetFullChat(ownerCtx, channel.ID)
if err != nil {
t.Fatalf("messages.getFullChat legacy wrapper: %v", err)
}
if got := full.FullChat.(*tg.ChannelFull).ID; got != channel.ID {
t.Fatalf("messages.getFullChat id = %d, want %d", got, channel.ID)
}
viewedID := defaultMsg.ID
views, err := r.onMessagesGetMessagesViews(ownerCtx, &tg.MessagesGetMessagesViewsRequest{
Peer: inputPeerChannel(channel),
ID: []int{viewedID},
})
if err != nil {
t.Fatalf("messages.getMessagesViews: %v", err)
}
if len(views.Views) != 1 || len(views.Chats) != 1 {
t.Fatalf("messages.getMessagesViews = %+v, want one view with channel context", views)
}
if got, ok := views.Views[0].GetViews(); !ok || got != 0 {
t.Fatalf("messages.getMessagesViews views = %d ok %v, want explicit zero before increment", got, ok)
}
incremented, err := r.onMessagesGetMessagesViews(ownerCtx, &tg.MessagesGetMessagesViewsRequest{
Peer: inputPeerChannel(channel),
ID: []int{viewedID},
Increment: true,
})
if err != nil {
t.Fatalf("messages.getMessagesViews increment: %v", err)
}
if got, ok := incrementalViewCount(incremented); !ok || got != 1 {
t.Fatalf("messages.getMessagesViews increment count = %d ok %v, want 1", got, ok)
}
repeated, err := r.onMessagesGetMessagesViews(ownerCtx, &tg.MessagesGetMessagesViewsRequest{
Peer: inputPeerChannel(channel),
ID: []int{viewedID},
Increment: true,
})
if err != nil {
t.Fatalf("messages.getMessagesViews repeat increment: %v", err)
}
if got, ok := incrementalViewCount(repeated); !ok || got != 1 {
t.Fatalf("messages.getMessagesViews repeated count = %d ok %v, want still 1", got, ok)
}
friendIncremented, err := r.onMessagesGetMessagesViews(friendCtx, &tg.MessagesGetMessagesViewsRequest{
Peer: inputPeerChannel(channel),
ID: []int{viewedID},
Increment: true,
})
if err != nil {
t.Fatalf("messages.getMessagesViews friend increment: %v", err)
}
if got, ok := incrementalViewCount(friendIncremented); !ok || got != 2 {
t.Fatalf("messages.getMessagesViews friend count = %d ok %v, want 2", got, ok)
}
if _, err := r.onMessagesReadMessageContents(ownerCtx, []int{1}); err != nil {
t.Fatalf("messages.readMessageContents: %v", err)
}
mentions, err := r.onMessagesGetUnreadMentions(ownerCtx, &tg.MessagesGetUnreadMentionsRequest{
Peer: inputPeerChannel(channel),
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getUnreadMentions: %v", err)
}
_, mentionChats, _ := searchMessagesPayload(t, mentions)
if len(mentionChats) != 1 {
t.Fatalf("messages.getUnreadMentions = %+v, want channel context", mentions)
}
if _, err := r.onMessagesReadMentions(ownerCtx, &tg.MessagesReadMentionsRequest{
Peer: inputPeerChannel(channel),
}); err != nil {
t.Fatalf("messages.readMentions: %v", err)
}
if ok, err := r.onMessagesReportSpam(ownerCtx, inputPeerChannel(channel)); err != nil || !ok {
t.Fatalf("messages.reportSpam = ok %v err %v, want true nil", ok, err)
}
if _, err := r.onMessagesReport(ownerCtx, &tg.MessagesReportRequest{
Peer: inputPeerChannel(channel),
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_REQUIRED") {
t.Fatalf("desktop messages.report without ids err = %v, want MESSAGE_ID_REQUIRED", err)
}
androidReportOptions, err := r.onMessagesReport(
WithClientInfo(ownerCtx, ClientInfo{
Type: ClientTypeAndroid,
AppVersion: "12.9.0 (69669) pbeta",
}),
&tg.MessagesReportRequest{Peer: inputPeerChannel(channel)},
)
if err != nil {
t.Fatalf("android messages.report initial options: %v", err)
}
if choices, ok := androidReportOptions.(*tg.ReportResultChooseOption); !ok || len(choices.Options) == 0 {
t.Fatalf("android messages.report initial options = %#v, want chooseOption", androidReportOptions)
}
if got := moderationReports.Reports(); len(got) != 1 {
t.Fatalf("android initial report option discovery persisted reports = %+v, want only earlier reportSpam", got)
}
if _, err := r.onMessagesReport(
WithClientInfo(ownerCtx, ClientInfo{Type: ClientTypeAndroid}),
&tg.MessagesReportRequest{
Peer: inputPeerChannel(channel),
Option: []byte("spam"),
},
); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_REQUIRED") {
t.Fatalf("android selected report option without ids err = %v, want MESSAGE_ID_REQUIRED", err)
}
reportOptions, err := r.onMessagesReport(ownerCtx, &tg.MessagesReportRequest{
Peer: inputPeerChannel(channel),
ID: []int{1},
})
if err != nil {
t.Fatalf("messages.report options: %v", err)
}
if choices, ok := reportOptions.(*tg.ReportResultChooseOption); !ok || len(choices.Options) == 0 {
t.Fatalf("messages.report options = %#v, want chooseOption", reportOptions)
}
reportComment, err := r.onMessagesReport(ownerCtx, &tg.MessagesReportRequest{
Peer: inputPeerChannel(channel),
ID: []int{1},
Option: []byte("other"),
})
if err != nil {
t.Fatalf("messages.report other: %v", err)
}
if _, ok := reportComment.(*tg.ReportResultAddComment); !ok {
t.Fatalf("messages.report other = %#v, want addComment", reportComment)
}
reported, err := r.onMessagesReport(ownerCtx, &tg.MessagesReportRequest{
Peer: inputPeerChannel(channel),
ID: []int{1},
Option: []byte("spam"),
})
if err != nil {
t.Fatalf("messages.report spam: %v", err)
}
if _, ok := reported.(*tg.ReportResultReported); !ok {
t.Fatalf("messages.report spam = %#v, want reported", reported)
}
if got := moderationReports.Reports(); len(got) != 2 {
t.Fatalf("moderation reports = %+v, want peer-spam and message reports", got)
}
if _, err := r.onMessagesReport(ownerCtx, &tg.MessagesReportRequest{
Peer: inputPeerChannel(channel),
ID: []int{1},
Option: []byte("bogus"),
}); err == nil || !strings.Contains(err.Error(), "OPTION_INVALID") {
t.Fatalf("messages.report invalid option err = %v, want OPTION_INVALID", err)
}
if ok, err := r.onMessagesReportMessagesDelivery(ownerCtx, &tg.MessagesReportMessagesDeliveryRequest{
Peer: inputPeerChannel(channel),
ID: []int{1},
}); err != nil || !ok {
t.Fatalf("messages.reportMessagesDelivery = ok %v err %v, want true nil", ok, err)
}
if ok, err := r.onMessagesReportReadMetrics(ownerCtx, &tg.MessagesReportReadMetricsRequest{
Peer: inputPeerChannel(channel),
Metrics: []tg.InputMessageReadMetric{{
MsgID: 1,
ViewID: 99,
TimeInViewMs: 10,
ActiveTimeInViewMs: 10,
HeightToViewportRatioPermille: 1000,
SeenRangeRatioPermille: 1000,
}},
}); err != nil || !ok {
t.Fatalf("messages.reportReadMetrics = ok %v err %v, want true nil", ok, err)
}
if events := telemetryEvents.Events(); len(events) != 2 ||
events[0].Kind != domain.ClientTelemetryMessageDelivery ||
events[1].Kind != domain.ClientTelemetryReadMetrics ||
len(moderationReports.Reports()) != 2 {
t.Fatalf("telemetry=%+v moderation=%+v, want separate durable streams",
events, moderationReports.Reports())
}
if ok, err := r.onMessagesReportMusicListen(ownerCtx, &tg.MessagesReportMusicListenRequest{
ID: &tg.InputDocument{ID: 1, AccessHash: 2},
ListenedDuration: 1,
}); err == nil || ok || !strings.Contains(err.Error(), "DOCUMENT_INVALID") {
t.Fatalf("messages.reportMusicListen = ok %v err %v, want DOCUMENT_INVALID", ok, err)
}
if _, err := r.onMessagesReportSponsoredMessage(ownerCtx, &tg.MessagesReportSponsoredMessageRequest{
RandomID: []byte("unseen-ad"),
}); err == nil || !strings.Contains(err.Error(), "RANDOM_ID_INVALID") {
t.Fatalf("unseen sponsored report err=%v, want RANDOM_ID_INVALID", err)
}
impression, err := domain.NewSponsoredMessageImpression(
owner.ID, []byte("ad"),
domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID},
0, []byte(`{"schema_version":1,"text":"test sponsored message"}`),
time.Now().UTC(), time.Now().UTC().Add(time.Hour),
)
if err != nil {
t.Fatal(err)
}
if _, _, err := moderationReports.CreateSponsoredMessageImpression(ownerCtx, impression); err != nil {
t.Fatal(err)
}
sponsoredOptions, err := r.onMessagesReportSponsoredMessage(ownerCtx, &tg.MessagesReportSponsoredMessageRequest{
RandomID: []byte("ad"),
})
if err != nil {
t.Fatalf("messages.reportSponsoredMessage options: %v", err)
}
if choices, ok := sponsoredOptions.(*tg.ChannelsSponsoredMessageReportResultChooseOption); !ok ||
len(choices.Options) == 0 {
t.Fatalf("sponsored options=%#v, want chooseOption", sponsoredOptions)
}
sponsoredReport, err := r.onMessagesReportSponsoredMessage(ownerCtx, &tg.MessagesReportSponsoredMessageRequest{
RandomID: []byte("ad"),
Option: []byte("spam"),
})
if err != nil {
t.Fatalf("messages.reportSponsoredMessage: %v", err)
}
if _, ok := sponsoredReport.(*tg.ChannelsSponsoredMessageReportResultReported); !ok {
t.Fatalf("messages.reportSponsoredMessage = %#v, want reported", sponsoredReport)
}
sendReactionReq := &tg.MessagesSendReactionRequest{
Peer: inputPeerChannel(channel),
MsgID: viewedID,
Reaction: []tg.ReactionClass{&tg.ReactionEmoji{Emoticon: "\U0001f44d"}},
}
sendReactionReq.SetReaction(sendReactionReq.Reaction)
sendReactionReq.SetAddToRecent(true)
if updates, err := r.onMessagesSendReaction(ownerCtx, sendReactionReq); err != nil {
t.Fatalf("messages.sendReaction: %v", err)
} else if got := updates.(*tg.Updates).Updates; len(got) != 1 {
t.Fatalf("messages.sendReaction updates = %+v, want one reaction update", got)
} else if update, ok := got[0].(*tg.UpdateMessageReactions); !ok || update.MsgID != viewedID || len(update.Reactions.Results) != 1 || update.Reactions.Results[0].Count != 1 || update.Reactions.Results[0].ChosenOrder != 1 {
t.Fatalf("messages.sendReaction update = %#v, want one chosen reaction", got[0])
}
tooManyReactionsReq := &tg.MessagesSendReactionRequest{
Peer: inputPeerChannel(channel),
MsgID: viewedID,
Reaction: make([]tg.ReactionClass, maxReactionVector+1),
}
tooManyReactionsReq.SetReaction(tooManyReactionsReq.Reaction)
if _, err := r.onMessagesSendReaction(ownerCtx, tooManyReactionsReq); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("messages.sendReaction huge reaction vector err = %v, want LIMIT_INVALID", err)
}
reactionUpdates, err := r.onMessagesGetMessagesReactions(ownerCtx, &tg.MessagesGetMessagesReactionsRequest{
Peer: inputPeerChannel(channel),
ID: []int{viewedID},
})
if err != nil {
t.Fatalf("messages.getMessagesReactions: %v", err)
}
if got := reactionUpdates.(*tg.Updates).Updates; len(got) != 1 {
t.Fatalf("messages.getMessagesReactions updates = %+v, want one reaction update", got)
} else if update, ok := got[0].(*tg.UpdateMessageReactions); !ok || update.MsgID != viewedID || len(update.Reactions.Results) != 1 || update.Reactions.Results[0].Count != 1 {
t.Fatalf("messages.getMessagesReactions update = %#v, want one reaction", got[0])
}
reactionList, err := r.onMessagesGetMessageReactionsList(ownerCtx, &tg.MessagesGetMessageReactionsListRequest{
Peer: inputPeerChannel(channel),
ID: viewedID,
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getMessageReactionsList: %v", err)
}
if reactionList.Count != 1 || len(reactionList.Reactions) != 1 || len(reactionList.Chats) != 1 || len(reactionList.Users) != 1 {
t.Fatalf("messages.getMessageReactionsList = %+v, want one reaction with channel context", reactionList)
}
if peer, ok := reactionList.Reactions[0].PeerID.(*tg.PeerUser); !ok || peer.UserID != owner.ID || !reactionList.Reactions[0].My {
t.Fatalf("messages.getMessageReactionsList reaction = %+v, want current user reaction", reactionList.Reactions[0])
}
friendReactionReq := &tg.MessagesSendReactionRequest{
Peer: inputPeerChannel(channel),
MsgID: viewedID,
Reaction: []tg.ReactionClass{&tg.ReactionEmoji{Emoticon: "\U0001f44d"}},
}
friendReactionReq.SetReaction(friendReactionReq.Reaction)
if _, err := r.onMessagesSendReaction(friendCtx, friendReactionReq); err != nil {
t.Fatalf("messages.sendReaction by friend: %v", err)
}
if ok, err := r.onMessagesReportReaction(ownerCtx, &tg.MessagesReportReactionRequest{
Peer: inputPeerChannel(channel),
ID: viewedID,
ReactionPeer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash},
}); err != nil || !ok {
t.Fatalf("messages.reportReaction = ok %v err %v, want true nil", ok, err)
}
unreadReactions, err := r.onMessagesGetUnreadReactions(ownerCtx, &tg.MessagesGetUnreadReactionsRequest{
Peer: inputPeerChannel(channel),
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getUnreadReactions: %v", err)
}
unreadMessages, unreadChats, unreadUsers := searchMessagesPayload(t, unreadReactions)
if len(unreadMessages) != 1 || len(unreadChats) != 1 || len(unreadUsers) == 0 {
t.Fatalf("messages.getUnreadReactions = %+v, want one unread reaction with channel context", unreadReactions)
}
unreadMessage, ok := unreadMessages[0].(*tg.Message)
if !ok || unreadMessage.ID != viewedID {
t.Fatalf("messages.getUnreadReactions message = %#v, want message %d", unreadMessages[0], viewedID)
}
reactions, ok := unreadMessage.GetReactions()
hasUnreadRecent := false
for _, recent := range reactions.RecentReactions {
if recent.Unread {
hasUnreadRecent = true
break
}
}
if !ok || !hasUnreadRecent {
t.Fatalf("messages.getUnreadReactions reactions = %+v ok %v, want unread recent reaction", reactions, ok)
}
if _, err := r.onMessagesReadReactions(ownerCtx, &tg.MessagesReadReactionsRequest{
Peer: inputPeerChannel(channel),
}); err != nil {
t.Fatalf("messages.readReactions: %v", err)
}
unreadAfterRead, err := r.onMessagesGetUnreadReactions(ownerCtx, &tg.MessagesGetUnreadReactionsRequest{
Peer: inputPeerChannel(channel),
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getUnreadReactions after read: %v", err)
}
unreadAfterReadMessages, _, _ := searchMessagesPayload(t, unreadAfterRead)
if len(unreadAfterReadMessages) != 0 {
t.Fatalf("messages.getUnreadReactions after read = %+v, want empty messages", unreadAfterRead)
}
common, err := r.onMessagesGetCommonChats(ownerCtx, &tg.MessagesGetCommonChatsRequest{
UserID: &tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash},
Limit: 40,
})
if err != nil {
t.Fatalf("messages.getCommonChats: %v", err)
}
commonChats := common.(*tg.MessagesChats).Chats
if len(commonChats) != 1 || commonChats[0].(*tg.Channel).ID != channel.ID {
t.Fatalf("messages.getCommonChats = %+v, want current shared megagroup", common)
}
commonNext, err := r.onMessagesGetCommonChats(ownerCtx, &tg.MessagesGetCommonChatsRequest{
UserID: &tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash},
MaxID: channel.ID,
Limit: 40,
})
if err != nil {
t.Fatalf("messages.getCommonChats next page: %v", err)
}
if len(commonNext.(*tg.MessagesChats).Chats) != 0 {
t.Fatalf("messages.getCommonChats next page = %+v, want empty after max id", commonNext)
}
fullFriend, err := r.onUsersGetFullUser(ownerCtx, &tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash})
if err != nil {
t.Fatalf("users.getFullUser friend: %v", err)
}
if fullFriend.FullUser.CommonChatsCount != 1 {
t.Fatalf("users.getFullUser commonChatsCount = %d, want 1", fullFriend.FullUser.CommonChatsCount)
}
if _, err := r.onMessagesGetCommonChats(ownerCtx, &tg.MessagesGetCommonChatsRequest{
UserID: &tg.InputUserSelf{},
Limit: 1,
}); err == nil || !strings.Contains(err.Error(), "USER_ID_INVALID") {
t.Fatalf("messages.getCommonChats self err = %v, want USER_ID_INVALID", err)
}
if _, err := r.onMessagesGetAttachedStickers(ownerCtx, nil); err == nil || !strings.Contains(err.Error(), "MEDIA_EMPTY") {
t.Fatalf("messages.getAttachedStickers nil err = %v, want MEDIA_EMPTY", err)
}
attached, err := r.onMessagesGetAttachedStickers(ownerCtx, &tg.InputStickeredMediaDocument{
ID: &tg.InputDocument{ID: 1, AccessHash: 2},
})
if err != nil || len(attached) != 0 {
t.Fatalf("messages.getAttachedStickers = %+v err=%v, want empty", attached, err)
}
customEmojiDocs, err := r.onMessagesGetCustomEmojiDocuments(ownerCtx, []int64{1, 2})
if err != nil || len(customEmojiDocs) != 0 {
t.Fatalf("messages.getCustomEmojiDocuments = %+v err=%v, want empty", customEmojiDocs, err)
}
if _, err := r.onMessagesGetCustomEmojiDocuments(ownerCtx, make([]int64, maxEmojiDocuments+1)); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("messages.getCustomEmojiDocuments too many err = %v, want LIMIT_INVALID", err)
}
stickerSets, err := r.onMessagesSearchStickerSets(ownerCtx, &tg.MessagesSearchStickerSetsRequest{Q: "cat"})
if err != nil {
t.Fatalf("messages.searchStickerSets: %v", err)
}
if found, ok := stickerSets.(*tg.MessagesFoundStickerSets); !ok || len(found.Sets) != 0 {
t.Fatalf("messages.searchStickerSets = %#v, want empty found sets", stickerSets)
}
stickers, err := r.onMessagesSearchStickers(ownerCtx, &tg.MessagesSearchStickersRequest{
Q: "cat",
LangCode: []string{"en"},
Limit: 50,
})
if err != nil {
t.Fatalf("messages.searchStickers: %v", err)
}
if found, ok := stickers.(*tg.MessagesFoundStickers); !ok || len(found.Stickers) != 0 {
t.Fatalf("messages.searchStickers = %#v, want empty found stickers", stickers)
}
if _, err := r.onMessagesSearchStickers(ownerCtx, &tg.MessagesSearchStickersRequest{
Limit: maxSearchResultsLimit + 1,
}); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("messages.searchStickers huge limit err = %v, want LIMIT_INVALID", err)
}
// emoji 关键词已从官方 seed(catalog),返回真实词典而非空 stub。
keywords, err := r.onMessagesGetEmojiKeywords(ownerCtx, "en")
if err != nil || keywords.LangCode != "en" || keywords.Version == 0 || len(keywords.Keywords) == 0 {
t.Fatalf("messages.getEmojiKeywords lang=%q version=%d count=%d err=%v, want seeded en keywords",
keywords.LangCode, keywords.Version, len(keywords.Keywords), err)
}
// 客户端版本(7)落后于固化版本 → 下发全量,Version 推进到固化版本。
keywordsDiff, err := r.onMessagesGetEmojiKeywordsDifference(ownerCtx, &tg.MessagesGetEmojiKeywordsDifferenceRequest{
LangCode: "en",
FromVersion: 7,
})
if err != nil || keywordsDiff.FromVersion != 7 || keywordsDiff.Version <= 7 || len(keywordsDiff.Keywords) == 0 {
t.Fatalf("messages.getEmojiKeywordsDifference from=%d version=%d count=%d err=%v, want full diff to seeded version",
keywordsDiff.FromVersion, keywordsDiff.Version, len(keywordsDiff.Keywords), err)
}
extended, err := r.onMessagesGetExtendedMedia(ownerCtx, &tg.MessagesGetExtendedMediaRequest{
Peer: inputPeerChannel(channel),
ID: []int{1},
})
if err != nil {
t.Fatalf("messages.getExtendedMedia: %v", err)
}
if len(extended.(*tg.Updates).Updates) != 0 {
t.Fatalf("messages.getExtendedMedia = %+v, want empty updates", extended)
}
topReactions, err := r.onMessagesGetTopReactions(ownerCtx, &tg.MessagesGetTopReactionsRequest{Limit: 3})
if err != nil {
t.Fatalf("messages.getTopReactions: %v", err)
}
topPage, ok := topReactions.(*tg.MessagesReactions)
if !ok || topPage.Hash == 0 || len(topPage.Reactions) != 3 {
t.Fatalf("messages.getTopReactions = %#v, want three hashable reactions", topReactions)
}
if emoji, ok := topPage.Reactions[0].(*tg.ReactionEmoji); !ok || emoji.Emoticon != "\U0001f44d" {
t.Fatalf("messages.getTopReactions first reaction = %#v, want account top thumb", topPage.Reactions[0])
}
if emoji, ok := topPage.Reactions[1].(*tg.ReactionEmoji); !ok || emoji.Emoticon != "\u2764\ufe0f" {
t.Fatalf("messages.getTopReactions fallback reaction = %#v, want catalog heart", topPage.Reactions[1])
}
topNotModified, err := r.onMessagesGetTopReactions(ownerCtx, &tg.MessagesGetTopReactionsRequest{Limit: 3, Hash: topPage.Hash})
if err != nil {
t.Fatalf("messages.getTopReactions hash: %v", err)
}
if _, ok := topNotModified.(*tg.MessagesReactionsNotModified); !ok {
t.Fatalf("messages.getTopReactions hash = %#v, want notModified", topNotModified)
}
recentReactions, err := r.onMessagesGetRecentReactions(ownerCtx, &tg.MessagesGetRecentReactionsRequest{Limit: 40})
if err != nil {
t.Fatalf("messages.getRecentReactions: %v", err)
}
recentPage, ok := recentReactions.(*tg.MessagesReactions)
if !ok || recentPage.Hash == 0 || len(recentPage.Reactions) != 1 {
t.Fatalf("messages.getRecentReactions = %#v, want one reaction with non-zero hash", recentReactions)
}
if emoji, ok := recentPage.Reactions[0].(*tg.ReactionEmoji); !ok || emoji.Emoticon != "\U0001f44d" {
t.Fatalf("messages.getRecentReactions reaction = %#v, want thumb emoji", recentPage.Reactions[0])
}
recentNotModified, err := r.onMessagesGetRecentReactions(ownerCtx, &tg.MessagesGetRecentReactionsRequest{Limit: 40, Hash: recentPage.Hash})
if err != nil {
t.Fatalf("messages.getRecentReactions hash: %v", err)
}
if _, ok := recentNotModified.(*tg.MessagesReactionsNotModified); !ok {
t.Fatalf("messages.getRecentReactions hash = %#v, want notModified", recentNotModified)
}
if ok, err := r.onMessagesClearRecentReactions(ownerCtx); err != nil || !ok {
t.Fatalf("messages.clearRecentReactions = ok %v err %v, want true nil", ok, err)
}
recentAfterClear, err := r.onMessagesGetRecentReactions(ownerCtx, &tg.MessagesGetRecentReactionsRequest{Limit: 40, Hash: recentPage.Hash})
if err != nil {
t.Fatalf("messages.getRecentReactions after clear: %v", err)
}
clearedPage, ok := recentAfterClear.(*tg.MessagesReactions)
if !ok || clearedPage.Hash != 0 || len(clearedPage.Reactions) != 0 {
t.Fatalf("messages.getRecentReactions after clear = %#v, want empty page hash 0", recentAfterClear)
}
savedTagsReq := &tg.MessagesGetSavedReactionTagsRequest{
Peer: inputPeerChannel(channel),
}
savedTagsReq.SetPeer(savedTagsReq.Peer)
savedTags, err := r.onMessagesGetSavedReactionTags(ownerCtx, savedTagsReq)
if err != nil {
t.Fatalf("messages.getSavedReactionTags: %v", err)
}
if got := savedTags.(*tg.MessagesSavedReactionTags).Tags; len(got) != 0 {
t.Fatalf("messages.getSavedReactionTags = %+v, want empty", got)
}
staleEmptyTags, err := r.onMessagesGetSavedReactionTags(ownerCtx, &tg.MessagesGetSavedReactionTagsRequest{Hash: 123})
if err != nil {
t.Fatalf("messages.getSavedReactionTags stale empty hash: %v", err)
}
staleEmptyPage, ok := staleEmptyTags.(*tg.MessagesSavedReactionTags)
if !ok || staleEmptyPage.Hash != 0 || len(staleEmptyPage.Tags) != 0 {
t.Fatalf("messages.getSavedReactionTags stale empty hash = %#v, want empty page hash 0", staleEmptyTags)
}
r.deps.Messages = appmessages.NewService(memory.NewMessageStore(), nil)
if _, err := f.users.SetPremiumUntil(ownerCtx, owner.ID, int(time.Now().Add(time.Hour).Unix())); err != nil {
t.Fatalf("grant owner premium for saved tag rename: %v", err)
}
updateTagReq := &tg.MessagesUpdateSavedReactionTagRequest{Reaction: &tg.ReactionEmoji{Emoticon: "ok"}}
updateTagReq.SetTitle("Work")
if _, err := r.onMessagesUpdateSavedReactionTag(ownerCtx, updateTagReq); err == nil || !strings.Contains(err.Error(), "REACTION_INVALID") {
t.Fatalf("messages.updateSavedReactionTag unassigned err = %v, want REACTION_INVALID", err)
}
globalTags, err := r.onMessagesGetSavedReactionTags(ownerCtx, &tg.MessagesGetSavedReactionTagsRequest{})
if err != nil {
t.Fatalf("messages.getSavedReactionTags global: %v", err)
}
globalPage, ok := globalTags.(*tg.MessagesSavedReactionTags)
if !ok || globalPage.Hash != 0 || len(globalPage.Tags) != 0 {
t.Fatalf("messages.getSavedReactionTags global = %#v, want empty", globalTags)
}
peerTagsAfterUpdate, err := r.onMessagesGetSavedReactionTags(ownerCtx, savedTagsReq)
if err != nil {
t.Fatalf("messages.getSavedReactionTags peer after update: %v", err)
}
if got := peerTagsAfterUpdate.(*tg.MessagesSavedReactionTags).Tags; len(got) != 0 {
t.Fatalf("messages.getSavedReactionTags peer after update = %+v, want empty until saved-message tag store exists", got)
}
longTagReq := &tg.MessagesUpdateSavedReactionTagRequest{Reaction: &tg.ReactionEmoji{Emoticon: "ok"}}
longTagReq.SetTitle("abcdefghijklmnop")
if _, err := r.onMessagesUpdateSavedReactionTag(ownerCtx, longTagReq); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("messages.updateSavedReactionTag long title err = %v, want LIMIT_INVALID", err)
}
if _, err := r.onMessagesUpdateSavedReactionTag(ownerCtx, &tg.MessagesUpdateSavedReactionTagRequest{
Reaction: &tg.ReactionCustomEmoji{DocumentID: 1},
}); err == nil || !strings.Contains(err.Error(), "REACTION_INVALID") {
t.Fatalf("messages.updateSavedReactionTag custom emoji err = %v, want REACTION_INVALID", err)
}
tagReactions, err := r.onMessagesGetDefaultTagReactions(ownerCtx, 0)
if err != nil {
t.Fatalf("messages.getDefaultTagReactions: %v", err)
}
defaultPage, ok := tagReactions.(*tg.MessagesReactions)
if !ok || defaultPage.Hash == 0 || len(defaultPage.Reactions) == 0 {
t.Fatalf("messages.getDefaultTagReactions = %#v, want non-empty hashable catalog", tagReactions)
}
r.deps.Messages = nil
// poll 链路已是真实现:对非 poll 消息一律 MESSAGE_ID_INVALID与官方一致
if _, err := r.onMessagesSendVote(ownerCtx, &tg.MessagesSendVoteRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
Options: [][]byte{[]byte("a")},
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.sendVote err = %v, want MESSAGE_ID_INVALID for non-poll message", err)
}
if _, err := r.onMessagesSendVote(ownerCtx, &tg.MessagesSendVoteRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
Options: make([][]byte, maxPollVoteOptions+1),
}); err == nil || !strings.Contains(err.Error(), "OPTIONS_TOO_MUCH") {
t.Fatalf("messages.sendVote too many options err = %v, want OPTIONS_TOO_MUCH", err)
}
if _, err := r.onMessagesGetPollResults(ownerCtx, &tg.MessagesGetPollResultsRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.getPollResults err = %v, want MESSAGE_ID_INVALID for non-poll message", err)
}
pollVotesReq := &tg.MessagesGetPollVotesRequest{
Peer: inputPeerChannel(channel),
ID: 1,
Option: []byte("a"),
Limit: 10,
}
pollVotesReq.SetOption(pollVotesReq.Option)
if _, err := r.onMessagesGetPollVotes(ownerCtx, pollVotesReq); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.getPollVotes err = %v, want MESSAGE_ID_INVALID for non-poll message", err)
}
if _, err := r.onMessagesGetPollVotes(ownerCtx, &tg.MessagesGetPollVotesRequest{
Peer: inputPeerChannel(channel),
ID: 1,
Limit: maxSearchResultsLimit + 1,
}); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("messages.getPollVotes huge limit err = %v, want LIMIT_INVALID", err)
}
if _, err := r.onMessagesAddPollAnswer(ownerCtx, &tg.MessagesAddPollAnswerRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
Answer: &tg.InputPollAnswer{Text: tg.TextWithEntities{Text: "extra"}},
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.addPollAnswer err = %v, want MESSAGE_ID_INVALID without poll store", err)
}
if _, err := r.onMessagesDeletePollAnswer(ownerCtx, &tg.MessagesDeletePollAnswerRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
Option: []byte("a"),
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.deletePollAnswer err = %v, want MESSAGE_ID_INVALID without poll store", err)
}
verify := newFakeBotVerifications()
r.deps.BotVerifications = verify
const unreadPollIcon = int64(8800024)
unreadPollPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}
verify.marks[unreadPollPeer] = domain.CustomVerification{
VerifierBotID: 777000123,
Peer: unreadPollPeer,
IconDocumentID: unreadPollIcon,
Description: "Verified poll peer",
}
unreadPollVotes, err := r.onMessagesGetUnreadPollVotes(ownerCtx, &tg.MessagesGetUnreadPollVotesRequest{
Peer: inputPeerChannel(channel),
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getUnreadPollVotes: %v", err)
}
assertMessagesEnvelopeBotVerificationIcon(t, unreadPollVotes, unreadPollPeer, unreadPollIcon)
if len(unreadPollVotes.(*tg.MessagesMessages).Messages) != 0 {
t.Fatalf("messages.getUnreadPollVotes = %+v, want empty messages", unreadPollVotes)
}
if _, err := r.onMessagesReadPollVotes(ownerCtx, &tg.MessagesReadPollVotesRequest{
Peer: inputPeerChannel(channel),
}); err != nil {
t.Fatalf("messages.readPollVotes: %v", err)
}
if _, err := r.onMessagesAppendTodoList(ownerCtx, &tg.MessagesAppendTodoListRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
}); err == nil || !strings.Contains(err.Error(), "TODO_NOT_MODIFIED") {
t.Fatalf("messages.appendTodoList empty err = %v, want TODO_NOT_MODIFIED", err)
}
if _, err := r.onMessagesAppendTodoList(ownerCtx, &tg.MessagesAppendTodoListRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
List: []tg.TodoItem{{Title: tg.TextWithEntities{Text: "item"}}},
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.appendTodoList err = %v, want MESSAGE_ID_INVALID without todo store", err)
}
if _, err := r.onMessagesToggleTodoCompleted(ownerCtx, &tg.MessagesToggleTodoCompletedRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
}); err == nil || !strings.Contains(err.Error(), "TODO_NOT_MODIFIED") {
t.Fatalf("messages.toggleTodoCompleted empty err = %v, want TODO_NOT_MODIFIED", err)
}
if _, err := r.onMessagesToggleTodoCompleted(ownerCtx, &tg.MessagesToggleTodoCompletedRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
Completed: []int{1},
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.toggleTodoCompleted err = %v, want MESSAGE_ID_INVALID without todo store", err)
}
counters, err := r.onMessagesGetSearchCounters(ownerCtx, &tg.MessagesGetSearchCountersRequest{
Peer: inputPeerChannel(channel),
Filters: []tg.MessagesFilterClass{&tg.InputMessagesFilterEmpty{}},
})
if err != nil {
t.Fatalf("messages.getSearchCounters: %v", err)
}
if len(counters) != 1 || counters[0].Count != 0 {
t.Fatalf("messages.getSearchCounters = %+v, want one zero counter", counters)
}
calendar, err := r.onMessagesGetSearchResultsCalendar(ownerCtx, &tg.MessagesGetSearchResultsCalendarRequest{
Peer: inputPeerChannel(channel),
Filter: &tg.InputMessagesFilterPhotos{},
OffsetID: 7,
OffsetDate: 1700000000,
})
if err != nil {
t.Fatalf("messages.getSearchResultsCalendar: %v", err)
}
if calendar.Count != 0 || calendar.MinDate != 1700000000 || calendar.MinMsgID != 7 || len(calendar.Messages) != 0 {
t.Fatalf("messages.getSearchResultsCalendar = %+v, want empty stable-offset result", calendar)
}
positions, err := r.onMessagesGetSearchResultsPositions(ownerCtx, &tg.MessagesGetSearchResultsPositionsRequest{
Peer: inputPeerChannel(channel),
Filter: &tg.InputMessagesFilterPhotos{},
OffsetID: 7,
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getSearchResultsPositions: %v", err)
}
if positions.Count != 0 || len(positions.Positions) != 0 {
t.Fatalf("messages.getSearchResultsPositions = %+v, want empty positions", positions)
}
if _, err := r.onMessagesGetSearchResultsPositions(ownerCtx, &tg.MessagesGetSearchResultsPositionsRequest{
Peer: inputPeerChannel(channel),
Filter: &tg.InputMessagesFilterPhotos{},
OffsetID: 7,
Limit: maxSearchResultsLimit + 1,
}); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("messages.getSearchResultsPositions huge limit err = %v, want LIMIT_INVALID", err)
}
replies, err := r.onMessagesGetReplies(ownerCtx, &tg.MessagesGetRepliesRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
Limit: 10,
})
if err != nil {
t.Fatalf("messages.getReplies: %v", err)
}
_, replyChats, _ := searchMessagesPayload(t, replies)
if len(replyChats) != 1 {
t.Fatalf("messages.getReplies = %+v, want channel context", replies)
}
discussion, err := r.onMessagesGetDiscussionMessage(ownerCtx, &tg.MessagesGetDiscussionMessageRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
})
if err != nil {
t.Fatalf("messages.getDiscussionMessage: %v", err)
}
if len(discussion.Messages) != 1 || len(discussion.Chats) != 1 || discussion.UnreadCount != 0 {
t.Fatalf("messages.getDiscussionMessage = %+v, want root message with channel context", discussion)
}
if _, ok := discussion.Messages[0].(*tg.MessageService); !ok {
t.Fatalf("messages.getDiscussionMessage message = %T, want service root", discussion.Messages[0])
}
if _, err := r.onMessagesReadDiscussion(ownerCtx, &tg.MessagesReadDiscussionRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
ReadMaxID: 1,
}); err != nil {
t.Fatalf("messages.readDiscussion err = %v, want nil", err)
}
if _, err := r.onMessagesGetDiscussionMessage(ownerCtx, &tg.MessagesGetDiscussionMessageRequest{
Peer: inputPeerChannelWithHash(channel, channel.AccessHash+1),
MsgID: 1,
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_PRIVATE") {
t.Fatalf("messages.getDiscussionMessage bad hash err = %v, want CHANNEL_PRIVATE", err)
}
if _, err := r.onMessagesReadDiscussion(ownerCtx, &tg.MessagesReadDiscussionRequest{
Peer: inputPeerChannel(channel),
MsgID: 1,
ReadMaxID: domain.MaxMessageBoxID + 1,
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
t.Fatalf("messages.readDiscussion huge read_max_id err = %v, want MESSAGE_ID_INVALID", err)
}
if _, err := r.onMessagesGetForumTopics(ownerCtx, &tg.MessagesGetForumTopicsRequest{
Peer: inputPeerChannel(channel),
Limit: 10,
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_FORUM_MISSING") {
t.Fatalf("messages.getForumTopics non-forum err = %v, want CHANNEL_FORUM_MISSING", err)
}
if _, err := r.onMessagesGetForumTopicsByID(ownerCtx, &tg.MessagesGetForumTopicsByIDRequest{
Peer: inputPeerChannel(channel),
Topics: []int{1},
}); err == nil || !strings.Contains(err.Error(), "CHANNEL_FORUM_MISSING") {
t.Fatalf("messages.getForumTopicsByID non-forum err = %v, want CHANNEL_FORUM_MISSING", err)
}
onlines, err := r.onMessagesGetOnlines(ownerCtx, inputPeerChannel(channel))
if err != nil {
t.Fatalf("messages.getOnlines: %v", err)
}
if onlines.Onlines != 1 {
t.Fatalf("messages.getOnlines = %+v, want count 1", onlines)
}
stats, err := r.onStatsGetMegagroupStats(ownerCtx, &tg.StatsGetMegagroupStatsRequest{
Channel: inputChannel(channel),
})
if err != nil {
t.Fatalf("stats.getMegagroupStats: %v", err)
}
if stats.GrowthGraph == nil || stats.MembersGraph == nil {
t.Fatalf("megagroup stats = %+v, want non-nil graph stubs", stats)
}
if _, err := r.onStatsGetBroadcastStats(ownerCtx, &tg.StatsGetBroadcastStatsRequest{
Channel: inputChannel(channel),
}); err == nil || !strings.Contains(err.Error(), "BROADCAST_REQUIRED") {
t.Fatalf("stats.getBroadcastStats on megagroup err = %v, want BROADCAST_REQUIRED", err)
}
messageStats, err := r.onStatsGetMessageStats(ownerCtx, &tg.StatsGetMessageStatsRequest{
Channel: inputChannel(channel),
MsgID: 1,
})
if err != nil {
t.Fatalf("stats.getMessageStats: %v", err)
}
if messageStats.ViewsGraph == nil || messageStats.ReactionsByEmotionGraph == nil {
t.Fatalf("message stats = %+v, want non-nil graph stubs", messageStats)
}
forwards, err := r.onStatsGetMessagePublicForwards(ownerCtx, &tg.StatsGetMessagePublicForwardsRequest{
Channel: inputChannel(channel),
MsgID: 1,
Limit: 10,
})
if err != nil {
t.Fatalf("stats.getMessagePublicForwards: %v", err)
}
if forwards.Count != 0 || len(forwards.Forwards) != 0 {
t.Fatalf("message public forwards = %+v, want empty", forwards)
}
if _, err := r.onStatsGetMessagePublicForwards(ownerCtx, &tg.StatsGetMessagePublicForwardsRequest{
Channel: inputChannel(channel),
MsgID: 1,
Limit: maxStatsPublicForwardsLimit + 1,
}); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("stats.getMessagePublicForwards huge limit err = %v, want LIMIT_INVALID", err)
}
graph, err := r.onStatsLoadAsyncGraph(ownerCtx, &tg.StatsLoadAsyncGraphRequest{Token: "stale"})
if err != nil {
t.Fatalf("stats.loadAsyncGraph: %v", err)
}
if _, ok := graph.(*tg.StatsGraphError); !ok {
t.Fatalf("stats.loadAsyncGraph = %T, want statsGraphError", graph)
}
boostStatus, err := r.onPremiumGetBoostsStatus(ownerCtx, inputPeerChannel(channel))
if err != nil {
t.Fatalf("premium.getBoostsStatus: %v", err)
}
if boostStatus.Level != 0 || boostStatus.CurrentLevelBoosts != 0 || boostStatus.Boosts != 0 {
t.Fatalf("premium.getBoostsStatus = level %d current %d boosts %d, want empty real state", boostStatus.Level, boostStatus.CurrentLevelBoosts, boostStatus.Boosts)
}
if boostStatus.BoostURL == "" {
t.Fatalf("premium.getBoostsStatus missing boost_url")
}
if next, ok := boostStatus.GetNextLevelBoosts(); !ok || next <= boostStatus.CurrentLevelBoosts {
t.Fatalf("premium.getBoostsStatus real NextLevelBoosts set=%v val=%d, want > CurrentLevelBoosts %d", ok, next, boostStatus.CurrentLevelBoosts)
}
boostStatusTDesktop, err := r.onPremiumGetBoostsStatus(WithClientInfo(ownerCtx, ClientInfo{
Type: ClientTypeTDesktop,
AppVersion: "6.8.4 x64",
}), inputPeerChannel(channel))
if err != nil {
t.Fatalf("premium.getBoostsStatus TDesktop: %v", err)
}
if boostStatusTDesktop.Level != boostStatus.Level || boostStatusTDesktop.CurrentLevelBoosts != boostStatus.CurrentLevelBoosts || boostStatusTDesktop.Boosts != boostStatus.Boosts {
t.Fatalf("premium.getBoostsStatus TDesktop = %+v, want same real status as generic client %+v", boostStatusTDesktop, boostStatus)
}
boosts, err := r.onPremiumGetBoostsList(ownerCtx, &tg.PremiumGetBoostsListRequest{
Peer: inputPeerChannel(channel),
Limit: 10,
})
if err != nil {
t.Fatalf("premium.getBoostsList: %v", err)
}
if boosts.Count != 0 || len(boosts.Boosts) != 0 || len(boosts.Users) != 0 {
t.Fatalf("premium.getBoostsList = %+v, want empty", boosts)
}
if _, err := r.onPremiumGetBoostsList(ownerCtx, &tg.PremiumGetBoostsListRequest{
Peer: inputPeerChannel(channel),
Limit: domain.MaxPremiumBoostsListLimit + 1,
}); err == nil || !strings.Contains(err.Error(), "LIMIT_INVALID") {
t.Fatalf("premium.getBoostsList huge limit err = %v, want LIMIT_INVALID", err)
}
if _, err := f.users.SetPremiumUntil(f.ctx, owner.ID, int(time.Now().Add(time.Hour).Unix())); err != nil {
t.Fatalf("grant owner premium: %v", err)
}
myBoosts, err := r.onPremiumGetMyBoosts(ownerCtx)
if err != nil {
t.Fatalf("premium.getMyBoosts: %v", err)
}
if len(myBoosts.MyBoosts) != 1 || myBoosts.MyBoosts[0].Slot != domain.DefaultPremiumBoostSlotID || myBoosts.MyBoosts[0].Date != 0 || myBoosts.MyBoosts[0].Expires != 0 {
t.Fatalf("premium.getMyBoosts = %+v, want one free base slot", myBoosts)
}
if _, ok := myBoosts.MyBoosts[0].GetPeer(); ok || len(myBoosts.Chats) != 0 || len(myBoosts.Users) != 0 {
t.Fatalf("premium.getMyBoosts free slot refs = %+v, want no peer refs", myBoosts)
}
applyReq := &tg.PremiumApplyBoostRequest{
Peer: inputPeerChannel(channel),
}
applyReq.SetSlots([]int{1})
applied, err := r.onPremiumApplyBoost(ownerCtx, applyReq)
if err != nil {
t.Fatalf("premium.applyBoost: %v", err)
}
if len(applied.MyBoosts) != 1 {
t.Fatalf("premium.applyBoost = %+v, want assigned base slot", applied)
}
appliedPeer, ok := applied.MyBoosts[0].GetPeer()
if !ok {
t.Fatalf("premium.applyBoost assigned slot missing peer: %+v", applied.MyBoosts[0])
}
if peer, ok := appliedPeer.(*tg.PeerChannel); !ok || peer.ChannelID != channel.ID {
t.Fatalf("premium.applyBoost peer = %#v, want channel %d", appliedPeer, channel.ID)
}
boostStatusAfterApply, err := r.onPremiumGetBoostsStatus(ownerCtx, inputPeerChannel(channel))
if err != nil {
t.Fatalf("premium.getBoostsStatus after apply: %v", err)
}
if boostStatusAfterApply.Boosts != 1 || !boostStatusAfterApply.MyBoost {
t.Fatalf("premium.getBoostsStatus after apply = %+v, want one self boost", boostStatusAfterApply)
}
if _, err := r.onPremiumApplyBoost(ownerCtx, applyReq); err == nil || !strings.Contains(err.Error(), "BOOST_NOT_MODIFIED") {
t.Fatalf("premium.applyBoost same peer err = %v, want BOOST_NOT_MODIFIED", err)
}
if _, err := r.onPremiumApplyBoost(ownerCtx, &tg.PremiumApplyBoostRequest{
Peer: inputPeerChannel(channel),
}); err == nil || !strings.Contains(err.Error(), "BOOSTS_EMPTY") {
t.Fatalf("premium.applyBoost missing slots err = %v, want BOOSTS_EMPTY", err)
}
badSlotReq := &tg.PremiumApplyBoostRequest{Peer: inputPeerChannel(channel)}
badSlotReq.SetSlots([]int{2})
if _, err := r.onPremiumApplyBoost(ownerCtx, badSlotReq); err == nil || !strings.Contains(err.Error(), "SLOTS_INVALID") {
t.Fatalf("premium.applyBoost unsupported slot err = %v, want SLOTS_INVALID", err)
}
userBoosts, err := r.onPremiumGetUserBoosts(ownerCtx, &tg.PremiumGetUserBoostsRequest{
Peer: inputPeerChannel(channel),
UserID: &tg.InputUser{UserID: invited.ID, AccessHash: invited.AccessHash},
})
if err != nil {
t.Fatalf("premium.getUserBoosts: %v", err)
}
if userBoosts.Count != 0 || len(userBoosts.Boosts) != 0 {
t.Fatalf("premium.getUserBoosts invited = %+v, want empty", userBoosts)
}
ownerBoosts, err := r.onPremiumGetUserBoosts(ownerCtx, &tg.PremiumGetUserBoostsRequest{
Peer: inputPeerChannel(channel),
UserID: inputUser(owner),
})
if err != nil {
t.Fatalf("premium.getUserBoosts owner: %v", err)
}
if ownerBoosts.Count != 1 || len(ownerBoosts.Boosts) != 1 {
t.Fatalf("premium.getUserBoosts owner = %+v, want one boost", ownerBoosts)
}
if boostedList, err := r.onPremiumGetBoostsList(ownerCtx, &tg.PremiumGetBoostsListRequest{
Peer: inputPeerChannel(channel),
Limit: 10,
}); err != nil {
t.Fatalf("premium.getBoostsList after apply: %v", err)
} else if boostedList.Count != 1 || len(boostedList.Boosts) != 1 || len(boostedList.Users) != 1 {
t.Fatalf("premium.getBoostsList after apply = %+v, want one boost and user", boostedList)
}
if _, err := r.onMessagesAddChatUser(ownerCtx, &tg.MessagesAddChatUserRequest{
ChatID: channel.ID,
UserID: &tg.InputUser{UserID: invited.ID, AccessHash: invited.AccessHash},
}); err != nil {
t.Fatalf("messages.addChatUser legacy wrapper: %v", err)
}
seedPhoto := domain.Photo{ID: 7701, AccessHash: 7702, DCID: 2}
if _, err := f.channels.SetChannelPhoto(f.ctx, owner.ID, channel.ID, &seedPhoto, 1700007701); err != nil {
t.Fatalf("seed channel photo before legacy clear: %v", err)
}
if _, err := r.onMessagesEditChatPhoto(ownerCtx, &tg.MessagesEditChatPhotoRequest{
ChatID: channel.ID,
Photo: &tg.InputChatPhotoEmpty{},
}); err != nil {
t.Fatalf("messages.editChatPhoto legacy wrapper: %v", err)
}
if _, err := r.onChannelsEditPhoto(ownerCtx, &tg.ChannelsEditPhotoRequest{
Channel: inputChannel(channel),
Photo: &tg.InputChatUploadedPhoto{},
}); err == nil || !strings.Contains(err.Error(), "PHOTO_INVALID") {
t.Fatalf("channels.editPhoto uploaded photo err = %v, want PHOTO_INVALID", err)
}
if _, err := r.onChannelsEditPhoto(ownerCtx, &tg.ChannelsEditPhotoRequest{
Channel: inputChannel(channel),
Photo: &tg.InputChatPhoto{ID: &tg.InputPhoto{ID: 1, AccessHash: 2, FileReference: []byte{3}}},
}); err == nil || !strings.Contains(err.Error(), "PHOTO_INVALID") {
t.Fatalf("channels.editPhoto existing photo err = %v, want PHOTO_INVALID", err)
}
if ok, err := r.onMessagesEditChatAdmin(ownerCtx, &tg.MessagesEditChatAdminRequest{
ChatID: channel.ID,
UserID: &tg.InputUser{UserID: invited.ID, AccessHash: invited.AccessHash},
IsAdmin: true,
}); err != nil || !ok {
t.Fatalf("messages.editChatAdmin legacy wrapper = %v, %v", ok, err)
}
if _, err := r.onMessagesEditChatParticipantRank(ownerCtx, &tg.MessagesEditChatParticipantRankRequest{
Peer: &tg.InputPeerChat{ChatID: channel.ID},
Participant: &tg.InputPeerUser{UserID: invited.ID, AccessHash: invited.AccessHash},
Rank: "ops",
}); err != nil {
t.Fatalf("messages.editChatParticipantRank legacy wrapper: %v", err)
}
if ok, err := r.onMessagesEditChatAbout(ownerCtx, &tg.MessagesEditChatAboutRequest{
Peer: &tg.InputPeerChat{ChatID: channel.ID},
About: "legacy about",
}); err != nil || !ok {
t.Fatalf("messages.editChatAbout legacy wrapper = %v, %v", ok, err)
}
fullAfterAbout, err := r.onMessagesGetFullChat(ownerCtx, channel.ID)
if err != nil {
t.Fatalf("messages.getFullChat after editChatAbout: %v", err)
}
if got := fullAfterAbout.FullChat.(*tg.ChannelFull).About; got != "legacy about" {
t.Fatalf("full chat about = %q, want legacy about", got)
}
if _, err := r.onMessagesEditChatCreator(ownerCtx, &tg.MessagesEditChatCreatorRequest{
Peer: &tg.InputPeerChat{ChatID: channel.ID},
UserID: &tg.InputUser{UserID: invited.ID, AccessHash: invited.AccessHash},
}); err == nil {
t.Fatalf("messages.editChatCreator = nil error, want explicit password/2FA error")
}
permUpdates, err := r.onMessagesEditChatDefaultBannedRights(ownerCtx, &tg.MessagesEditChatDefaultBannedRightsRequest{
Peer: inputPeerChannel(channel),
BannedRights: tg.ChatBannedRights{
SendMessages: true,
},
})
if err != nil {
t.Fatalf("messages.editChatDefaultBannedRights: %v", err)
}
if len(permUpdates.(*tg.Updates).Updates) == 0 {
t.Fatalf("messages.editChatDefaultBannedRights updates = %+v, want updateChannel", permUpdates)
}
if _, err := r.onMessagesDeleteChatUser(ownerCtx, &tg.MessagesDeleteChatUserRequest{
ChatID: channel.ID,
UserID: &tg.InputUser{UserID: invited.ID, AccessHash: invited.AccessHash},
}); err != nil {
t.Fatalf("messages.deleteChatUser legacy wrapper: %v", err)
}
}