Sync telesrv 36eda30 (feat(communities): implement Layer 228 community aggregates). Skipped telesrv docs changes per public sync rules; normalized the public appearance seed label.
285 lines
12 KiB
Go
285 lines
12 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
func mustCommunityTestUser(t *testing.T, store *UserStore, firstName, phone string) domain.User {
|
|
t.Helper()
|
|
user, err := store.Create(context.Background(), domain.User{AccessHash: int64(len(phone) + len(firstName)), Phone: phone, FirstName: firstName})
|
|
if err != nil {
|
|
t.Fatalf("create user %q: %v", firstName, err)
|
|
}
|
|
return user
|
|
}
|
|
|
|
func mustCommunityTestChannel(t *testing.T, store *ChannelStore, 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 := store.CreateChannel(context.Background(), domain.CreateChannelRequest{
|
|
CreatorUserID: creator.ID,
|
|
Title: title,
|
|
Megagroup: true,
|
|
MemberUserIDs: memberIDs,
|
|
Date: 1_800_000_000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel %q: %v", title, err)
|
|
}
|
|
return created.Channel
|
|
}
|
|
|
|
func TestCommunityLifecycleRequestsSearchAndModeration(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := NewUserStore()
|
|
owner := mustCommunityTestUser(t, users, "Community Owner", "15551000001")
|
|
member := mustCommunityTestUser(t, users, "Alice Searchable", "15551000002")
|
|
channels := NewChannelStore()
|
|
initial := mustCommunityTestChannel(t, channels, owner, "Initial", member)
|
|
store := NewCommunityStore(users, channels, nil, nil)
|
|
|
|
created, err := store.CreateCommunity(ctx, domain.CreateCommunityRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Engineering",
|
|
InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: initial.ID},
|
|
Visibility: domain.CommunityPeerHidden,
|
|
Date: 1_800_000_001,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create community: %v", err)
|
|
}
|
|
if len(created.Links) != 1 || created.Links[0].Visibility != domain.CommunityPeerHidden {
|
|
t.Fatalf("initial links = %+v, want one hidden link", created.Links)
|
|
}
|
|
if len(created.ServiceMessages) != 1 || created.ServiceMessages[0].Message.Action == nil ||
|
|
created.ServiceMessages[0].Message.Action.Type != domain.ChannelActionChangeCommunity ||
|
|
created.ServiceMessages[0].Message.Action.CommunityID != created.Community.ID || created.ServiceMessages[0].Event.Pts == 0 {
|
|
t.Fatalf("create service messages = %+v, want durable change-community action", created.ServiceMessages)
|
|
}
|
|
initialView, err := channels.GetChannel(ctx, owner.ID, initial.ID)
|
|
if err != nil || initialView.Channel.LinkedCommunityID != created.Community.ID {
|
|
t.Fatalf("initial linked community = %d, err=%v, want %d", initialView.Channel.LinkedCommunityID, err, created.Community.ID)
|
|
}
|
|
|
|
_, err = store.CreateCommunity(ctx, domain.CreateCommunityRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Duplicate",
|
|
InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: initial.ID},
|
|
Visibility: domain.CommunityPeerVisible,
|
|
Date: 1_800_000_002,
|
|
})
|
|
if !errors.Is(err, domain.ErrCommunityPeerLinked) {
|
|
t.Fatalf("reuse linked peer error = %v, want ErrCommunityPeerLinked", err)
|
|
}
|
|
|
|
owned := mustCommunityTestChannel(t, channels, member, "Member Owned")
|
|
requested, err := store.ToggleCommunityPeerLink(ctx, domain.CommunityTogglePeerLinkRequest{
|
|
ActorUserID: member.ID,
|
|
CommunityID: created.Community.ID,
|
|
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: owned.ID},
|
|
Visibility: domain.CommunityPeerVisible,
|
|
Date: 1_800_000_003,
|
|
})
|
|
if err != nil || !requested.RequestCreated {
|
|
t.Fatalf("link request = %+v, err=%v, want pending request", requested, err)
|
|
}
|
|
page, err := store.ListCommunityPeerLinkRequests(ctx, owner.ID, created.Community.ID, "", 20)
|
|
if err != nil || page.TotalCount != 1 || len(page.Requests) != 1 || page.Requests[0].RequestedBy != member.ID {
|
|
t.Fatalf("request page = %+v, err=%v", page, err)
|
|
}
|
|
approved, err := store.DecideCommunityPeerLinkRequest(ctx, owner.ID, created.Community.ID, requested.Peer, false, 1_800_000_004)
|
|
if err != nil || approved.Link == nil || approved.RequestedBy != member.ID || approved.ServiceMessage == nil {
|
|
t.Fatalf("approved request = %+v, err=%v", approved, err)
|
|
}
|
|
ownerView, err := store.GetCommunity(ctx, owner.ID, created.Community.ID)
|
|
if err != nil {
|
|
t.Fatalf("owner community view after approval: %v", err)
|
|
}
|
|
for _, link := range ownerView.Links {
|
|
if link.Peer == approved.Peer && link.CanViewHistory {
|
|
t.Fatalf("community admin without private-channel membership advertised can_view_history")
|
|
}
|
|
}
|
|
privateVisible := mustCommunityTestChannel(t, channels, owner, "Private Visible")
|
|
linked, err := store.ToggleCommunityPeerLink(ctx, domain.CommunityTogglePeerLinkRequest{
|
|
ActorUserID: owner.ID,
|
|
CommunityID: created.Community.ID,
|
|
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: privateVisible.ID},
|
|
Visibility: domain.CommunityPeerVisible,
|
|
Date: 1_800_000_004,
|
|
})
|
|
if err != nil || linked.Link == nil {
|
|
t.Fatalf("link private visible channel = %+v, err=%v", linked, err)
|
|
}
|
|
memberView, err := store.GetCommunity(ctx, member.ID, created.Community.ID)
|
|
if err != nil {
|
|
t.Fatalf("member community view: %v", err)
|
|
}
|
|
foundPrivate := false
|
|
for _, link := range memberView.Links {
|
|
if link.Peer.ID == privateVisible.ID {
|
|
foundPrivate = true
|
|
if link.CanViewHistory {
|
|
t.Fatalf("private visible channel advertised can_view_history to non-member")
|
|
}
|
|
}
|
|
}
|
|
if !foundPrivate {
|
|
t.Fatalf("visible private channel missing from member community view")
|
|
}
|
|
|
|
_, err = store.ToggleCommunityPeerLink(ctx, domain.CommunityTogglePeerLinkRequest{
|
|
ActorUserID: owner.ID,
|
|
CommunityID: created.Community.ID,
|
|
Peer: approved.Peer,
|
|
Visibility: domain.CommunityPeerHidden,
|
|
Date: 1_800_000_005,
|
|
})
|
|
if !errors.Is(err, domain.ErrCommunityPeerLinked) {
|
|
t.Fatalf("change visibility in place error = %v, want unlink/relink requirement", err)
|
|
}
|
|
|
|
participants, err := store.ListCommunityParticipants(ctx, owner.ID, created.Community.ID, domain.ChannelParticipantsFilter{
|
|
Kind: domain.ChannelParticipantsSearch,
|
|
Query: "searchABLE",
|
|
}, 0, 20)
|
|
if err != nil || participants.Count != 1 || len(participants.Participants) != 1 || participants.Participants[0].UserID != member.ID {
|
|
t.Fatalf("participant name search = %+v, err=%v, want member", participants, err)
|
|
}
|
|
admins, err := store.ListCommunityParticipants(ctx, member.ID, created.Community.ID, domain.ChannelParticipantsFilter{
|
|
Kind: domain.ChannelParticipantsAdmins,
|
|
}, 0, 100)
|
|
if err != nil || admins.Count != 1 || len(admins.Participants) != 1 || admins.Participants[0].UserID != owner.ID {
|
|
t.Fatalf("member-visible Community admins = %+v, err=%v, want creator", admins, err)
|
|
}
|
|
if _, err := store.ListCommunityParticipants(ctx, member.ID, created.Community.ID, domain.ChannelParticipantsFilter{
|
|
Kind: domain.ChannelParticipantsKicked,
|
|
}, 0, 100); !errors.Is(err, domain.ErrCommunityAdminRequired) {
|
|
t.Fatalf("member Community kicked list error = %v, want admin required", err)
|
|
}
|
|
outsider := mustCommunityTestUser(t, users, "Community Outsider", "15551000003")
|
|
if _, err := store.ListCommunityParticipants(ctx, outsider.ID, created.Community.ID, domain.ChannelParticipantsFilter{
|
|
Kind: domain.ChannelParticipantsAdmins,
|
|
}, 0, 100); !errors.Is(err, domain.ErrCommunityPrivate) {
|
|
t.Fatalf("outsider Community admins error = %v, want private", err)
|
|
}
|
|
|
|
ban, err := store.ToggleCommunityParticipantBanned(ctx, owner.ID, created.Community.ID, member.ID, false, 1_800_000_006)
|
|
if err != nil {
|
|
t.Fatalf("ban community participant: %v", err)
|
|
}
|
|
if !ban.Changed || len(ban.ChannelBans) != 1 || len(ban.RemovedLinks) != 1 || ban.RemovedLinks[0].Peer.ID != owned.ID {
|
|
t.Fatalf("ban result = %+v, want one channel ban and owned-link removal", ban)
|
|
}
|
|
if action := ban.RemovedLinks[0].ServiceMessage.Message.Action; action == nil || action.Type != domain.ChannelActionChangeCommunity || action.CommunityID != 0 {
|
|
t.Fatalf("unlink service action = %+v, want change-community(0)", action)
|
|
}
|
|
if got := ban.RemovedLinks[0].ServiceMessage.Channel.LinkedCommunityID; got != 0 {
|
|
t.Fatalf("unlink service channel linked community = %d, want 0", got)
|
|
}
|
|
kicked, err := channels.GetParticipant(ctx, owner.ID, initial.ID, member.ID)
|
|
if err != nil || kicked.Status != domain.ChannelMemberKicked || !kicked.BannedRights.ViewMessages {
|
|
t.Fatalf("linked channel participant = %+v, err=%v, want kicked", kicked, err)
|
|
}
|
|
ownedView, err := channels.GetChannel(ctx, member.ID, owned.ID)
|
|
if err != nil || ownedView.Channel.LinkedCommunityID != 0 {
|
|
t.Fatalf("owned channel linked community = %d, err=%v, want 0", ownedView.Channel.LinkedCommunityID, err)
|
|
}
|
|
if _, err := store.GetCommunity(ctx, member.ID, created.Community.ID); !errors.Is(err, domain.ErrCommunityPrivate) {
|
|
t.Fatalf("banned member get community error = %v, want private", err)
|
|
}
|
|
repeated, err := store.ToggleCommunityParticipantBanned(ctx, owner.ID, created.Community.ID, member.ID, false, 1_800_000_007)
|
|
if err != nil || repeated.Changed || len(repeated.ChannelBans) != 0 || len(repeated.RemovedLinks) != 0 {
|
|
t.Fatalf("repeated ban = %+v err=%v, want idempotent no-op", repeated, err)
|
|
}
|
|
}
|
|
|
|
func TestCommunityCollapsedPinAndMixedOrder(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := NewUserStore()
|
|
owner := mustCommunityTestUser(t, users, "Owner", "15551000011")
|
|
channels := NewChannelStore()
|
|
store := NewCommunityStore(users, channels, nil, nil)
|
|
|
|
makeCommunity := func(title string) domain.CommunityView {
|
|
channel := mustCommunityTestChannel(t, channels, owner, title+" Channel")
|
|
view, err := store.CreateCommunity(ctx, domain.CreateCommunityRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: title,
|
|
InitialPeer: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID},
|
|
Visibility: domain.CommunityPeerVisible,
|
|
Date: 1_800_000_100,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create %s: %v", title, err)
|
|
}
|
|
if _, changed, err := store.SetCommunityCollapsed(ctx, owner.ID, view.Community.ID, true); err != nil || !changed {
|
|
t.Fatalf("collapse %s: changed=%v err=%v", title, changed, err)
|
|
}
|
|
if changed, err := store.SetCommunityPinned(ctx, owner.ID, view.Community.ID, true); err != nil || !changed {
|
|
t.Fatalf("pin %s: changed=%v err=%v", title, changed, err)
|
|
}
|
|
return view
|
|
}
|
|
one := makeCommunity("One")
|
|
two := makeCommunity("Two")
|
|
changed, err := store.ReorderCommunityPinned(ctx, owner.ID, []domain.Peer{
|
|
{Type: domain.PeerTypeChannel, ID: 77},
|
|
{Type: domain.PeerTypeCommunity, ID: one.Community.ID},
|
|
{Type: domain.PeerTypeUser, ID: 88},
|
|
{Type: domain.PeerTypeCommunity, ID: two.Community.ID},
|
|
}, true)
|
|
if err != nil || !changed {
|
|
t.Fatalf("mixed pinned reorder: changed=%v err=%v", changed, err)
|
|
}
|
|
oneView, _ := store.GetCommunity(ctx, owner.ID, one.Community.ID)
|
|
twoView, _ := store.GetCommunity(ctx, owner.ID, two.Community.ID)
|
|
if !oneView.State.Pinned || !twoView.State.Pinned || oneView.State.PinnedOrder <= twoView.State.PinnedOrder {
|
|
t.Fatalf("pinned orders one=%+v two=%+v, want global mixed order preserved", oneView.State, twoView.State)
|
|
}
|
|
uncollapsed, changed, err := store.SetCommunityCollapsed(ctx, owner.ID, one.Community.ID, false)
|
|
if err != nil || !changed || uncollapsed.State.Pinned {
|
|
t.Fatalf("uncollapse state = %+v changed=%v err=%v, want pin cleared", uncollapsed.State, changed, err)
|
|
}
|
|
}
|
|
|
|
func TestCommunityCanUseOwnedBotAsInitialPeer(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := NewUserStore()
|
|
owner := mustCommunityTestUser(t, users, "Bot Owner", "15551000021")
|
|
bots := NewBotStore(users)
|
|
bot, _, err := bots.CreateBotAccount(ctx, domain.User{
|
|
AccessHash: 91, FirstName: "Community Bot", Username: "community_bot",
|
|
}, domain.BotProfile{OwnerUserID: owner.ID})
|
|
if err != nil {
|
|
t.Fatalf("create bot: %v", err)
|
|
}
|
|
store := NewCommunityStore(users, NewChannelStore(), bots, nil)
|
|
created, err := store.CreateCommunity(ctx, domain.CreateCommunityRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Bot Community",
|
|
InitialPeer: domain.Peer{Type: domain.PeerTypeUser, ID: bot.ID},
|
|
Visibility: domain.CommunityPeerVisible,
|
|
Date: 1_800_000_200,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create bot community: %v", err)
|
|
}
|
|
if len(created.Links) != 1 || created.Links[0].Peer.ID != bot.ID || !created.Links[0].CanViewHistory {
|
|
t.Fatalf("bot community links = %+v", created.Links)
|
|
}
|
|
if len(created.ServiceMessages) != 0 {
|
|
t.Fatalf("bot link service messages = %+v, want none", created.ServiceMessages)
|
|
}
|
|
updatedBot, ok, err := users.ByID(ctx, bot.ID)
|
|
if err != nil || !ok || updatedBot.LinkedCommunityID != created.Community.ID {
|
|
t.Fatalf("bot linked community = %d ok=%v err=%v", updatedBot.LinkedCommunityID, ok, err)
|
|
}
|
|
}
|