130 lines
5.3 KiB
Go
130 lines
5.3 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
func TestChannelStoreInviteBatchAdvancesDistinctReadModelsOncePostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
users := NewUserStore(pool)
|
|
|
|
owner, err := users.Create(ctx, domain.User{
|
|
AccessHash: 960001,
|
|
Phone: "+1960" + suffix + "00",
|
|
FirstName: "BatchInviteOwner",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
members := make([]domain.User, 8)
|
|
userIDs := make([]int64, len(members))
|
|
for i := range members {
|
|
members[i], err = users.Create(ctx, domain.User{
|
|
AccessHash: int64(960100 + i),
|
|
Phone: fmt.Sprintf("+1960%s%02d", suffix, i+1),
|
|
FirstName: fmt.Sprintf("BatchInvite%02d", i+1),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create member %d: %v", i, err)
|
|
}
|
|
// Deliberately reverse the input. The store must establish one canonical
|
|
// lock/write order independent of the request order.
|
|
userIDs[len(members)-1-i] = members[i].ID
|
|
}
|
|
allUserIDs := append([]int64{owner.ID}, userIDs...)
|
|
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[])`, allUserIDs)
|
|
})
|
|
|
|
channels := NewChannelStore(pool)
|
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: owner.ID,
|
|
Title: "Batch Invite " + suffix,
|
|
Megagroup: true,
|
|
Date: 1700019600,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelID = created.Channel.ID
|
|
|
|
version := func(model string, ownerID int64, peerType string, peerID int64) int64 {
|
|
t.Helper()
|
|
var got int64
|
|
err := pool.QueryRow(ctx, `
|
|
SELECT version
|
|
FROM read_model_versions
|
|
WHERE model=$1 AND owner_user_id=$2 AND peer_type=$3 AND peer_id=$4`, model, ownerID, peerType, peerID).Scan(&got)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return got
|
|
}
|
|
participantsBefore := version("channel_participants", 0, "channel", channelID)
|
|
dialogOwnerBefore := make(map[int64]int64, len(userIDs))
|
|
for _, userID := range userIDs {
|
|
dialogOwnerBefore[userID] = version("dialog_owner", userID, "user", userID)
|
|
}
|
|
|
|
invited, err := channels.InviteToChannel(ctx, channelID, owner.ID, userIDs, 1700019601)
|
|
if err != nil {
|
|
t.Fatalf("batch invite: %v", err)
|
|
}
|
|
if len(invited.Members) != len(userIDs) {
|
|
t.Fatalf("invited members = %d, want %d", len(invited.Members), len(userIDs))
|
|
}
|
|
if len(invited.Recipients) != 0 {
|
|
t.Fatalf("durable invite recipients = %v, want realtime audience derived from session fabric", invited.Recipients)
|
|
}
|
|
if invited.Event.Pts != created.Channel.Pts+1 || invited.Event.PtsCount != 1 || invited.Channel.Pts != invited.Event.Pts {
|
|
t.Fatalf("invite pts=(event:%d/%d channel:%d), want one slot after %d", invited.Event.Pts, invited.Event.PtsCount, invited.Channel.Pts, created.Channel.Pts)
|
|
}
|
|
if invited.Message.Action == nil || invited.Message.Action.Type != domain.ChannelActionChatAddUser || len(invited.Message.Action.UserIDs) != len(userIDs) {
|
|
t.Fatalf("invite service action = %+v, want all invited users", invited.Message.Action)
|
|
}
|
|
|
|
if got := version("channel_participants", 0, "channel", channelID); got != participantsBefore+1 {
|
|
t.Fatalf("channel participants version = %d, want %d", got, participantsBefore+1)
|
|
}
|
|
for _, userID := range userIDs {
|
|
if got := version("channel_member", userID, "channel", channelID); got != 1 {
|
|
t.Errorf("channel_member version user %d = %d, want 1", userID, got)
|
|
}
|
|
if got := version("dialog_light", userID, "channel", channelID); got != 1 {
|
|
t.Errorf("dialog_light version user %d = %d, want 1", userID, got)
|
|
}
|
|
if got := version("channel_active_memberships", userID, "user", userID); got != 1 {
|
|
t.Errorf("active memberships version user %d = %d, want 1", userID, got)
|
|
}
|
|
if got := version("dialog_owner", userID, "user", userID); got != dialogOwnerBefore[userID]+1 {
|
|
t.Errorf("dialog_owner version user %d = %d, want %d", userID, got, dialogOwnerBefore[userID]+1)
|
|
}
|
|
}
|
|
|
|
var memberRows, indexRows, dialogRows, adminRows int
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM channel_members WHERE channel_id=$1 AND user_id=ANY($2::bigint[]) AND status='active'`, channelID, userIDs).Scan(&memberRows); err != nil {
|
|
t.Fatalf("count member rows: %v", err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM user_channel_member_index WHERE channel_id=$1 AND user_id=ANY($2::bigint[]) AND status='active'`, channelID, userIDs).Scan(&indexRows); err != nil {
|
|
t.Fatalf("count membership indexes: %v", err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM channel_dialogs WHERE channel_id=$1 AND user_id=ANY($2::bigint[]) AND unread_count=1 AND unread_reactions_count=0`, channelID, userIDs).Scan(&dialogRows); err != nil {
|
|
t.Fatalf("count dialog rows: %v", err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM channel_admin_log_events WHERE channel_id=$1 AND event_type='participant_invite'`, channelID).Scan(&adminRows); err != nil {
|
|
t.Fatalf("count invite admin logs: %v", err)
|
|
}
|
|
if memberRows != len(userIDs) || indexRows != len(userIDs) || dialogRows != len(userIDs) || adminRows != len(userIDs) {
|
|
t.Fatalf("batch rows member/index/dialog/admin = %d/%d/%d/%d, want %d each", memberRows, indexRows, dialogRows, adminRows, len(userIDs))
|
|
}
|
|
}
|