owpengram-server/internal/store/memory/community_test.go
A da6a57e1a3 feat: sync community aggregates
Sync telesrv 36eda30 (feat(communities): implement Layer 228 community aggregates).

Skipped telesrv docs changes per public sync rules; normalized the public appearance seed label.
2026-07-20 16:46:02 +08:00

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