owpengram-server/internal/rpc/forum_general_read_rpc_test.go

182 lines
6.5 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"
"testing"
"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"
)
// TestChannelsReadHistoryAdvancesForumGeneralReadRPC 覆盖 forum「General 话题幽灵未读」修复:
// 频道级 channels.readHistory 推进频道级已读后,必须把 General(topic 1) 的话题级已读水位也推进
// General 消息即频道根历史,被频道级已读覆盖),否则 getForumTopics 会一直报 General 未读。
// 同时验证反向不变量:读「非 General 子话题」(readDiscussion) 绝不污染 General —— 零话题间 cross-talk。
func TestChannelsReadHistoryAdvancesForumGeneralReadRPC(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 71, Phone: "15550007301", FirstName: "Owner"})
member, _ := userStore.Create(ctx, domain.User{AccessHash: 72, Phone: "15550007302", FirstName: "Member"})
channelStore := memory.NewChannelStore()
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: appchannels.NewService(channelStore),
}, zaptest.NewLogger(t), clock.System)
ownerCtx := WithUserID(ctx, owner.ID)
memberCtx := WithUserID(ctx, member.ID)
created, err := r.onChannelsCreateChannel(ownerCtx, &tg.ChannelsCreateChannelRequest{Title: "Forum", Megagroup: true})
if err != nil {
t.Fatalf("create channel: %v", err)
}
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
input := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
forumPeer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
if _, err := r.onChannelsInviteToChannel(ownerCtx, &tg.ChannelsInviteToChannelRequest{
Channel: input,
Users: []tg.InputUserClass{&tg.InputUser{UserID: member.ID, AccessHash: member.AccessHash}},
}); err != nil {
t.Fatalf("invite member: %v", err)
}
if _, err := r.onChannelsToggleForum(ownerCtx, &tg.ChannelsToggleForumRequest{Channel: input, Enabled: true, Tabs: true}); err != nil {
t.Fatalf("toggle forum: %v", err)
}
// 一个非 General 子话题,用于 cross-talk 反例。
createdTopic, err := r.onMessagesCreateForumTopic(ownerCtx, &tg.MessagesCreateForumTopicRequest{
Peer: forumPeer,
Title: "Side",
IconColor: domain.DefaultForumTopicIconColor,
RandomID: 7301001,
})
if err != nil {
t.Fatalf("create forum topic: %v", err)
}
sideTopicID := forumTopicRootMessageID(t, createdTopic, "Side")
sendGeneral := func(text string, randomID int64) int {
sent, err := r.onMessagesSendMessage(ownerCtx, &tg.MessagesSendMessageRequest{
Peer: forumPeer,
Message: text,
RandomID: randomID,
})
if err != nil {
t.Fatalf("send general %q: %v", text, err)
}
return forumNewChannelMessageID(t, sent, text)
}
sendSide := func(text string, randomID int64) int {
reply := &tg.InputReplyToMessage{ReplyToMsgID: 0}
reply.SetTopMsgID(sideTopicID)
req := &tg.MessagesSendMessageRequest{Peer: forumPeer, Message: text, RandomID: randomID}
req.SetReplyTo(reply)
sent, err := r.onMessagesSendMessage(ownerCtx, req)
if err != nil {
t.Fatalf("send side %q: %v", text, err)
}
return forumNewChannelMessageID(t, sent, text)
}
// id 序Side 根 < g1 < g2 < side1 < g3。Side 消息夹在 General 消息中间,
// 是「读 Side 用 GREATEST 误清 General」这类污染的最小复现条件。
_ = sendGeneral("g1", 7301002)
_ = sendGeneral("g2", 7301003)
sideMsg := sendSide("side1", 7301004)
g3 := sendGeneral("g3", 7301005)
generalUnread := func() int {
topics, err := r.onMessagesGetForumTopicsByID(memberCtx, &tg.MessagesGetForumTopicsByIDRequest{
Peer: forumPeer,
Topics: []int{forumGeneralTopicID},
})
if err != nil {
t.Fatalf("member getForumTopicsByID General: %v", err)
}
for _, item := range topics.Topics {
if topic, ok := item.(*tg.ForumTopic); ok && topic.ID == forumGeneralTopicID {
return topic.UnreadCount
}
}
t.Fatalf("member getForumTopicsByID General missing in %+v", topics.Topics)
return -1
}
before := generalUnread()
if before == 0 {
t.Fatalf("General unread before any member read = 0, want owner's General messages counted as unread")
}
// 反向不变量读「Side」子话题readDiscussion不得污染 General。readDiscussion 内部对频道级
// ReadHistory 的「保守叠加」只经 service不触达 channels.readHistory 的 RPC handler故不回灌 General。
if ok, err := r.onMessagesReadDiscussion(memberCtx, &tg.MessagesReadDiscussionRequest{
Peer: forumPeer,
MsgID: sideTopicID,
ReadMaxID: sideMsg,
}); err != nil || !ok {
t.Fatalf("member readDiscussion Side = ok %v err %v, want true", ok, err)
}
if after := generalUnread(); after != before {
t.Fatalf("General unread after reading Side topic = %d, want unchanged %d (no cross-topic pollution)", after, before)
}
// 修复点:直接的 channels.readHistory 把频道级读到顶后General 话题级水位随之推进 → 未读清零。
if ok, err := r.onChannelsReadHistory(memberCtx, &tg.ChannelsReadHistoryRequest{
Channel: input,
MaxID: g3,
}); err != nil || !ok {
t.Fatalf("member channels.readHistory = ok %v err %v, want true", ok, err)
}
if after := generalUnread(); after != 0 {
t.Fatalf("General unread after channels.readHistory = %d, want 0 (channel-level read covers General)", after)
}
}
func forumNewChannelMessageID(t *testing.T, updates tg.UpdatesClass, text string) int {
t.Helper()
u, ok := updates.(*tg.Updates)
if !ok {
t.Fatalf("updates = %T, want *tg.Updates", updates)
}
for _, update := range u.Updates {
newMsg, ok := update.(*tg.UpdateNewChannelMessage)
if !ok {
continue
}
if msg, ok := newMsg.Message.(*tg.Message); ok && msg.Message == text {
return msg.ID
}
}
t.Fatalf("updates = %+v, want new channel message %q", u.Updates, text)
return 0
}
func forumTopicRootMessageID(t *testing.T, updates tg.UpdatesClass, title string) int {
t.Helper()
u, ok := updates.(*tg.Updates)
if !ok {
t.Fatalf("updates = %T, want *tg.Updates", updates)
}
for _, update := range u.Updates {
newMsg, ok := update.(*tg.UpdateNewChannelMessage)
if !ok {
continue
}
msg, ok := newMsg.Message.(*tg.MessageService)
if !ok {
continue
}
if action, ok := msg.Action.(*tg.MessageActionTopicCreate); ok && action.Title == title {
return msg.ID
}
}
t.Fatalf("updates = %+v, want topic create service message %q", u.Updates, title)
return 0
}