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