chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
182
internal/rpc/forum_general_read_rpc_test.go
Normal file
182
internal/rpc/forum_general_read_rpc_test.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue