Ctrl K
ComunidadGuías…

internal/store/postgres/channel_discussion_read_target_integration_test.go

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

Archivo original ↗Preguntar a KiBot
package postgres

import (
	"context"
	"errors"
	"testing"

	"telesrv/internal/domain"
)

// TestResolveDiscussionReadTargetPostgres locks the one-query readDiscussion
// projection against the real schema. It verifies linked-root mapping and the
// durable idempotent boundary without invoking the full discussion aggregate.
func TestResolveDiscussionReadTargetPostgres(t *testing.T) {
	pool := testPool(t)
	ctx := context.Background()
	suffix := randomSuffix(t)
	users := NewUserStore(pool)
	owner, err := users.Create(ctx, domain.User{
		AccessHash: 991,
		Phone:      "+1991" + suffix + "01",
		FirstName:  "DiscussionReadOwner",
	})
	if err != nil {
		t.Fatalf("create owner: %v", err)
	}
	subscriber, err := users.Create(ctx, domain.User{
		AccessHash: 992,
		Phone:      "+1992" + suffix + "02",
		FirstName:  "DiscussionGuest",
	})
	if err != nil {
		t.Fatalf("create subscriber: %v", err)
	}
	channels := NewChannelStore(pool,
		WithChannelRowCache(NewChannelRowCache(32)),
		WithChannelMemberCache(NewChannelMemberCache(64)))
	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, subscriber.ID})
	})
	broadcast, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: owner.ID,
		Title:         "Discussion Read Source " + suffix,
		Broadcast:     true,
		Date:          1700002900,
	})
	if err != nil {
		t.Fatalf("create broadcast: %v", err)
	}
	channelIDs = append(channelIDs, broadcast.Channel.ID)
	group, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
		CreatorUserID: owner.ID,
		Title:         "Discussion Read Group " + suffix,
		Megagroup:     true,
		Date:          1700002901,
	})
	if err != nil {
		t.Fatalf("create group: %v", err)
	}
	channelIDs = append(channelIDs, group.Channel.ID)
	if _, err := channels.SetDiscussionGroup(ctx, owner.ID, broadcast.Channel.ID, group.Channel.ID); err != nil {
		t.Fatalf("set discussion group: %v", err)
	}
	if _, err := channels.InviteToChannel(ctx, broadcast.Channel.ID, owner.ID, []int64{subscriber.ID}, 1700002902); err != nil {
		t.Fatalf("invite broadcast subscriber: %v", err)
	}
	post, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
		UserID:    owner.ID,
		ChannelID: broadcast.Channel.ID,
		RandomID:  9912901,
		Message:   "discussion read target",
		Date:      1700002902,
	})
	if err != nil {
		t.Fatalf("send post: %v", err)
	}
	if post.Discussion == nil {
		t.Fatal("send post discussion result is nil")
	}
	rootID := post.Discussion.Message.ID
	guestView, err := channels.GetChannel(ctx, subscriber.ID, group.Channel.ID)
	if err != nil || !guestView.Self.Guest || guestView.Self.Status != domain.ChannelMemberLeft {
		t.Fatalf("linked guest view = %+v err %v", guestView.Self, err)
	}
	guestTarget, err := channels.ResolveDiscussionReadTarget(ctx, subscriber.ID, broadcast.Channel.ID, post.Message.ID, rootID)
	if err != nil || !guestTarget.Guest {
		t.Fatalf("linked guest read target = %+v err %v", guestTarget, err)
	}
	directGuestTarget, err := channels.ResolveDiscussionReadTarget(ctx, subscriber.ID, group.Channel.ID, rootID, rootID)
	if err != nil || !directGuestTarget.Guest || directGuestTarget.ChannelID != group.Channel.ID {
		t.Fatalf("direct linked-group guest read target = %+v err %v", directGuestTarget, err)
	}
	if _, err := channels.ListActiveChannelBotMemberIDs(ctx, subscriber.ID, group.Channel.ID, 20); err != nil {
		t.Fatalf("linked guest bot-delivery preflight: %v", err)
	}
	if _, err := channels.ListActiveChannelBotMembers(ctx, subscriber.ID, group.Channel.ID, 0, 20); err != nil {
		t.Fatalf("linked guest bot participants: %v", err)
	}
	if _, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
		UserID: subscriber.ID, ChannelID: group.Channel.ID, RandomID: 9912902,
		Message: "guest comment", Date: 1700002903,
		ReplyTo: &domain.MessageReply{MessageID: rootID, TopMessageID: rootID},
	}); err != nil {
		t.Fatalf("send linked guest comment: %v", err)
	}
	var persistedGuest bool
	if err := pool.QueryRow(ctx, `SELECT EXISTS (
SELECT 1 FROM channel_members WHERE channel_id = $1 AND user_id = $2
)`, group.Channel.ID, subscriber.ID).Scan(&persistedGuest); err != nil {
		t.Fatalf("check guest member row: %v", err)
	}
	if persistedGuest {
		t.Fatal("linked guest send persisted a discussion-group member row")
	}
	target, err := channels.ResolveDiscussionReadTarget(ctx, owner.ID, broadcast.Channel.ID, post.Message.ID, rootID)
	if err != nil {
		t.Fatalf("resolve before read: %v", err)
	}
	if target.ChannelID != group.Channel.ID || target.RootID != rootID || target.AlreadyRead {
		t.Fatalf("target before read = %+v, want linked unread root", target)
	}
	read, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
		UserID:    owner.ID,
		ChannelID: group.Channel.ID,
		MaxID:     rootID,
		Date:      1700002903,
	})
	if err != nil || !read.Changed {
		t.Fatalf("read linked group = %+v err %v, want changed", read, err)
	}
	target, err = channels.ResolveDiscussionReadTarget(ctx, owner.ID, broadcast.Channel.ID, post.Message.ID, rootID)
	if err != nil || !target.AlreadyRead {
		t.Fatalf("resolve after read = %+v err %v, want already read", target, err)
	}
	if _, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
		UserID: subscriber.ID, ChannelID: group.Channel.ID, RandomID: 9912903,
		ReplyTo: &domain.MessageReply{TopMessageID: rootID + 10000},
		Media:   &domain.MessageMedia{Kind: domain.MessageMediaKindContact, Contact: &domain.MessageContact{PhoneNumber: "+1991", FirstName: "Bad"}},
		Date:    1700002904,
	}); !errors.Is(err, domain.ErrReplyMessageIDInvalid) {
		t.Fatalf("invalid top-only media reply err=%v, want ErrReplyMessageIDInvalid", err)
	}
	mediaComment, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
		UserID: subscriber.ID, ChannelID: group.Channel.ID, RandomID: 9912904,
		ReplyTo: &domain.MessageReply{TopMessageID: rootID},
		Media:   &domain.MessageMedia{Kind: domain.MessageMediaKindContact, Contact: &domain.MessageContact{PhoneNumber: "+1991", FirstName: "Media"}},
		Date:    1700002905,
	})
	if err != nil {
		t.Fatalf("send top-only media comment: %v", err)
	}
	if mediaComment.Message.ReplyTo == nil || mediaComment.Message.ReplyTo.MessageID != rootID || mediaComment.Message.ReplyTo.TopMessageID != rootID || mediaComment.Message.Pts != post.Discussion.Message.Pts+2 {
		t.Fatalf("top-only media comment = %+v, want root %d and contiguous channel pts", mediaComment.Message, rootID)
	}
	var persistedTop int
	if err := pool.QueryRow(ctx, `SELECT reply_to_top_id FROM channel_messages WHERE channel_id=$1 AND id=$2`, group.Channel.ID, mediaComment.Message.ID).Scan(&persistedTop); err != nil || persistedTop != rootID {
		t.Fatalf("persisted media reply top=%d err=%v, want %d", persistedTop, err, rootID)
	}
}