chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,259 @@
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 → slice,offset 翻页到尾页。
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)
}
// reorder:bob 也置顶后强排为 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, message_date, body, entities)
VALUES ($1, $1, $2::bigint, 1700000800, 'legacy', '[]'::jsonb)
RETURNING id`, owner.ID, 841100+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])
}
}