owpengram-server/internal/rpc/messages_reactions_policy_rpc_test.go

347 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"github.com/gotd/td/clock"
"github.com/gotd/td/tg"
"go.uber.org/zap/zaptest"
"strings"
appchannels "telesrv/internal/app/channels"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
"testing"
)
type reactionPolicyFixture struct {
router *Router
channelSvc *appchannels.Service
sessions *captureSessions
channel domain.Channel
messageID int
ownerID int64
memberID int64
member2ID int64
}
func newReactionPolicyFixture(t *testing.T, broadcast bool) reactionPolicyFixture {
t.Helper()
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550003001", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
member, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550003002", FirstName: "Member"})
if err != nil {
t.Fatalf("create member: %v", err)
}
member2, err := userStore.Create(ctx, domain.User{AccessHash: 33, Phone: "15550003003", FirstName: "Member2"})
if err != nil {
t.Fatalf("create member2: %v", err)
}
channelStore := memory.NewChannelStore()
channelSvc := appchannels.NewService(channelStore)
created, err := channelSvc.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Reaction Policy",
Broadcast: broadcast,
Megagroup: !broadcast,
MemberUserIDs: []int64{member.ID, member2.ID},
Date: 1000,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
sent, err := channelSvc.SendMessage(ctx, owner.ID, domain.SendChannelMessageRequest{
ChannelID: created.Channel.ID,
RandomID: 9001,
Message: "react to me",
Date: 1100,
})
if err != nil {
t.Fatalf("send message: %v", err)
}
sessions := &captureSessions{}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: channelSvc,
Sessions: sessions,
}, zaptest.NewLogger(t), clock.System)
return reactionPolicyFixture{
router: r,
channelSvc: channelSvc,
sessions: sessions,
channel: created.Channel,
messageID: sent.Message.ID,
ownerID: owner.ID,
memberID: member.ID,
member2ID: member2.ID,
}
}
func (f reactionPolicyFixture) sendReaction(t *testing.T, userID int64, emoticons ...string) (tg.UpdatesClass, error) {
t.Helper()
reactions := make([]tg.ReactionClass, 0, len(emoticons))
if len(emoticons) > 0 {
for _, emoticon := range emoticons {
reactions = append(reactions, &tg.ReactionEmoji{Emoticon: emoticon})
}
}
return f.sendTLReactions(t, userID, reactions...)
}
func (f reactionPolicyFixture) sendTLReactions(t *testing.T, userID int64, reactions ...tg.ReactionClass) (tg.UpdatesClass, error) {
t.Helper()
req := &tg.MessagesSendReactionRequest{
Peer: &tg.InputPeerChannel{ChannelID: f.channel.ID, AccessHash: f.channel.AccessHash},
MsgID: f.messageID,
}
if len(reactions) > 0 {
req.SetReaction(reactions)
}
return f.router.onMessagesSendReaction(WithUserID(context.Background(), userID), req)
}
func reactionUpdateFromUpdates(t *testing.T, updates tg.UpdatesClass) *tg.UpdateMessageReactions {
t.Helper()
box, ok := updates.(*tg.Updates)
if !ok || len(box.Updates) == 0 {
t.Fatalf("updates = %T %+v, want non-empty *tg.Updates", updates, updates)
}
update, ok := box.Updates[0].(*tg.UpdateMessageReactions)
if !ok {
t.Fatalf("update = %T, want *tg.UpdateMessageReactions", box.Updates[0])
}
return update
}
func TestSendReactionRespectsChannelReactionPolicy(t *testing.T) {
f := newReactionPolicyFixture(t, false)
ctx := context.Background()
if _, err := f.channelSvc.SetAvailableReactions(ctx, f.ownerID, f.channel.ID, domain.ChannelReactionPolicy{
Type: domain.ChannelReactionPolicySome,
Emoticons: []string{"\U0001f44d"},
}); err != nil {
t.Fatalf("set whitelist policy: %v", err)
}
if _, err := f.sendReaction(t, f.memberID, "❤"); err == nil || !strings.Contains(err.Error(), "REACTION_INVALID") {
t.Fatalf("off-whitelist reaction err = %v, want REACTION_INVALID", err)
}
if _, err := f.sendReaction(t, f.memberID, "\U0001f44d"); err != nil {
t.Fatalf("whitelisted reaction: %v", err)
}
if _, err := f.channelSvc.SetAvailableReactions(ctx, f.ownerID, f.channel.ID, domain.ChannelReactionPolicy{
Type: domain.ChannelReactionPolicyNone,
}); err != nil {
t.Fatalf("set none policy: %v", err)
}
if _, err := f.sendReaction(t, f.ownerID, "\U0001f44d"); err == nil || !strings.Contains(err.Error(), "REACTION_INVALID") {
t.Fatalf("reaction under none policy err = %v, want REACTION_INVALID", err)
}
// 策略收紧后撤销存量 reaction 必须仍然可行。
if _, err := f.sendReaction(t, f.memberID); err != nil {
t.Fatalf("retract reaction under none policy: %v", err)
}
}
func TestSendReactionAllowsCustomEmojiFromChannelPolicy(t *testing.T) {
f := newReactionPolicyFixture(t, false)
ctx := context.Background()
const customDocumentID int64 = 7770001
updates, err := f.router.onMessagesSetChatAvailableReactions(WithUserID(ctx, f.ownerID), &tg.MessagesSetChatAvailableReactionsRequest{
Peer: &tg.InputPeerChannel{ChannelID: f.channel.ID, AccessHash: f.channel.AccessHash},
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{
&tg.ReactionCustomEmoji{DocumentID: customDocumentID},
}},
})
if err != nil {
t.Fatalf("set custom emoji reactions: %v", err)
}
if _, ok := updates.(*tg.Updates); !ok {
t.Fatalf("set custom emoji updates = %T, want *tg.Updates", updates)
}
if _, err := f.sendReaction(t, f.memberID, "\U0001f44d"); err == nil || !strings.Contains(err.Error(), "REACTION_INVALID") {
t.Fatalf("off-whitelist emoji err = %v, want REACTION_INVALID", err)
}
f.sessions.channelViewers = map[int64][]int64{f.channel.ID: {f.member2ID}}
sent, err := f.sendTLReactions(t, f.memberID, &tg.ReactionCustomEmoji{DocumentID: customDocumentID})
if err != nil {
t.Fatalf("send custom emoji reaction: %v", err)
}
update := reactionUpdateFromUpdates(t, sent)
if len(update.Reactions.Results) != 1 || update.Reactions.Results[0].Count != 1 || update.Reactions.Results[0].ChosenOrder != 1 {
t.Fatalf("custom reaction results = %+v, want one chosen custom reaction", update.Reactions.Results)
}
reaction, ok := update.Reactions.Results[0].Reaction.(*tg.ReactionCustomEmoji)
if !ok || reaction.DocumentID != customDocumentID {
t.Fatalf("custom reaction = %T %+v, want document %d", update.Reactions.Results[0].Reaction, update.Reactions.Results[0].Reaction, customDocumentID)
}
pushed := f.sessions.pushedUserIDs()
foundOtherViewer := false
for _, userID := range pushed {
if userID == f.member2ID {
foundOtherViewer = true
break
}
}
if !foundOtherViewer {
t.Fatalf("pushed users = %+v, want other online member %d", pushed, f.member2ID)
}
}
func TestSendReactionNormalizesDefaultReactionDocumentID(t *testing.T) {
f := newReactionPolicyFixture(t, false)
f.router.deps.Files = &fakeFiles{reactions: []domain.AvailableReaction{
{Reaction: "\U0001f44d", ActivateAnimationID: 8801},
}}
ctx := context.Background()
if _, err := f.channelSvc.SetAvailableReactions(ctx, f.ownerID, f.channel.ID, domain.ChannelReactionPolicy{
Type: domain.ChannelReactionPolicySome,
Emoticons: []string{"\U0001f44d"},
}); err != nil {
t.Fatalf("set emoji whitelist policy: %v", err)
}
updates, err := f.sendTLReactions(t, f.memberID, &tg.ReactionCustomEmoji{DocumentID: 8801})
if err != nil {
t.Fatalf("send default reaction by document id: %v", err)
}
update := reactionUpdateFromUpdates(t, updates)
if len(update.Reactions.Results) != 1 {
t.Fatalf("reaction results = %+v, want one normalized emoji result", update.Reactions.Results)
}
emoji, ok := update.Reactions.Results[0].Reaction.(*tg.ReactionEmoji)
if !ok || emoji.Emoticon != "\U0001f44d" {
t.Fatalf("normalized reaction = %T %+v, want reactionEmoji thumbs up", update.Reactions.Results[0].Reaction, update.Reactions.Results[0].Reaction)
}
}
func TestSendReactionEnforcesUniqueReactionsLimit(t *testing.T) {
f := newReactionPolicyFixture(t, false)
ctx := context.Background()
// reactions_limit 是 appConfig reactions_uniq_max 的 per-chat 覆盖,用 1 触发上限。
if _, err := f.channelSvc.SetAvailableReactions(ctx, f.ownerID, f.channel.ID, domain.ChannelReactionPolicy{
Type: domain.ChannelReactionPolicyAll,
Limit: 1,
}); err != nil {
t.Fatalf("set uniq limit policy: %v", err)
}
if _, err := f.sendReaction(t, f.ownerID, "\U0001f44d"); err != nil {
t.Fatalf("first reaction: %v", err)
}
if _, err := f.sendReaction(t, f.memberID, "❤"); err == nil || !strings.Contains(err.Error(), "REACTIONS_TOO_MANY") {
t.Fatalf("new unique emoji past limit err = %v, want REACTIONS_TOO_MANY", err)
}
// 追加已存在的种类不受 uniq 上限约束。
updates, err := f.sendReaction(t, f.memberID, "\U0001f44d")
if err != nil {
t.Fatalf("existing emoji reaction: %v", err)
}
update := reactionUpdateFromUpdates(t, updates)
if len(update.Reactions.Results) != 1 || update.Reactions.Results[0].Count != 2 {
t.Fatalf("results = %+v, want one emoji with count 2", update.Reactions.Results)
}
}
func TestSendReactionOverLimitMessageStillAllowsExistingKinds(t *testing.T) {
f := newReactionPolicyFixture(t, false)
ctx := context.Background()
// 默认策略下先造出 {👍, ❤} 两个去重种类,再把 reactions_limit 调低到 1
// 模拟「存量已超限」(管理员事后调低 / 部署前无 uniq 闸门的旧数据)。
if _, err := f.sendReaction(t, f.ownerID, "\U0001f44d"); err != nil {
t.Fatalf("seed owner reaction: %v", err)
}
if _, err := f.sendReaction(t, f.memberID, "❤"); err != nil {
t.Fatalf("seed member reaction: %v", err)
}
if _, err := f.channelSvc.SetAvailableReactions(ctx, f.ownerID, f.channel.ID, domain.ChannelReactionPolicy{
Type: domain.ChannelReactionPolicyAll,
Limit: 1,
}); err != nil {
t.Fatalf("lower uniq limit: %v", err)
}
// 重发自己已有的 reactionno-op必须放行。
if _, err := f.sendReaction(t, f.ownerID, "\U0001f44d"); err != nil {
t.Fatalf("owner re-send own reaction on over-limit message: %v", err)
}
if _, err := f.sendReaction(t, f.memberID, "❤"); err != nil {
t.Fatalf("member re-send own reaction on over-limit message: %v", err)
}
// 第三人给已有种类投票必须放行(不引入新种类)。
if _, err := f.sendReaction(t, f.member2ID, "\U0001f44d"); err != nil {
t.Fatalf("third user piles onto existing kind on over-limit message: %v", err)
}
// 引入新种类仍然 REACTIONS_TOO_MANY。
if _, err := f.sendReaction(t, f.member2ID, "\U0001f525"); err == nil || !strings.Contains(err.Error(), "REACTIONS_TOO_MANY") {
t.Fatalf("new kind on over-limit message err = %v, want REACTIONS_TOO_MANY", err)
}
}
func TestSendReactionTrimsVectorToPerUserMax(t *testing.T) {
f := newReactionPolicyFixture(t, false)
// 超出 reactions_user_max_default 的向量保留尾部最新项,不报错。
updates, err := f.sendReaction(t, f.memberID, "\U0001f44d", "❤")
if err != nil {
t.Fatalf("send oversized reaction vector: %v", err)
}
update := reactionUpdateFromUpdates(t, updates)
if len(update.Reactions.Results) != 1 {
t.Fatalf("results = %+v, want single trimmed reaction", update.Reactions.Results)
}
emoji, ok := update.Reactions.Results[0].Reaction.(*tg.ReactionEmoji)
if !ok || emoji.Emoticon != "❤" {
t.Fatalf("kept reaction = %+v, want newest vector entry kept", update.Reactions.Results[0].Reaction)
}
if update.Reactions.Results[0].ChosenOrder != 1 {
t.Fatalf("chosen_order = %d, want 1", update.Reactions.Results[0].ChosenOrder)
}
recent, ok := update.Reactions.GetRecentReactions()
if !ok || len(recent) != 1 {
t.Fatalf("megagroup recent reactions = %+v set=%v, want one entry", recent, ok)
}
}
func TestBroadcastReactionsHideRecentReactors(t *testing.T) {
f := newReactionPolicyFixture(t, true)
updates, err := f.sendReaction(t, f.memberID, "\U0001f44d")
if err != nil {
t.Fatalf("send broadcast reaction: %v", err)
}
update := reactionUpdateFromUpdates(t, updates)
if len(update.Reactions.Results) != 1 || update.Reactions.Results[0].Count != 1 {
t.Fatalf("broadcast results = %+v, want count-only aggregate", update.Reactions.Results)
}
if recent, ok := update.Reactions.GetRecentReactions(); ok && len(recent) > 0 {
t.Fatalf("broadcast recent reactions = %+v, want anonymous (absent)", recent)
}
if update.Reactions.CanSeeList {
t.Fatalf("broadcast can_see_list = true, want false")
}
}
func TestChannelRealtimeFanoutPassesCapToProvider(t *testing.T) {
f := newReactionPolicyFixture(t, false)
f.sessions.channelViewers = map[int64][]int64{f.channel.ID: {f.memberID}}
if _, err := f.sendReaction(t, f.memberID, "\U0001f44d"); err != nil {
t.Fatalf("send reaction: %v", err)
}
f.sessions.mu.Lock()
gotLimit := f.sessions.channelViewersLimit
f.sessions.mu.Unlock()
if gotLimit != domain.MaxChannelRealtimeFanout {
t.Fatalf("online viewers limit = %d, want MaxChannelRealtimeFanout %d", gotLimit, domain.MaxChannelRealtimeFanout)
}
}