3198 lines
103 KiB
Go
3198 lines
103 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
func TestChannelStoreSendMessageFansOutDialogRows(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 31,
|
|
Phone: "+1777" + suffix + "01",
|
|
FirstName: "ChannelOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{
|
|
AccessHash: 32,
|
|
Phone: "+1777" + suffix + "02",
|
|
FirstName: "ChannelFriend",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Dialog Top " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{friend.ID},
|
|
Date: 1700000300,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 901,
|
|
Message: "first visible channel text",
|
|
Date: 1700000301,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send channel message: %v", err)
|
|
}
|
|
|
|
var friendTop, friendReadInbox, friendUnread int
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT top_message_id, read_inbox_max_id, unread_count
|
|
FROM channel_dialogs
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, friend.ID).Scan(&friendTop, &friendReadInbox, &friendUnread); err != nil {
|
|
t.Fatalf("read friend dialog row after send: %v", err)
|
|
}
|
|
if friendTop != sent.Message.ID || friendReadInbox != 0 || friendUnread != 2 {
|
|
t.Fatalf("friend dialog row top=%d read=%d unread=%d, want top %d read 0 unread 2", friendTop, friendReadInbox, friendUnread, sent.Message.ID)
|
|
}
|
|
|
|
var ownerTop, ownerReadInbox, ownerReadOutbox, ownerUnread int
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT top_message_id, read_inbox_max_id, read_outbox_max_id, unread_count
|
|
FROM channel_dialogs
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, owner.ID).Scan(&ownerTop, &ownerReadInbox, &ownerReadOutbox, &ownerUnread); err != nil {
|
|
t.Fatalf("read owner dialog row after send: %v", err)
|
|
}
|
|
if ownerTop != sent.Message.ID || ownerReadInbox != sent.Message.ID || ownerReadOutbox != sent.Message.ID || ownerUnread != 0 {
|
|
t.Fatalf("owner dialog row top=%d read_in=%d read_out=%d unread=%d, want sent/read/unread 0 for %d", ownerTop, ownerReadInbox, ownerReadOutbox, ownerUnread, sent.Message.ID)
|
|
}
|
|
|
|
var ownerMemberReadInbox, ownerMemberReadOutbox int
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT read_inbox_max_id, read_outbox_max_id
|
|
FROM channel_members
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, owner.ID).Scan(&ownerMemberReadInbox, &ownerMemberReadOutbox); err != nil {
|
|
t.Fatalf("read owner member row after send: %v", err)
|
|
}
|
|
if ownerMemberReadInbox != sent.Message.ID || ownerMemberReadOutbox != 0 {
|
|
t.Fatalf("owner member read_in=%d read_out=%d, want read_in %d and read_out unchanged before peer read", ownerMemberReadInbox, ownerMemberReadOutbox, sent.Message.ID)
|
|
}
|
|
|
|
dialogs, err := channels.ListChannelDialogs(ctx, friend.ID, domain.DialogFilter{Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("list friend dialogs: %v", err)
|
|
}
|
|
if len(dialogs.Dialogs) != 1 || dialogs.Dialogs[0].TopMessage != sent.Message.ID {
|
|
t.Fatalf("friend dialogs = %+v, want top message %d", dialogs.Dialogs, sent.Message.ID)
|
|
}
|
|
if len(dialogs.Messages) != 1 || dialogs.Messages[0].Body != "first visible channel text" {
|
|
t.Fatalf("friend dialog messages = %+v, want latest channel text", dialogs.Messages)
|
|
}
|
|
if dialogs.Dialogs[0].UnreadCount != 2 {
|
|
t.Fatalf("friend unread = %d, want create service + latest text", dialogs.Dialogs[0].UnreadCount)
|
|
}
|
|
|
|
read, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
|
UserID: friend.ID,
|
|
ChannelID: channelID,
|
|
MaxID: sent.Message.ID,
|
|
Date: 1700000302,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("read history: %v", err)
|
|
}
|
|
if read.Dialog.UnreadCount != 0 || read.Dialog.ReadInboxMaxID != sent.Message.ID {
|
|
t.Fatalf("read dialog = %+v, want fully read through latest", read.Dialog)
|
|
}
|
|
if len(read.OutboxUpdates) != 1 || read.OutboxUpdates[0].UserID != owner.ID || read.OutboxUpdates[0].MaxID != sent.Message.ID {
|
|
t.Fatalf("read outbox updates = %+v, want owner max id %d", read.OutboxUpdates, sent.Message.ID)
|
|
}
|
|
ownerView, err := channels.GetChannel(ctx, owner.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get owner channel after read: %v", err)
|
|
}
|
|
if ownerView.Dialog.ReadOutboxMaxID != sent.Message.ID {
|
|
t.Fatalf("owner dialog read_outbox = %d, want %d", ownerView.Dialog.ReadOutboxMaxID, sent.Message.ID)
|
|
}
|
|
if changed, err := channels.SetChannelDialogPinned(ctx, owner.ID, channelID, true); err != nil || !changed {
|
|
t.Fatalf("set owner channel pinned = changed %v err %v, want changed", changed, err)
|
|
}
|
|
if err := channels.ReorderChannelPinnedDialogs(ctx, owner.ID, []domain.Peer{
|
|
{Type: domain.PeerTypeChannel, ID: channelID},
|
|
}, true); err != nil {
|
|
t.Fatalf("reorder owner channel pinned: %v", err)
|
|
}
|
|
if changed, err := channels.SetChannelDialogUnreadMark(ctx, owner.ID, channelID, true); err != nil || !changed {
|
|
t.Fatalf("set owner channel unread mark = changed %v err %v, want changed", changed, err)
|
|
}
|
|
if err := channels.EditChannelPeerFolders(ctx, owner.ID, []domain.FolderPeerUpdate{
|
|
{Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}, FolderID: domain.DialogArchiveFolderID},
|
|
}); err != nil {
|
|
t.Fatalf("edit owner channel folder: %v", err)
|
|
}
|
|
ownerDialogs, err := channels.GetChannelDialogs(ctx, owner.ID, []int64{channelID})
|
|
if err != nil {
|
|
t.Fatalf("get owner channel dialogs after settings: %v", err)
|
|
}
|
|
if len(ownerDialogs.Dialogs) != 1 || !ownerDialogs.Dialogs[0].Pinned || ownerDialogs.Dialogs[0].PinnedOrder != 1 || !ownerDialogs.Dialogs[0].UnreadMark || ownerDialogs.Dialogs[0].FolderID != domain.DialogArchiveFolderID {
|
|
t.Fatalf("owner channel dialog settings = %+v, want pinned/unread/archive", ownerDialogs.Dialogs)
|
|
}
|
|
unreadMarks, err := channels.ListChannelUnreadMarked(ctx, owner.ID)
|
|
if err != nil {
|
|
t.Fatalf("list owner channel unread marks: %v", err)
|
|
}
|
|
if len(unreadMarks) != 1 || unreadMarks[0].ID != channelID || unreadMarks[0].Type != domain.PeerTypeChannel {
|
|
t.Fatalf("channel unread marks = %+v, want channel", unreadMarks)
|
|
}
|
|
|
|
readers, err := channels.ListMessageReadParticipants(ctx, domain.ChannelReadParticipantsRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MessageID: sent.Message.ID,
|
|
Limit: domain.MaxChannelReadParticipants,
|
|
Date: 1700000303,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list message read participants: %v", err)
|
|
}
|
|
if len(readers.Participants) != 1 || readers.Participants[0].UserID != friend.ID || readers.Participants[0].Date != 1700000302 {
|
|
t.Fatalf("read participants = %+v, want friend read date", readers.Participants)
|
|
}
|
|
|
|
cleared, err := channels.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
|
|
UserID: friend.ID,
|
|
ChannelID: channelID,
|
|
MaxID: sent.Message.ID,
|
|
Date: 1700000302,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("local clear history: %v", err)
|
|
}
|
|
if cleared.AvailableMinID != sent.Message.ID {
|
|
t.Fatalf("local clear available_min_id = %d, want %d", cleared.AvailableMinID, sent.Message.ID)
|
|
}
|
|
staleClear, err := channels.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
|
|
UserID: friend.ID,
|
|
ChannelID: channelID,
|
|
MaxID: created.Message.ID,
|
|
Date: 1700000303,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("stale local clear history: %v", err)
|
|
}
|
|
if staleClear.AvailableMinID != sent.Message.ID {
|
|
t.Fatalf("stale local clear available_min_id = %d, want monotonic %d", staleClear.AvailableMinID, sent.Message.ID)
|
|
}
|
|
afterClear, err := channels.GetChannel(ctx, friend.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get channel after clear: %v", err)
|
|
}
|
|
if afterClear.Dialog.TopMessageID != 0 {
|
|
t.Fatalf("dialog after clear = %+v, want no visible top", afterClear.Dialog)
|
|
}
|
|
|
|
next, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 902,
|
|
Message: "after local clear",
|
|
Date: 1700000304,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send after clear: %v", err)
|
|
}
|
|
afterNext, err := channels.GetChannel(ctx, friend.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get channel after next: %v", err)
|
|
}
|
|
if afterNext.Dialog.TopMessageID != next.Message.ID || afterNext.Dialog.UnreadCount != 1 {
|
|
t.Fatalf("dialog after next = %+v, want top %d unread 1", afterNext.Dialog, next.Message.ID)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreReadOutboxDoesNotRegressSenderDialogUnread(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 35,
|
|
Phone: "+1777" + suffix + "17",
|
|
FirstName: "ReadOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
member, err := users.Create(ctx, domain.User{
|
|
AccessHash: 36,
|
|
Phone: "+1777" + suffix + "18",
|
|
FirstName: "ReadMember",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create member: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Read Outbox " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{member.ID},
|
|
Date: 1700000340,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
ownerMsg, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 9341,
|
|
Message: "owner message before member reply",
|
|
Date: 1700000341,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send owner message: %v", err)
|
|
}
|
|
if _, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
MaxID: ownerMsg.Message.ID,
|
|
Date: 1700000342,
|
|
}); err != nil {
|
|
t.Fatalf("member read owner message: %v", err)
|
|
}
|
|
memberMsg, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 9342,
|
|
Message: "member reply should stay read for sender",
|
|
Date: 1700000343,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send member message: %v", err)
|
|
}
|
|
|
|
var storedReadInbox, storedUnread int
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT read_inbox_max_id, unread_count
|
|
FROM channel_dialogs
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&storedReadInbox, &storedUnread); err != nil {
|
|
t.Fatalf("read member dialog after self send: %v", err)
|
|
}
|
|
if storedReadInbox != memberMsg.Message.ID || storedUnread != 0 {
|
|
t.Fatalf("member dialog after self send read=%d unread=%d, want read %d unread 0", storedReadInbox, storedUnread, memberMsg.Message.ID)
|
|
}
|
|
|
|
read, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MaxID: memberMsg.Message.ID,
|
|
Date: 1700000344,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("owner read member message: %v", err)
|
|
}
|
|
if len(read.OutboxUpdates) != 1 || read.OutboxUpdates[0].UserID != member.ID || read.OutboxUpdates[0].MaxID != memberMsg.Message.ID {
|
|
t.Fatalf("read outbox updates = %+v, want member max id %d", read.OutboxUpdates, memberMsg.Message.ID)
|
|
}
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT read_inbox_max_id, unread_count
|
|
FROM channel_dialogs
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&storedReadInbox, &storedUnread); err != nil {
|
|
t.Fatalf("read member dialog after owner read: %v", err)
|
|
}
|
|
if storedReadInbox != memberMsg.Message.ID || storedUnread != 0 {
|
|
t.Fatalf("member dialog after owner read read=%d unread=%d, want read %d unread 0", storedReadInbox, storedUnread, memberMsg.Message.ID)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreChannelUnreadExcludesOwnOutgoing(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 39,
|
|
Phone: "+1777" + suffix + "19",
|
|
FirstName: "OwnUnreadOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Own Unread " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700000350,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 9351,
|
|
Message: "own outgoing should not be unread",
|
|
Date: 1700000351,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send owner message: %v", err)
|
|
}
|
|
readBeforeOwnMessage := sent.Message.ID - 1
|
|
if _, err := pool.Exec(ctx, `
|
|
UPDATE channel_members
|
|
SET read_inbox_max_id = $3, unread_mark = false
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, owner.ID, readBeforeOwnMessage); err != nil {
|
|
t.Fatalf("regress owner member read watermark: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
UPDATE channel_dialogs
|
|
SET read_inbox_max_id = $3, unread_count = 0, unread_mark = false
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, owner.ID, readBeforeOwnMessage); err != nil {
|
|
t.Fatalf("regress owner dialog unread: %v", err)
|
|
}
|
|
|
|
dialogs, err := channels.GetChannelDialogs(ctx, owner.ID, []int64{channelID})
|
|
if err != nil {
|
|
t.Fatalf("get owner channel dialogs: %v", err)
|
|
}
|
|
if len(dialogs.Dialogs) != 1 {
|
|
t.Fatalf("dialogs = %+v, want one dialog", dialogs.Dialogs)
|
|
}
|
|
if dialogs.Dialogs[0].UnreadCount != 0 {
|
|
t.Fatalf("owner dialog unread = %d, want own outgoing excluded", dialogs.Dialogs[0].UnreadCount)
|
|
}
|
|
unreadOnly, err := channels.ListChannelDialogs(ctx, owner.ID, domain.DialogFilter{
|
|
Folder: &domain.DialogFolder{ExcludeRead: true, Groups: true},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list unread-only channel dialogs: %v", err)
|
|
}
|
|
for _, dialog := range unreadOnly.Dialogs {
|
|
if dialog.Peer.ID == channelID {
|
|
t.Fatalf("unread-only dialogs include own-outgoing-only channel: %+v", unreadOnly.Dialogs)
|
|
}
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
UPDATE channel_dialogs
|
|
SET unread_count = 99
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, owner.ID); err != nil {
|
|
t.Fatalf("corrupt owner dialog unread before read repair: %v", err)
|
|
}
|
|
|
|
read, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MaxID: sent.Message.ID,
|
|
Date: 1700000352,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("read owner channel history: %v", err)
|
|
}
|
|
if read.StillUnreadCount != 0 || read.Dialog.UnreadCount != 0 {
|
|
t.Fatalf("read result = %+v, want no unread own outgoing messages", read)
|
|
}
|
|
var storedUnread int
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT unread_count
|
|
FROM channel_dialogs
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, owner.ID).Scan(&storedUnread); err != nil {
|
|
t.Fatalf("read stored owner unread: %v", err)
|
|
}
|
|
if storedUnread != 0 {
|
|
t.Fatalf("stored owner unread = %d, want repaired to 0", storedUnread)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreConcurrentSendAndReadHistoryDoNotSurfaceDeadlock(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 37,
|
|
Phone: "+1777" + suffix + "21",
|
|
FirstName: "ConcurrentOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
member, err := users.Create(ctx, domain.User{
|
|
AccessHash: 38,
|
|
Phone: "+1777" + suffix + "22",
|
|
FirstName: "ConcurrentMember",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create member: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(context.Background(), "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(context.Background(), "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Send Read Race " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{member.ID},
|
|
Date: 1700000450,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
first, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 1700000451,
|
|
Message: "seed",
|
|
Date: 1700000451,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("seed send: %v", err)
|
|
}
|
|
|
|
for i := 0; i < 20; i++ {
|
|
start := make(chan struct{})
|
|
errs := make(chan error, 2)
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
go func(iter int) {
|
|
defer wg.Done()
|
|
<-start
|
|
_, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
RandomID: int64(1700000500 + iter),
|
|
Message: fmt.Sprintf("race send %d", iter),
|
|
Date: 1700000500 + iter,
|
|
})
|
|
errs <- err
|
|
}(i)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-start
|
|
_, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
MaxID: first.Message.ID,
|
|
Date: 1700000600,
|
|
})
|
|
errs <- err
|
|
}()
|
|
close(start)
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("concurrent send/read iteration %d: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreJoinInitialReadWatermarkSkipsExistingHistory(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 131,
|
|
Phone: "+1777" + suffix + "11",
|
|
FirstName: "JoinOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{
|
|
AccessHash: 132,
|
|
Phone: "+1777" + suffix + "12",
|
|
FirstName: "JoinFriend",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Join Watermark " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700000320,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
first, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 905,
|
|
Message: "before join",
|
|
Date: 1700000321,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send existing message: %v", err)
|
|
}
|
|
joined, err := channels.JoinChannel(ctx, channelID, friend.ID, 1700000322)
|
|
if err != nil {
|
|
t.Fatalf("join channel: %v", err)
|
|
}
|
|
if _, err := channels.JoinChannel(ctx, channelID, friend.ID, 1700000323); !errors.Is(err, domain.ErrUserAlreadyParticipant) {
|
|
t.Fatalf("duplicate join err = %v, want ErrUserAlreadyParticipant", err)
|
|
}
|
|
if len(joined.Members) != 1 || joined.Members[0].ReadInboxMaxID != joined.Message.ID {
|
|
t.Fatalf("joined member = %+v message=%+v, want read watermark at self join service", joined.Members, joined.Message)
|
|
}
|
|
view, err := channels.GetChannel(ctx, friend.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get joined channel: %v", err)
|
|
}
|
|
if view.Dialog.UnreadCount != 0 || view.Self.ReadInboxMaxID != joined.Message.ID {
|
|
t.Fatalf("joined view dialog/self = %+v / %+v, want no unread and read at join service", view.Dialog, view.Self)
|
|
}
|
|
readers, err := channels.ListMessageReadParticipants(ctx, domain.ChannelReadParticipantsRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MessageID: first.Message.ID,
|
|
Limit: domain.MaxChannelReadParticipants,
|
|
Date: 1700000323,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list read participants existing message: %v", err)
|
|
}
|
|
if len(readers.Participants) != 0 {
|
|
t.Fatalf("existing message readers after join = %+v, want none from initial watermark", readers.Participants)
|
|
}
|
|
future, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 906,
|
|
Message: "after join",
|
|
Date: 1700000323,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send future message: %v", err)
|
|
}
|
|
after, err := channels.GetChannel(ctx, friend.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get channel after future: %v", err)
|
|
}
|
|
if after.Dialog.TopMessageID != future.Message.ID || after.Dialog.UnreadCount != 1 {
|
|
t.Fatalf("joined dialog after future = %+v, want top %d unread 1", after.Dialog, future.Message.ID)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreJoinRejectsKickedMember(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 33,
|
|
Phone: "+1777" + suffix + "21",
|
|
FirstName: "BanOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
member, err := users.Create(ctx, domain.User{
|
|
AccessHash: 34,
|
|
Phone: "+1777" + suffix + "22",
|
|
FirstName: "BanMember",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create member: %v", err)
|
|
}
|
|
helper, err := users.Create(ctx, domain.User{
|
|
AccessHash: 35,
|
|
Phone: "+1777" + suffix + "23",
|
|
FirstName: "BanHelper",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create helper: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID, helper.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Ban Join " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{member.ID, helper.ID},
|
|
Date: 1700000305,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
ptsFloor := created.Channel.Pts
|
|
banned, err := channels.EditChannelBanned(ctx, domain.EditChannelBannedRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Participant: domain.Peer{Type: domain.PeerTypeUser, ID: member.ID},
|
|
BannedRights: domain.ChannelBannedRights{
|
|
ViewMessages: true,
|
|
UntilDate: 1700001300,
|
|
},
|
|
Date: 1700000306,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("kick member: %v", err)
|
|
}
|
|
if banned.Event.Pts != 0 || banned.Event.PtsCount != 0 || banned.Channel.Pts != ptsFloor {
|
|
t.Fatalf("kick affected channel pts = event(%d,%d) channel %d, want no pts advance from %d", banned.Event.Pts, banned.Event.PtsCount, banned.Channel.Pts, ptsFloor)
|
|
}
|
|
banDiff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Pts: ptsFloor,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("difference after kick: %v", err)
|
|
}
|
|
if len(banDiff.Events) != 0 || banDiff.Pts != ptsFloor {
|
|
t.Fatalf("difference after kick = %+v, want no durable participant event at pts %d", banDiff, ptsFloor)
|
|
}
|
|
if _, err := channels.JoinChannel(ctx, channelID, member.ID, 1700000307); !errors.Is(err, domain.ErrChannelUserBanned) {
|
|
t.Fatalf("kicked JoinChannel err = %v, want ErrChannelUserBanned", err)
|
|
}
|
|
if _, err := channels.InviteToChannel(ctx, channelID, helper.ID, []int64{member.ID}, 1700000308); !errors.Is(err, domain.ErrUserKicked) {
|
|
t.Fatalf("helper InviteToChannel kicked err = %v, want ErrUserKicked", err)
|
|
}
|
|
restored, err := channels.InviteToChannel(ctx, channelID, owner.ID, []int64{member.ID}, 1700000309)
|
|
if err != nil {
|
|
t.Fatalf("owner InviteToChannel kicked: %v", err)
|
|
}
|
|
if len(restored.Members) != 1 || restored.Members[0].Status != domain.ChannelMemberActive || restored.Members[0].BannedRights != (domain.ChannelBannedRights{}) {
|
|
t.Fatalf("restored members = %+v, want active unbanned member", restored.Members)
|
|
}
|
|
if restored.Channel.ParticipantsCount != 3 || restored.Channel.KickedCount != 0 {
|
|
t.Fatalf("restored counts = participants:%d kicked:%d, want 3/0", restored.Channel.ParticipantsCount, restored.Channel.KickedCount)
|
|
}
|
|
if _, err := channels.InviteToChannel(ctx, channelID, owner.ID, []int64{member.ID}, 1700000310); !errors.Is(err, domain.ErrUserAlreadyParticipant) {
|
|
t.Fatalf("duplicate InviteToChannel err = %v, want ErrUserAlreadyParticipant", err)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreInviteInitialReadWatermarkSkipsExistingHistory(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 41,
|
|
Phone: "+1888" + suffix + "01",
|
|
FirstName: "InviteOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
invited, err := users.Create(ctx, domain.User{
|
|
AccessHash: 42,
|
|
Phone: "+1888" + suffix + "02",
|
|
FirstName: "InviteMember",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create invited: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, invited.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Invite Watermark " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700000320,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
|
|
first, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 902,
|
|
Message: "already visible before invite",
|
|
Date: 1700000321,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send existing channel message: %v", err)
|
|
}
|
|
if _, err := channels.InviteToChannel(ctx, channelID, owner.ID, []int64{invited.ID}, 1700000322); err != nil {
|
|
t.Fatalf("invite to channel: %v", err)
|
|
}
|
|
|
|
view, err := channels.GetChannel(ctx, invited.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get invited channel: %v", err)
|
|
}
|
|
if view.Self.ReadInboxMaxID != first.Message.ID || view.Dialog.ReadInboxMaxID != first.Message.ID {
|
|
t.Fatalf("invited read watermark self/dialog = %d/%d, want existing top %d", view.Self.ReadInboxMaxID, view.Dialog.ReadInboxMaxID, first.Message.ID)
|
|
}
|
|
if view.Dialog.UnreadCount != 1 {
|
|
t.Fatalf("invited unread = %d, want only invite service message unread", view.Dialog.UnreadCount)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreImportInviteInitialReadWatermarkSkipsExistingHistory(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 51,
|
|
Phone: "+1888" + suffix + "11",
|
|
FirstName: "ImportOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
joiner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 52,
|
|
Phone: "+1888" + suffix + "12",
|
|
FirstName: "ImportJoiner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create joiner: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, joiner.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Import Watermark " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700000330,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
first, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 912,
|
|
Message: "already visible before import",
|
|
Date: 1700000331,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send existing channel message: %v", err)
|
|
}
|
|
invite, err := channels.ExportInvite(ctx, domain.ExportChannelInviteRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Title: "join",
|
|
Date: 1700000332,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("export invite: %v", err)
|
|
}
|
|
joined, err := channels.ImportInvite(ctx, domain.ImportChannelInviteRequest{
|
|
UserID: joiner.ID,
|
|
Hash: invite.Invite.Hash,
|
|
Date: 1700000333,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("import invite: %v", err)
|
|
}
|
|
if len(joined.Members) != 1 || joined.Members[0].ReadInboxMaxID != joined.Message.ID || joined.Members[0].ReadOutboxMaxID != joined.Message.ID {
|
|
t.Fatalf("imported member = %+v message=%+v, want read watermarks at self join service", joined.Members, joined.Message)
|
|
}
|
|
view, err := channels.GetChannel(ctx, joiner.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get imported channel: %v", err)
|
|
}
|
|
if view.Dialog.UnreadCount != 0 || view.Self.ReadInboxMaxID != joined.Message.ID {
|
|
t.Fatalf("imported view dialog/self = %+v / %+v, want no unread and read at join service", view.Dialog, view.Self)
|
|
}
|
|
readers, err := channels.ListMessageReadParticipants(ctx, domain.ChannelReadParticipantsRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MessageID: first.Message.ID,
|
|
Limit: domain.MaxChannelReadParticipants,
|
|
Date: 1700000334,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list read participants existing message: %v", err)
|
|
}
|
|
if len(readers.Participants) != 0 {
|
|
t.Fatalf("existing message readers after import = %+v, want none from initial watermark", readers.Participants)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreImportInviteRequestNeededAndUsageLimitErrors(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 61,
|
|
Phone: "+1888" + suffix + "21",
|
|
FirstName: "InviteErrorOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
first, err := users.Create(ctx, domain.User{
|
|
AccessHash: 62,
|
|
Phone: "+1888" + suffix + "22",
|
|
FirstName: "InviteErrorFirst",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create first: %v", err)
|
|
}
|
|
second, err := users.Create(ctx, domain.User{
|
|
AccessHash: 63,
|
|
Phone: "+1888" + suffix + "23",
|
|
FirstName: "InviteErrorSecond",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create second: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, first.ID, second.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Invite Errors " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700000340,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
requested, err := channels.ExportInvite(ctx, domain.ExportChannelInviteRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Title: "approval",
|
|
RequestNeeded: true,
|
|
Date: 1700000341,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("export request-needed invite: %v", err)
|
|
}
|
|
if _, err := channels.ImportInvite(ctx, domain.ImportChannelInviteRequest{
|
|
UserID: first.ID,
|
|
Hash: requested.Invite.Hash,
|
|
Date: 1700000342,
|
|
}); !errors.Is(err, domain.ErrInviteRequestSent) {
|
|
t.Fatalf("import request-needed err = %v, want ErrInviteRequestSent", err)
|
|
}
|
|
limited, err := channels.ExportInvite(ctx, domain.ExportChannelInviteRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Title: "one",
|
|
UsageLimit: 1,
|
|
Date: 1700000343,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("export limited invite: %v", err)
|
|
}
|
|
if _, err := channels.ImportInvite(ctx, domain.ImportChannelInviteRequest{
|
|
UserID: first.ID,
|
|
Hash: limited.Invite.Hash,
|
|
Date: 1700000344,
|
|
}); err != nil {
|
|
t.Fatalf("first import limited invite: %v", err)
|
|
}
|
|
if _, err := channels.ImportInvite(ctx, domain.ImportChannelInviteRequest{
|
|
UserID: second.ID,
|
|
Hash: limited.Invite.Hash,
|
|
Date: 1700000345,
|
|
}); !errors.Is(err, domain.ErrUsersTooMuch) {
|
|
t.Fatalf("second import limited invite err = %v, want ErrUsersTooMuch", err)
|
|
}
|
|
}
|
|
|
|
func TestChannelStorePendingJoinRequestsSummaryAndInviteAdmins(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
userIDs := make([]int64, 0, 11)
|
|
createUser := func(label string, accessHash int64, phoneSuffix int) domain.User {
|
|
t.Helper()
|
|
user, err := users.Create(ctx, domain.User{
|
|
AccessHash: accessHash,
|
|
Phone: fmt.Sprintf("+1889%s%02d", suffix, phoneSuffix),
|
|
FirstName: label,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create %s: %v", label, err)
|
|
}
|
|
userIDs = append(userIDs, user.ID)
|
|
return user
|
|
}
|
|
owner := createUser("PendingOwner", 71, 1)
|
|
inviteAdmin := createUser("PendingInviteAdmin", 72, 2)
|
|
plainMember := createUser("PendingPlainMember", 73, 3)
|
|
changeAdmin := createUser("PendingChangeAdmin", 74, 4)
|
|
requesters := make([]domain.User, 0, domain.MaxChannelPendingJoinRecentRequesters+2)
|
|
for i := 0; i < domain.MaxChannelPendingJoinRecentRequesters+2; i++ {
|
|
requesters = append(requesters, createUser("PendingRequester", int64(80+i), 10+i))
|
|
}
|
|
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", userIDs)
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Pending Summary " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{inviteAdmin.ID, plainMember.ID, changeAdmin.ID},
|
|
Date: 1700000360,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MemberID: inviteAdmin.ID,
|
|
AdminRights: domain.ChannelAdminRights{
|
|
InviteUsers: true,
|
|
},
|
|
Date: 1700000361,
|
|
}); err != nil {
|
|
t.Fatalf("promote invite admin: %v", err)
|
|
}
|
|
if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MemberID: changeAdmin.ID,
|
|
AdminRights: domain.ChannelAdminRights{
|
|
ChangeInfo: true,
|
|
},
|
|
Date: 1700000362,
|
|
}); err != nil {
|
|
t.Fatalf("promote change-info admin: %v", err)
|
|
}
|
|
invite, err := channels.ExportInvite(ctx, domain.ExportChannelInviteRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Title: "approval",
|
|
RequestNeeded: true,
|
|
Date: 1700000363,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("export invite: %v", err)
|
|
}
|
|
for i, requester := range requesters {
|
|
_, err := channels.ImportInvite(ctx, domain.ImportChannelInviteRequest{
|
|
UserID: requester.ID,
|
|
Hash: invite.Invite.Hash,
|
|
Date: 1700000370 + i,
|
|
})
|
|
if !errors.Is(err, domain.ErrInviteRequestSent) {
|
|
t.Fatalf("import pending %d err = %v, want ErrInviteRequestSent", i, err)
|
|
}
|
|
}
|
|
pending, err := channels.PendingJoinRequests(ctx, channelID, 99)
|
|
if err != nil {
|
|
t.Fatalf("pending join requests: %v", err)
|
|
}
|
|
if pending.Count != len(requesters) || len(pending.RecentRequesters) != domain.MaxChannelPendingJoinRecentRequesters {
|
|
t.Fatalf("pending summary = %+v, want bounded recent with full count", pending)
|
|
}
|
|
if pending.RecentRequesters[0] != requesters[len(requesters)-1].ID ||
|
|
pending.RecentRequesters[len(pending.RecentRequesters)-1] != requesters[2].ID {
|
|
t.Fatalf("recent requesters = %+v, want newest first", pending.RecentRequesters)
|
|
}
|
|
admins, err := channels.ListChannelInviteAdminMemberIDs(ctx, channelID, 0)
|
|
if err != nil {
|
|
t.Fatalf("invite admins: %v", err)
|
|
}
|
|
want := []int64{owner.ID, inviteAdmin.ID, changeAdmin.ID}
|
|
if !reflect.DeepEqual(admins, want) {
|
|
t.Fatalf("invite admins = %+v, want %+v", admins, want)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreImportInviteUsageLimitSeesConcurrentIncrement(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 64,
|
|
Phone: "+1888" + suffix + "31",
|
|
FirstName: "InviteLimitOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
joiner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 65,
|
|
Phone: "+1888" + suffix + "32",
|
|
FirstName: "InviteLimitJoiner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create joiner: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, joiner.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Invite Limit Race " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700000350,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
invite, err := channels.ExportInvite(ctx, domain.ExportChannelInviteRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Title: "single",
|
|
UsageLimit: 1,
|
|
Date: 1700000351,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("export limited invite: %v", err)
|
|
}
|
|
|
|
lockTx, err := pool.Begin(ctx)
|
|
if err != nil {
|
|
t.Fatalf("begin lock tx: %v", err)
|
|
}
|
|
defer func() { _ = lockTx.Rollback(ctx) }()
|
|
if _, err := lockTx.Exec(ctx, `
|
|
UPDATE channel_invites
|
|
SET usage_count = usage_limit
|
|
WHERE channel_id = $1 AND invite_id = $2`, channelID, invite.Invite.InviteID); err != nil {
|
|
t.Fatalf("lock and update invite usage: %v", err)
|
|
}
|
|
|
|
importCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := channels.ImportInvite(importCtx, domain.ImportChannelInviteRequest{
|
|
UserID: joiner.ID,
|
|
Hash: invite.Invite.Hash,
|
|
Date: 1700000352,
|
|
})
|
|
errCh <- err
|
|
}()
|
|
time.Sleep(100 * time.Millisecond)
|
|
if err := lockTx.Commit(ctx); err != nil {
|
|
t.Fatalf("commit lock tx: %v", err)
|
|
}
|
|
err = <-errCh
|
|
if !errors.Is(err, domain.ErrUsersTooMuch) {
|
|
t.Fatalf("concurrent import err = %v, want ErrUsersTooMuch after seeing committed usage_count", err)
|
|
}
|
|
if _, err := channels.GetChannel(ctx, joiner.ID, channelID); !errors.Is(err, domain.ErrChannelPrivate) {
|
|
t.Fatalf("joiner channel after rejected import err = %v, want ErrChannelPrivate", err)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreListDialogsUsesDateAndOffset(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 35,
|
|
Phone: "+1777" + suffix + "03",
|
|
FirstName: "DialogPageOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
var channelIDs []int64
|
|
t.Cleanup(func() {
|
|
if len(channelIDs) > 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
older, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Older Dialog " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700000310,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create older channel: %v", err)
|
|
}
|
|
channelIDs = append(channelIDs, older.Channel.ID)
|
|
newer, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Newer Dialog " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700000320,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create newer channel: %v", err)
|
|
}
|
|
channelIDs = append(channelIDs, newer.Channel.ID)
|
|
|
|
first, err := channels.ListChannelDialogs(ctx, owner.ID, domain.DialogFilter{Limit: 1})
|
|
if err != nil {
|
|
t.Fatalf("list first channel dialogs: %v", err)
|
|
}
|
|
if len(first.Dialogs) != 1 || first.Dialogs[0].Peer.ID != newer.Channel.ID {
|
|
t.Fatalf("first page dialogs = %+v, want newer channel by top date", first.Dialogs)
|
|
}
|
|
|
|
next, err := channels.ListChannelDialogs(ctx, owner.ID, domain.DialogFilter{
|
|
OffsetDate: first.Dialogs[0].TopMessageDate,
|
|
OffsetID: first.Dialogs[0].TopMessage,
|
|
HasOffsetPeer: true,
|
|
OffsetPeer: first.Dialogs[0].Peer,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list next channel dialogs: %v", err)
|
|
}
|
|
if len(next.Dialogs) != 1 || next.Dialogs[0].Peer.ID != older.Channel.ID {
|
|
t.Fatalf("next page dialogs = %+v, want older channel without repeating offset peer", next.Dialogs)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreDifferenceStartsAtMemberAvailableMinPts(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 41,
|
|
Phone: "+1778" + suffix + "01",
|
|
FirstName: "PtsOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
member, err := users.Create(ctx, domain.User{
|
|
AccessHash: 42,
|
|
Phone: "+1778" + suffix + "02",
|
|
FirstName: "PtsMember",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create member: %v", err)
|
|
}
|
|
joiner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 43,
|
|
Phone: "+1778" + suffix + "03",
|
|
FirstName: "PtsJoiner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create joiner: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID, joiner.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "PTS Floor " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{member.ID},
|
|
Date: 1700000350,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
ptsFloor := created.Channel.Pts
|
|
promoted, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MemberID: member.ID,
|
|
AdminRights: domain.ChannelAdminRights{
|
|
InviteUsers: true,
|
|
},
|
|
Date: 1700000351,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("edit admin: %v", err)
|
|
}
|
|
if promoted.Event.Pts != 0 || promoted.Event.PtsCount != 0 || promoted.Channel.Pts != ptsFloor {
|
|
t.Fatalf("promote affected channel pts = event(%d,%d) channel %d, want no pts advance from %d", promoted.Event.Pts, promoted.Event.PtsCount, promoted.Channel.Pts, ptsFloor)
|
|
}
|
|
adminDiff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
Pts: ptsFloor,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("difference after promote: %v", err)
|
|
}
|
|
if len(adminDiff.Events) != 0 || adminDiff.Pts != ptsFloor {
|
|
t.Fatalf("difference after promote = %+v, want no durable participant event at pts %d", adminDiff, ptsFloor)
|
|
}
|
|
joined, err := channels.JoinChannel(ctx, channelID, joiner.ID, 1700000352)
|
|
if err != nil {
|
|
t.Fatalf("join channel: %v", err)
|
|
}
|
|
if len(joined.Members) != 1 || joined.Members[0].AvailableMinPts != ptsFloor {
|
|
t.Fatalf("joined members = %+v, want available_min_pts %d", joined.Members, ptsFloor)
|
|
}
|
|
diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
|
|
UserID: joiner.ID,
|
|
ChannelID: channelID,
|
|
Pts: 0,
|
|
Limit: 100,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list channel difference: %v", err)
|
|
}
|
|
if diff.Pts != joined.Channel.Pts {
|
|
t.Fatalf("diff pts = %d, want current channel pts %d", diff.Pts, joined.Channel.Pts)
|
|
}
|
|
for _, msg := range diff.NewMessages {
|
|
if msg.Pts <= ptsFloor {
|
|
t.Fatalf("diff leaks pre-join message %+v at or before available_min_pts %d", msg, ptsFloor)
|
|
}
|
|
}
|
|
for _, event := range diff.OtherUpdates {
|
|
if event.Pts <= ptsFloor {
|
|
t.Fatalf("diff leaks pre-join event %+v at or before available_min_pts %d", event, ptsFloor)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestChannelStorePublicPreviewDifferenceAllowsNonMember(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 241,
|
|
Phone: "+1778" + suffix + "41",
|
|
FirstName: "PreviewDiffOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
viewer, err := users.Create(ctx, domain.User{
|
|
AccessHash: 242,
|
|
Phone: "+1778" + suffix + "42",
|
|
FirstName: "PreviewDiffViewer",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create viewer: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, viewer.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Preview Difference " + suffix,
|
|
Broadcast: true,
|
|
Date: 1700000370,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
if _, err := channels.UpdateUsername(ctx, domain.UpdateChannelUsernameRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Username: "preview_diff_" + suffix,
|
|
}); err != nil {
|
|
t.Fatalf("update username: %v", err)
|
|
}
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 1700000371,
|
|
Message: "public preview difference",
|
|
Date: 1700000371,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send channel message: %v", err)
|
|
}
|
|
|
|
diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
|
|
UserID: viewer.ID,
|
|
ChannelID: channelID,
|
|
Pts: created.Event.Pts,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list public preview difference: %v", err)
|
|
}
|
|
if !diff.Final || diff.Pts != sent.Event.Pts || len(diff.NewMessages) != 1 || diff.NewMessages[0].Body != "public preview difference" {
|
|
t.Fatalf("preview diff = %+v, want one public preview message at current pts", diff)
|
|
}
|
|
if diff.Dialog.UnreadCount != 0 || diff.Dialog.ReadInboxMaxID < sent.Message.ID {
|
|
t.Fatalf("preview diff dialog = %+v, want read-only public preview dialog", diff.Dialog)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreListDialogsDerivesRecipientTopWithoutWriteFanout(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 41,
|
|
Phone: "+1777" + suffix + "31",
|
|
FirstName: "DialogTopOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
member, err := users.Create(ctx, domain.User{
|
|
AccessHash: 42,
|
|
Phone: "+1777" + suffix + "32",
|
|
FirstName: "DialogTopMember",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create member: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Dialog Top " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{member.ID},
|
|
Date: 1700000330,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
if _, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
MaxID: created.Message.ID,
|
|
Date: 1700000331,
|
|
}); err != nil {
|
|
t.Fatalf("read initial service message: %v", err)
|
|
}
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 9331,
|
|
Message: "recipient top without write fanout",
|
|
Date: 1700000332,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send channel message: %v", err)
|
|
}
|
|
|
|
list, err := channels.ListChannelDialogs(ctx, member.ID, 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 TestChannelStoreBroadcastUnreadDerivesDespiteStaleCache(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 421,
|
|
Phone: "+1777" + suffix + "33",
|
|
FirstName: "BroadcastUnreadOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
member, err := users.Create(ctx, domain.User{
|
|
AccessHash: 422,
|
|
Phone: "+1777" + suffix + "34",
|
|
FirstName: "BroadcastUnreadMember",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create member: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Broadcast Unread " + suffix,
|
|
Broadcast: true,
|
|
MemberUserIDs: []int64{member.ID},
|
|
Date: 1700000333,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create broadcast channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
if _, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
MaxID: created.Message.ID,
|
|
Date: 1700000334,
|
|
}); err != nil {
|
|
t.Fatalf("read initial broadcast service message: %v", err)
|
|
}
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 9333,
|
|
Message: "broadcast unread derives despite stale cache",
|
|
Date: 1700000335,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send broadcast message: %v", err)
|
|
}
|
|
|
|
var storedUnread int
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT unread_count
|
|
FROM channel_dialogs
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&storedUnread); err != nil {
|
|
t.Fatalf("read stale broadcast dialog cache: %v", err)
|
|
}
|
|
if storedUnread != 0 {
|
|
t.Fatalf("stored broadcast unread cache = %d, want no send fanout", storedUnread)
|
|
}
|
|
|
|
list, err := channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("list broadcast channel dialogs: %v", err)
|
|
}
|
|
if len(list.Dialogs) != 1 || list.Dialogs[0].TopMessage != sent.Message.ID || list.Dialogs[0].UnreadCount != 1 {
|
|
t.Fatalf("broadcast dialogs = %+v, want sent top and dynamic unread=1", list.Dialogs)
|
|
}
|
|
|
|
unreadOnly, err := channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{
|
|
Folder: &domain.DialogFolder{ExcludeRead: true, Broadcasts: true},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list unread-only broadcast dialogs: %v", err)
|
|
}
|
|
if len(unreadOnly.Dialogs) != 1 || unreadOnly.Dialogs[0].Peer.ID != channelID {
|
|
t.Fatalf("unread-only broadcast dialogs = %+v, want stale-cache channel included", unreadOnly.Dialogs)
|
|
}
|
|
|
|
view, err := channels.GetChannel(ctx, member.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get broadcast channel: %v", err)
|
|
}
|
|
if view.Dialog.UnreadCount != 1 || view.Dialog.TopMessageID != sent.Message.ID {
|
|
t.Fatalf("broadcast view dialog = %+v, want dynamic unread=1", view.Dialog)
|
|
}
|
|
dialogs, err := channels.GetChannelDialogs(ctx, member.ID, []int64{channelID})
|
|
if err != nil {
|
|
t.Fatalf("get broadcast channel dialogs: %v", err)
|
|
}
|
|
if len(dialogs.Dialogs) != 1 || dialogs.Dialogs[0].UnreadCount != 1 {
|
|
t.Fatalf("get broadcast dialogs = %+v, want dynamic unread=1", dialogs.Dialogs)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreLargeMegagroupUnreadDerivesDespiteStaleCache(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 431,
|
|
Phone: "+1777" + suffix + "35",
|
|
FirstName: "LargeUnreadOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
member, err := users.Create(ctx, domain.User{
|
|
AccessHash: 432,
|
|
Phone: "+1777" + suffix + "36",
|
|
FirstName: "LargeUnreadMember",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create member: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Large Unread " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{member.ID},
|
|
Date: 1700000336,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create large megagroup: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
if _, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
MaxID: created.Message.ID,
|
|
Date: 1700000337,
|
|
}); err != nil {
|
|
t.Fatalf("read initial large service message: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
UPDATE channels
|
|
SET participants_count = $2
|
|
WHERE id = $1`, channelID, domain.MaxSynchronousChannelDialogFanout+1); err != nil {
|
|
t.Fatalf("mark megagroup as over synchronous fanout threshold: %v", err)
|
|
}
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 9336,
|
|
Message: "large megagroup unread derives despite stale cache",
|
|
Date: 1700000338,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send large megagroup message: %v", err)
|
|
}
|
|
|
|
var storedTop, storedUnread int
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT top_message_id, unread_count
|
|
FROM channel_dialogs
|
|
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&storedTop, &storedUnread); err != nil {
|
|
t.Fatalf("read stale large dialog cache: %v", err)
|
|
}
|
|
if storedTop == sent.Message.ID || storedUnread != 0 {
|
|
t.Fatalf("stored large dialog cache top=%d unread=%d, want stale top and unread=0", storedTop, storedUnread)
|
|
}
|
|
|
|
list, err := channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("list large channel dialogs: %v", err)
|
|
}
|
|
if len(list.Dialogs) != 1 || list.Dialogs[0].TopMessage != sent.Message.ID || list.Dialogs[0].UnreadCount != 1 {
|
|
t.Fatalf("large dialogs = %+v, want sent top and dynamic unread=1", list.Dialogs)
|
|
}
|
|
ownerView, err := channels.GetChannel(ctx, owner.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get large channel for sender: %v", err)
|
|
}
|
|
if ownerView.Dialog.UnreadCount != 0 {
|
|
t.Fatalf("large sender dialog = %+v, want own outgoing excluded from dynamic unread", ownerView.Dialog)
|
|
}
|
|
|
|
unreadOnly, err := channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{
|
|
Folder: &domain.DialogFolder{ExcludeRead: true, Groups: true},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list unread-only large dialogs: %v", err)
|
|
}
|
|
if len(unreadOnly.Dialogs) != 1 || unreadOnly.Dialogs[0].Peer.ID != channelID {
|
|
t.Fatalf("unread-only large dialogs = %+v, want stale-cache channel included", unreadOnly.Dialogs)
|
|
}
|
|
|
|
view, err := channels.GetChannel(ctx, member.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get large channel: %v", err)
|
|
}
|
|
if view.Dialog.UnreadCount != 1 || view.Dialog.TopMessageID != sent.Message.ID {
|
|
t.Fatalf("large view dialog = %+v, want dynamic unread=1", view.Dialog)
|
|
}
|
|
dialogs, err := channels.GetChannelDialogs(ctx, member.ID, []int64{channelID})
|
|
if err != nil {
|
|
t.Fatalf("get large channel dialogs: %v", err)
|
|
}
|
|
if len(dialogs.Dialogs) != 1 || dialogs.Dialogs[0].UnreadCount != 1 {
|
|
t.Fatalf("get large dialogs = %+v, want dynamic unread=1", dialogs.Dialogs)
|
|
}
|
|
|
|
cleared, err := channels.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
MaxID: sent.Message.ID,
|
|
Date: 1700000339,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("local clear large history: %v", err)
|
|
}
|
|
if cleared.AvailableMinID != sent.Message.ID {
|
|
t.Fatalf("large local clear available_min_id = %d, want %d", cleared.AvailableMinID, sent.Message.ID)
|
|
}
|
|
afterClear, err := channels.GetChannel(ctx, member.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("get large channel after local clear: %v", err)
|
|
}
|
|
if afterClear.Dialog.TopMessageID != 0 || afterClear.Dialog.UnreadCount != 0 {
|
|
t.Fatalf("large dialog after local clear = %+v, want no visible unread top", afterClear.Dialog)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreLargeMegagroupUnreadSkipsDeletedHole(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 441,
|
|
Phone: "+1777" + suffix + "37",
|
|
FirstName: "DeletedHoleOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
member, err := users.Create(ctx, domain.User{
|
|
AccessHash: 442,
|
|
Phone: "+1777" + suffix + "38",
|
|
FirstName: "DeletedHoleMember",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create member: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Deleted Hole " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{member.ID},
|
|
Date: 1700000339,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create deleted-hole megagroup: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
if _, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
MaxID: created.Message.ID,
|
|
Date: 1700000340,
|
|
}); err != nil {
|
|
t.Fatalf("read initial deleted-hole service message: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
UPDATE channels
|
|
SET participants_count = $2
|
|
WHERE id = $1`, channelID, domain.MaxSynchronousChannelDialogFanout+1); err != nil {
|
|
t.Fatalf("mark deleted-hole megagroup over threshold: %v", err)
|
|
}
|
|
first, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 9339,
|
|
Message: "deleted unread hole",
|
|
Date: 1700000341,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send first large message: %v", err)
|
|
}
|
|
second, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 9340,
|
|
Message: "remaining unread message",
|
|
Date: 1700000342,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send second large message: %v", err)
|
|
}
|
|
deleted, err := channels.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
IDs: []int{first.Message.ID},
|
|
Date: 1700000343,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("delete non-top unread message: %v", err)
|
|
}
|
|
if len(deleted.DeletedIDs) != 1 || deleted.DeletedIDs[0] != first.Message.ID {
|
|
t.Fatalf("deleted ids = %+v, want first message only", deleted.DeletedIDs)
|
|
}
|
|
|
|
list, err := channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("list deleted-hole dialogs: %v", err)
|
|
}
|
|
if len(list.Dialogs) != 1 || list.Dialogs[0].TopMessage != second.Message.ID || list.Dialogs[0].UnreadCount != 1 {
|
|
t.Fatalf("deleted-hole dialogs = %+v, want only non-deleted unread top counted", list.Dialogs)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreListDialogsSeeksBeyondQueryWindow(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 36,
|
|
Phone: "+1777" + suffix + "04",
|
|
FirstName: "DialogSeekOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
count := channelDialogQueryLimit + 5
|
|
ids := make([]int64, count)
|
|
baseID := owner.ID * 1000
|
|
for i := range ids {
|
|
ids[i] = baseID + int64(i+1)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", ids)
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
|
|
})
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO channels (
|
|
id, access_hash, creator_user_id, title, broadcast, megagroup,
|
|
participants_count, admins_count, top_message_id, pts, date
|
|
)
|
|
SELECT id, id + 900000, $2, 'Bulk Dialog ' || ord, false, true, 1, 1, 1, 1, (1700000400 + ord)::int
|
|
FROM unnest($1::bigint[]) WITH ORDINALITY AS t(id, ord)`, ids, owner.ID); err != nil {
|
|
t.Fatalf("bulk insert channels: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO channel_members (channel_id, user_id, role, status, joined_at)
|
|
SELECT id, $2, 'creator', 'active', 1700000400
|
|
FROM unnest($1::bigint[]) AS t(id)`, ids, owner.ID); err != nil {
|
|
t.Fatalf("bulk insert channel members: %v", err)
|
|
}
|
|
|
|
channels := NewChannelStore(pool)
|
|
var cursor domain.Dialog
|
|
var sixth domain.ChannelDialogList
|
|
for page := 0; page < 6; page++ {
|
|
filter := domain.DialogFilter{Limit: 100}
|
|
if page > 0 {
|
|
filter.OffsetDate = cursor.TopMessageDate
|
|
filter.OffsetID = cursor.TopMessage
|
|
filter.HasOffsetPeer = true
|
|
filter.OffsetPeer = cursor.Peer
|
|
}
|
|
got, err := channels.ListChannelDialogs(ctx, owner.ID, filter)
|
|
if err != nil {
|
|
t.Fatalf("list channel dialogs page %d: %v", page+1, err)
|
|
}
|
|
if len(got.Dialogs) == 0 {
|
|
t.Fatalf("page %d unexpectedly empty after cursor %+v", page+1, cursor)
|
|
}
|
|
cursor = got.Dialogs[len(got.Dialogs)-1]
|
|
if page == 5 {
|
|
sixth = got
|
|
}
|
|
}
|
|
if len(sixth.Dialogs) != 5 {
|
|
t.Fatalf("sixth page len = %d, want remaining 5 beyond query window", len(sixth.Dialogs))
|
|
}
|
|
if sixth.Dialogs[0].Peer.ID != ids[4] || sixth.Dialogs[4].Peer.ID != ids[0] {
|
|
t.Fatalf("sixth page dialogs = %+v, want oldest five descending by date", sixth.Dialogs)
|
|
}
|
|
|
|
included, err := channels.ListChannelDialogs(ctx, owner.ID, domain.DialogFilter{
|
|
Folder: &domain.DialogFolder{
|
|
IncludePeers: []domain.DialogFolderPeer{{
|
|
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: ids[0]},
|
|
}},
|
|
},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list included channel dialog beyond query window: %v", err)
|
|
}
|
|
if len(included.Dialogs) != 1 || included.Dialogs[0].Peer.ID != ids[0] {
|
|
t.Fatalf("included dialogs = %+v, want oldest included channel beyond query window", included.Dialogs)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreListDialogsFolderFiltersBeforeQueryLimit(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 37,
|
|
Phone: "+1777" + suffix + "05",
|
|
FirstName: "DialogFolderOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
count := channelDialogQueryLimit + 5
|
|
ids := make([]int64, count)
|
|
baseID := owner.ID*1000 + 100000
|
|
for i := range ids {
|
|
ids[i] = baseID + int64(i+1)
|
|
}
|
|
archivedID := ids[0]
|
|
t.Cleanup(func() {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", ids)
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
|
|
})
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO channels (
|
|
id, access_hash, creator_user_id, title, broadcast, megagroup,
|
|
participants_count, admins_count, top_message_id, pts, date
|
|
)
|
|
SELECT id, id + 910000, $2, 'Folder Dialog ' || ord, false, true, 1, 1, 1, 1, (1700000500 + ord)::int
|
|
FROM unnest($1::bigint[]) WITH ORDINALITY AS t(id, ord)`, ids, owner.ID); err != nil {
|
|
t.Fatalf("bulk insert channels: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO channel_members (channel_id, user_id, role, status, joined_at)
|
|
SELECT id, $2, 'creator', 'active', 1700000500
|
|
FROM unnest($1::bigint[]) AS t(id)`, ids, owner.ID); err != nil {
|
|
t.Fatalf("bulk insert channel members: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO channel_dialogs (user_id, channel_id, folder_id, top_message_id, top_message_date)
|
|
VALUES ($1, $2, $3, 1, 1700000500)`, owner.ID, archivedID, domain.DialogArchiveFolderID); err != nil {
|
|
t.Fatalf("archive oldest channel dialog: %v", err)
|
|
}
|
|
|
|
archive, err := NewChannelStore(pool).ListChannelDialogs(ctx, owner.ID, domain.DialogFilter{
|
|
HasFolderID: true,
|
|
FolderID: domain.DialogArchiveFolderID,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list archive channel dialogs: %v", err)
|
|
}
|
|
if len(archive.Dialogs) != 1 || archive.Dialogs[0].Peer.ID != archivedID {
|
|
t.Fatalf("archive dialogs = %+v, want archived channel beyond first query window", archive.Dialogs)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreEditAboutPersistsAndChecksPermission(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 131,
|
|
Phone: "+1888" + suffix + "01",
|
|
FirstName: "AboutOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
member, err := users.Create(ctx, domain.User{
|
|
AccessHash: 132,
|
|
Phone: "+1888" + suffix + "02",
|
|
FirstName: "AboutMember",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create member: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, member.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "About " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{member.ID},
|
|
Date: 1700000600,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
|
|
if _, err := channels.EditChannelAbout(ctx, domain.EditChannelAboutRequest{
|
|
UserID: member.ID,
|
|
ChannelID: channelID,
|
|
About: "member cannot edit",
|
|
Date: 1700000601,
|
|
}); !errors.Is(err, domain.ErrChannelAdminRequired) {
|
|
t.Fatalf("EditChannelAbout by member err = %v, want ErrChannelAdminRequired", err)
|
|
}
|
|
|
|
updated, err := channels.EditChannelAbout(ctx, domain.EditChannelAboutRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
About: "owner about",
|
|
Date: 1700000602,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("EditChannelAbout by owner: %v", err)
|
|
}
|
|
if updated.About != "owner about" {
|
|
t.Fatalf("updated about = %q, want owner about", updated.About)
|
|
}
|
|
view, err := channels.GetChannel(ctx, member.ID, channelID)
|
|
if err != nil {
|
|
t.Fatalf("GetChannel by member: %v", err)
|
|
}
|
|
if view.Channel.About != "owner about" {
|
|
t.Fatalf("member view about = %q, want owner about", view.Channel.About)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreSendMessageResolvesReplyTopID(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 41,
|
|
Phone: "+1778" + suffix + "01",
|
|
FirstName: "ReplyOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{
|
|
AccessHash: 42,
|
|
Phone: "+1778" + suffix + "02",
|
|
FirstName: "ReplyFriend",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Reply Top " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{friend.ID},
|
|
Date: 1700000350,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
root, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 911,
|
|
Message: "root",
|
|
Date: 1700000351,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send root: %v", err)
|
|
}
|
|
reply, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: friend.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 912,
|
|
Message: "reply",
|
|
ReplyTo: &domain.MessageReply{MessageID: root.Message.ID, QuoteText: "root"},
|
|
Date: 1700000352,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send reply: %v", err)
|
|
}
|
|
channelPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}
|
|
if reply.Message.ReplyTo == nil || reply.Message.ReplyTo.Peer != channelPeer || reply.Message.ReplyTo.TopMessageID != root.Message.ID {
|
|
t.Fatalf("reply metadata = %+v, want channel peer and top id %d", reply.Message.ReplyTo, root.Message.ID)
|
|
}
|
|
nested, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 913,
|
|
Message: "nested",
|
|
ReplyTo: &domain.MessageReply{MessageID: reply.Message.ID},
|
|
Date: 1700000353,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send nested reply: %v", err)
|
|
}
|
|
if nested.Message.ReplyTo == nil || nested.Message.ReplyTo.TopMessageID != root.Message.ID {
|
|
t.Fatalf("nested reply metadata = %+v, want inherited top id %d", nested.Message.ReplyTo, root.Message.ID)
|
|
}
|
|
_, err = channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 914,
|
|
Message: "bad quote offset",
|
|
ReplyTo: &domain.MessageReply{
|
|
MessageID: root.Message.ID,
|
|
QuoteText: "root",
|
|
QuoteOffset: domain.MaxMessageReplyQuoteOffset + 1,
|
|
},
|
|
Date: 1700000354,
|
|
})
|
|
if !errors.Is(err, domain.ErrReplyMessageIDInvalid) {
|
|
t.Fatalf("bad quote offset err = %v, want ErrReplyMessageIDInvalid", err)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreHistorySupportsOffsetDateOnly(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 37,
|
|
Phone: "+1778" + suffix + "03",
|
|
FirstName: "HistoryDateOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "History Date " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700000360,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
if _, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 921,
|
|
Message: "old",
|
|
Date: 1700000361,
|
|
}); err != nil {
|
|
t.Fatalf("send old: %v", err)
|
|
}
|
|
if _, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 922,
|
|
Message: "new",
|
|
Date: 1700000362,
|
|
}); err != nil {
|
|
t.Fatalf("send new: %v", err)
|
|
}
|
|
|
|
history, err := channels.ListChannelHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
|
|
ChannelID: channelID,
|
|
OffsetDate: 1700000362,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list channel history: %v", err)
|
|
}
|
|
if len(history.Messages) != 2 || history.Messages[0].Body != "old" || history.Messages[1].Action == nil {
|
|
t.Fatalf("history = %+v, want only messages older than offset date", history.Messages)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreDifferenceUsesDurableMessageSnapshots(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 39,
|
|
Phone: "+1778" + suffix + "01",
|
|
FirstName: "SnapshotOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{
|
|
AccessHash: 40,
|
|
Phone: "+1778" + suffix + "02",
|
|
FirstName: "SnapshotFriend",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Snapshot Diff " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{friend.ID},
|
|
Date: 1700000380,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 941,
|
|
Message: "original",
|
|
Date: 1700000381,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send channel message: %v", err)
|
|
}
|
|
if _, err := channels.EditChannelMessage(ctx, domain.EditChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
ID: sent.Message.ID,
|
|
Message: "first edit",
|
|
EditDate: 1700000382,
|
|
}); err != nil {
|
|
t.Fatalf("first edit: %v", err)
|
|
}
|
|
if _, err := channels.EditChannelMessage(ctx, domain.EditChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
ID: sent.Message.ID,
|
|
Message: "second edit",
|
|
EditDate: 1700000383,
|
|
}); err != nil {
|
|
t.Fatalf("second edit: %v", err)
|
|
}
|
|
duplicate, found, err := channels.duplicateChannelMessage(ctx, channelID, owner.ID, sent.Message.RandomID)
|
|
if err != nil {
|
|
t.Fatalf("duplicate channel message: %v", err)
|
|
}
|
|
if !found || !duplicate.Duplicate || duplicate.Event.Type != domain.ChannelUpdateNewMessage || duplicate.Message.Body != "original" || duplicate.Event.Message.Body != "original" {
|
|
t.Fatalf("duplicate after edit = %+v found=%v, want original new-message snapshot", duplicate, found)
|
|
}
|
|
|
|
diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
|
|
UserID: friend.ID,
|
|
ChannelID: channelID,
|
|
Pts: created.Event.Pts,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list channel difference: %v", err)
|
|
}
|
|
if len(diff.NewMessages) != 1 || diff.NewMessages[0].Body != "original" {
|
|
t.Fatalf("new messages = %+v, want original send snapshot", diff.NewMessages)
|
|
}
|
|
if len(diff.OtherUpdates) != 2 {
|
|
t.Fatalf("other updates = %+v, want two edit snapshots", diff.OtherUpdates)
|
|
}
|
|
if diff.OtherUpdates[0].Message.Body != "first edit" || diff.OtherUpdates[1].Message.Body != "second edit" {
|
|
t.Fatalf("edit snapshots = %q/%q, want first edit/second edit", diff.OtherUpdates[0].Message.Body, diff.OtherUpdates[1].Message.Body)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreSendFailureBeforePtsAllocationDoesNotRecordNoopGap(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 41,
|
|
Phone: "+1888" + suffix + "01",
|
|
FirstName: "NoopOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
outsider, err := users.Create(ctx, domain.User{
|
|
AccessHash: 42,
|
|
Phone: "+1888" + suffix + "02",
|
|
FirstName: "NoopOutsider",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create outsider: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, outsider.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Noop Gap " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700000400,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
|
|
_, err = channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: outsider.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 991,
|
|
Message: "outsider should fail",
|
|
Date: 1700000401,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("SendChannelMessage outsider unexpectedly succeeded")
|
|
}
|
|
|
|
var gapRows int
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT count(*)::int
|
|
FROM channel_update_events
|
|
WHERE channel_id = $1 AND pts = 2`, channelID).Scan(&gapRows); err != nil {
|
|
t.Fatalf("count events after failed send: %v", err)
|
|
}
|
|
if gapRows != 0 {
|
|
t.Fatalf("events after failed send = %d, want no pts allocation before member validation", gapRows)
|
|
}
|
|
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 992,
|
|
Message: "after noop gap",
|
|
Date: 1700000402,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send owner after gap: %v", err)
|
|
}
|
|
if sent.Event.Pts != 2 {
|
|
t.Fatalf("next channel pts = %d, want 2 after failed send before pts allocation", sent.Event.Pts)
|
|
}
|
|
diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Pts: 1,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list channel difference: %v", err)
|
|
}
|
|
if diff.Pts != 2 || len(diff.Events) != 1 || diff.Events[0].Type != domain.ChannelUpdateNewMessage || diff.Events[0].Pts != 2 {
|
|
t.Fatalf("diff after failed send = %+v, want only message pts=2", diff)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreDifferenceTooLongSnapshot(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 45,
|
|
Phone: "+1889" + suffix + "01",
|
|
FirstName: "TooLongOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{
|
|
AccessHash: 46,
|
|
Phone: "+1889" + suffix + "02",
|
|
FirstName: "TooLongFriend",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "TooLong Snapshot " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{friend.ID},
|
|
Date: 1700000410,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
|
|
lastPts := created.Event.Pts
|
|
for i := 0; i < 12; i++ {
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: int64(10_000 + i),
|
|
Message: "too long snapshot",
|
|
Date: 1700000411 + i,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send channel message %d: %v", i, err)
|
|
}
|
|
lastPts = sent.Event.Pts
|
|
}
|
|
|
|
diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
|
|
UserID: friend.ID,
|
|
ChannelID: channelID,
|
|
Pts: 0,
|
|
Limit: 3,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list channel difference: %v", err)
|
|
}
|
|
if !diff.TooLong || !diff.Final || diff.Pts != lastPts {
|
|
t.Fatalf("diff = %+v, want tooLong final snapshot at pts %d", diff, lastPts)
|
|
}
|
|
if len(diff.NewMessages) == 0 || len(diff.NewMessages) > domain.MaxChannelDifferenceTooLongMessages {
|
|
t.Fatalf("tooLong snapshot messages = %d, want bounded latest messages", len(diff.NewMessages))
|
|
}
|
|
if diff.Dialog.TopMessageID == 0 || diff.Dialog.UnreadCount == 0 {
|
|
t.Fatalf("tooLong dialog = %+v, want current dialog state", diff.Dialog)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreAdminLogFiltersAndSearch(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 51,
|
|
Phone: "+1999" + suffix + "01",
|
|
FirstName: "AdminLogOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{
|
|
AccessHash: 52,
|
|
Phone: "+1999" + suffix + "02",
|
|
FirstName: "AdminLogFriend",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
invited, err := users.Create(ctx, domain.User{
|
|
AccessHash: 53,
|
|
Phone: "+1999" + suffix + "03",
|
|
FirstName: "AdminLogInvited",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create invited: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID, invited.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Admin Log " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{friend.ID},
|
|
Date: 1700000500,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
|
|
if _, err := channels.EditChannelAdmin(ctx, domain.EditChannelAdminRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MemberID: friend.ID,
|
|
AdminRights: domain.ChannelAdminRights{
|
|
ChangeInfo: true,
|
|
InviteUsers: true,
|
|
PinMessages: true,
|
|
},
|
|
Rank: "ops",
|
|
Date: 1700000501,
|
|
}); err != nil {
|
|
t.Fatalf("edit admin: %v", err)
|
|
}
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 501,
|
|
Message: "needle admin log body",
|
|
Date: 1700000502,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send channel message: %v", err)
|
|
}
|
|
if _, err := channels.UpdatePinnedMessage(ctx, domain.UpdateChannelPinnedMessageRequest{
|
|
UserID: friend.ID,
|
|
ChannelID: channelID,
|
|
MessageID: sent.Message.ID,
|
|
Pinned: true,
|
|
Date: 1700000503,
|
|
}); err != nil {
|
|
t.Fatalf("pin message: %v", err)
|
|
}
|
|
if _, err := channels.InviteToChannel(ctx, channelID, friend.ID, []int64{invited.ID}, 1700000504); err != nil {
|
|
t.Fatalf("invite to channel: %v", err)
|
|
}
|
|
|
|
searched, err := channels.ListAdminLog(ctx, domain.ChannelAdminLogRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Query: "needle",
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("search admin log: %v", err)
|
|
}
|
|
if len(searched.Events) == 0 {
|
|
t.Fatalf("search admin log returned no events, want message body match")
|
|
}
|
|
|
|
pinned, err := channels.ListAdminLog(ctx, domain.ChannelAdminLogRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
Filter: domain.ChannelAdminLogFilter{Pinned: true},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("pinned admin log: %v", err)
|
|
}
|
|
if len(pinned.Events) != 1 || pinned.Events[0].Type != domain.ChannelAdminLogUpdatePinned || pinned.Events[0].Message == nil {
|
|
t.Fatalf("pinned events = %+v, want one update_pinned with message", pinned.Events)
|
|
}
|
|
|
|
byFriend, err := channels.ListAdminLog(ctx, domain.ChannelAdminLogRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
AdminUserIDs: []int64{friend.ID},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("friend admin log: %v", err)
|
|
}
|
|
if len(byFriend.Events) == 0 {
|
|
t.Fatalf("friend admin log returned no events, want pin/invite")
|
|
}
|
|
for _, event := range byFriend.Events {
|
|
if event.UserID != friend.ID {
|
|
t.Fatalf("friend admin log event actor = %d, want %d in %+v", event.UserID, friend.ID, byFriend.Events)
|
|
}
|
|
}
|
|
|
|
if _, err := channels.ListAdminLog(ctx, domain.ChannelAdminLogRequest{
|
|
UserID: invited.ID,
|
|
ChannelID: channelID,
|
|
Limit: 10,
|
|
}); err != domain.ErrChannelAdminRequired {
|
|
t.Fatalf("member admin log err = %v, want ErrChannelAdminRequired", err)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreDeleteHistoryForEveryoneBatchesHugeMaxID(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 71,
|
|
Phone: "+1998" + suffix + "01",
|
|
FirstName: "BulkChannelOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{
|
|
AccessHash: 72,
|
|
Phone: "+1998" + suffix + "02",
|
|
FirstName: "BulkChannelFriend",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Bulk Delete " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{friend.ID},
|
|
Date: 1700000600,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
|
|
total := domain.MaxDeleteHistoryBatch + 2
|
|
if _, err := pool.Exec(ctx, `
|
|
WITH src AS (
|
|
SELECT generate_series(2, $3::int + 1) AS id
|
|
),
|
|
msgs AS (
|
|
INSERT INTO channel_messages (
|
|
channel_id,
|
|
id,
|
|
random_id,
|
|
sender_user_id,
|
|
from_peer_type,
|
|
from_peer_id,
|
|
message_date,
|
|
body,
|
|
entities,
|
|
pts
|
|
)
|
|
SELECT
|
|
$1::bigint,
|
|
id,
|
|
920000000 + id,
|
|
$2::bigint,
|
|
'user',
|
|
$2::bigint,
|
|
1700000600 + id,
|
|
'bulk channel history',
|
|
'[]'::jsonb,
|
|
id
|
|
FROM src
|
|
RETURNING id, message_date
|
|
)
|
|
INSERT INTO channel_update_events (
|
|
channel_id,
|
|
pts,
|
|
pts_count,
|
|
date,
|
|
event_type,
|
|
message_id,
|
|
sender_user_id,
|
|
payload
|
|
)
|
|
SELECT
|
|
$1::bigint,
|
|
id,
|
|
1,
|
|
message_date,
|
|
'new_channel_message',
|
|
id,
|
|
$2::bigint,
|
|
'{}'::jsonb
|
|
FROM msgs
|
|
`, channelID, owner.ID, total); err != nil {
|
|
t.Fatalf("seed bulk channel messages: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
UPDATE channels
|
|
SET top_message_id = $2,
|
|
pts = $2,
|
|
updated_at = now()
|
|
WHERE id = $1`, channelID, total+1); err != nil {
|
|
t.Fatalf("update channel bulk top: %v", err)
|
|
}
|
|
|
|
first, err := channels.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MaxID: int(^uint(0) >> 1),
|
|
ForEveryone: true,
|
|
Date: 1700000700,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DeleteChannelHistory first batch: %v", err)
|
|
}
|
|
wantFirstPts := total + 1 + domain.MaxDeleteHistoryBatch
|
|
if first.Offset != 1 || first.Event.Pts != wantFirstPts || first.Event.PtsCount != domain.MaxDeleteHistoryBatch || len(first.DeletedIDs) != domain.MaxDeleteHistoryBatch {
|
|
t.Fatalf("first batch = %+v, want offset=1 pts=%d pts_count=%d", first, wantFirstPts, domain.MaxDeleteHistoryBatch)
|
|
}
|
|
var remaining int
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM channel_messages WHERE channel_id = $1 AND NOT deleted`, channelID).Scan(&remaining); err != nil {
|
|
t.Fatalf("count remaining after first batch: %v", err)
|
|
}
|
|
if remaining != 3 {
|
|
t.Fatalf("remaining after first batch = %d, want create service + two oldest messages", remaining)
|
|
}
|
|
|
|
second, err := channels.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
MaxID: int(^uint(0) >> 1),
|
|
ForEveryone: true,
|
|
Date: 1700000701,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DeleteChannelHistory second batch: %v", err)
|
|
}
|
|
if second.Offset != 0 || second.Event.Pts != wantFirstPts+3 || second.Event.PtsCount != 3 || len(second.DeletedIDs) != 3 {
|
|
t.Fatalf("second batch = %+v, want final offset=0 pts=%d pts_count=3", second, wantFirstPts+3)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreCommonChannelsOnlySharedMegagroups(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 51, Phone: "+1888" + suffix + "01", FirstName: "CommonOwner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{AccessHash: 52, Phone: "+1888" + suffix + "02", FirstName: "CommonFriend"})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
other, err := users.Create(ctx, domain.User{AccessHash: 53, Phone: "+1888" + suffix + "03", FirstName: "CommonOther"})
|
|
if err != nil {
|
|
t.Fatalf("create other: %v", err)
|
|
}
|
|
var channelIDs []int64
|
|
t.Cleanup(func() {
|
|
if len(channelIDs) != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID, other.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
create := func(title string, broadcast bool, memberIDs []int64, date int) domain.CreateChannelResult {
|
|
t.Helper()
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: title,
|
|
Broadcast: broadcast,
|
|
Megagroup: !broadcast,
|
|
MemberUserIDs: memberIDs,
|
|
Date: date,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create %s: %v", title, err)
|
|
}
|
|
channelIDs = append(channelIDs, created.Channel.ID)
|
|
return created
|
|
}
|
|
first := create("common one "+suffix, false, []int64{friend.ID}, 1700000800)
|
|
second := create("common two "+suffix, false, []int64{friend.ID}, 1700000801)
|
|
create("broadcast excluded "+suffix, true, []int64{friend.ID}, 1700000802)
|
|
left := create("left excluded "+suffix, false, []int64{friend.ID}, 1700000803)
|
|
if _, err := channels.LeaveChannel(ctx, left.Channel.ID, friend.ID, 1700000804); err != nil {
|
|
t.Fatalf("leave channel: %v", err)
|
|
}
|
|
create("not shared "+suffix, false, []int64{other.ID}, 1700000805)
|
|
|
|
page, err := channels.ListCommonChannels(ctx, domain.CommonChannelsRequest{
|
|
UserID: owner.ID,
|
|
TargetUserID: friend.ID,
|
|
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 := channels.ListCommonChannels(ctx, domain.CommonChannelsRequest{
|
|
UserID: owner.ID,
|
|
TargetUserID: friend.ID,
|
|
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 := channels.ListCommonChannels(ctx, domain.CommonChannelsRequest{
|
|
UserID: owner.ID,
|
|
TargetUserID: friend.ID,
|
|
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 TestChannelStoreLeftChannelsReturnsPagedLeftMemberships(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 61, Phone: "+1889" + suffix + "01", FirstName: "LeftOwner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{AccessHash: 62, Phone: "+1889" + suffix + "02", FirstName: "LeftFriend"})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
var channelIDs []int64
|
|
t.Cleanup(func() {
|
|
if len(channelIDs) != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
create := func(title string, broadcast bool, date int) domain.CreateChannelResult {
|
|
t.Helper()
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: title,
|
|
Broadcast: broadcast,
|
|
Megagroup: !broadcast,
|
|
MemberUserIDs: []int64{friend.ID},
|
|
Date: date,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create %s: %v", title, err)
|
|
}
|
|
channelIDs = append(channelIDs, created.Channel.ID)
|
|
return created
|
|
}
|
|
older := create("older left "+suffix, false, 1700000810)
|
|
newer := create("newer left "+suffix, true, 1700000811)
|
|
create("active excluded "+suffix, false, 1700000812)
|
|
if _, err := channels.LeaveChannel(ctx, older.Channel.ID, friend.ID, 1700000813); err != nil {
|
|
t.Fatalf("leave older channel: %v", err)
|
|
}
|
|
if _, err := channels.LeaveChannel(ctx, newer.Channel.ID, friend.ID, 1700000814); err != nil {
|
|
t.Fatalf("leave newer channel: %v", err)
|
|
}
|
|
|
|
page, err := channels.ListLeftChannels(ctx, friend.ID, 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 := channels.ListLeftChannels(ctx, friend.ID, 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 := channels.ListLeftChannels(ctx, friend.ID, 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 := channels.ListLeftChannels(ctx, friend.ID, domain.MaxLeftChannelsOffset+1, 1); !errors.Is(err, domain.ErrChannelInvalid) {
|
|
t.Fatalf("huge offset err = %v, want ErrChannelInvalid", err)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreDiscussionGroupLinksAreBidirectional(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 71, Phone: "+1890" + suffix + "01", FirstName: "DiscussionOwner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
var channelIDs []int64
|
|
t.Cleanup(func() {
|
|
if len(channelIDs) != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
create := func(title string, broadcast bool, date int) domain.CreateChannelResult {
|
|
t.Helper()
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: title,
|
|
Broadcast: broadcast,
|
|
Megagroup: !broadcast,
|
|
Date: date,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create %s: %v", title, err)
|
|
}
|
|
channelIDs = append(channelIDs, created.Channel.ID)
|
|
return created
|
|
}
|
|
broadcast := create("discussion broadcast "+suffix, true, 1700000820)
|
|
firstGroup := create("discussion first "+suffix, false, 1700000821)
|
|
secondGroup := create("discussion second "+suffix, false, 1700000822)
|
|
|
|
candidates, err := channels.ListDiscussionGroups(ctx, owner.ID, 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 groups = %+v, want newest creator megagroups", candidates)
|
|
}
|
|
|
|
linked, err := channels.SetDiscussionGroup(ctx, owner.ID, broadcast.Channel.ID, firstGroup.Channel.ID)
|
|
if err != nil {
|
|
t.Fatalf("link first discussion group: %v", err)
|
|
}
|
|
if len(linked.Channels) != 2 {
|
|
t.Fatalf("linked changed channels = %+v, want broadcast and group", linked.Channels)
|
|
}
|
|
gotBroadcast, err := channels.GetChannelByID(ctx, broadcast.Channel.ID)
|
|
if err != nil {
|
|
t.Fatalf("get linked broadcast: %v", err)
|
|
}
|
|
gotFirst, err := channels.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 %d group %d, want bidirectional", gotBroadcast.LinkedChatID, gotFirst.LinkedChatID)
|
|
}
|
|
|
|
replaced, err := channels.SetDiscussionGroup(ctx, owner.ID, 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, _ = channels.GetChannelByID(ctx, broadcast.Channel.ID)
|
|
gotFirst, _ = channels.GetChannelByID(ctx, firstGroup.Channel.ID)
|
|
gotSecond, err := channels.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)
|
|
}
|
|
|
|
if _, err := channels.SetDiscussionGroup(ctx, owner.ID, 0, secondGroup.Channel.ID); err != nil {
|
|
t.Fatalf("unlink from group side: %v", err)
|
|
}
|
|
gotBroadcast, _ = channels.GetChannelByID(ctx, broadcast.Channel.ID)
|
|
gotSecond, _ = channels.GetChannelByID(ctx, secondGroup.Channel.ID)
|
|
if gotBroadcast.LinkedChatID != 0 || gotSecond.LinkedChatID != 0 {
|
|
t.Fatalf("unlink = broadcast %d group %d, want both cleared", gotBroadcast.LinkedChatID, gotSecond.LinkedChatID)
|
|
}
|
|
if _, err := channels.SetDiscussionGroup(ctx, owner.ID, 0, secondGroup.Channel.ID); !errors.Is(err, domain.ErrLinkNotModified) {
|
|
t.Fatalf("repeat unlink err = %v, want ErrLinkNotModified", err)
|
|
}
|
|
if _, err := channels.SetPreHistoryHidden(ctx, owner.ID, firstGroup.Channel.ID, true); err != nil {
|
|
t.Fatalf("hide first group prehistory: %v", err)
|
|
}
|
|
if _, err := channels.SetDiscussionGroup(ctx, owner.ID, broadcast.Channel.ID, firstGroup.Channel.ID); !errors.Is(err, domain.ErrMegagroupPrehistoryHidden) {
|
|
t.Fatalf("hidden prehistory err = %v, want ErrMegagroupPrehistoryHidden", err)
|
|
}
|
|
}
|
|
|
|
func TestChannelStoreReadMessageContentsClearsVisibleUnreadReactions(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 81,
|
|
Phone: "+1891" + suffix + "01",
|
|
FirstName: "ReactionOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
friend, err := users.Create(ctx, domain.User{
|
|
AccessHash: 82,
|
|
Phone: "+1891" + suffix + "02",
|
|
FirstName: "ReactionFriend",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create friend: %v", err)
|
|
}
|
|
var channelID int64
|
|
t.Cleanup(func() {
|
|
if channelID != 0 {
|
|
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
|
}
|
|
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID})
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Visible Reaction " + suffix,
|
|
Megagroup: true,
|
|
MemberUserIDs: []int64{friend.ID},
|
|
Date: 1700000900,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
RandomID: 90_001,
|
|
Message: "react to this",
|
|
Date: 1700000901,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("send channel message: %v", err)
|
|
}
|
|
if _, err := channels.SetChannelMessageReactions(ctx, domain.SetChannelMessageReactionsRequest{
|
|
UserID: friend.ID,
|
|
ChannelID: channelID,
|
|
MessageID: sent.Message.ID,
|
|
Reactions: []domain.MessageReaction{{
|
|
Type: domain.MessageReactionEmoji,
|
|
Emoticon: "\U0001f525",
|
|
}},
|
|
Date: 1700000902,
|
|
}); err != nil {
|
|
t.Fatalf("set channel reaction: %v", err)
|
|
}
|
|
dialogs, err := channels.GetChannelDialogs(ctx, owner.ID, []int64{channelID})
|
|
if err != nil {
|
|
t.Fatalf("get owner 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 := channels.ListChannelUnreadReactions(ctx, owner.ID, domain.ChannelUnreadReactionsFilter{
|
|
ChannelID: channelID,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list unread reactions: %v", err)
|
|
}
|
|
if len(unread.Messages) != 1 || unread.Messages[0].ID != sent.Message.ID || unread.Messages[0].Reactions == nil || !hasUnreadChannelReactionPG(*unread.Messages[0].Reactions) {
|
|
t.Fatalf("unread reactions = %+v, want unread sent message", unread.Messages)
|
|
}
|
|
|
|
read, err := channels.ReadChannelMessageContents(ctx, domain.ReadChannelMessageContentsRequest{
|
|
UserID: owner.ID,
|
|
ChannelID: channelID,
|
|
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 || hasUnreadChannelReactionPG(*read.Messages[0].Reactions) {
|
|
t.Fatalf("read messages = %+v, want returned reaction marked read", read.Messages)
|
|
}
|
|
unreadAfter, err := channels.ListChannelUnreadReactions(ctx, owner.ID, domain.ChannelUnreadReactionsFilter{
|
|
ChannelID: channelID,
|
|
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 := channels.GetChannelDialogs(ctx, owner.ID, []int64{channelID})
|
|
if err != nil {
|
|
t.Fatalf("get owner dialogs after read contents: %v", err)
|
|
}
|
|
if len(dialogsAfter.Dialogs) != 1 || dialogsAfter.Dialogs[0].UnreadReactions != 0 {
|
|
t.Fatalf("owner dialogs after read contents = %+v, want unread reactions 0", dialogsAfter.Dialogs)
|
|
}
|
|
var stillUnread bool
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT unread
|
|
FROM channel_message_reactions
|
|
WHERE channel_id = $1 AND message_id = $2 AND reacted_user_id = $3`,
|
|
channelID, sent.Message.ID, friend.ID).Scan(&stillUnread); err != nil {
|
|
t.Fatalf("read reaction row: %v", err)
|
|
}
|
|
if stillUnread {
|
|
t.Fatal("reaction row still unread after read contents")
|
|
}
|
|
}
|
|
|
|
func hasUnreadChannelReactionPG(reactions domain.ChannelMessageReactions) bool {
|
|
for _, recent := range reactions.Recent {
|
|
if recent.Unread {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|