777 lines
25 KiB
Go
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
|
|
}
|