Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
6799
internal/store/memory/channel.go
Normal file
6799
internal/store/memory/channel.go
Normal file
File diff suppressed because it is too large
Load diff
714
internal/store/memory/channel_test.go
Normal file
714
internal/store/memory/channel_test.go
Normal file
|
|
@ -0,0 +1,714 @@
|
|||
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 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
|
||||
}
|
||||
229
internal/store/memory/dialog_test.go
Normal file
229
internal/store/memory/dialog_test.go
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestDialogStoreFiltersAndPaginates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewDialogStore()
|
||||
userID := int64(100)
|
||||
list := domain.DialogList{
|
||||
Dialogs: []domain.Dialog{
|
||||
{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1}, TopMessage: 10, TopMessageDate: 1000, Pinned: true},
|
||||
{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 2}, TopMessage: 9, TopMessageDate: 900},
|
||||
{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 3}, TopMessage: 8, TopMessageDate: 800},
|
||||
},
|
||||
Messages: []domain.Message{
|
||||
{ID: 10, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1}, Body: "pinned"},
|
||||
{ID: 9, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 2}, Body: "first"},
|
||||
{ID: 8, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 3}, Body: "second"},
|
||||
},
|
||||
}
|
||||
if err := store.SaveList(ctx, userID, list); err != nil {
|
||||
t.Fatalf("SaveList: %v", err)
|
||||
}
|
||||
|
||||
first, err := store.ListByUser(ctx, userID, domain.DialogFilter{ExcludePinned: true, Limit: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByUser first page: %v", err)
|
||||
}
|
||||
if first.Count != 2 || len(first.Dialogs) != 1 || first.Dialogs[0].Peer.ID != 2 || len(first.Messages) != 1 || first.Messages[0].ID != 9 {
|
||||
t.Fatalf("first page = %+v, want peer 2 with count 2 and top message", first)
|
||||
}
|
||||
|
||||
next, err := store.ListByUser(ctx, userID, domain.DialogFilter{
|
||||
ExcludePinned: true,
|
||||
OffsetDate: first.Dialogs[0].TopMessageDate,
|
||||
OffsetID: first.Dialogs[0].TopMessage,
|
||||
HasOffsetPeer: true,
|
||||
OffsetPeer: first.Dialogs[0].Peer,
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByUser next page: %v", err)
|
||||
}
|
||||
if next.Count != 2 || len(next.Dialogs) != 1 || next.Dialogs[0].Peer.ID != 3 || len(next.Messages) != 1 || next.Messages[0].ID != 8 {
|
||||
t.Fatalf("next page = %+v, want peer 3 with count 2 and top message", next)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialogStoreFoldersAndCustomFilters(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewDialogStore()
|
||||
userID := int64(100)
|
||||
contactPeer := domain.Peer{Type: domain.PeerTypeUser, ID: 1}
|
||||
archivedPeer := domain.Peer{Type: domain.PeerTypeUser, ID: 2}
|
||||
strangerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: 3}
|
||||
if err := store.SaveList(ctx, userID, domain.DialogList{
|
||||
Dialogs: []domain.Dialog{
|
||||
{Peer: contactPeer, TopMessage: 10, TopMessageDate: 1000},
|
||||
{Peer: archivedPeer, TopMessage: 9, TopMessageDate: 900, FolderID: domain.DialogArchiveFolderID},
|
||||
{Peer: strangerPeer, TopMessage: 8, TopMessageDate: 800, UnreadCount: 1},
|
||||
},
|
||||
Users: []domain.User{
|
||||
{ID: contactPeer.ID, Contact: true},
|
||||
{ID: archivedPeer.ID, Contact: true},
|
||||
{ID: strangerPeer.ID},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveList: %v", err)
|
||||
}
|
||||
|
||||
main, err := store.ListByUser(ctx, userID, domain.DialogFilter{HasFolderID: true, FolderID: domain.DialogMainFolderID, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByUser main: %v", err)
|
||||
}
|
||||
if len(main.Dialogs) != 2 || main.Dialogs[0].Peer != contactPeer || main.Dialogs[1].Peer != strangerPeer {
|
||||
t.Fatalf("main dialogs = %+v, want non-archived dialogs", main.Dialogs)
|
||||
}
|
||||
archive, err := store.ListByUser(ctx, userID, domain.DialogFilter{HasFolderID: true, FolderID: domain.DialogArchiveFolderID, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByUser archive: %v", err)
|
||||
}
|
||||
if len(archive.Dialogs) != 1 || archive.Dialogs[0].Peer != archivedPeer {
|
||||
t.Fatalf("archive dialogs = %+v, want archived peer", archive.Dialogs)
|
||||
}
|
||||
|
||||
folder := domain.DialogFolder{
|
||||
ID: 2,
|
||||
Contacts: true,
|
||||
ExcludeArchived: true,
|
||||
IncludePeers: []domain.DialogFolderPeer{{Peer: strangerPeer}},
|
||||
}
|
||||
if err := store.UpsertFolder(ctx, userID, folder); err != nil {
|
||||
t.Fatalf("UpsertFolder: %v", err)
|
||||
}
|
||||
custom, err := store.ListByUser(ctx, userID, domain.DialogFilter{HasFolderID: true, FolderID: 2, Folder: &folder, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByUser custom: %v", err)
|
||||
}
|
||||
if len(custom.Dialogs) != 2 || custom.Dialogs[0].Peer != contactPeer || custom.Dialogs[1].Peer != strangerPeer {
|
||||
t.Fatalf("custom dialogs = %+v, want contact plus explicit stranger excluding archived", custom.Dialogs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserStoreStartsAtTimestampBase(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewUserStore()
|
||||
u, err := store.Create(ctx, domain.User{
|
||||
AccessHash: 1,
|
||||
Phone: "15550000001",
|
||||
FirstName: "Test",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
if u.ID != domain.UserIDSequenceBase {
|
||||
t.Fatalf("user id = %d, want base %d", u.ID, domain.UserIDSequenceBase)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialogStoreOffsetDateOnlyKeepsEnterpriseCountAndHash(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewDialogStore()
|
||||
userID := int64(100)
|
||||
if err := store.SaveList(ctx, userID, domain.DialogList{
|
||||
Dialogs: []domain.Dialog{
|
||||
{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1}, TopMessage: 10, TopMessageDate: 1000},
|
||||
{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 2}, TopMessage: 9, TopMessageDate: 900},
|
||||
{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 3}, TopMessage: 8, TopMessageDate: 800},
|
||||
},
|
||||
Messages: []domain.Message{
|
||||
{ID: 10, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1}, Body: "first"},
|
||||
{ID: 9, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 2}, Body: "second"},
|
||||
{ID: 8, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 3}, Body: "third"},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveList: %v", err)
|
||||
}
|
||||
|
||||
all, err := store.ListByUser(ctx, userID, domain.DialogFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByUser all: %v", err)
|
||||
}
|
||||
page, err := store.ListByUser(ctx, userID, domain.DialogFilter{OffsetDate: 900, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByUser offset date: %v", err)
|
||||
}
|
||||
if page.Count != 3 || page.Hash != all.Hash {
|
||||
t.Fatalf("page summary = count %d hash %d, want full count/hash %d/%d", page.Count, page.Hash, all.Count, all.Hash)
|
||||
}
|
||||
if len(page.Dialogs) != 1 || page.Dialogs[0].Peer.ID != 3 || len(page.Messages) != 1 || page.Messages[0].ID != 8 {
|
||||
t.Fatalf("page = %+v, want only dialog after offset date", page)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialogStoreEmptyPageKeepsEnterpriseCountAndHash(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewDialogStore()
|
||||
userID := int64(100)
|
||||
if err := store.SaveList(ctx, userID, domain.DialogList{
|
||||
Dialogs: []domain.Dialog{
|
||||
{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1}, TopMessage: 10, TopMessageDate: 1000},
|
||||
{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 2}, TopMessage: 9, TopMessageDate: 900},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveList: %v", err)
|
||||
}
|
||||
|
||||
all, err := store.ListByUser(ctx, userID, domain.DialogFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByUser all: %v", err)
|
||||
}
|
||||
empty, err := store.ListByUser(ctx, userID, domain.DialogFilter{
|
||||
OffsetDate: 900,
|
||||
OffsetID: 9,
|
||||
HasOffsetPeer: true,
|
||||
OffsetPeer: domain.Peer{Type: domain.PeerTypeUser, ID: 2},
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByUser empty page: %v", err)
|
||||
}
|
||||
if empty.Count != 2 || empty.Hash != all.Hash || len(empty.Dialogs) != 0 {
|
||||
t.Fatalf("empty page = %+v, want no page rows but full count/hash", empty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialogStoreListByPeersReturnsExistingAndPlaceholders(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewDialogStore()
|
||||
userID := int64(100)
|
||||
official := domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID}
|
||||
missing := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
|
||||
if err := store.SaveList(ctx, userID, domain.DialogList{
|
||||
Dialogs: []domain.Dialog{
|
||||
{Peer: official, TopMessage: 10, TopMessageDate: 1000, UnreadCount: 1},
|
||||
},
|
||||
Messages: []domain.Message{
|
||||
{ID: 10, Peer: official, From: official, Body: "login"},
|
||||
},
|
||||
Users: []domain.User{domain.OfficialSystemUser()},
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveList: %v", err)
|
||||
}
|
||||
|
||||
got, err := store.ListByPeers(ctx, userID, []domain.Peer{official, missing, official})
|
||||
if err != nil {
|
||||
t.Fatalf("ListByPeers: %v", err)
|
||||
}
|
||||
if got.Count != 2 || len(got.Dialogs) != 2 {
|
||||
t.Fatalf("dialogs = %+v, want existing official and missing placeholder", got)
|
||||
}
|
||||
if got.Dialogs[0].Peer != official || got.Dialogs[0].TopMessage != 10 {
|
||||
t.Fatalf("first dialog = %+v, want official top message", got.Dialogs[0])
|
||||
}
|
||||
if got.Dialogs[1].Peer != missing || got.Dialogs[1].TopMessage != 0 {
|
||||
t.Fatalf("second dialog = %+v, want missing placeholder", got.Dialogs[1])
|
||||
}
|
||||
if len(got.Messages) != 1 || got.Messages[0].ID != 10 {
|
||||
t.Fatalf("messages = %+v, want only official top message", got.Messages)
|
||||
}
|
||||
if len(got.Users) != 1 || got.Users[0].ID != domain.OfficialSystemUserID {
|
||||
t.Fatalf("users = %+v, want official user", got.Users)
|
||||
}
|
||||
}
|
||||
2883
internal/store/memory/memory.go
Normal file
2883
internal/store/memory/memory.go
Normal file
File diff suppressed because it is too large
Load diff
531
internal/store/memory/message_test.go
Normal file
531
internal/store/memory/message_test.go
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestMessageStoreSendPrivateTextCreatesBothOwnerBoxes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dialogs := NewDialogStore()
|
||||
messages := NewMessageStore(dialogs)
|
||||
req := domain.SendPrivateTextRequest{
|
||||
SenderUserID: 1000000001,
|
||||
RecipientUserID: 1000000002,
|
||||
RandomID: 99,
|
||||
Message: "hello",
|
||||
Date: 1700000100,
|
||||
}
|
||||
|
||||
got, err := messages.SendPrivateText(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText: %v", err)
|
||||
}
|
||||
if got.SenderMessage.ID != 1 || got.SenderMessage.OwnerUserID != req.SenderUserID || !got.SenderMessage.Out || got.SenderMessage.Pts != 1 {
|
||||
t.Fatalf("sender message = %+v, want first outgoing box with pts=1", got.SenderMessage)
|
||||
}
|
||||
if got.RecipientMessage.ID != 1 || got.RecipientMessage.OwnerUserID != req.RecipientUserID || got.RecipientMessage.Out || got.RecipientMessage.Pts != 1 {
|
||||
t.Fatalf("recipient message = %+v, want first incoming box with pts=1", got.RecipientMessage)
|
||||
}
|
||||
if got.SenderMessage.UID == 0 || got.SenderMessage.UID != got.RecipientMessage.UID {
|
||||
t.Fatalf("uid = sender %d recipient %d, want shared private message uid", got.SenderMessage.UID, got.RecipientMessage.UID)
|
||||
}
|
||||
|
||||
second, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: req.SenderUserID,
|
||||
RecipientUserID: req.RecipientUserID,
|
||||
RandomID: 100,
|
||||
Message: "again",
|
||||
Date: 1700000110,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText second: %v", err)
|
||||
}
|
||||
if second.SenderMessage.ID != 2 || second.SenderMessage.Pts != 2 || second.RecipientMessage.ID != 2 || second.RecipientMessage.Pts != 2 {
|
||||
t.Fatalf("second send = %+v/%+v, want per-owner box_id and pts to advance", second.SenderMessage, second.RecipientMessage)
|
||||
}
|
||||
|
||||
dup, err := messages.SendPrivateText(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText duplicate: %v", err)
|
||||
}
|
||||
if !dup.Duplicate || dup.SenderMessage.ID != got.SenderMessage.ID || dup.RecipientMessage.ID != got.RecipientMessage.ID {
|
||||
t.Fatalf("duplicate = %+v, want original message boxes", dup)
|
||||
}
|
||||
|
||||
senderHistory, err := messages.ListByUser(ctx, req.SenderUserID, domain.MessageFilter{HasPeer: true, Peer: got.SenderMessage.Peer, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("sender history: %v", err)
|
||||
}
|
||||
recipientHistory, err := messages.ListByUser(ctx, req.RecipientUserID, domain.MessageFilter{HasPeer: true, Peer: got.RecipientMessage.Peer, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("recipient history: %v", err)
|
||||
}
|
||||
if len(senderHistory.Messages) != 2 || len(recipientHistory.Messages) != 2 {
|
||||
t.Fatalf("history sizes = sender %d recipient %d, want both owner partitions populated", len(senderHistory.Messages), len(recipientHistory.Messages))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStorePrivateMessageReactionsAreSharedAcrossOwnerBoxes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
messages := NewMessageStore()
|
||||
aliceID := int64(1000000001)
|
||||
bobID := int64(1000000002)
|
||||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: aliceID,
|
||||
RecipientUserID: bobID,
|
||||
RandomID: 101,
|
||||
Message: "react to me",
|
||||
Date: 1700000100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText: %v", err)
|
||||
}
|
||||
reaction := domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "\U0001f44d"}
|
||||
res, err := messages.SetMessageReactions(ctx, domain.SetPrivateMessageReactionsRequest{
|
||||
UserID: bobID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: aliceID},
|
||||
MessageID: sent.RecipientMessage.ID,
|
||||
Reactions: []domain.MessageReaction{
|
||||
reaction,
|
||||
},
|
||||
Big: true,
|
||||
Date: 1700000200,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SetMessageReactions: %v", err)
|
||||
}
|
||||
if len(res.Messages) != 2 {
|
||||
t.Fatalf("reaction result messages = %d, want both owner boxes", len(res.Messages))
|
||||
}
|
||||
|
||||
aliceReactions, err := messages.GetMessageReactions(ctx, domain.PrivateMessageReactionsRequest{
|
||||
OwnerUserID: aliceID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||||
IDs: []int{sent.SenderMessage.ID},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("alice GetMessageReactions: %v", err)
|
||||
}
|
||||
if len(aliceReactions.Messages) != 1 || aliceReactions.Messages[0].Reactions == nil {
|
||||
t.Fatalf("alice reactions = %+v, want one enriched message", aliceReactions)
|
||||
}
|
||||
if got := aliceReactions.Messages[0].Reactions.Results; len(got) != 1 || got[0].Reaction != reaction || got[0].Count != 1 || got[0].ChosenOrder != 0 {
|
||||
t.Fatalf("alice reaction counts = %+v, want one peer reaction without chosen order", got)
|
||||
}
|
||||
if got := aliceReactions.Messages[0].Reactions.Recent; len(got) != 1 || got[0].UserID != bobID || !got[0].Big || got[0].My {
|
||||
t.Fatalf("alice recent reactions = %+v, want bob non-my big reaction", got)
|
||||
}
|
||||
|
||||
bobReactions, err := messages.GetMessageReactions(ctx, domain.PrivateMessageReactionsRequest{
|
||||
OwnerUserID: bobID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: aliceID},
|
||||
IDs: []int{sent.RecipientMessage.ID},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("bob GetMessageReactions: %v", err)
|
||||
}
|
||||
if got := bobReactions.Messages[0].Reactions.Results; len(got) != 1 || got[0].ChosenOrder != 1 {
|
||||
t.Fatalf("bob reaction counts = %+v, want own chosen order", got)
|
||||
}
|
||||
if got := bobReactions.Messages[0].Reactions.Recent; len(got) != 1 || !got[0].My {
|
||||
t.Fatalf("bob recent reactions = %+v, want my reaction", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreSendPrivateTextReplyAndForwardMetadata(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
messages := NewMessageStore()
|
||||
aliceID := int64(1000000001)
|
||||
bobID := int64(1000000002)
|
||||
first, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: aliceID,
|
||||
RecipientUserID: bobID,
|
||||
RandomID: 501,
|
||||
Message: "first",
|
||||
Date: 1700000100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("seed SendPrivateText: %v", err)
|
||||
}
|
||||
reply, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: aliceID,
|
||||
RecipientUserID: bobID,
|
||||
RandomID: 502,
|
||||
Message: "reply",
|
||||
Silent: true,
|
||||
NoForwards: true,
|
||||
ReplyTo: &domain.MessageReply{
|
||||
MessageID: first.SenderMessage.ID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||||
QuoteText: "fir",
|
||||
QuoteOffset: 0,
|
||||
},
|
||||
Date: 1700000110,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("reply SendPrivateText: %v", err)
|
||||
}
|
||||
if reply.SenderMessage.ReplyTo == nil || reply.SenderMessage.ReplyTo.MessageID != first.SenderMessage.ID {
|
||||
t.Fatalf("sender reply = %+v, want sender-side message id", reply.SenderMessage.ReplyTo)
|
||||
}
|
||||
if reply.RecipientMessage.ReplyTo == nil || reply.RecipientMessage.ReplyTo.MessageID != first.RecipientMessage.ID {
|
||||
t.Fatalf("recipient reply = %+v, want translated recipient-side message id", reply.RecipientMessage.ReplyTo)
|
||||
}
|
||||
if !reply.SenderMessage.Silent || !reply.SenderMessage.NoForwards {
|
||||
t.Fatalf("reply flags = silent %v noforwards %v, want true/true", reply.SenderMessage.Silent, reply.SenderMessage.NoForwards)
|
||||
}
|
||||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: aliceID,
|
||||
RecipientUserID: bobID,
|
||||
RandomID: 504,
|
||||
Message: "bad quote offset",
|
||||
ReplyTo: &domain.MessageReply{
|
||||
MessageID: first.SenderMessage.ID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||||
QuoteText: "fir",
|
||||
QuoteOffset: domain.MaxMessageReplyQuoteOffset + 1,
|
||||
},
|
||||
Date: 1700000115,
|
||||
}); !errors.Is(err, domain.ErrReplyMessageIDInvalid) {
|
||||
t.Fatalf("bad quote offset err = %v, want ErrReplyMessageIDInvalid", err)
|
||||
}
|
||||
|
||||
forwarded, err := messages.ForwardPrivateMessages(ctx, domain.ForwardPrivateMessagesRequest{
|
||||
OwnerUserID: aliceID,
|
||||
FromPeer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||||
ToUserID: bobID,
|
||||
MessageIDs: []int{first.SenderMessage.ID},
|
||||
RandomIDs: []int64{503},
|
||||
ReplyTo: &domain.MessageReply{
|
||||
MessageID: first.SenderMessage.ID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||||
},
|
||||
Date: 1700000120,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ForwardPrivateMessages: %v", err)
|
||||
}
|
||||
if len(forwarded.SenderMessages) != 1 || forwarded.SenderMessages[0].Forward == nil || forwarded.SenderMessages[0].Forward.From.ID != aliceID {
|
||||
t.Fatalf("forwarded messages = %+v, want original author header", forwarded.SenderMessages)
|
||||
}
|
||||
if forwarded.SenderMessages[0].ReplyTo == nil || forwarded.SenderMessages[0].ReplyTo.MessageID != first.SenderMessage.ID {
|
||||
t.Fatalf("forward reply = %+v, want target dialog reply header", forwarded.SenderMessages[0].ReplyTo)
|
||||
}
|
||||
if _, err := messages.ForwardPrivateMessages(ctx, domain.ForwardPrivateMessagesRequest{
|
||||
OwnerUserID: aliceID,
|
||||
FromPeer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||||
ToUserID: aliceID,
|
||||
MessageIDs: []int{reply.SenderMessage.ID},
|
||||
RandomIDs: []int64{504},
|
||||
Date: 1700000130,
|
||||
}); err != domain.ErrChatForwardsRestricted {
|
||||
t.Fatalf("forward protected err=%v, want ErrChatForwardsRestricted", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreListByUserSupportsForwardAndAroundHistoryOffsets(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
messages := NewMessageStore()
|
||||
aliceID := int64(1000000001)
|
||||
bobID := int64(1000000002)
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: aliceID}
|
||||
|
||||
for i := 1; i <= 6; i++ {
|
||||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: aliceID,
|
||||
RecipientUserID: bobID,
|
||||
RandomID: int64(600 + i),
|
||||
Message: "history",
|
||||
Date: 1700000000 + i,
|
||||
}); err != nil {
|
||||
t.Fatalf("seed message %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
around, err := messages.ListByUser(ctx, bobID, domain.MessageFilter{
|
||||
HasPeer: true,
|
||||
Peer: peer,
|
||||
OffsetID: 3,
|
||||
AddOffset: -3,
|
||||
Limit: 6,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("around history: %v", err)
|
||||
}
|
||||
if got := messageIDs(around.Messages); !sameInts(got, []int{6, 5, 4, 3, 2, 1}) {
|
||||
t.Fatalf("around ids = %v, want unread/newer side plus older context", got)
|
||||
}
|
||||
|
||||
forward, err := messages.ListByUser(ctx, bobID, domain.MessageFilter{
|
||||
HasPeer: true,
|
||||
Peer: peer,
|
||||
OffsetID: 3,
|
||||
AddOffset: -3,
|
||||
Limit: 3,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("forward history: %v", err)
|
||||
}
|
||||
if got := messageIDs(forward.Messages); !sameInts(got, []int{6, 5, 4}) {
|
||||
t.Fatalf("forward ids = %v, want messages newer than offset", got)
|
||||
}
|
||||
|
||||
hugePositive, err := messages.ListByUser(ctx, bobID, domain.MessageFilter{
|
||||
HasPeer: true,
|
||||
Peer: peer,
|
||||
AddOffset: 1 << 30,
|
||||
Limit: 3,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("huge positive add_offset history: %v", err)
|
||||
}
|
||||
if len(hugePositive.Messages) != 0 {
|
||||
t.Fatalf("huge positive add_offset ids = %v, want bounded empty page", messageIDs(hugePositive.Messages))
|
||||
}
|
||||
|
||||
hugeNegative, err := messages.ListByUser(ctx, bobID, domain.MessageFilter{
|
||||
HasPeer: true,
|
||||
Peer: peer,
|
||||
OffsetID: 3,
|
||||
AddOffset: -1 << 30,
|
||||
Limit: 3,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("huge negative add_offset history: %v", err)
|
||||
}
|
||||
if got := messageIDs(hugeNegative.Messages); !sameInts(got, []int{6, 5, 4}) {
|
||||
t.Fatalf("huge negative add_offset ids = %v, want clamped forward page", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreReadHistoryEmitsInboxAndOutboxReceipts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dialogs := NewDialogStore()
|
||||
messages := NewMessageStore(dialogs)
|
||||
senderID := int64(1000000001)
|
||||
recipientID := int64(1000000002)
|
||||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: senderID,
|
||||
RecipientUserID: recipientID,
|
||||
RandomID: 101,
|
||||
Message: "hello",
|
||||
Date: 1700000100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText: %v", err)
|
||||
}
|
||||
|
||||
read, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||||
OwnerUserID: recipientID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: senderID},
|
||||
Date: 1700000200,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ReadHistory: %v", err)
|
||||
}
|
||||
if !read.Changed || read.InboxEvent.Type != domain.UpdateEventReadHistoryInbox || read.InboxEvent.Pts != 2 || read.InboxEvent.MaxID != sent.RecipientMessage.ID {
|
||||
t.Fatalf("inbox read = %+v, want recipient inbox pts=2 max recipient id", read)
|
||||
}
|
||||
if !read.OutboxChanged || read.OutboxUserID != senderID || read.OutboxEvent.Type != domain.UpdateEventReadHistoryOutbox || read.OutboxEvent.MaxID != sent.SenderMessage.ID {
|
||||
t.Fatalf("outbox read = %+v, want sender outbox receipt with sender message id", read)
|
||||
}
|
||||
date, err := messages.GetOutboxReadDate(ctx, domain.OutboxReadDateRequest{
|
||||
OwnerUserID: senderID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: recipientID},
|
||||
ID: sent.SenderMessage.ID,
|
||||
})
|
||||
if err != nil || date != 1700000200 {
|
||||
t.Fatalf("outbox read date = %d err=%v, want read date", date, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreReadMessageContentsReturnsExistingOwnerIDs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
messages := NewMessageStore()
|
||||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: 1001,
|
||||
RecipientUserID: 1002,
|
||||
RandomID: 88,
|
||||
Message: "voice placeholder",
|
||||
Date: 1700000300,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText: %v", err)
|
||||
}
|
||||
got, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
|
||||
OwnerUserID: 1002,
|
||||
IDs: []int{sent.RecipientMessage.ID, domain.MaxMessageBoxID},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ReadMessageContents: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got.MessageIDs, []int{sent.RecipientMessage.ID}) {
|
||||
t.Fatalf("MessageIDs = %v, want existing recipient id", got.MessageIDs)
|
||||
}
|
||||
if _, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
|
||||
OwnerUserID: 1002,
|
||||
IDs: []int{0},
|
||||
}); !errors.Is(err, domain.ErrMessageIDInvalid) {
|
||||
t.Fatalf("invalid id error = %v, want ErrMessageIDInvalid", err)
|
||||
}
|
||||
}
|
||||
|
||||
func messageIDs(messages []domain.Message) []int {
|
||||
out := make([]int, 0, len(messages))
|
||||
for _, msg := range messages {
|
||||
out = append(out, msg.ID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sameInts(got, want []int) bool {
|
||||
if len(got) != len(want) {
|
||||
return false
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != want[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestMessageStoreEditMessageUpdatesBothBoxes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dialogs := NewDialogStore()
|
||||
messages := NewMessageStore(dialogs)
|
||||
senderID := int64(1000000001)
|
||||
recipientID := int64(1000000002)
|
||||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: senderID,
|
||||
RecipientUserID: recipientID,
|
||||
RandomID: 102,
|
||||
Message: "before",
|
||||
Date: 1700000100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText: %v", err)
|
||||
}
|
||||
|
||||
edited, err := messages.EditMessage(ctx, domain.EditMessageRequest{
|
||||
OwnerUserID: senderID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: recipientID},
|
||||
ID: sent.SenderMessage.ID,
|
||||
Message: "after",
|
||||
EditDate: 1700000200,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EditMessage: %v", err)
|
||||
}
|
||||
if len(edited.Edited) != 2 || edited.Self().Message.Body != "after" || edited.Self().Event.Type != domain.UpdateEventEditMessage {
|
||||
t.Fatalf("edited = %+v, want both owner boxes and self edit event", edited)
|
||||
}
|
||||
recipientHistory, err := messages.ListByUser(ctx, recipientID, domain.MessageFilter{HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: senderID}, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("recipient history: %v", err)
|
||||
}
|
||||
if len(recipientHistory.Messages) != 1 || recipientHistory.Messages[0].Body != "after" || recipientHistory.Messages[0].EditDate != 1700000200 {
|
||||
t.Fatalf("recipient history = %+v, want edited body/date", recipientHistory.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreDeleteHistoryDeletesOrPreservesDialogAndRebuilds(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dialogs := NewDialogStore()
|
||||
messages := NewMessageStore(dialogs)
|
||||
senderID := int64(1000000001)
|
||||
recipientID := int64(1000000002)
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: recipientID}
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: senderID,
|
||||
RecipientUserID: recipientID,
|
||||
RandomID: int64(100 + i),
|
||||
Message: "hello",
|
||||
Date: 1700000200 + i,
|
||||
}); err != nil {
|
||||
t.Fatalf("seed send %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
deleted, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||||
OwnerUserID: senderID,
|
||||
Peer: peer,
|
||||
Date: 1700000300,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteHistory: %v", err)
|
||||
}
|
||||
if self := deleted.Self(); self.Event.Pts != 4 || self.Event.PtsCount != 2 || len(self.MessageIDs) != 2 {
|
||||
t.Fatalf("delete result = %+v, want sender delete range pts=4 count=2", self)
|
||||
}
|
||||
senderHistory, err := messages.ListByUser(ctx, senderID, domain.MessageFilter{HasPeer: true, Peer: peer, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("sender history: %v", err)
|
||||
}
|
||||
recipientHistory, err := messages.ListByUser(ctx, recipientID, domain.MessageFilter{HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: senderID}, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("recipient history: %v", err)
|
||||
}
|
||||
if len(senderHistory.Messages) != 0 || len(recipientHistory.Messages) != 2 {
|
||||
t.Fatalf("history sizes sender=%d recipient=%d, want sender cleared only", len(senderHistory.Messages), len(recipientHistory.Messages))
|
||||
}
|
||||
senderDialogs, err := dialogs.ListByUser(ctx, senderID, domain.DialogFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("sender dialogs after delete: %v", err)
|
||||
}
|
||||
if len(senderDialogs.Dialogs) != 0 {
|
||||
t.Fatalf("sender dialogs = %+v, want dialog deleted after full history delete", senderDialogs.Dialogs)
|
||||
}
|
||||
|
||||
rebuilt, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: senderID,
|
||||
RecipientUserID: recipientID,
|
||||
RandomID: 200,
|
||||
Message: "rebuilt",
|
||||
Date: 1700000400,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send after delete: %v", err)
|
||||
}
|
||||
senderDialogs, err = dialogs.ListByUser(ctx, senderID, domain.DialogFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("sender dialogs after rebuild: %v", err)
|
||||
}
|
||||
if len(senderDialogs.Dialogs) != 1 || senderDialogs.Dialogs[0].Peer != peer || senderDialogs.Dialogs[0].TopMessage != rebuilt.SenderMessage.ID {
|
||||
t.Fatalf("rebuilt dialogs = %+v, want one dialog with new top message %d", senderDialogs.Dialogs, rebuilt.SenderMessage.ID)
|
||||
}
|
||||
|
||||
preservedOwner := int64(1000000003)
|
||||
preservedPeerID := int64(1000000004)
|
||||
preservedPeer := domain.Peer{Type: domain.PeerTypeUser, ID: preservedPeerID}
|
||||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: preservedOwner,
|
||||
RecipientUserID: preservedPeerID,
|
||||
RandomID: 300,
|
||||
Message: "clear but keep dialog",
|
||||
Date: 1700000500,
|
||||
}); err != nil {
|
||||
t.Fatalf("seed preserved send: %v", err)
|
||||
}
|
||||
if _, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||||
OwnerUserID: preservedOwner,
|
||||
Peer: preservedPeer,
|
||||
JustClear: true,
|
||||
Date: 1700000600,
|
||||
}); err != nil {
|
||||
t.Fatalf("DeleteHistory just_clear: %v", err)
|
||||
}
|
||||
preservedDialogs, err := dialogs.ListByUser(ctx, preservedOwner, domain.DialogFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("preserved dialogs: %v", err)
|
||||
}
|
||||
if len(preservedDialogs.Dialogs) != 1 || preservedDialogs.Dialogs[0].Peer != preservedPeer || preservedDialogs.Dialogs[0].TopMessage != 0 || len(preservedDialogs.Messages) != 0 {
|
||||
t.Fatalf("preserved dialogs = %+v messages=%+v, want empty dialog kept after just_clear", preservedDialogs.Dialogs, preservedDialogs.Messages)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue