Ctrl K
ComunidadGuías…

internal/store/postgres/channel_difference_integration_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package postgres

import (
	"context"
	"telesrv/internal/domain"
	"telesrv/internal/store"
	"testing"
)

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 TestChannelStorePublicPreviewDifferenceReplaysVisibleMessages(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.Events) != 1 || len(diff.NewMessages) != 1 || len(diff.OtherUpdates) != 0 {
		t.Fatalf("preview diff = %+v, want one durable public preview message at pts %d", diff, sent.Event.Pts)
	}
	if diff.NewMessages[0].ID != sent.Message.ID || diff.NewMessages[0].Body != sent.Message.Body {
		t.Fatalf("preview diff message = %+v, want sent message %+v", diff.NewMessages[0], sent.Message)
	}
	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)
	}
	edited, err := channels.EditChannelMessage(ctx, domain.EditChannelMessageRequest{
		UserID: owner.ID, ChannelID: channelID, ID: sent.Message.ID,
		Message: "public preview edited", EditDate: 1700000372,
	})
	if err != nil {
		t.Fatalf("edit public preview message: %v", err)
	}
	pinned, err := channels.UpdatePinnedMessage(ctx, domain.UpdateChannelPinnedMessageRequest{
		UserID: owner.ID, ChannelID: channelID, MessageID: sent.Message.ID,
		Pinned: true, Date: 1700000373,
	})
	if err != nil {
		t.Fatalf("pin public preview message: %v", err)
	}
	deleted, err := channels.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{
		UserID: owner.ID, ChannelID: channelID, IDs: []int{sent.Message.ID}, Date: 1700000374,
	})
	if err != nil {
		t.Fatalf("delete public preview message: %v", err)
	}
	mutations, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
		UserID: viewer.ID, ChannelID: channelID, Pts: sent.Event.Pts, Limit: 10,
	})
	if err != nil {
		t.Fatalf("list public preview mutations: %v", err)
	}
	if !mutations.Final || mutations.Pts != deleted.Event.Pts || len(mutations.NewMessages) != 0 || len(mutations.OtherUpdates) != 3 {
		t.Fatalf("public preview mutations = %+v, want edit/pin/delete through pts %d", mutations, deleted.Event.Pts)
	}
	wantTypes := []domain.ChannelUpdateEventType{
		domain.ChannelUpdateEditMessage,
		domain.ChannelUpdatePinnedMessages,
		domain.ChannelUpdateDeleteMessages,
	}
	for i, want := range wantTypes {
		if mutations.OtherUpdates[i].Type != want {
			t.Fatalf("public preview mutation[%d] = %+v, want %s", i, mutations.OtherUpdates[i], want)
		}
	}
	if edited.Event.Pts >= pinned.Event.Pts || pinned.Event.Pts >= deleted.Event.Pts {
		t.Fatalf("public preview mutation pts = edit %d pin %d delete %d, want strictly increasing", edited.Event.Pts, pinned.Event.Pts, deleted.Event.Pts)
	}
}

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
	sendReq := domain.SendChannelMessageRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		RandomID:  941,
		Message:   "original",
		Date:      1700000381,
	}
	sent, err := channels.SendChannelMessage(ctx, sendReq)
	if err != nil {
		t.Fatalf("send channel message: %v", err)
	}
	fingerprint, err := store.ChannelSendFingerprint(sendReq)
	if err != nil {
		t.Fatalf("fingerprint channel message: %v", err)
	}
	replayReq := domain.ChannelSendReplayRequest{ChannelID: channelID, SenderUserID: owner.ID, RandomID: sent.Message.RandomID, IdempotencyFingerprint: fingerprint}
	type replayState struct {
		pts    int
		events int
	}
	loadReplayState := func() replayState {
		t.Helper()
		var state replayState
		if err := pool.QueryRow(ctx, `SELECT pts FROM channels WHERE id = $1`, channelID).Scan(&state.pts); err != nil {
			t.Fatalf("load channel pts: %v", err)
		}
		if err := pool.QueryRow(ctx, `SELECT count(*) FROM channel_update_events WHERE channel_id = $1`, channelID).Scan(&state.events); err != nil {
			t.Fatalf("count channel events: %v", err)
		}
		return state
	}
	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)
	}
	beforeReplay := loadReplayState()
	duplicate, found, err := channels.LookupChannelSendReplay(ctx, replayReq)
	if err != nil {
		t.Fatalf("duplicate channel message: %v", err)
	}
	if !found || !duplicate.Duplicate || duplicate.Event.Type != domain.ChannelUpdateNewMessage || duplicate.Message.Body != "second edit" || duplicate.Event.Message.Body != "second edit" || duplicate.Event.Pts != sent.Event.Pts {
		t.Fatalf("duplicate after edit = %+v found=%v, want current snapshot with first-send pts", duplicate, found)
	}
	if after := loadReplayState(); after != beforeReplay {
		t.Fatalf("edit replay mutated channel state = %+v, want %+v", after, beforeReplay)
	}

	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)
	}
	deleted, err := channels.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{
		UserID: owner.ID, ChannelID: channelID, IDs: []int{sent.Message.ID}, Date: 1700000384,
	})
	if err != nil {
		t.Fatalf("delete channel message: %v", err)
	}
	beforeReplay = loadReplayState()
	duplicate, found, err = channels.LookupChannelSendReplay(ctx, replayReq)
	if err != nil {
		t.Fatalf("duplicate deleted channel message: %v", err)
	}
	if !found || !duplicate.Duplicate || duplicate.Message.Body != "original" || duplicate.Message.Pts != sent.Message.Pts || duplicate.Event.Pts != sent.Event.Pts {
		t.Fatalf("duplicate after delete = %+v found=%v, want immutable first-send snapshot", duplicate, found)
	}
	if duplicate.ReplayDeleteEvent == nil || duplicate.ReplayDeleteEvent.Pts != deleted.Event.Pts || len(duplicate.ReplayDeleteEvent.MessageIDs) != 1 || duplicate.ReplayDeleteEvent.MessageIDs[0] != sent.Message.ID {
		t.Fatalf("duplicate delete receipt = %+v, want durable event %+v", duplicate.ReplayDeleteEvent, deleted.Event)
	}
	if after := loadReplayState(); after != beforeReplay {
		t.Fatalf("delete replay mutated channel state = %+v, want %+v", after, beforeReplay)
	}
}

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, created.Channel.Pts).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 rollback check",
		Date:      1700000402,
	})
	if err != nil {
		t.Fatalf("send owner after gap: %v", err)
	}
	if sent.Event.Pts != created.Channel.Pts+1 {
		t.Fatalf("next channel pts = %d, want %d after failed send before pts allocation", sent.Event.Pts, created.Channel.Pts+1)
	}
	diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		Pts:       created.Channel.Pts,
		Limit:     10,
	})
	if err != nil {
		t.Fatalf("list channel difference: %v", err)
	}
	if diff.Pts != sent.Event.Pts || len(diff.Events) != 1 || diff.Events[0].Type != domain.ChannelUpdateNewMessage || diff.Events[0].Pts != sent.Event.Pts {
		t.Fatalf("diff after failed send = %+v, want only message pts=%d", diff, sent.Event.Pts)
	}
}

func TestChannelDifferenceStopsAtPtsHole(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)

	owner, err := NewUserStore(pool).Create(ctx, domain.User{
		AccessHash: 49,
		Phone:      "+1888" + suffix + "31",
		FirstName:  "ChannelHoleOwner",
	})
	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:         "Channel Hole " + suffix,
		Megagroup:     true,
		Date:          1700000460,
	})
	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:  995,
		Message:   "missing event",
		Date:      1700000461,
	})
	if err != nil {
		t.Fatalf("send first: %v", err)
	}
	second, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		RandomID:  996,
		Message:   "after hole",
		Date:      1700000462,
	})
	if err != nil {
		t.Fatalf("send second: %v", err)
	}
	if _, err := pool.Exec(ctx, "DELETE FROM channel_update_events WHERE channel_id = $1 AND pts = $2", channelID, first.Event.Pts); err != nil {
		t.Fatalf("delete channel event: %v", err)
	}

	diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
		UserID:    owner.ID,
		ChannelID: channelID,
		Pts:       created.Channel.Pts,
		Limit:     10,
	})
	if err != nil {
		t.Fatalf("list difference: %v", err)
	}
	if diff.Pts != created.Channel.Pts || len(diff.Events) != 0 || diff.Final {
		t.Fatalf("diff across hole = %+v, second pts=%d; want stop at previous pts", diff, second.Event.Pts)
	}
}

func TestReserveChannelPtsRollsBackWithTransaction(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)

	owner, err := NewUserStore(pool).Create(ctx, domain.User{
		AccessHash: 50,
		Phone:      "+1888" + suffix + "41",
		FirstName:  "ChannelPtsRollback",
	})
	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:         "Channel Pts Rollback " + suffix,
		Megagroup:     true,
		Date:          1700000470,
	})
	if err != nil {
		t.Fatalf("create channel: %v", err)
	}
	channelID = created.Channel.ID
	tx, err := pool.Begin(ctx)
	if err != nil {
		t.Fatalf("begin: %v", err)
	}
	allocated, err := channels.reserveChannelPts(ctx, tx, channelID)
	if err != nil {
		_ = tx.Rollback(ctx)
		t.Fatalf("reserve channel pts: %v", err)
	}
	if allocated != created.Channel.Pts+1 {
		_ = tx.Rollback(ctx)
		t.Fatalf("allocated channel pts = %d, want %d", allocated, created.Channel.Pts+1)
	}
	if err := tx.Rollback(ctx); err != nil {
		t.Fatalf("rollback: %v", err)
	}
	got, err := channels.MaxChannelPts(ctx, channelID)
	if err != nil {
		t.Fatalf("MaxChannelPts: %v", err)
	}
	if got != created.Channel.Pts {
		t.Fatalf("channel pts after rollback = %d, want unchanged %d", got, created.Channel.Pts)
	}
	batch, err := channels.MaxChannelPtsBatch(ctx, []int64{channelID, -channelID, channelID})
	if err != nil {
		t.Fatalf("MaxChannelPtsBatch: %v", err)
	}
	if len(batch) != 1 || batch[channelID] != created.Channel.Pts {
		t.Fatalf("batch channel pts = %v, want only %d:%d", batch, channelID, created.Channel.Pts)
	}
}

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)
	}
}