owpengram-server/internal/store/memory/channel_test.go
A 6fd690a06e fix: keep participant changes out of channel pts
(cherry picked from commit 07b2497664bd108dec84f6cfe43715540faf2688)
2026-06-09 15:00:25 +08:00

777 lines
25 KiB
Go

package memory
import (
"context"
"errors"
"reflect"
"testing"
"telesrv/internal/domain"
)
func TestChannelRealtimeRecipientsAreCapped(t *testing.T) {
store := NewChannelStore()
memberIDs := make([]int64, domain.MaxChannelRealtimeFanout+25)
for i := range memberIDs {
memberIDs[i] = int64(10_000 + i)
}
created, err := store.CreateChannel(context.Background(), domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "large realtime cap",
Megagroup: true,
MemberUserIDs: memberIDs,
Date: 1_700_000_100,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
if got := len(created.Recipients); got != domain.MaxChannelRealtimeFanout {
t.Fatalf("create recipients = %d, want capped %d", got, domain.MaxChannelRealtimeFanout)
}
recipients, err := store.ListActiveChannelMemberIDs(context.Background(), 1, created.Channel.ID, 0)
if err != nil {
t.Fatalf("list active members: %v", err)
}
if got := len(recipients); got != domain.MaxChannelRealtimeFanout {
t.Fatalf("listed active members = %d, want capped %d", got, domain.MaxChannelRealtimeFanout)
}
}
func TestChannelAdminAndBanDoNotAdvanceChannelPts(t *testing.T) {
ctx := context.Background()
store := NewChannelStore()
created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "participant state no pts",
Megagroup: true,
MemberUserIDs: []int64{2},
Date: 1_700_000_120,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
channelID := created.Channel.ID
ptsFloor := created.Channel.Pts
promoted, err := store.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
UserID: 1,
ChannelID: channelID,
MemberID: 2,
AdminRights: domain.ChannelAdminRights{
InviteUsers: true,
},
Date: 1_700_000_121,
})
if err != nil {
t.Fatalf("edit admin: %v", err)
}
if promoted.Event.Pts != 0 || promoted.Event.PtsCount != 0 || promoted.Channel.Pts != ptsFloor {
t.Fatalf("edit admin pts = event(%d,%d) channel %d, want unchanged %d", promoted.Event.Pts, promoted.Event.PtsCount, promoted.Channel.Pts, ptsFloor)
}
banned, err := store.EditChannelBanned(ctx, domain.EditChannelBannedRequest{
UserID: 1,
ChannelID: channelID,
Participant: domain.Peer{Type: domain.PeerTypeUser, ID: 2},
BannedRights: domain.ChannelBannedRights{
ViewMessages: true,
UntilDate: 1_700_001_121,
},
Date: 1_700_000_122,
})
if err != nil {
t.Fatalf("edit banned: %v", err)
}
if banned.Event.Pts != 0 || banned.Event.PtsCount != 0 || banned.Channel.Pts != ptsFloor {
t.Fatalf("edit banned pts = event(%d,%d) channel %d, want unchanged %d", banned.Event.Pts, banned.Event.PtsCount, banned.Channel.Pts, ptsFloor)
}
diff, err := store.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
UserID: 1,
ChannelID: channelID,
Pts: ptsFloor,
Limit: 10,
})
if err != nil {
t.Fatalf("list difference: %v", err)
}
if len(diff.Events) != 0 || diff.Pts != ptsFloor {
t.Fatalf("difference after participant state change = %+v, want no durable events at pts %d", diff, ptsFloor)
}
}
func TestPendingJoinRequestsSummaryAndInviteAdmins(t *testing.T) {
ctx := context.Background()
store := NewChannelStore()
created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "pending join requests",
Megagroup: true,
MemberUserIDs: []int64{2, 3, 4},
Date: 1_700_000_150,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
channelID := created.Channel.ID
if _, err := store.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
UserID: 1,
ChannelID: channelID,
MemberID: 2,
AdminRights: domain.ChannelAdminRights{
InviteUsers: true,
},
Date: 1_700_000_151,
}); err != nil {
t.Fatalf("promote invite admin: %v", err)
}
if _, err := store.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
UserID: 1,
ChannelID: channelID,
MemberID: 4,
AdminRights: domain.ChannelAdminRights{
ChangeInfo: true,
},
Date: 1_700_000_152,
}); err != nil {
t.Fatalf("promote change-info admin: %v", err)
}
invite, err := store.ExportInvite(ctx, domain.ExportChannelInviteRequest{
UserID: 1,
ChannelID: channelID,
Title: "approval",
RequestNeeded: true,
Date: 1_700_000_153,
})
if err != nil {
t.Fatalf("export invite: %v", err)
}
for i := 0; i < domain.MaxChannelPendingJoinRecentRequesters+2; i++ {
_, err := store.ImportInvite(ctx, domain.ImportChannelInviteRequest{
UserID: int64(10 + i),
Hash: invite.Invite.Hash,
Date: 1_700_000_160 + i,
})
if !errors.Is(err, domain.ErrInviteRequestSent) {
t.Fatalf("import pending %d err = %v, want ErrInviteRequestSent", i, err)
}
}
pending, err := store.PendingJoinRequests(ctx, channelID, 99)
if err != nil {
t.Fatalf("pending join requests: %v", err)
}
if pending.Count != domain.MaxChannelPendingJoinRecentRequesters+2 || len(pending.RecentRequesters) != domain.MaxChannelPendingJoinRecentRequesters {
t.Fatalf("pending summary = %+v, want bounded recent with full count", pending)
}
if pending.RecentRequesters[0] != 16 || pending.RecentRequesters[len(pending.RecentRequesters)-1] != 12 {
t.Fatalf("recent requesters = %+v, want newest first", pending.RecentRequesters)
}
admins, err := store.ListChannelInviteAdminMemberIDs(ctx, channelID, 0)
if err != nil {
t.Fatalf("invite admins: %v", err)
}
want := []int64{1, 2, 4}
if !reflect.DeepEqual(admins, want) {
t.Fatalf("invite admins = %+v, want %+v", admins, want)
}
}
func TestCommonChannelsOnlySharedMegagroups(t *testing.T) {
ctx := context.Background()
store := NewChannelStore()
first, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "common one",
Megagroup: true,
MemberUserIDs: []int64{2},
Date: 1_700_000_170,
})
if err != nil {
t.Fatalf("create first common channel: %v", err)
}
second, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "common two",
Megagroup: true,
MemberUserIDs: []int64{2},
Date: 1_700_000_171,
})
if err != nil {
t.Fatalf("create second common channel: %v", err)
}
if _, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "broadcast excluded",
Broadcast: true,
MemberUserIDs: []int64{2},
Date: 1_700_000_172,
}); err != nil {
t.Fatalf("create broadcast channel: %v", err)
}
left, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "left excluded",
Megagroup: true,
MemberUserIDs: []int64{2},
Date: 1_700_000_173,
})
if err != nil {
t.Fatalf("create left channel: %v", err)
}
if _, err := store.LeaveChannel(ctx, left.Channel.ID, 2, 1_700_000_174); err != nil {
t.Fatalf("leave channel: %v", err)
}
if _, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "not shared",
Megagroup: true,
MemberUserIDs: []int64{3},
Date: 1_700_000_175,
}); err != nil {
t.Fatalf("create non-shared channel: %v", err)
}
page, err := store.ListCommonChannels(ctx, domain.CommonChannelsRequest{
UserID: 1,
TargetUserID: 2,
Limit: 10,
})
if err != nil {
t.Fatalf("list common channels: %v", err)
}
if page.Count != 2 || len(page.Channels) != 2 || page.Channels[0].ID != first.Channel.ID || page.Channels[1].ID != second.Channel.ID {
t.Fatalf("common channels = %+v, want two shared megagroups in id order", page)
}
next, err := store.ListCommonChannels(ctx, domain.CommonChannelsRequest{
UserID: 1,
TargetUserID: 2,
MaxID: first.Channel.ID,
Limit: 1,
})
if err != nil {
t.Fatalf("list common channels after max id: %v", err)
}
if next.Count != 2 || len(next.Channels) != 1 || next.Channels[0].ID != second.Channel.ID {
t.Fatalf("paged common channels = %+v, want second channel with full count", next)
}
countOnly, err := store.ListCommonChannels(ctx, domain.CommonChannelsRequest{
UserID: 1,
TargetUserID: 2,
CountOnly: true,
})
if err != nil {
t.Fatalf("count common channels: %v", err)
}
if countOnly.Count != 2 || len(countOnly.Channels) != 0 {
t.Fatalf("count-only common channels = %+v, want count without channels", countOnly)
}
}
func TestLeftChannelsReturnsPagedLeftMemberships(t *testing.T) {
ctx := context.Background()
store := NewChannelStore()
older, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "older left",
Megagroup: true,
MemberUserIDs: []int64{2},
Date: 1_700_000_180,
})
if err != nil {
t.Fatalf("create older channel: %v", err)
}
newer, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "newer left broadcast",
Broadcast: true,
MemberUserIDs: []int64{2},
Date: 1_700_000_181,
})
if err != nil {
t.Fatalf("create newer channel: %v", err)
}
if _, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "active excluded",
Megagroup: true,
MemberUserIDs: []int64{2},
Date: 1_700_000_182,
}); err != nil {
t.Fatalf("create active channel: %v", err)
}
if _, err := store.LeaveChannel(ctx, older.Channel.ID, 2, 1_700_000_183); err != nil {
t.Fatalf("leave older channel: %v", err)
}
if _, err := store.LeaveChannel(ctx, newer.Channel.ID, 2, 1_700_000_184); err != nil {
t.Fatalf("leave newer channel: %v", err)
}
page, err := store.ListLeftChannels(ctx, 2, 0, 1)
if err != nil {
t.Fatalf("list left channels: %v", err)
}
if page.Count != 2 || len(page.Channels) != 1 || page.Channels[0].Channel.ID != newer.Channel.ID || page.Channels[0].Self.Status != domain.ChannelMemberLeft {
t.Fatalf("first left page = %+v, want newest left channel and full count", page)
}
next, err := store.ListLeftChannels(ctx, 2, 1, 1)
if err != nil {
t.Fatalf("list next left channels: %v", err)
}
if next.Count != 2 || len(next.Channels) != 1 || next.Channels[0].Channel.ID != older.Channel.ID {
t.Fatalf("second left page = %+v, want older left channel", next)
}
empty, err := store.ListLeftChannels(ctx, 2, 2, 1)
if err != nil {
t.Fatalf("list empty left page: %v", err)
}
if empty.Count != 2 || len(empty.Channels) != 0 {
t.Fatalf("empty left page = %+v, want full count and no chats", empty)
}
if _, err := store.ListLeftChannels(ctx, 2, domain.MaxLeftChannelsOffset+1, 1); !errors.Is(err, domain.ErrChannelInvalid) {
t.Fatalf("huge offset err = %v, want ErrChannelInvalid", err)
}
}
func TestDiscussionGroupLinksAreBidirectionalAndReplaceOldLinks(t *testing.T) {
ctx := context.Background()
store := NewChannelStore()
broadcast, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "broadcast",
Broadcast: true,
Date: 1_700_000_190,
})
if err != nil {
t.Fatalf("create broadcast: %v", err)
}
firstGroup, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "first group",
Megagroup: true,
Date: 1_700_000_191,
})
if err != nil {
t.Fatalf("create first group: %v", err)
}
secondGroup, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "second group",
Megagroup: true,
Date: 1_700_000_192,
})
if err != nil {
t.Fatalf("create second group: %v", err)
}
if _, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "broadcast excluded",
Broadcast: true,
Date: 1_700_000_193,
}); err != nil {
t.Fatalf("create excluded broadcast: %v", err)
}
candidates, err := store.ListDiscussionGroups(ctx, 1, 10)
if err != nil {
t.Fatalf("list discussion groups: %v", err)
}
if len(candidates) != 2 || candidates[0].ID != secondGroup.Channel.ID || candidates[1].ID != firstGroup.Channel.ID {
t.Fatalf("discussion candidates = %+v, want creator megagroups newest id first", candidates)
}
linked, err := store.SetDiscussionGroup(ctx, 1, broadcast.Channel.ID, firstGroup.Channel.ID)
if err != nil {
t.Fatalf("link first group: %v", err)
}
if len(linked.Channels) != 2 {
t.Fatalf("linked changed channels = %+v, want broadcast and group", linked.Channels)
}
gotBroadcast, err := store.GetChannelByID(ctx, broadcast.Channel.ID)
if err != nil {
t.Fatalf("get linked broadcast: %v", err)
}
gotFirst, err := store.GetChannelByID(ctx, firstGroup.Channel.ID)
if err != nil {
t.Fatalf("get linked first group: %v", err)
}
if gotBroadcast.LinkedChatID != firstGroup.Channel.ID || gotFirst.LinkedChatID != broadcast.Channel.ID {
t.Fatalf("first link = broadcast %+v group %+v, want bidirectional ids", gotBroadcast, gotFirst)
}
replaced, err := store.SetDiscussionGroup(ctx, 1, broadcast.Channel.ID, secondGroup.Channel.ID)
if err != nil {
t.Fatalf("replace discussion group: %v", err)
}
if len(replaced.Channels) != 3 {
t.Fatalf("replace changed channels = %+v, want broadcast, old group, new group", replaced.Channels)
}
gotBroadcast, _ = store.GetChannelByID(ctx, broadcast.Channel.ID)
gotFirst, _ = store.GetChannelByID(ctx, firstGroup.Channel.ID)
gotSecond, err := store.GetChannelByID(ctx, secondGroup.Channel.ID)
if err != nil {
t.Fatalf("get linked second group: %v", err)
}
if gotBroadcast.LinkedChatID != secondGroup.Channel.ID || gotSecond.LinkedChatID != broadcast.Channel.ID || gotFirst.LinkedChatID != 0 {
t.Fatalf("replace link = broadcast %d first %d second %d, want old cleared and new bidirectional",
gotBroadcast.LinkedChatID, gotFirst.LinkedChatID, gotSecond.LinkedChatID)
}
unlinked, err := store.SetDiscussionGroup(ctx, 1, 0, secondGroup.Channel.ID)
if err != nil {
t.Fatalf("unlink from group side: %v", err)
}
if len(unlinked.Channels) != 2 {
t.Fatalf("unlink changed channels = %+v, want broadcast and group", unlinked.Channels)
}
gotBroadcast, _ = store.GetChannelByID(ctx, broadcast.Channel.ID)
gotSecond, _ = store.GetChannelByID(ctx, secondGroup.Channel.ID)
if gotBroadcast.LinkedChatID != 0 || gotSecond.LinkedChatID != 0 {
t.Fatalf("unlink = broadcast %d second %d, want both cleared", gotBroadcast.LinkedChatID, gotSecond.LinkedChatID)
}
if _, err := store.SetDiscussionGroup(ctx, 1, 0, secondGroup.Channel.ID); !errors.Is(err, domain.ErrLinkNotModified) {
t.Fatalf("repeat unlink err = %v, want ErrLinkNotModified", err)
}
if _, err := store.SetPreHistoryHidden(ctx, 1, firstGroup.Channel.ID, true); err != nil {
t.Fatalf("hide first group prehistory: %v", err)
}
if _, err := store.SetDiscussionGroup(ctx, 1, broadcast.Channel.ID, firstGroup.Channel.ID); !errors.Is(err, domain.ErrMegagroupPrehistoryHidden) {
t.Fatalf("hidden group link err = %v, want ErrMegagroupPrehistoryHidden", err)
}
}
func TestChannelDeleteHistoryCapsHugeMaxID(t *testing.T) {
ctx := context.Background()
store := NewChannelStore()
created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "bounded delete history",
Megagroup: true,
Date: 1_700_000_200,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
totalMessages := domain.MaxDeleteHistoryBatch + 2
for i := 0; i < totalMessages; i++ {
if _, err := store.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: 1,
ChannelID: created.Channel.ID,
RandomID: int64(10_000 + i),
Message: "bulk",
Date: 1_700_000_201 + i,
}); err != nil {
t.Fatalf("send channel message %d: %v", i, err)
}
}
first, err := store.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
UserID: 1,
ChannelID: created.Channel.ID,
MaxID: int(^uint(0) >> 1),
ForEveryone: true,
Date: 1_700_001_300,
})
if err != nil {
t.Fatalf("delete first batch: %v", err)
}
if first.Offset != 1 || len(first.DeletedIDs) != domain.MaxDeleteHistoryBatch || first.Event.PtsCount != domain.MaxDeleteHistoryBatch {
t.Fatalf("first batch = %+v, want capped page with offset", first)
}
second, err := store.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
UserID: 1,
ChannelID: created.Channel.ID,
MaxID: int(^uint(0) >> 1),
ForEveryone: true,
Date: 1_700_001_301,
})
if err != nil {
t.Fatalf("delete second batch: %v", err)
}
if second.Offset != 0 || len(second.DeletedIDs) != 3 || second.Event.PtsCount != 3 {
t.Fatalf("second batch = %+v, want final bounded page", second)
}
}
func TestChannelDeleteHistoryLocalClearReturnsMonotonicAvailableMinID(t *testing.T) {
ctx := context.Background()
store := NewChannelStore()
created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "monotonic local clear",
Megagroup: true,
Date: 1_700_000_250,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
first, err := store.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: 1,
ChannelID: created.Channel.ID,
RandomID: 30_001,
Message: "first visible",
Date: 1_700_000_251,
})
if err != nil {
t.Fatalf("send first message: %v", err)
}
second, err := store.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: 1,
ChannelID: created.Channel.ID,
RandomID: 30_002,
Message: "second visible",
Date: 1_700_000_252,
})
if err != nil {
t.Fatalf("send second message: %v", err)
}
high, err := store.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
UserID: 1,
ChannelID: created.Channel.ID,
MaxID: second.Message.ID,
Date: 1_700_000_253,
})
if err != nil {
t.Fatalf("clear high watermark: %v", err)
}
if high.AvailableMinID != second.Message.ID {
t.Fatalf("high available_min_id = %d, want %d", high.AvailableMinID, second.Message.ID)
}
stale, err := store.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
UserID: 1,
ChannelID: created.Channel.ID,
MaxID: first.Message.ID,
Date: 1_700_000_254,
})
if err != nil {
t.Fatalf("clear stale low watermark: %v", err)
}
if stale.AvailableMinID != second.Message.ID {
t.Fatalf("stale available_min_id = %d, want monotonic %d", stale.AvailableMinID, second.Message.ID)
}
history, err := store.ListChannelHistory(ctx, 1, domain.ChannelHistoryFilter{ChannelID: created.Channel.ID, Limit: 10})
if err != nil {
t.Fatalf("list history: %v", err)
}
if len(history.Messages) != 0 {
t.Fatalf("history after stale clear = %+v, want no visible messages", history.Messages)
}
dialogs, err := store.GetChannelDialogs(ctx, 1, []int64{created.Channel.ID})
if err != nil {
t.Fatalf("get channel dialog: %v", err)
}
if len(dialogs.Dialogs) != 1 {
t.Fatalf("dialogs = %+v, want one dialog", dialogs.Dialogs)
}
if dialogs.Dialogs[0].TopMessage != 0 || dialogs.Dialogs[0].ReadInboxMaxID != second.Message.ID || dialogs.Dialogs[0].UnreadCount != 0 {
t.Fatalf("dialog after stale clear = %+v, want top=0 read=%d unread=0", dialogs.Dialogs[0], second.Message.ID)
}
}
func TestChannelListDialogsDerivesRecipientTopWithoutWriteFanout(t *testing.T) {
ctx := context.Background()
store := NewChannelStore()
created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "single copy dialog top",
Megagroup: true,
MemberUserIDs: []int64{2},
Date: 1_700_000_300,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
if _, err := store.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
UserID: 2,
ChannelID: created.Channel.ID,
MaxID: created.Message.ID,
Date: 1_700_000_301,
}); err != nil {
t.Fatalf("read initial service message: %v", err)
}
sent, err := store.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: 1,
ChannelID: created.Channel.ID,
RandomID: 88,
Message: "visible without write fanout",
Date: 1_700_000_302,
})
if err != nil {
t.Fatalf("send channel message: %v", err)
}
list, err := store.ListChannelDialogs(ctx, 2, domain.DialogFilter{Limit: 10})
if err != nil {
t.Fatalf("list recipient channel dialogs: %v", err)
}
if len(list.Dialogs) != 1 {
t.Fatalf("dialogs = %+v, want one channel dialog", list.Dialogs)
}
dialog := list.Dialogs[0]
if dialog.TopMessage != sent.Message.ID || dialog.TopMessageDate != sent.Message.Date || dialog.UnreadCount != 1 {
t.Fatalf("recipient dialog = %+v, want top sent message and unread=1", dialog)
}
if len(list.Messages) != 1 || list.Messages[0].ID != sent.Message.ID {
t.Fatalf("dialog messages = %+v, want sent top message", list.Messages)
}
}
func TestChannelUnreadExcludesOwnOutgoing(t *testing.T) {
ctx := context.Background()
store := NewChannelStore()
created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "own outgoing unread",
Megagroup: true,
Date: 1_700_000_360,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
sent, err := store.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: 1,
ChannelID: created.Channel.ID,
RandomID: 36_001,
Message: "own outgoing only",
Date: 1_700_000_361,
})
if err != nil {
t.Fatalf("send channel message: %v", err)
}
store.mu.Lock()
member := store.members[created.Channel.ID][1]
member.ReadInboxMaxID = sent.Message.ID - 1
store.members[created.Channel.ID][1] = member
dialog := store.dialogs[1][created.Channel.ID]
dialog.ReadInboxMaxID = sent.Message.ID - 1
dialog.UnreadCount = 99
store.dialogs[1][created.Channel.ID] = dialog
store.mu.Unlock()
dialogs, err := store.GetChannelDialogs(ctx, 1, []int64{created.Channel.ID})
if err != nil {
t.Fatalf("get channel dialogs: %v", err)
}
if len(dialogs.Dialogs) != 1 || dialogs.Dialogs[0].UnreadCount != 0 {
t.Fatalf("dialogs = %+v, want own outgoing excluded from unread", dialogs.Dialogs)
}
read, err := store.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
UserID: 1,
ChannelID: created.Channel.ID,
MaxID: sent.Message.ID,
Date: 1_700_000_362,
})
if err != nil {
t.Fatalf("read channel history: %v", err)
}
if read.StillUnreadCount != 0 || read.Dialog.UnreadCount != 0 {
t.Fatalf("read result = %+v, want no own-outgoing unread", read)
}
}
func TestChannelReadMessageContentsClearsVisibleUnreadReactions(t *testing.T) {
ctx := context.Background()
store := NewChannelStore()
created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: 1,
Title: "visible unread reaction",
Megagroup: true,
MemberUserIDs: []int64{2},
Date: 1_700_000_400,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
sent, err := store.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: 1,
ChannelID: created.Channel.ID,
RandomID: 40_001,
Message: "react to this",
Date: 1_700_000_401,
})
if err != nil {
t.Fatalf("send channel message: %v", err)
}
if _, err := store.SetChannelMessageReactions(ctx, domain.SetChannelMessageReactionsRequest{
UserID: 2,
ChannelID: created.Channel.ID,
MessageID: sent.Message.ID,
Reactions: []domain.MessageReaction{{
Type: domain.MessageReactionEmoji,
Emoticon: "\U0001f525",
}},
Date: 1_700_000_402,
}); err != nil {
t.Fatalf("set channel reaction: %v", err)
}
dialogs, err := store.GetChannelDialogs(ctx, 1, []int64{created.Channel.ID})
if err != nil {
t.Fatalf("get owner channel dialogs: %v", err)
}
if len(dialogs.Dialogs) != 1 || dialogs.Dialogs[0].UnreadReactions != 1 {
t.Fatalf("owner dialogs = %+v, want one unread reaction", dialogs.Dialogs)
}
unread, err := store.ListChannelUnreadReactions(ctx, 1, domain.ChannelUnreadReactionsFilter{
ChannelID: created.Channel.ID,
Limit: 10,
})
if err != nil {
t.Fatalf("list unread reactions: %v", err)
}
if len(unread.Messages) != 1 || unread.Messages[0].ID != sent.Message.ID {
t.Fatalf("unread reactions = %+v, want sent message", unread.Messages)
}
if unread.Messages[0].Reactions == nil || !hasUnreadChannelReaction(*unread.Messages[0].Reactions) {
t.Fatalf("unread message reactions = %+v, want unread recent reaction", unread.Messages[0].Reactions)
}
read, err := store.ReadChannelMessageContents(ctx, domain.ReadChannelMessageContentsRequest{
UserID: 1,
ChannelID: created.Channel.ID,
IDs: []int{sent.Message.ID},
})
if err != nil {
t.Fatalf("read channel message contents: %v", err)
}
if !reflect.DeepEqual(read.ClearedUnreadReactionMessageIDs, []int{sent.Message.ID}) {
t.Fatalf("cleared reaction ids = %+v, want [%d]", read.ClearedUnreadReactionMessageIDs, sent.Message.ID)
}
if len(read.Messages) != 1 || read.Messages[0].Reactions == nil || hasUnreadChannelReaction(*read.Messages[0].Reactions) {
t.Fatalf("read messages = %+v, want reaction returned as read", read.Messages)
}
unreadAfter, err := store.ListChannelUnreadReactions(ctx, 1, domain.ChannelUnreadReactionsFilter{
ChannelID: created.Channel.ID,
Limit: 10,
})
if err != nil {
t.Fatalf("list unread reactions after read contents: %v", err)
}
if len(unreadAfter.Messages) != 0 {
t.Fatalf("unread reactions after read contents = %+v, want empty", unreadAfter.Messages)
}
dialogsAfter, err := store.GetChannelDialogs(ctx, 1, []int64{created.Channel.ID})
if err != nil {
t.Fatalf("get dialogs after read contents: %v", err)
}
if len(dialogsAfter.Dialogs) != 1 || dialogsAfter.Dialogs[0].UnreadReactions != 0 {
t.Fatalf("dialogs after read contents = %+v, want unread reactions 0", dialogsAfter.Dialogs)
}
}
func hasUnreadChannelReaction(reactions domain.ChannelMessageReactions) bool {
for _, recent := range reactions.Recent {
if recent.Unread {
return true
}
}
return false
}