owpengram-server/internal/store/postgres/channel_topic_read_integration_test.go

263 lines
9.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package postgres
import (
"context"
"telesrv/internal/domain"
"testing"
)
// TestChannelTopicReadIsolation 验证 forum 话题级已读不被频道级单一水位串扰:
// 交错发送使 topicB 的消息 id 都小于 topicA 的最新消息 id读 topicA 到最新后,
// 旧的频道级水位会把 topicB 误判已读,而 per-topic 水位下 topicB 未读保持不变。
// 同时验证 outbox 已读回执S6
func TestChannelTopicReadIsolation(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: "+1778" + suffix + "81", FirstName: "TROwner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
member, err := users.Create(ctx, domain.User{AccessHash: 82, Phone: "+1778" + suffix + "82", FirstName: "TRMember"})
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: "TopicRead " + suffix,
Megagroup: true,
Forum: true,
MemberUserIDs: []int64{member.ID},
Date: 1700002100,
})
if err != nil {
t.Fatalf("create forum channel: %v", err)
}
channelID = created.Channel.ID
topicA, err := channels.CreateForumTopic(ctx, domain.CreateChannelForumTopicRequest{UserID: owner.ID, ChannelID: channelID, Title: "A " + suffix, RandomID: 222001, Date: 1700002101})
if err != nil {
t.Fatalf("topic A: %v", err)
}
topicB, err := channels.CreateForumTopic(ctx, domain.CreateChannelForumTopicRequest{UserID: owner.ID, ChannelID: channelID, Title: "B " + suffix, RandomID: 222002, Date: 1700002102})
if err != nil {
t.Fatalf("topic B: %v", err)
}
send := func(rid int64, topicID, date int) domain.ChannelMessage {
t.Helper()
res, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: owner.ID,
ChannelID: channelID,
RandomID: rid,
Message: "m",
ReplyTo: &domain.MessageReply{TopMessageID: topicID, ForumTopic: true},
Date: date,
})
if err != nil {
t.Fatalf("send to topic %d: %v", topicID, err)
}
return res.Message
}
// 交错发送topicB 两条都在 topicA 最新一条之前id 更小。
_ = send(222003, topicA.Topic.TopicID, 1700002103) // A1
_ = send(222004, topicB.Topic.TopicID, 1700002104) // B1
_ = send(222005, topicB.Topic.TopicID, 1700002105) // B2
a2 := send(222006, topicA.Topic.TopicID, 1700002106) // A2全局最大 id
if u := topicUnread(t, channels, ctx, member.ID, channelID, topicA.Topic.TopicID); u != 2 {
t.Fatalf("before read: topicA unread = %d, want 2", u)
}
if u := topicUnread(t, channels, ctx, member.ID, channelID, topicB.Topic.TopicID); u != 2 {
t.Fatalf("before read: topicB unread = %d, want 2", u)
}
res, err := channels.ReadChannelTopicHistory(ctx, domain.ReadChannelTopicHistoryRequest{
UserID: member.ID,
ChannelID: channelID,
TopicID: topicA.Topic.TopicID,
MaxID: a2.ID,
Date: 1700002110,
})
if err != nil {
t.Fatalf("read topic A: %v", err)
}
if !res.Changed || res.MaxID != a2.ID {
t.Fatalf("read topic A result = %+v, want changed with maxID %d", res, a2.ID)
}
// 关键断言:读 topicA 后 topicA 未读=0topicB 未读仍=2不被频道级高水位污染
if u := topicUnread(t, channels, ctx, member.ID, channelID, topicA.Topic.TopicID); u != 0 {
t.Fatalf("after read: topicA unread = %d, want 0", u)
}
if u := topicUnread(t, channels, ctx, member.ID, channelID, topicB.Topic.TopicID); u != 2 {
t.Fatalf("after read: topicB unread = %d, want 2 (cross-topic contamination)", u)
}
// 幂等:再读一次同水位 → Changed=false。
again, err := channels.ReadChannelTopicHistory(ctx, domain.ReadChannelTopicHistoryRequest{
UserID: member.ID, ChannelID: channelID, TopicID: topicA.Topic.TopicID, MaxID: a2.ID, Date: 1700002111,
})
if err != nil || again.Changed {
t.Fatalf("re-read topic A = %+v err %v, want not changed", again, err)
}
// outbox 回执S6owner 在 topicA 的消息被 member 读到 a2 → 回执含 owner。
found := false
for _, o := range res.OutboxUpdates {
if o.UserID == owner.ID && o.MaxID == a2.ID {
found = true
}
}
if !found {
t.Fatalf("outbox updates = %+v, want owner %d at %d", res.OutboxUpdates, owner.ID, a2.ID)
}
}
func topicUnread(t *testing.T, channels *ChannelStore, ctx context.Context, userID, channelID int64, topicID int) int {
t.Helper()
list, err := channels.GetForumTopicsByID(ctx, userID, channelID, []int{topicID})
if err != nil {
t.Fatalf("get topic %d: %v", topicID, err)
}
for _, topic := range list.Topics {
if topic.TopicID == topicID {
return topic.UnreadCount
}
}
t.Fatalf("topic %d not found in %+v", topicID, list.Topics)
return -1
}
// TestChannelGeneralTopicRead 验证 General 话题id=1独立已读General 未读排除其它话题的根
// 服务消息,且 ReadChannelTopicHistory(General) 只推进 General 自己的水位。
func TestChannelGeneralTopicRead(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
owner, err := users.Create(ctx, domain.User{AccessHash: 83, Phone: "+1778" + suffix + "83", FirstName: "GenOwner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
member, err := users.Create(ctx, domain.User{AccessHash: 84, Phone: "+1778" + suffix + "84", FirstName: "GenMember"})
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: "General " + suffix, Megagroup: true, Forum: true,
MemberUserIDs: []int64{member.ID}, Date: 1700003100,
})
if err != nil {
t.Fatalf("create forum channel: %v", err)
}
channelID = created.Channel.ID
// 建一个普通话题:其根服务消息 reply_to_top_id=0必须被 General 现算排除。
if _, err := channels.CreateForumTopic(ctx, domain.CreateChannelForumTopicRequest{
UserID: owner.ID, ChannelID: channelID, Title: "Topic " + suffix, RandomID: 333009, Date: 1700003101,
}); err != nil {
t.Fatalf("create topic: %v", err)
}
sendGeneral := func(rid int64, date int) domain.ChannelMessage {
t.Helper()
// General 消息按真实客户端行为不带 top_msg_idreply_to_top_id=0由 General 现算归并。
res, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: owner.ID,
ChannelID: channelID,
RandomID: rid,
Message: "g",
Date: date,
})
if err != nil {
t.Fatalf("send general: %v", err)
}
return res.Message
}
// 基线:建话题后、发 General 消息前的 General 未读(话题根已排除,仅含频道创建系统消息)。
base, err := channels.GeneralForumTopic(ctx, member.ID, channelID)
if err != nil {
t.Fatalf("general baseline: %v", err)
}
_ = sendGeneral(333010, 1700003110)
g2 := sendGeneral(333011, 1700003111)
// 基线之后新建话题 A2 并发一条消息A2 的根服务消息与普通消息都绝不能计入 General。
topicA2, err := channels.CreateForumTopic(ctx, domain.CreateChannelForumTopicRequest{
UserID: owner.ID, ChannelID: channelID, Title: "A2 " + suffix, RandomID: 333012, Date: 1700003112,
})
if err != nil {
t.Fatalf("create topic A2: %v", err)
}
a1, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: owner.ID, ChannelID: channelID, RandomID: 333013, Message: "a",
ReplyTo: &domain.MessageReply{TopMessageID: topicA2.Topic.TopicID, ForumTopic: true}, Date: 1700003113,
})
if err != nil {
t.Fatalf("send topic A2 message: %v", err)
}
gen, err := channels.GeneralForumTopic(ctx, member.ID, channelID)
if err != nil {
t.Fatalf("general topic: %v", err)
}
// 只多 2 条 General 消息A2 根服务消息与 a1 都不计入(否则差为 4
if gen.UnreadCount != base.UnreadCount+2 {
t.Fatalf("general unread = %d, want baseline %d + 2 (A2 root+message excluded)", gen.UnreadCount, base.UnreadCount)
}
if gen.TopMessageID != g2.ID {
t.Fatalf("general top message = %d, want %d (A2 root/message excluded)", gen.TopMessageID, g2.ID)
}
// 读话题 A2 不污染 General。
if _, err := channels.ReadChannelTopicHistory(ctx, domain.ReadChannelTopicHistoryRequest{
UserID: member.ID, ChannelID: channelID, TopicID: topicA2.Topic.TopicID, MaxID: a1.Message.ID, Date: 1700003120,
}); err != nil {
t.Fatalf("read topic A2: %v", err)
}
genAfterTopic, err := channels.GeneralForumTopic(ctx, member.ID, channelID)
if err != nil {
t.Fatalf("general after topic read: %v", err)
}
if genAfterTopic.UnreadCount != gen.UnreadCount {
t.Fatalf("reading topic A2 changed general unread %d -> %d (contamination)", gen.UnreadCount, genAfterTopic.UnreadCount)
}
// 读 General 清零并推进 General 自己的水位。
res, err := channels.ReadChannelTopicHistory(ctx, domain.ReadChannelTopicHistoryRequest{
UserID: member.ID, ChannelID: channelID, TopicID: domain.ForumGeneralTopicID, MaxID: g2.ID, Date: 1700003121,
})
if err != nil || !res.Changed {
t.Fatalf("read general = %+v err %v, want changed", res, err)
}
genAfter, err := channels.GeneralForumTopic(ctx, member.ID, channelID)
if err != nil {
t.Fatalf("general after read: %v", err)
}
if genAfter.UnreadCount != 0 || genAfter.ReadInboxMaxID != g2.ID {
t.Fatalf("general after read = %+v, want unread 0 read_inbox %d", genAfter, g2.ID)
}
}