owpengram-server/internal/rpc/forum_reply_topic_rpc_test.go
Astra c7a77c23c8 forum: fix reply_to_top_id for replies inside a forum
resolveChannelReply applied discussion-thread logic (reply_to_top_id =
the replied-to message's own id) to forum replies. Replying to a General
message produced reply_to_top_id = <that id>, a topic no client can
resolve: the reply vanished from every topic view and reply-jump on
strict clients said "message doesn't exist".

Forum replies now inherit the target's topic via domain.ForumReplyTopicID
(target's topic, or its own id if it's a topic-create, else General), and
General (topic 1) is accepted as a valid virtual topic everywhere, so
sends carrying top_msg_id: 1 are no longer rejected. Non-forum discussion
threads are unchanged.
2026-09-14 11:55:23 +01:00

120 lines
4.7 KiB
Go

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"
)
// A reply inside a forum must inherit the *target's* topic, never the target's
// own message id. Regression: replying to a General message produced
// reply_to_top_id = <that message's id>, a topic that no client can resolve, so
// the reply vanished from every topic view and reply-jump said "doesn't exist".
func TestForumReplyInheritsTargetTopic(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 91, Phone: "15550009101", FirstName: "Owner"})
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)
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}
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
if _, err := r.onChannelsToggleForum(ownerCtx, &tg.ChannelsToggleForumRequest{Channel: input, Enabled: true, Tabs: true}); err != nil {
t.Fatalf("toggle forum: %v", err)
}
topicUpd, err := r.onMessagesCreateForumTopic(ownerCtx, &tg.MessagesCreateForumTopicRequest{
Peer: peer, Title: "Test", IconColor: domain.DefaultForumTopicIconColor, RandomID: 9101001,
})
if err != nil {
t.Fatalf("create topic: %v", err)
}
testTopicID := forumTopicRootMessageID(t, topicUpd, "Test")
send := func(text string, randomID int64, reply *tg.InputReplyToMessage) *tg.Message {
req := &tg.MessagesSendMessageRequest{Peer: peer, Message: text, RandomID: randomID}
if reply != nil {
req.SetReplyTo(reply)
}
upd, err := r.onMessagesSendMessage(ownerCtx, req)
if err != nil {
t.Fatalf("send %q: %v", text, err)
}
for _, u := range upd.(*tg.Updates).Updates {
if nm, ok := u.(*tg.UpdateNewChannelMessage); ok {
if m, ok := nm.Message.(*tg.Message); ok && m.Message == text {
return m
}
}
}
t.Fatalf("no new message for %q in %+v", text, upd)
return nil
}
topID := func(m *tg.Message) int {
h, ok := m.ReplyTo.(*tg.MessageReplyHeader)
if !ok {
t.Fatalf("message %d has reply header %T, want *MessageReplyHeader", m.ID, m.ReplyTo)
}
id, _ := h.GetReplyToTopID()
if !h.ForumTopic {
t.Fatalf("message %d reply header missing forum_topic flag: %+v", m.ID, h)
}
return id
}
// A plain General message (no reply header).
g1 := send("g1", 9101002, nil)
// Reply to it -> topic must be General (1), not g1.ID.
r1 := send("r1", 9101003, &tg.InputReplyToMessage{ReplyToMsgID: g1.ID})
if got := topID(r1); got != domain.ForumGeneralTopicID {
t.Fatalf("reply to a General message: reply_to_top_id = %d, want %d (General), not the target id %d",
got, domain.ForumGeneralTopicID, g1.ID)
}
// Reply again, this time the client also passes top_msg_id: 1 (General).
// Previously this was rejected because General has no channel_forum_topics row.
replyWithTop := &tg.InputReplyToMessage{ReplyToMsgID: g1.ID}
replyWithTop.SetTopMsgID(domain.ForumGeneralTopicID)
r2 := send("r2", 9101004, replyWithTop)
if got := topID(r2); got != domain.ForumGeneralTopicID {
t.Fatalf("reply with top_msg_id=1: reply_to_top_id = %d, want %d", got, domain.ForumGeneralTopicID)
}
// Post directly into the "Test" topic, then reply to a plain message there.
tInTopic := &tg.InputReplyToMessage{ReplyToMsgID: 0}
tInTopic.SetTopMsgID(testTopicID)
m1 := send("t1", 9101005, tInTopic)
if got := topID(m1); got != testTopicID {
t.Fatalf("message in Test topic: reply_to_top_id = %d, want %d", got, testTopicID)
}
rt := send("rt", 9101006, &tg.InputReplyToMessage{ReplyToMsgID: m1.ID})
if got := topID(rt); got != testTopicID {
t.Fatalf("reply inside Test topic: reply_to_top_id = %d, want %d (topic), not %d", got, testTopicID, m1.ID)
}
// Replying to a General message while claiming a mismatched topic is rejected.
bad := &tg.InputReplyToMessage{ReplyToMsgID: g1.ID}
bad.SetTopMsgID(testTopicID)
req := &tg.MessagesSendMessageRequest{Peer: peer, Message: "bad", RandomID: 9101007}
req.SetReplyTo(bad)
if _, err := r.onMessagesSendMessage(ownerCtx, req); err == nil {
t.Fatal("reply with a topic id that doesn't match the target's topic was accepted")
}
}