owpengram-server/internal/rpc/channels_legacy_settings_rpc_test.go

478 lines
20 KiB
Go
Raw Permalink 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"
"fmt"
"github.com/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/tg"
"go.uber.org/zap/zaptest"
appchannels "telesrv/internal/app/channels"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
"testing"
)
func TestLegacyChannelSettingsRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 81, Phone: "15550002181", FirstName: "Owner"})
friend, _ := userStore.Create(ctx, domain.User{AccessHash: 82, Phone: "15550002182", FirstName: "Friend"})
channelStore := memory.NewChannelStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
}, zaptest.NewLogger(t), clock.System)
created, err := r.onMessagesCreateChat(WithUserID(ctx, owner.ID), &tg.MessagesCreateChatRequest{
Users: []tg.InputUserClass{&tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash}},
Title: "Legacy Settings Group",
})
if err != nil {
t.Fatalf("create chat: %v", err)
}
channel := created.Updates.(*tg.Updates).Chats[0].(*tg.Channel)
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
themeUpdates, err := r.onMessagesSetChatTheme(WithUserID(ctx, owner.ID), &tg.MessagesSetChatThemeRequest{Peer: peer})
if err != nil {
t.Fatalf("set chat theme channel peer: %v", err)
}
if len(themeUpdates.(*tg.Updates).Chats) != 1 {
t.Fatalf("set chat theme updates = %+v, want channel context", themeUpdates)
}
privateTheme, err := r.onMessagesSetChatTheme(WithUserID(ctx, owner.ID), &tg.MessagesSetChatThemeRequest{
Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash},
Theme: &tg.InputChatThemeEmpty{},
})
if err != nil {
t.Fatalf("set chat theme private peer: %v", err)
}
if len(privateTheme.(*tg.Updates).Updates) != 0 {
t.Fatalf("private set chat theme updates = %+v, want empty compat ack", privateTheme)
}
setReactionsReq := &tg.MessagesSetChatAvailableReactionsRequest{
Peer: peer,
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{&tg.ReactionEmoji{Emoticon: "\U0001f44d"}}},
}
setReactionsReq.SetReactionsLimit(8)
reactionUpdates, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), setReactionsReq)
if err != nil {
t.Fatalf("set available reactions: %v", err)
}
if len(reactionUpdates.(*tg.Updates).Chats) != 1 {
t.Fatalf("set reactions updates = %+v, want channel state update", reactionUpdates)
}
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
if err != nil {
t.Fatalf("get full channel after reactions: %v", err)
}
fullChannel := full.FullChat.(*tg.ChannelFull)
reactions, ok := fullChannel.GetAvailableReactions()
if !ok {
t.Fatalf("full channel reactions missing after set")
}
some, ok := reactions.(*tg.ChatReactionsSome)
if !ok || len(some.Reactions) != 1 {
t.Fatalf("full channel reactions = %#v, want one explicit reaction", reactions)
}
if fullChannel.ReactionsLimit != 8 {
t.Fatalf("full channel reactions limit = %d, want 8", fullChannel.ReactionsLimit)
}
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), &tg.MessagesSetChatAvailableReactionsRequest{
Peer: peer,
AvailableReactions: &tg.ChatReactionsSome{Reactions: make([]tg.ReactionClass, domain.MaxChannelReactionTypes+1)},
}); err == nil {
t.Fatalf("set too many reactions err = nil, want limit error")
}
noForwards, err := r.onMessagesToggleNoForwards(WithUserID(ctx, owner.ID), &tg.MessagesToggleNoForwardsRequest{
Peer: peer,
Enabled: true,
})
if err != nil {
t.Fatalf("toggle noforwards: %v", err)
}
if got := noForwards.(*tg.Updates).Chats[0].(*tg.Channel); !got.Noforwards {
t.Fatalf("noforwards channel = %+v, want enabled", got)
}
sent, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{
Peer: peer,
Message: "protected content",
RandomID: 8181,
})
if err != nil {
t.Fatalf("send protected message: %v", err)
}
msg := sent.(*tg.Updates).Updates[1].(*tg.UpdateNewChannelMessage).Message.(*tg.Message)
if !msg.Noforwards {
t.Fatalf("protected channel message = %+v, want noforwards inherited", msg)
}
}
// TestBroadcastChannelAcceptsFullReactionCatalog 复现并锁定真机 bug广播频道开启
// reactions 时DrKLO 把「启用全部标准 reaction」发成显式 chatReactionsSome 列表
// megagroup 走 chatReactionsAll列表长度等于 getAvailableReactions 目录大小
// (当前 ~74。此前 MaxChannelReactionItems=64 把它误判成 LIMIT_INVALID。修复后
// 任何不超过 MaxChannelReactionTypes 的列表都必须被接受。
func TestBroadcastChannelAcceptsFullReactionCatalog(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 91, Phone: "15550002191", FirstName: "Owner"})
channelStore := memory.NewChannelStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
}, zaptest.NewLogger(t), clock.System)
created, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
Title: "Broadcast Reactions",
Broadcast: true,
})
if err != nil {
t.Fatalf("create broadcast channel: %v", err)
}
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
if channel.Megagroup {
t.Fatalf("created channel = %+v, want broadcast (not megagroup)", channel)
}
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
// 用一个明显超过旧上限(64)的目录大小,证明修复生效。每个 emoticon 取互不相同
// 的合法短串即可(验证只关心非空且 rune 数 <= MaxChannelReactionEmoticonLength
const catalogSize = 74
if catalogSize <= 64 || catalogSize > domain.MaxChannelReactionTypes {
t.Fatalf("test catalog size %d must exceed the old 64 cap and stay within %d",
catalogSize, domain.MaxChannelReactionTypes)
}
reactions := make([]tg.ReactionClass, 0, catalogSize)
for i := 0; i < catalogSize; i++ {
reactions = append(reactions, &tg.ReactionEmoji{Emoticon: fmt.Sprintf("r%02d", i)})
}
setCatalogReq := &tg.MessagesSetChatAvailableReactionsRequest{
Peer: peer,
AvailableReactions: &tg.ChatReactionsSome{Reactions: reactions},
}
setCatalogReq.SetReactionsLimit(11)
updates, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), setCatalogReq)
if err != nil {
t.Fatalf("set full-catalog reactions on broadcast channel: %v", err)
}
if len(updates.(*tg.Updates).Chats) != 1 {
t.Fatalf("set reactions updates = %+v, want channel state update", updates)
}
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
if err != nil {
t.Fatalf("get full channel after reactions: %v", err)
}
fullChannel := full.FullChat.(*tg.ChannelFull)
stored, ok := fullChannel.GetAvailableReactions()
if !ok {
t.Fatalf("full channel reactions missing after set")
}
some, ok := stored.(*tg.ChatReactionsSome)
if !ok || len(some.Reactions) != catalogSize {
t.Fatalf("full channel reactions = %#v, want %d explicit reactions", stored, catalogSize)
}
}
func TestSetChatAvailableReactionsPreservesOptionalFlags(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 101, Phone: "15550002201", FirstName: "Owner"})
channelStore := memory.NewChannelStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
}, zaptest.NewLogger(t), clock.System)
created, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
Title: "Broadcast Optional Reactions",
Broadcast: true,
})
if err != nil {
t.Fatalf("create broadcast channel: %v", err)
}
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
initial := &tg.MessagesSetChatAvailableReactionsRequest{
Peer: peer,
AvailableReactions: &tg.ChatReactionsAll{AllowCustom: true},
}
initial.SetReactionsLimit(7)
initial.SetPaidEnabled(true)
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), initial); err != nil {
t.Fatalf("set initial reaction policy: %v", err)
}
omitOptional := &tg.MessagesSetChatAvailableReactionsRequest{
Peer: peer,
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{
&tg.ReactionEmoji{Emoticon: "\U0001f44d"},
}},
}
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), omitOptional); err != nil {
t.Fatalf("set reaction policy without optional flags: %v", err)
}
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
if err != nil {
t.Fatalf("get full channel after omitted flags: %v", err)
}
fullChannel := full.FullChat.(*tg.ChannelFull)
if fullChannel.ReactionsLimit != 7 {
t.Fatalf("reactions limit after omitted flag = %d, want preserved 7", fullChannel.ReactionsLimit)
}
disablePaid := &tg.MessagesSetChatAvailableReactionsRequest{
Peer: peer,
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{
&tg.ReactionEmoji{Emoticon: "\U0001f44d"},
&tg.ReactionEmoji{Emoticon: "\u2764"},
}},
}
disablePaid.SetPaidEnabled(false)
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), disablePaid); err != nil {
t.Fatalf("disable paid reactions without limit flag: %v", err)
}
full, err = r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
if err != nil {
t.Fatalf("get full channel after paid disable: %v", err)
}
fullChannel = full.FullChat.(*tg.ChannelFull)
if fullChannel.ReactionsLimit != 7 {
t.Fatalf("reactions limit after paid-only update = %d, want preserved 7", fullChannel.ReactionsLimit)
}
}
func TestSetChatAvailableReactionsStripsTDesktopPaidSentinel(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 101, Phone: "15550002204", FirstName: "Owner"})
channelStore := memory.NewChannelStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
}, zaptest.NewLogger(t), clock.System)
created, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
Title: "TDesktop Paid Sentinel",
Broadcast: true,
})
if err != nil {
t.Fatalf("create broadcast channel: %v", err)
}
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
tdesktopReq := &tg.MessagesSetChatAvailableReactionsRequest{
Peer: peer,
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{
&tg.ReactionPaid{},
&tg.ReactionEmoji{Emoticon: "\U0001f44d"},
&tg.ReactionEmoji{Emoticon: "\u2764"},
&tg.ReactionPaid{},
}},
}
tdesktopReq.SetReactionsLimit(11)
tdesktopReq.SetPaidEnabled(true)
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), tdesktopReq); err != nil {
t.Fatalf("set TDesktop paid sentinel reactions: %v", err)
}
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
if err != nil {
t.Fatalf("get full channel after TDesktop sentinel set: %v", err)
}
some := mustChannelFullSomeReactions(t, full)
if len(some.Reactions) != 2 {
t.Fatalf("stored reactions after stripping sentinel = %d, want 2", len(some.Reactions))
}
for i, reaction := range some.Reactions {
if _, ok := reaction.(*tg.ReactionPaid); ok {
t.Fatalf("stored reaction[%d] = reactionPaid, want paid state only in paid_reactions_available", i)
}
}
fullCatalog := make([]tg.ReactionClass, 0, domain.MaxChannelReactionTypes+1)
fullCatalog = append(fullCatalog, &tg.ReactionPaid{})
for i := 0; i < domain.MaxChannelReactionTypes; i++ {
fullCatalog = append(fullCatalog, &tg.ReactionEmoji{Emoticon: fmt.Sprintf("r%03d", i)})
}
maxReq := &tg.MessagesSetChatAvailableReactionsRequest{
Peer: peer,
AvailableReactions: &tg.ChatReactionsSome{Reactions: fullCatalog},
}
maxReq.SetPaidEnabled(true)
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), maxReq); err != nil {
t.Fatalf("set max normal reactions plus paid sentinel: %v", err)
}
full, err = r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
if err != nil {
t.Fatalf("get full channel after max sentinel set: %v", err)
}
some = mustChannelFullSomeReactions(t, full)
if len(some.Reactions) != domain.MaxChannelReactionTypes {
t.Fatalf("stored reactions after max sentinel set = %d, want %d", len(some.Reactions), domain.MaxChannelReactionTypes)
}
}
func TestAndroidChannelReactionEditorProjectsDefaultEmojiAsDocuments(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 111, Phone: "15550002211", FirstName: "Owner"})
channelStore := memory.NewChannelStore()
files := &fakeFiles{reactions: []domain.AvailableReaction{
{Reaction: "\U0001f44d", ActivateAnimationID: 7101},
{Reaction: "\U0001f525", ActivateAnimationID: 7102},
}}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
Files: files,
}, zaptest.NewLogger(t), clock.System)
created, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
Title: "Android Reaction Projection",
Broadcast: true,
})
if err != nil {
t.Fatalf("create broadcast channel: %v", err)
}
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), &tg.MessagesSetChatAvailableReactionsRequest{
Peer: peer,
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{
&tg.ReactionEmoji{Emoticon: "\U0001f44d"},
&tg.ReactionEmoji{Emoticon: "\U0001f525"},
}},
}); err != nil {
t.Fatalf("set reaction policy: %v", err)
}
desktopFull, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
if err != nil {
t.Fatalf("get desktop full channel: %v", err)
}
desktopSome := mustChannelFullSomeReactions(t, desktopFull)
if emoji, ok := desktopSome.Reactions[0].(*tg.ReactionEmoji); !ok || emoji.Emoticon != "\U0001f44d" {
t.Fatalf("desktop reaction[0] = %T %+v, want reactionEmoji thumbs up", desktopSome.Reactions[0], desktopSome.Reactions[0])
}
androidCtx := WithClientInfo(WithUserID(ctx, owner.ID), ClientInfo{Type: ClientTypeAndroid, AppVersion: "12.8.1"})
androidFull, err := r.onChannelsGetFullChannel(androidCtx, &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
if err != nil {
t.Fatalf("get android full channel: %v", err)
}
androidSome := mustChannelFullSomeReactions(t, androidFull)
if doc, ok := androidSome.Reactions[0].(*tg.ReactionCustomEmoji); !ok || doc.DocumentID != 7101 {
t.Fatalf("android reaction[0] = %T %+v, want reactionCustomEmoji 7101", androidSome.Reactions[0], androidSome.Reactions[0])
}
if doc, ok := androidSome.Reactions[1].(*tg.ReactionCustomEmoji); !ok || doc.DocumentID != 7102 {
t.Fatalf("android reaction[1] = %T %+v, want reactionCustomEmoji 7102", androidSome.Reactions[1], androidSome.Reactions[1])
}
}
func TestSetChatAvailableReactionsNormalizesDefaultReactionDocuments(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 112, Phone: "15550002212", FirstName: "Owner"})
channelStore := memory.NewChannelStore()
files := &fakeFiles{reactions: []domain.AvailableReaction{
{Reaction: "\U0001f44d", ActivateAnimationID: 7201},
}}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
Files: files,
}, zaptest.NewLogger(t), clock.System)
created, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
Title: "Android Reaction Save",
Broadcast: true,
})
if err != nil {
t.Fatalf("create broadcast channel: %v", err)
}
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), &tg.MessagesSetChatAvailableReactionsRequest{
Peer: peer,
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{
&tg.ReactionCustomEmoji{DocumentID: 7201},
}},
}); err != nil {
t.Fatalf("set reaction policy with default document id: %v", err)
}
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
if err != nil {
t.Fatalf("get full channel: %v", err)
}
some := mustChannelFullSomeReactions(t, full)
if emoji, ok := some.Reactions[0].(*tg.ReactionEmoji); !ok || emoji.Emoticon != "\U0001f44d" {
t.Fatalf("stored reaction[0] = %T %+v, want normalized reactionEmoji thumbs up", some.Reactions[0], some.Reactions[0])
}
}
func TestAvailableReactionDocumentMapsAreCached(t *testing.T) {
ctx := context.Background()
files := &countingAvailableReactionFiles{fakeFiles: &fakeFiles{reactions: []domain.AvailableReaction{
{Reaction: "\U0001f44d", ActivateAnimationID: 7301},
}}}
r := &Router{deps: Deps{Files: files}}
emojiToDoc, docToEmoji := r.availableReactionDocumentMaps(ctx)
if got := emojiToDoc["\U0001f44d"]; got != 7301 {
t.Fatalf("emoji->document map = %d, want 7301", got)
}
if got := docToEmoji[7301]; got != "\U0001f44d" {
t.Fatalf("document->emoji map = %q, want thumbs up", got)
}
files.fakeFiles.reactions = append(files.fakeFiles.reactions, domain.AvailableReaction{
Reaction: "\U0001f525",
ActivateAnimationID: 7302,
})
emojiToDoc, docToEmoji = r.availableReactionDocumentMaps(ctx)
if files.calls != 1 {
t.Fatalf("ListAvailableReactions calls = %d, want 1 cached load", files.calls)
}
if got := emojiToDoc["\U0001f525"]; got != 0 {
t.Fatalf("cached emoji->document map unexpectedly saw later catalog mutation: %d", got)
}
if got := docToEmoji[7302]; got != "" {
t.Fatalf("cached document->emoji map unexpectedly saw later catalog mutation: %q", got)
}
}
type countingAvailableReactionFiles struct {
*fakeFiles
calls int
}
func (f *countingAvailableReactionFiles) ListAvailableReactions(ctx context.Context) ([]domain.AvailableReaction, error) {
f.calls++
return f.fakeFiles.ListAvailableReactions(ctx)
}
func mustChannelFullSomeReactions(t *testing.T, full *tg.MessagesChatFull) *tg.ChatReactionsSome {
t.Helper()
channelFull, ok := full.FullChat.(*tg.ChannelFull)
if !ok {
t.Fatalf("full chat = %T, want *tg.ChannelFull", full.FullChat)
}
reactions, ok := channelFull.GetAvailableReactions()
if !ok {
t.Fatalf("channel full reactions missing")
}
some, ok := reactions.(*tg.ChatReactionsSome)
if !ok {
t.Fatalf("channel full reactions = %T %+v, want *tg.ChatReactionsSome", reactions, reactions)
}
if len(some.Reactions) == 0 {
t.Fatalf("channel full reactions empty")
}
return some
}