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