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 = , 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") } }