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

251 lines
8.6 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"
"errors"
"strings"
"testing"
"telesrv/internal/domain"
)
// TestChannelMultiPin 验证超级群多置顶模型多条共存、filterPinned 搜索、
// 上限、unpinAll 批量清除、删除被置顶消息后的缓存重算。
func TestChannelMultiPin(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
owner, err := users.Create(ctx, domain.User{AccessHash: 91, Phone: "+1676" + suffix + "01", FirstName: "MultiPinOwner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
member, err := users.Create(ctx, domain.User{AccessHash: 92, Phone: "+1676" + suffix + "02", FirstName: "MultiPinMember"})
if err != nil {
t.Fatalf("create member: %v", err)
}
t.Cleanup(func() {
_, _ = 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: "MultiPin " + suffix,
Megagroup: true,
MemberUserIDs: []int64{member.ID},
Date: 1700000920,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
channelID := created.Channel.ID
const pinCount = 11
ids := make([]int, 0, pinCount)
for i := 0; i < pinCount; i++ {
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: owner.ID,
ChannelID: channelID,
RandomID: int64(852000 + i),
Message: "pin-target",
Date: 1700000921 + i,
})
if err != nil {
t.Fatalf("send %d: %v", i, err)
}
ids = append(ids, sent.Message.ID)
}
pin := func(id int, pinned bool) (domain.UpdateChannelPinnedMessageResult, error) {
return channels.UpdatePinnedMessage(ctx, domain.UpdateChannelPinnedMessageRequest{
UserID: owner.ID,
ChannelID: channelID,
MessageID: id,
Pinned: pinned,
Date: 1700000950,
})
}
// 多条共存pin 前三条,互不替代。
for _, id := range ids[:3] {
res, err := pin(id, true)
if err != nil {
t.Fatalf("pin %d: %v", id, err)
}
if !res.Event.Pinned || len(res.Event.MessageIDs) != 1 || res.Event.MessageIDs[0] != id {
t.Fatalf("pin event = %+v, want pinned [%d]", res.Event, id)
}
}
view, err := channels.GetChannel(ctx, owner.ID, channelID)
if err != nil {
t.Fatalf("get channel: %v", err)
}
if view.Channel.PinnedMessageID != ids[2] {
t.Fatalf("pinned_message_id = %d, want latest pinned %d", view.Channel.PinnedMessageID, ids[2])
}
history, err := channels.ListChannelHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
ChannelID: channelID, PinnedOnly: true, Limit: 50,
})
if err != nil {
t.Fatalf("filterPinned search: %v", err)
}
if len(history.Messages) != 3 {
t.Fatalf("filterPinned messages = %d, want 3 coexisting pins: %+v", len(history.Messages), history.Messages)
}
for _, msg := range history.Messages {
if !msg.Pinned {
t.Fatalf("filterPinned message %d lacks pinned flag", msg.ID)
}
}
// 普通历史热路径保留 bounded has-more hint不为每页额外 COUNT。
hint, err := channels.ListChannelHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
ChannelID: channelID, PinnedOnly: true, Limit: 1,
})
if err != nil {
t.Fatalf("filterPinned hint page: %v", err)
}
if len(hint.Messages) != 1 || hint.Count != 2 {
t.Fatalf("filterPinned hint messages/count = %d/%d, want 1/2", len(hint.Messages), hint.Count)
}
// messages.search 首屏显式请求精确总数;页大小不能污染 Count。
exact, err := channels.ListChannelHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
ChannelID: channelID, PinnedOnly: true, Limit: 1, NeedTotalCount: true,
})
if err != nil {
t.Fatalf("filterPinned exact page: %v", err)
}
if len(exact.Messages) != 1 || exact.Count != 3 {
t.Fatalf("filterPinned exact messages/count = %d/%d, want 1/3", len(exact.Messages), exact.Count)
}
// messages.getSearchCounters/limit=0 只计数,不加载消息及 reply/reaction companion。
countOnly, err := channels.ListChannelHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
ChannelID: channelID, PinnedOnly: true, NeedTotalCount: true, CountOnly: true,
})
if err != nil {
t.Fatalf("filterPinned count-only: %v", err)
}
if len(countOnly.Messages) != 0 || countOnly.Count != 3 {
t.Fatalf("filterPinned count-only messages/count = %d/%d, want 0/3", len(countOnly.Messages), countOnly.Count)
}
// 精确计数只扫描该频道的 pinned 部分索引,不能退化为全频道消息扫描。
tx, err := pool.Begin(ctx)
if err != nil {
t.Fatalf("begin pinned count explain: %v", err)
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, "SET LOCAL enable_seqscan = off"); err != nil {
t.Fatalf("disable seqscan for pinned count explain: %v", err)
}
rows, err := tx.Query(ctx, `EXPLAIN (COSTS OFF)
SELECT count(*)::int
FROM channel_messages
WHERE channel_id = $1 AND NOT deleted AND pinned`, channelID)
if err != nil {
t.Fatalf("explain pinned count: %v", err)
}
var plan strings.Builder
for rows.Next() {
var line string
if err := rows.Scan(&line); err != nil {
rows.Close()
t.Fatalf("scan pinned count plan: %v", err)
}
plan.WriteString(line)
plan.WriteByte('\n')
}
if err := rows.Err(); err != nil {
rows.Close()
t.Fatalf("read pinned count plan: %v", err)
}
rows.Close()
if !strings.Contains(plan.String(), "channel_messages_live_pinned_idx") {
t.Fatalf("pinned count plan misses partial index:\n%s", plan.String())
}
// 普通历史页的消息行直接携带 pinned 标志(多置顶都标,不只最新)。
page, err := channels.ListChannelHistory(ctx, member.ID, domain.ChannelHistoryFilter{ChannelID: channelID, Limit: 50})
if err != nil {
t.Fatalf("list history: %v", err)
}
flagged := 0
for _, msg := range page.Messages {
if msg.Pinned {
flagged++
}
}
if flagged != 3 {
t.Fatalf("history pinned flags = %d, want 3", flagged)
}
// unpin 最新一条:其它置顶保留,缓存回落到次新。
if _, err := pin(ids[2], false); err != nil {
t.Fatalf("unpin latest: %v", err)
}
view, err = channels.GetChannel(ctx, owner.ID, channelID)
if err != nil {
t.Fatalf("get channel after unpin: %v", err)
}
if view.Channel.PinnedMessageID != ids[1] {
t.Fatalf("pinned_message_id after unpin = %d, want %d", view.Channel.PinnedMessageID, ids[1])
}
// 无数量上限(对齐官方):剩余全部 pin 上,共 pinCount 条同时置顶。
for _, id := range ids[2:] {
if _, err := pin(id, true); err != nil {
t.Fatalf("pin %d: %v", id, err)
}
}
history, err = channels.ListChannelHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
ChannelID: channelID, PinnedOnly: true, Limit: 50,
})
if err != nil {
t.Fatalf("filterPinned all pinned: %v", err)
}
if len(history.Messages) != pinCount {
t.Fatalf("filterPinned messages = %d, want %d coexisting pins", len(history.Messages), pinCount)
}
// 删除被置顶的最新消息:置顶集合自动排除,缓存重算。
latest := ids[pinCount-1]
if _, err := channels.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{
UserID: owner.ID, ChannelID: channelID, IDs: []int{latest}, Date: 1700000961,
}); err != nil {
t.Fatalf("delete pinned message: %v", err)
}
view, err = channels.GetChannel(ctx, owner.ID, channelID)
if err != nil {
t.Fatalf("get channel after delete: %v", err)
}
if view.Channel.PinnedMessageID != ids[pinCount-2] {
t.Fatalf("pinned_message_id after delete = %d, want %d", view.Channel.PinnedMessageID, ids[pinCount-2])
}
// unpinAll一条事件批量携带剩余全部置顶 id缓存清零再次调用 no-op。
res, err := channels.UnpinAllChannelMessages(ctx, domain.UnpinAllChannelMessagesRequest{
UserID: owner.ID, ChannelID: channelID, Date: 1700000962,
})
if err != nil {
t.Fatalf("unpin all: %v", err)
}
if res.Event.Pinned || len(res.Event.MessageIDs) != pinCount-1 {
t.Fatalf("unpin all event = %+v, want pinned=false with %d ids", res.Event, pinCount-1)
}
if res.Channel.PinnedMessageID != 0 {
t.Fatalf("pinned_message_id after unpin all = %d, want 0", res.Channel.PinnedMessageID)
}
if _, err := channels.UnpinAllChannelMessages(ctx, domain.UnpinAllChannelMessagesRequest{
UserID: owner.ID, ChannelID: channelID, Date: 1700000963,
}); !errors.Is(err, domain.ErrChannelNotModified) {
t.Fatalf("unpin all again err = %v, want ErrChannelNotModified", err)
}
history, err = channels.ListChannelHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
ChannelID: channelID, PinnedOnly: true, Limit: 50,
})
if err != nil {
t.Fatalf("filterPinned after unpin all: %v", err)
}
if len(history.Messages) != 0 {
t.Fatalf("filterPinned after unpin all = %+v, want empty", history.Messages)
}
}