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

267 lines
11 KiB
Go
Raw 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"
"testing"
"telesrv/internal/domain"
)
// TestMessageStoreSavedDialogsLifecycle 覆盖收藏夹分会话 PG 主链路:
// 直发/转发的 saved_peer 写入、fwd saved_from 持久化(重读非内存 echo
// 聚合分页、置顶、按子会话历史过滤与删除(含置顶清理)。
func TestMessageStoreSavedDialogsLifecycle(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
alice, err := users.Create(ctx, domain.User{AccessHash: 91, Phone: "+1672" + suffix + "01", FirstName: "SavedAlice"})
if err != nil {
t.Fatalf("create alice: %v", err)
}
bob, err := users.Create(ctx, domain.User{AccessHash: 92, Phone: "+1672" + suffix + "02", FirstName: "SavedBob"})
if err != nil {
t.Fatalf("create bob: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{alice.ID, bob.ID})
_, _ = pool.Exec(ctx, "DELETE FROM saved_dialog_pins WHERE user_id = ANY($1::bigint[])", []int64{alice.ID, bob.ID})
})
messages := NewMessageStore(pool)
selfPeer := domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID}
bobPeer := domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID}
// 直发笔记 → saved_peer = self。
note, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: alice.ID,
RecipientUserID: alice.ID,
RandomID: 841001,
Message: "note to self",
Date: 1700000700,
})
if err != nil {
t.Fatalf("send note: %v", err)
}
// bob 私聊来信 → alice 转发进收藏夹 → saved_peer = bob 且 fwd saved_from 持久化。
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: bob.ID,
RecipientUserID: alice.ID,
RandomID: 841002,
Message: "from bob",
Date: 1700000701,
}); err != nil {
t.Fatalf("bob send: %v", err)
}
aliceInbox, err := messages.ListByUser(ctx, alice.ID, domain.MessageFilter{
HasPeer: true, Peer: bobPeer, Limit: 1,
})
if err != nil || len(aliceInbox.Messages) != 1 {
t.Fatalf("alice inbox = %+v err %v", aliceInbox.Messages, err)
}
fwdRes, err := messages.ForwardPrivateMessages(ctx, domain.ForwardPrivateMessagesRequest{
OwnerUserID: alice.ID,
ToUserID: alice.ID,
FromPeer: bobPeer,
MessageIDs: []int{aliceInbox.Messages[0].ID},
RandomIDs: []int64{841003},
Date: 1700000702,
})
if err != nil || len(fwdRes.SenderMessages) != 1 {
t.Fatalf("forward to saved: %+v err %v", fwdRes, err)
}
fwdID := fwdRes.SenderMessages[0].ID
// 重读 self-chat非发送 echo验证持久化saved_peer 与 fwd saved_from 都在。
selfHistory, err := messages.ListByUser(ctx, alice.ID, domain.MessageFilter{
HasPeer: true, Peer: selfPeer, Limit: 10,
})
if err != nil || len(selfHistory.Messages) != 2 {
t.Fatalf("self history = %+v err %v, want note+forward", selfHistory.Messages, err)
}
for _, msg := range selfHistory.Messages {
switch msg.ID {
case note.SenderMessage.ID:
if msg.SavedPeer != selfPeer {
t.Fatalf("note saved_peer = %+v, want self", msg.SavedPeer)
}
case fwdID:
if msg.SavedPeer != bobPeer {
t.Fatalf("forward saved_peer = %+v, want bob", msg.SavedPeer)
}
if msg.Forward == nil || msg.Forward.SavedFrom != bobPeer || msg.Forward.SavedFromMsgID != aliceInbox.Messages[0].ID {
t.Fatalf("forward header = %+v, want persisted saved_from bob/%d", msg.Forward, aliceInbox.Messages[0].ID)
}
default:
t.Fatalf("unexpected self-chat message %d", msg.ID)
}
}
// 聚合:转发更晚在前;分页 limit=1 → sliceoffset 翻页到尾页。
list, err := messages.ListSavedDialogs(ctx, alice.ID, domain.SavedDialogsFilter{Limit: 20})
if err != nil {
t.Fatalf("list saved dialogs: %v", err)
}
if !list.Full || list.Count != 2 || len(list.Dialogs) != 2 ||
list.Dialogs[0].Peer != bobPeer || list.Dialogs[0].TopMessage != fwdID ||
list.Dialogs[1].Peer != selfPeer || list.Dialogs[1].TopMessage != note.SenderMessage.ID {
t.Fatalf("saved dialogs = %+v count %d full %v, want bob then self", list.Dialogs, list.Count, list.Full)
}
page, err := messages.ListSavedDialogs(ctx, alice.ID, domain.SavedDialogsFilter{Limit: 1})
if err != nil || page.Full || len(page.Dialogs) != 1 || page.Dialogs[0].Peer != bobPeer || page.Count != 2 {
t.Fatalf("first page = %+v err %v, want slice bob count 2", page, err)
}
tail, err := messages.ListSavedDialogs(ctx, alice.ID, domain.SavedDialogsFilter{Limit: 1, OffsetID: fwdID})
if err != nil || !tail.Full || len(tail.Dialogs) != 1 || tail.Dialogs[0].Peer != selfPeer {
t.Fatalf("tail page = %+v err %v, want full self", tail, err)
}
// 置顶self 置顶后首页在前、exclude 口径、ByPeers/pinned 一致。
changed, err := messages.ToggleSavedDialogPin(ctx, alice.ID, selfPeer, true)
if err != nil || !changed {
t.Fatalf("pin self = %v err %v", changed, err)
}
if changed, err = messages.ToggleSavedDialogPin(ctx, alice.ID, selfPeer, true); err != nil || changed {
t.Fatalf("re-pin self = %v err %v, want no-op", changed, err)
}
list, err = messages.ListSavedDialogs(ctx, alice.ID, domain.SavedDialogsFilter{Limit: 20})
if err != nil || len(list.Dialogs) != 2 || list.Dialogs[0].Peer != selfPeer || !list.Dialogs[0].Pinned {
t.Fatalf("after pin = %+v err %v, want pinned self first", list.Dialogs, err)
}
excluded, err := messages.ListSavedDialogs(ctx, alice.ID, domain.SavedDialogsFilter{ExcludePinned: true, Limit: 20})
if err != nil || excluded.Count != 1 || len(excluded.Dialogs) != 1 || excluded.Dialogs[0].Peer != bobPeer {
t.Fatalf("exclude pinned = %+v err %v, want only bob count 1", excluded, err)
}
pinned, err := messages.ListPinnedSavedDialogs(ctx, alice.ID)
if err != nil || len(pinned.Dialogs) != 1 || pinned.Dialogs[0].Peer != selfPeer {
t.Fatalf("pinned list = %+v err %v, want self", pinned.Dialogs, err)
}
byPeers, err := messages.ListSavedDialogsByPeers(ctx, alice.ID, []domain.Peer{bobPeer, {Type: domain.PeerTypeUser, ID: 424242}})
if err != nil || len(byPeers.Dialogs) != 1 || byPeers.Dialogs[0].Peer != bobPeer {
t.Fatalf("by peers = %+v err %v, want bob only", byPeers.Dialogs, err)
}
// reorderbob 也置顶后强排为 bob→self。
if _, err := messages.ToggleSavedDialogPin(ctx, alice.ID, bobPeer, true); err != nil {
t.Fatalf("pin bob: %v", err)
}
if err := messages.ReorderPinnedSavedDialogs(ctx, alice.ID, []domain.Peer{bobPeer, selfPeer}, true); err != nil {
t.Fatalf("reorder: %v", err)
}
pinned, err = messages.ListPinnedSavedDialogs(ctx, alice.ID)
if err != nil || len(pinned.Dialogs) != 2 || pinned.Dialogs[0].Peer != bobPeer || pinned.Dialogs[1].Peer != selfPeer {
t.Fatalf("after reorder = %+v err %v, want bob then self", pinned.Dialogs, err)
}
// 子会话历史过滤。
bobHistory, err := messages.ListByUser(ctx, alice.ID, domain.MessageFilter{
HasPeer: true, Peer: selfPeer, SavedPeer: bobPeer, Limit: 10, NeedTotalCount: true,
})
if err != nil || len(bobHistory.Messages) != 1 || bobHistory.Messages[0].ID != fwdID || bobHistory.Count != 1 {
t.Fatalf("bob sublist history = %+v count %d err %v", bobHistory.Messages, bobHistory.Count, err)
}
// 删除 bob 子会话:消息消失、置顶清理、带 pts 的 delete 事件。
delRes, err := messages.DeleteSavedHistory(ctx, domain.DeleteSavedHistoryRequest{
OwnerUserID: alice.ID,
SavedPeer: bobPeer,
Date: 1700000710,
})
if err != nil {
t.Fatalf("delete saved history: %v", err)
}
if delRes.More || len(delRes.MessageIDs) != 1 || delRes.MessageIDs[0] != fwdID || delRes.Event.Pts == 0 || delRes.Event.PtsCount != 1 {
t.Fatalf("delete result = %+v, want fwd deleted with pts", delRes)
}
list, err = messages.ListSavedDialogs(ctx, alice.ID, domain.SavedDialogsFilter{Limit: 20})
if err != nil || len(list.Dialogs) != 1 || list.Dialogs[0].Peer != selfPeer || list.Count != 1 {
t.Fatalf("after delete = %+v count %d err %v, want only self", list.Dialogs, list.Count, err)
}
pinned, err = messages.ListPinnedSavedDialogs(ctx, alice.ID)
if err != nil || len(pinned.Dialogs) != 1 || pinned.Dialogs[0].Peer != selfPeer {
t.Fatalf("pins after delete = %+v err %v, want bob pin cleared", pinned.Dialogs, err)
}
}
// TestSavedDialogsBackfillRule 验证 0087 回填 CASE 规则与 TDLib 语义对齐:
// fwd 作者可见或无 fwd → self仅 from_name → hidden author 2666000。
// migration 已在建库时应用这里手工插入老格式行saved_peer_type=”)后
// 重放回填语句验证规则本身。
func TestSavedDialogsBackfillRule(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
owner, err := users.Create(ctx, domain.User{AccessHash: 93, Phone: "+1673" + suffix + "01", FirstName: "BackfillOwner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
})
insert := func(boxID int, fwdPeerID int64, fwdName string, fwdDate int) {
t.Helper()
var privateID int64
if err := pool.QueryRow(ctx, `
INSERT INTO private_messages (
sender_user_id, recipient_user_id, random_id, request_fingerprint, recipient_delivered,
sender_box_id, sender_pts, recipient_box_id, recipient_pts,
message_date, body, entities
)
VALUES (
$1, $1, $2::bigint, decode(repeat('00', 32), 'hex'), false,
$3::int, $3::int, 0, 0,
1700000800, 'legacy', '[]'::jsonb
)
RETURNING id`, owner.ID, 841100+boxID, boxID).Scan(&privateID); err != nil {
t.Fatalf("insert legacy private message %d: %v", boxID, err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO message_boxes (
owner_user_id, box_id, private_message_id, message_sender_id,
peer_type, peer_id, from_user_id, message_date, outgoing, body, entities, pts,
fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date
) VALUES ($1, $2::int, $3::bigint, $1, 'user', $1, $1, 1700000800, true, 'legacy', '[]'::jsonb, $2::int,
CASE WHEN $4::bigint <> 0 THEN 'user' ELSE '' END, $4::bigint, $5, $6::int)`,
owner.ID, boxID, privateID, fwdPeerID, fwdName, fwdDate); err != nil {
t.Fatalf("insert legacy row %d: %v", boxID, err)
}
}
insert(1, 0, "", 0) // 直发笔记 → self
insert(2, 4242, "", 1700000500) // fwd 作者可见、saved_from 丢失 → self
insert(3, 0, "Hidden", 1700000501) // 仅 from_name → 2666000
if _, err := pool.Exec(ctx, `
UPDATE message_boxes
SET saved_peer_type = 'user',
saved_peer_id = CASE
WHEN fwd_date <> 0 AND fwd_from_peer_id = 0 AND fwd_from_name <> '' THEN 2666000
ELSE owner_user_id
END
WHERE peer_type = 'user'
AND peer_id = owner_user_id
AND saved_peer_type = ''`); err != nil {
t.Fatalf("replay backfill: %v", err)
}
messages := NewMessageStore(pool)
list, err := messages.ListSavedDialogs(ctx, owner.ID, domain.SavedDialogsFilter{Limit: 20})
if err != nil {
t.Fatalf("list after backfill: %v", err)
}
if len(list.Dialogs) != 2 {
t.Fatalf("backfill dialogs = %+v, want self + hidden author", list.Dialogs)
}
hidden := domain.Peer{Type: domain.PeerTypeUser, ID: domain.SavedHiddenAuthorUserID}
self := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
if list.Dialogs[0].Peer != hidden || list.Dialogs[0].TopMessage != 3 {
t.Fatalf("hidden sublist = %+v, want top 3", list.Dialogs[0])
}
if list.Dialogs[1].Peer != self || list.Dialogs[1].TopMessage != 2 {
t.Fatalf("self sublist = %+v, want top 2 (visible-author legacy forward)", list.Dialogs[1])
}
}