owpengram-server/internal/rpc/forum_topics_preview_rpc_test.go
Astra 7083faa786 forum: let non-members preview topic replies in a public channel
ListChannelReplies used getChannelForMemberOrLinkedGuest, so messages.getReplies
was member-only. ListChannelHistory (flat history) uses getChannelForViewer and
already allows a public channel's non-members to preview it. The mismatch meant
that on a public forum you could preview the flat history but not the topics -
and after leaving, tdesktop's topic view got CHANNEL_PRIVATE and sat on
"Loading..." forever instead of rendering a preview.

Switch the primary channel lookup in ListChannelReplies (both stores) to the
viewer-scope path. Private channels still return CHANNEL_PRIVATE to non-members;
the broadcast comment-thread lookup is unchanged.
2026-09-09 14:43:59 +01:00

133 lines
4.8 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 public forum's topic list is browsable before joining, like its history.
// Regression: getForumTopics used the member-only access path and returned
// CHANNEL_PRIVATE / an empty list to non-members, so the topic list (and even
// General) was invisible until they joined.
func TestGetForumTopicsVisibleToPublicNonMember(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 81, Phone: "15550008101", FirstName: "Owner"})
outsider, _ := userStore.Create(ctx, domain.User{AccessHash: 82, Phone: "15550008102", FirstName: "Outsider"})
channelStore := memory.NewChannelStore()
channelSvc := appchannels.NewService(channelStore)
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: channelSvc,
}, zaptest.NewLogger(t), clock.System)
ownerCtx := WithUserID(ctx, owner.ID)
outsiderCtx := WithUserID(ctx, outsider.ID)
created, err := r.onChannelsCreateChannel(ownerCtx, &tg.ChannelsCreateChannelRequest{Title: "Public 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.onChannelsToggleForum(ownerCtx, &tg.ChannelsToggleForumRequest{Channel: input, Enabled: true, Tabs: true}); err != nil {
t.Fatalf("toggle forum: %v", err)
}
if _, err := channelSvc.UpdateUsername(ctx, owner.ID, domain.UpdateChannelUsernameRequest{
ChannelID: channel.ID,
Username: "publicforum",
}); err != nil {
t.Fatalf("set channel username: %v", err)
}
if _, err := r.onMessagesCreateForumTopic(ownerCtx, &tg.MessagesCreateForumTopicRequest{
Peer: forumPeer,
Title: "Test",
IconColor: domain.DefaultForumTopicIconColor,
RandomID: 8101001,
}); err != nil {
t.Fatalf("create forum topic: %v", err)
}
res, err := r.onMessagesGetForumTopics(outsiderCtx, &tg.MessagesGetForumTopicsRequest{
Peer: forumPeer,
Limit: 100,
})
if err != nil {
t.Fatalf("getForumTopics as non-member: %v", err)
}
titles := map[string]bool{}
testTopicID := 0
for _, tc := range res.Topics {
switch topic := tc.(type) {
case *tg.ForumTopic:
titles[topic.Title] = true
if topic.Title == "Test" {
testTopicID = topic.ID
}
case *tg.ForumTopicDeleted:
}
}
if !titles["General"] {
t.Fatalf("non-member did not see the General topic: %+v", res.Topics)
}
if !titles["Test"] {
t.Fatalf("non-member did not see the Test topic: %+v", res.Topics)
}
// A non-member can also read the replies inside a topic (preview), the same
// way ListChannelHistory lets them preview a public group's flat history.
if testTopicID == 0 {
t.Fatal("no Test topic id to open")
}
if _, err := r.onMessagesGetReplies(outsiderCtx, &tg.MessagesGetRepliesRequest{
Peer: forumPeer,
MsgID: testTopicID,
Limit: 20,
}); err != nil {
t.Fatalf("getReplies as non-member of a public forum: %v", err)
}
// The forum's own channel must come back with left=true so the client still
// offers a Join button instead of treating the forum as already joined.
var forumChat *tg.Channel
for _, c := range res.Chats {
if ch, ok := c.(*tg.Channel); ok && ch.ID == channel.ID {
forumChat = ch
}
}
if forumChat == nil {
t.Fatalf("forum channel missing from getForumTopics chats: %+v", res.Chats)
}
if !forumChat.Left {
t.Fatalf("non-member forum chat = %#v, want left=true", forumChat)
}
// A private forum still refuses a non-member.
priv, err := r.onChannelsCreateChannel(ownerCtx, &tg.ChannelsCreateChannelRequest{Title: "Private Forum", Megagroup: true})
if err != nil {
t.Fatalf("create private channel: %v", err)
}
privCh := priv.(*tg.Updates).Chats[0].(*tg.Channel)
privInput := &tg.InputChannel{ChannelID: privCh.ID, AccessHash: privCh.AccessHash}
privPeer := &tg.InputPeerChannel{ChannelID: privCh.ID, AccessHash: privCh.AccessHash}
if _, err := r.onChannelsToggleForum(ownerCtx, &tg.ChannelsToggleForumRequest{Channel: privInput, Enabled: true, Tabs: true}); err != nil {
t.Fatalf("toggle private forum: %v", err)
}
if _, err := r.onMessagesGetForumTopics(outsiderCtx, &tg.MessagesGetForumTopicsRequest{Peer: privPeer, Limit: 100}); err == nil {
t.Fatal("non-member read a private forum's topic list")
}
if _, err := r.onMessagesGetReplies(outsiderCtx, &tg.MessagesGetRepliesRequest{Peer: privPeer, MsgID: 1, Limit: 20}); err == nil {
t.Fatal("non-member read a private forum topic's replies")
}
}