owpengram-server/internal/rpc/communities_rpc_test.go
A 08c763b683 fix: sync legacy community projection
Sync telesrv 07644e0 (fix(communities): hide aggregates from legacy layers).
2026-07-20 16:50:37 +08:00

334 lines
15 KiB
Go

package rpc
import (
"context"
"testing"
"time"
"github.com/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tgerr"
"go.uber.org/zap/zaptest"
appchannels "telesrv/internal/app/channels"
appcommunities "telesrv/internal/app/communities"
appdialogs "telesrv/internal/app/dialogs"
appstories "telesrv/internal/app/stories"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
func communityRPCChannel(t *testing.T, service *appchannels.Service, creator domain.User, title string, members ...domain.User) domain.Channel {
t.Helper()
memberIDs := make([]int64, 0, len(members))
for _, member := range members {
memberIDs = append(memberIDs, member.ID)
}
created, err := service.CreateChannel(context.Background(), creator.ID, domain.CreateChannelRequest{
CreatorUserID: creator.ID,
Title: title,
Megagroup: true,
MemberUserIDs: memberIDs,
Date: 1_800_100_000,
})
if err != nil {
t.Fatalf("create channel %q: %v", title, err)
}
return created.Channel
}
func TestCommunityDialogsSharePinnedLimit(t *testing.T) {
ctx := WithLayer(context.Background(), communitiesLayer)
users := memory.NewUserStore()
owner, err := users.Create(ctx, domain.User{AccessHash: 711, Phone: "15552000011", FirstName: "Pin Owner"})
if err != nil {
t.Fatal(err)
}
channels := memory.NewChannelStore()
channelService := appchannels.NewService(channels)
communityService := appcommunities.NewService(memory.NewCommunityStore(users, channels, nil, nil))
r := New(Config{}, Deps{
Users: appusers.NewService(users), Channels: channelService,
Communities: communityService, Dialogs: appdialogs.NewService(memory.NewDialogStore(), channels),
}, zaptest.NewLogger(t), clock.System)
inputs := make([]*tg.InputChannel, 0, domain.MaxPinnedDialogsMainFolder)
for i := 0; i < domain.MaxPinnedDialogsMainFolder; i++ {
channel := communityRPCChannel(t, channelService, owner, "Pinned Community Channel")
view, err := communityService.Create(ctx, owner.ID, domain.CreateCommunityRequest{
Title: "Pinned Community", InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID},
Visibility: domain.CommunityPeerVisible, Date: 1_800_110_000 + i,
})
if err != nil {
t.Fatalf("create community %d: %v", i, err)
}
if _, _, err := communityService.SetCollapsed(ctx, owner.ID, view.Community.ID, true); err != nil {
t.Fatalf("collapse community %d: %v", i, err)
}
inputs = append(inputs, &tg.InputChannel{ChannelID: view.Community.ID, AccessHash: view.Community.AccessHash})
}
for i := 0; i < domain.MaxPinnedDialogsMainFolder-1; i++ {
input := inputs[i]
toggle := &tg.MessagesToggleDialogPinRequest{Peer: &tg.InputDialogPeerCommunity{Community: input}}
toggle.SetPinned(true)
ok, err := r.onMessagesToggleDialogPin(WithUserID(ctx, owner.ID), toggle)
if err != nil || !ok {
t.Fatalf("pin community %d = %v, %v", i, ok, err)
}
}
joined, err := communityService.ListJoined(ctx, owner.ID)
if err != nil || len(joined) != domain.MaxPinnedDialogsMainFolder {
t.Fatalf("joined Communities before ordinary pin = %+v, %v", joined, err)
}
for i := 0; i < domain.MaxPinnedDialogsMainFolder-1; i++ {
if !joined[i].State.Pinned || !joined[i].State.Collapsed {
t.Fatalf("joined Community %d state before ordinary pin = %+v", i, joined[i].State)
}
}
ordinary := communityRPCChannel(t, channelService, owner, "Ordinary Pinned Channel")
ordinaryToggle := &tg.MessagesToggleDialogPinRequest{Peer: &tg.InputDialogPeer{Peer: &tg.InputPeerChannel{
ChannelID: ordinary.ID, AccessHash: ordinary.AccessHash,
}}}
ordinaryToggle.SetPinned(true)
if ok, err := r.onMessagesToggleDialogPin(WithUserID(ctx, owner.ID), ordinaryToggle); err != nil || !ok {
t.Fatalf("pin ordinary dialog at shared limit = %v, %v", ok, err)
}
pinned, err := r.pinnedDialogsList(ctx, owner.ID, domain.DialogMainFolderID)
if err != nil {
t.Fatal(err)
}
order := combinedPinnedDialogPeers(pinned)
if len(order) != domain.MaxPinnedDialogsMainFolder || order[0] != (domain.Peer{Type: domain.PeerTypeChannel, ID: ordinary.ID}) {
t.Fatalf("combined pinned order = %+v (dialogs=%+v communities=%+v count=%d), want ordinary dialog promoted above Communities", order, pinned.Dialogs, pinned.Communities, pinned.Count)
}
legacyPinned, err := r.pinnedDialogsList(WithLayer(ctx, 227), owner.ID, domain.DialogMainFolderID)
if err != nil {
t.Fatal(err)
}
if len(legacyPinned.Communities) != 0 || len(legacyPinned.Dialogs) != 1 || legacyPinned.Count != 1 {
t.Fatalf("Layer 227 pinned dialogs = %+v, want only the ordinary pinned dialog", legacyPinned)
}
legacyOrdinary := communityRPCChannel(t, channelService, owner, "Legacy Ordinary Pinned Channel")
legacyToggle := &tg.MessagesToggleDialogPinRequest{Peer: &tg.InputDialogPeer{Peer: &tg.InputPeerChannel{
ChannelID: legacyOrdinary.ID, AccessHash: legacyOrdinary.AccessHash,
}}}
legacyToggle.SetPinned(true)
if ok, err := r.onMessagesToggleDialogPin(WithLayer(WithUserID(ctx, owner.ID), 227), legacyToggle); err == nil || ok || !tgerr.Is(err, "PINNED_DIALOGS_TOO_MUCH") {
t.Fatalf("Layer 227 pin beyond shared account limit = %v, %v", ok, err)
}
overLimit := &tg.MessagesToggleDialogPinRequest{Peer: &tg.InputDialogPeerCommunity{Community: inputs[len(inputs)-1]}}
overLimit.SetPinned(true)
if ok, err := r.onMessagesToggleDialogPin(WithUserID(ctx, owner.ID), overLimit); err == nil || ok || !tgerr.Is(err, "PINNED_DIALOGS_TOO_MUCH") {
t.Fatalf("pin over shared limit = %v, %v", ok, err)
}
if _, err := r.onMessagesReorderPinnedDialogs(WithUserID(ctx, owner.ID), &tg.MessagesReorderPinnedDialogsRequest{
FolderID: domain.DialogArchiveFolderID,
Order: []tg.InputDialogPeerClass{&tg.InputDialogPeerCommunity{Community: inputs[0]}},
}); err == nil || !tgerr.Is(err, "FOLDER_ID_INVALID") {
t.Fatalf("archive Community reorder error = %v", err)
}
}
func TestCommunitiesRPCLayer228Lifecycle(t *testing.T) {
ctx := WithLayer(context.Background(), communitiesLayer)
userStore := memory.NewUserStore()
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 701, Phone: "15552000001", FirstName: "Owner"})
member, _ := userStore.Create(ctx, domain.User{AccessHash: 702, Phone: "15552000002", FirstName: "Member"})
channelStore := memory.NewChannelStore()
channelService := appchannels.NewService(channelStore)
initial := communityRPCChannel(t, channelService, owner, "Initial", member)
communityService := appcommunities.NewService(memory.NewCommunityStore(userStore, channelStore, nil, nil))
storyStore := memory.NewStoryStore()
if _, err := storyStore.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}, ID: 1,
Date: 1_800_100_001, ExpireDate: 1_900_100_001, Public: true,
}}); err != nil {
t.Fatalf("seed owner story: %v", err)
}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Channels: channelService,
Communities: communityService,
Stories: appstories.NewService(storyStore),
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1_800_100_100, 0)})
createdResult, err := r.onCommunitiesCreate(WithUserID(ctx, owner.ID), &tg.CommunitiesCreateRequest{
Hidden: true,
Title: "Official Community",
About: "Layer 228",
Peer: &tg.InputPeerChannel{ChannelID: initial.ID, AccessHash: initial.AccessHash},
})
if err != nil {
t.Fatalf("communities.create: %v", err)
}
created, ok := createdResult.(*tg.Updates)
if !ok || len(created.Chats) != 1 {
t.Fatalf("create result = %#v, want Updates with Community", createdResult)
}
community, ok := created.Chats[0].(*tg.Community)
if !ok || community.Title != "Official Community" || !community.Creator {
t.Fatalf("create chat = %#v", created.Chats[0])
}
inputCommunity := &tg.InputChannel{ChannelID: community.ID, AccessHash: community.AccessHash}
joinedResult, err := r.onCommunitiesGetJoined(WithUserID(ctx, member.ID))
if err != nil {
t.Fatalf("communities.getJoinedCommunities: %v", err)
}
joined := joinedResult.(*tg.MessagesChats)
if len(joined.Chats) != 1 || joined.Chats[0].(*tg.Community).ID != community.ID {
t.Fatalf("joined communities = %+v", joined.Chats)
}
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, member.ID), inputCommunity)
if err != nil {
t.Fatalf("channels.getFullChannel community: %v", err)
}
communityFull, ok := full.FullChat.(*tg.CommunityFull)
if !ok || communityFull.About != "Layer 228" || len(communityFull.LinkedPeers) != 1 || len(full.Chats) != 2 {
t.Fatalf("community full = %#v chats=%+v", full.FullChat, full.Chats)
}
collapsedResult, err := r.onCommunitiesToggleCollapsed(WithUserID(ctx, owner.ID), &tg.CommunitiesToggleCommunityCollapsedInDialogsRequest{
Collapsed: true,
Community: inputCommunity,
})
if err != nil {
t.Fatalf("toggle collapsed: %v", err)
}
collapsed := collapsedResult.(*tg.Updates)
if len(collapsed.Chats) == 0 || !collapsed.Chats[0].(*tg.Community).CollapsedInDialogs {
t.Fatalf("collapsed updates = %+v", collapsed.Chats)
}
legacyList, err := r.withCommunityDialogList(WithLayer(ctx, 227), owner.ID, domain.DialogFilter{}, domain.DialogList{Count: 7})
if err != nil || len(legacyList.Communities) != 0 || legacyList.Count != 7 {
t.Fatalf("Layer 227 community dialog projection = %+v err=%v, want unchanged list", legacyList, err)
}
list, err := r.withCommunityDialogList(ctx, owner.ID, domain.DialogFilter{}, domain.DialogList{})
if err != nil || len(list.Communities) != 1 || list.Count != 1 {
t.Fatalf("community dialog list = %+v err=%v", list, err)
}
dialogs := tgMessagesDialogs(owner.ID, list).(*tg.MessagesDialogs)
if len(dialogs.Dialogs) != 1 {
t.Fatalf("dialogs = %+v, want dialogCommunity", dialogs.Dialogs)
}
if dialog, ok := dialogs.Dialogs[0].(*tg.DialogCommunity); !ok || dialog.CommunityID != community.ID {
t.Fatalf("dialog = %#v, want community %d", dialogs.Dialogs[0], community.ID)
}
ownedOne := communityRPCChannel(t, channelService, member, "Owned One")
ownedTwo := communityRPCChannel(t, channelService, member, "Owned Two")
requestLink := func(channel domain.Channel) {
t.Helper()
ok, err := r.onCommunitiesTogglePeerLink(WithUserID(ctx, member.ID), &tg.CommunitiesTogglePeerLinkRequest{
Visible: true,
Community: inputCommunity,
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
})
if err == nil || ok || !tgerr.Is(err, "COMMUNITY_REQUEST_CREATED") {
t.Fatalf("request link %d = %v, %v", channel.ID, ok, err)
}
}
requestLink(ownedOne)
requestLink(ownedTwo)
requests, err := r.onCommunitiesGetPeerLinkRequests(WithUserID(ctx, owner.ID), &tg.CommunitiesGetPeerLinkRequestsRequest{
Community: inputCommunity,
Limit: 20,
})
if err != nil || requests.TotalCount != 2 || len(requests.Requests) != 2 {
t.Fatalf("peer link requests = %+v err=%v", requests, err)
}
// The Community owner is deliberately not a member of Owned One. Approval
// must use the request's validated ownership rather than ordinary channel
// membership access.
approved, err := r.onCommunitiesTogglePeerLinkRequestApproval(WithUserID(ctx, owner.ID), &tg.CommunitiesTogglePeerLinkRequestApprovalRequest{
Community: inputCommunity,
Peer: &tg.InputPeerChannel{ChannelID: ownedOne.ID, AccessHash: ownedOne.AccessHash},
})
if err != nil || !approved {
t.Fatalf("approve peer link = %v, %v", approved, err)
}
approved, err = r.onCommunitiesToggleAllPeerLinkRequestApproval(WithUserID(ctx, owner.ID), &tg.CommunitiesToggleAllPeerLinkRequestApprovalRequest{Community: inputCommunity})
if err != nil || !approved {
t.Fatalf("approve all peer links = %v, %v", approved, err)
}
joinedChats, err := r.onCommunitiesGetParticipantJoinedChats(WithUserID(ctx, owner.ID), &tg.CommunitiesGetParticipantJoinedChatsRequest{
Community: inputCommunity,
Participant: &tg.InputPeerUser{UserID: member.ID, AccessHash: member.AccessHash},
})
if err != nil || len(joinedChats.JoinedChatIDs) != 3 || len(joinedChats.CreatorChatIDs) != 2 {
t.Fatalf("participant joined chats = %+v err=%v", joinedChats, err)
}
participantsResult, err := r.onChannelsGetParticipants(WithUserID(ctx, owner.ID), &tg.ChannelsGetParticipantsRequest{
Channel: inputCommunity,
Filter: &tg.ChannelParticipantsSearch{Q: "memBER"},
Limit: 20,
})
if err != nil {
t.Fatalf("community participants search: %v", err)
}
participants := participantsResult.(*tg.ChannelsChannelParticipants)
if participants.Count != 1 || len(participants.Participants) != 1 {
t.Fatalf("community participants = %+v", participants)
}
adminsResult, err := r.onChannelsGetParticipants(WithUserID(ctx, member.ID), &tg.ChannelsGetParticipantsRequest{
Channel: inputCommunity,
Filter: &tg.ChannelParticipantsAdmins{},
Limit: 100,
})
if err != nil {
t.Fatalf("ordinary member Community admins: %v", err)
}
admins := adminsResult.(*tg.ChannelsChannelParticipants)
if admins.Count != 1 || len(admins.Participants) != 1 {
t.Fatalf("ordinary member Community admins = %+v, want creator", admins)
}
if _, err := r.onChannelsGetParticipants(WithUserID(ctx, member.ID), &tg.ChannelsGetParticipantsRequest{
Channel: inputCommunity,
Filter: &tg.ChannelParticipantsBanned{Q: ""},
Limit: 100,
}); err == nil || !tgerr.Is(err, "CHAT_ADMIN_REQUIRED") {
t.Fatalf("ordinary member Community banned list err = %v, want CHAT_ADMIN_REQUIRED", err)
}
recent, err := r.onStoriesGetPeerMaxIDs(WithUserID(ctx, owner.ID), []tg.InputPeerClass{
&tg.InputPeerSelf{},
&tg.InputPeerChannel{ChannelID: community.ID, AccessHash: community.AccessHash},
&tg.InputPeerSelf{},
})
if err != nil {
t.Fatalf("stories.getPeerMaxIDs with Community slot: %v", err)
}
if len(recent) != 3 {
t.Fatalf("stories.getPeerMaxIDs slots = %d, want 3", len(recent))
}
for _, index := range []int{0, 2} {
if maxID, ok := recent[index].GetMaxID(); !ok || maxID != 1 {
t.Fatalf("stories.getPeerMaxIDs[%d] max_id = %d ok=%v, want 1 true", index, maxID, ok)
}
}
if maxID, ok := recent[1].GetMaxID(); ok || maxID != 0 || recent[1].Live {
t.Fatalf("stories.getPeerMaxIDs Community slot = %+v, want empty recentStory", recent[1])
}
if _, err := r.onStoriesGetPeerMaxIDs(WithUserID(ctx, owner.ID), []tg.InputPeerClass{
&tg.InputPeerChannel{ChannelID: community.ID, AccessHash: community.AccessHash + 1},
}); err == nil || !tgerr.Is(err, "CHANNEL_PRIVATE") {
t.Fatalf("stories.getPeerMaxIDs Community wrong hash err = %v, want CHANNEL_PRIVATE", err)
}
banned, err := r.onCommunitiesToggleParticipantBanned(WithUserID(ctx, owner.ID), &tg.CommunitiesToggleParticipantBannedRequest{
Community: inputCommunity,
Participant: &tg.InputPeerUser{UserID: member.ID, AccessHash: member.AccessHash},
})
if err != nil || !banned {
t.Fatalf("toggle participant banned = %v, %v", banned, err)
}
joinedResult, err = r.onCommunitiesGetJoined(WithUserID(ctx, member.ID))
if err != nil || len(joinedResult.(*tg.MessagesChats).Chats) != 0 {
t.Fatalf("banned member joined communities = %#v err=%v", joinedResult, err)
}
}