chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
355
internal/store/postgres/message_read_edit_integration_test.go
Normal file
355
internal/store/postgres/message_read_edit_integration_test.go
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"telesrv/internal/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMessageStoreReadAndEditEmitDurableEvents(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
|
||||
users := NewUserStore(pool)
|
||||
sender, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 31,
|
||||
Phone: "+1666" + suffix + "11",
|
||||
FirstName: "ReadSender",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create sender: %v", err)
|
||||
}
|
||||
recipient, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 32,
|
||||
Phone: "+1666" + suffix + "12",
|
||||
FirstName: "ReadRecipient",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create recipient: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{sender.ID, recipient.ID})
|
||||
})
|
||||
|
||||
messages := NewMessageStore(pool)
|
||||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: sender.ID,
|
||||
RecipientUserID: recipient.ID,
|
||||
RandomID: 223344,
|
||||
Message: "before edit",
|
||||
Date: 1700000300,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText: %v", err)
|
||||
}
|
||||
read, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||||
OwnerUserID: recipient.ID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: sender.ID},
|
||||
Date: 1700000310,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ReadHistory: %v", err)
|
||||
}
|
||||
if !read.Changed || read.InboxEvent.Pts != 2 || read.InboxEvent.Type != domain.UpdateEventReadHistoryInbox || read.InboxEvent.MaxID != sent.RecipientMessage.ID {
|
||||
t.Fatalf("read inbox = %+v, want recipient pts=2 max recipient id", read)
|
||||
}
|
||||
if !read.OutboxChanged || read.OutboxEvent.Pts != 2 || read.OutboxEvent.Type != domain.UpdateEventReadHistoryOutbox || read.OutboxEvent.MaxID != sent.SenderMessage.ID {
|
||||
t.Fatalf("read outbox = %+v, want sender pts=2 max sender id", read)
|
||||
}
|
||||
readDate, err := messages.GetOutboxReadDate(ctx, domain.OutboxReadDateRequest{
|
||||
OwnerUserID: sender.ID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID},
|
||||
ID: sent.SenderMessage.ID,
|
||||
})
|
||||
if err != nil || readDate != 1700000310 {
|
||||
t.Fatalf("outbox read date = %d err=%v, want read date", readDate, err)
|
||||
}
|
||||
|
||||
edited, err := messages.EditMessage(ctx, domain.EditMessageRequest{
|
||||
OwnerUserID: sender.ID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID},
|
||||
ID: sent.SenderMessage.ID,
|
||||
Message: "after edit",
|
||||
EditDate: 1700000320,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EditMessage: %v", err)
|
||||
}
|
||||
if self := edited.Self(); self.Event.Pts != 3 || self.Event.Type != domain.UpdateEventEditMessage || self.Message.Body != "after edit" {
|
||||
t.Fatalf("self edit = %+v, want sender edit event pts=3", self)
|
||||
}
|
||||
recipientHistory, err := messages.ListByUser(ctx, recipient.ID, domain.MessageFilter{
|
||||
HasPeer: true,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: sender.ID},
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("recipient history: %v", err)
|
||||
}
|
||||
if len(recipientHistory.Messages) != 1 || recipientHistory.Messages[0].Body != "after edit" || recipientHistory.Messages[0].EditDate != 1700000320 {
|
||||
t.Fatalf("recipient history = %+v, want edited message visible", recipientHistory.Messages)
|
||||
}
|
||||
|
||||
senderEvents, err := NewUpdateEventStore(pool).ListAfter(ctx, sender.ID, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("sender events: %v", err)
|
||||
}
|
||||
if len(senderEvents) != 3 || senderEvents[1].Type != domain.UpdateEventReadHistoryOutbox || senderEvents[2].Type != domain.UpdateEventEditMessage {
|
||||
t.Fatalf("sender events = %+v, want new/read_outbox/edit", senderEvents)
|
||||
}
|
||||
recipientEvents, err := NewUpdateEventStore(pool).ListAfter(ctx, recipient.ID, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("recipient events: %v", err)
|
||||
}
|
||||
if len(recipientEvents) != 3 || recipientEvents[1].Type != domain.UpdateEventReadHistoryInbox || recipientEvents[2].Type != domain.UpdateEventEditMessage || recipientEvents[2].Message.Body != "after edit" {
|
||||
t.Fatalf("recipient events = %+v, want new/read_inbox/edit with edited body", recipientEvents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreReadHistoryStaleUnreadRepairDoesNotAppendPts(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
|
||||
users := NewUserStore(pool)
|
||||
sender, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 41,
|
||||
Phone: "+1666" + suffix + "21",
|
||||
FirstName: "StaleReadSender",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create sender: %v", err)
|
||||
}
|
||||
recipient, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 42,
|
||||
Phone: "+1666" + suffix + "22",
|
||||
FirstName: "StaleReadRecipient",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create recipient: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{sender.ID, recipient.ID})
|
||||
})
|
||||
|
||||
messages := NewMessageStore(pool)
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: sender.ID}
|
||||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: sender.ID,
|
||||
RecipientUserID: recipient.ID,
|
||||
RandomID: 323344,
|
||||
Message: "before stale repair",
|
||||
Date: 1700000300,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText: %v", err)
|
||||
}
|
||||
if _, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||||
OwnerUserID: recipient.ID,
|
||||
Peer: peer,
|
||||
MaxID: sent.RecipientMessage.ID,
|
||||
Date: 1700000310,
|
||||
}); err != nil {
|
||||
t.Fatalf("first ReadHistory: %v", err)
|
||||
}
|
||||
eventsBefore, err := NewUpdateEventStore(pool).ListAfter(ctx, recipient.ID, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("list events before repair: %v", err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `
|
||||
UPDATE dialogs
|
||||
SET unread_count = 1,
|
||||
unread_mentions_count = 1,
|
||||
unread_reactions_count = 1,
|
||||
unread_mark = true
|
||||
WHERE user_id = $1
|
||||
AND peer_type = $2
|
||||
AND peer_id = $3
|
||||
`, recipient.ID, string(domain.PeerTypeUser), sender.ID); err != nil {
|
||||
t.Fatalf("corrupt unread count: %v", err)
|
||||
}
|
||||
|
||||
read, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||||
OwnerUserID: recipient.ID,
|
||||
Peer: peer,
|
||||
MaxID: sent.RecipientMessage.ID,
|
||||
Date: 1700000320,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("second ReadHistory: %v", err)
|
||||
}
|
||||
if read.Changed || read.InboxEvent.Pts != 0 || read.OutboxChanged {
|
||||
t.Fatalf("stale unread repair = %+v, want no read pts/outbox event", read)
|
||||
}
|
||||
eventsAfter, err := NewUpdateEventStore(pool).ListAfter(ctx, recipient.ID, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("list events after repair: %v", err)
|
||||
}
|
||||
if len(eventsAfter) != len(eventsBefore) {
|
||||
t.Fatalf("recipient events after repair = %d, want unchanged %d", len(eventsAfter), len(eventsBefore))
|
||||
}
|
||||
var unreadCount, unreadMentions, unreadReactions int
|
||||
var unreadMark bool
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT unread_count, unread_mentions_count, unread_reactions_count, unread_mark
|
||||
FROM dialogs
|
||||
WHERE user_id = $1
|
||||
AND peer_type = $2
|
||||
AND peer_id = $3
|
||||
`, recipient.ID, string(domain.PeerTypeUser), sender.ID).Scan(&unreadCount, &unreadMentions, &unreadReactions, &unreadMark); err != nil {
|
||||
t.Fatalf("load repaired dialog: %v", err)
|
||||
}
|
||||
// readHistory 只清 message unread/mentions/manual mark;reaction 角标
|
||||
// 由 messages.readReactions 单独清除,与官方语义一致。
|
||||
if unreadCount != 0 || unreadMentions != 0 || unreadReactions != 1 || unreadMark {
|
||||
t.Fatalf("dialog unread fields = count %d mentions %d reactions %d mark %v, want history cleared with reaction badge preserved", unreadCount, unreadMentions, unreadReactions, unreadMark)
|
||||
}
|
||||
// stale counter 场景没有存活的 reaction_unread 行,readReactions 清 0
|
||||
// 行但仍要把角标计数自愈归零。
|
||||
if cleared, err := messages.ReadPeerReactions(ctx, recipient.ID, domain.Peer{Type: domain.PeerTypeUser, ID: sender.ID}); err != nil || cleared != 0 {
|
||||
t.Fatalf("ReadPeerReactions = %d err %v, want stale repair without live rows", cleared, err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT unread_reactions_count
|
||||
FROM dialogs
|
||||
WHERE user_id = $1 AND peer_type = $2 AND peer_id = $3
|
||||
`, recipient.ID, string(domain.PeerTypeUser), sender.ID).Scan(&unreadReactions); err != nil {
|
||||
t.Fatalf("load dialog after readReactions: %v", err)
|
||||
}
|
||||
if unreadReactions != 0 {
|
||||
t.Fatalf("unread reactions after readReactions = %d, want 0", unreadReactions)
|
||||
}
|
||||
next, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: sender.ID,
|
||||
RecipientUserID: recipient.ID,
|
||||
RandomID: 323345,
|
||||
Message: "after stale repair",
|
||||
Date: 1700000330,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("next SendPrivateText: %v", err)
|
||||
}
|
||||
if next.RecipientMessage.Pts != 3 {
|
||||
t.Fatalf("next recipient pts = %d, want 3 after no-op read repair", next.RecipientMessage.Pts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreReadHistoryClampsFutureMaxIDAndRepairsDialog(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
|
||||
users := NewUserStore(pool)
|
||||
sender, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 43,
|
||||
Phone: "+1667" + suffix + "11",
|
||||
FirstName: "FutureReadSender",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create sender: %v", err)
|
||||
}
|
||||
recipient, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 44,
|
||||
Phone: "+1667" + suffix + "12",
|
||||
FirstName: "FutureReadRecipient",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create recipient: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{sender.ID, recipient.ID})
|
||||
})
|
||||
|
||||
messages := NewMessageStore(pool)
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: sender.ID}
|
||||
first, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: sender.ID,
|
||||
RecipientUserID: recipient.ID,
|
||||
RandomID: 823101,
|
||||
Message: "first",
|
||||
Date: 1700000500,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText first: %v", err)
|
||||
}
|
||||
read, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||||
OwnerUserID: recipient.ID,
|
||||
Peer: peer,
|
||||
MaxID: domain.MaxMessageBoxID,
|
||||
Date: 1700000510,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ReadHistory future max: %v", err)
|
||||
}
|
||||
if read.MaxID != first.RecipientMessage.ID || read.InboxEvent.MaxID != first.RecipientMessage.ID {
|
||||
t.Fatalf("read = %+v, want max clamped to current top %d", read, first.RecipientMessage.ID)
|
||||
}
|
||||
|
||||
second, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: sender.ID,
|
||||
RecipientUserID: recipient.ID,
|
||||
RandomID: 823102,
|
||||
Message: "second",
|
||||
Date: 1700000520,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText second: %v", err)
|
||||
}
|
||||
var unreadCount, readInboxMaxID, topMessageID int
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT unread_count, read_inbox_max_id, top_message_id
|
||||
FROM dialogs
|
||||
WHERE user_id = $1
|
||||
AND peer_type = $2
|
||||
AND peer_id = $3
|
||||
`, recipient.ID, string(domain.PeerTypeUser), sender.ID).Scan(&unreadCount, &readInboxMaxID, &topMessageID); err != nil {
|
||||
t.Fatalf("load dialog after second send: %v", err)
|
||||
}
|
||||
if unreadCount != 1 || readInboxMaxID != first.RecipientMessage.ID || topMessageID != second.RecipientMessage.ID {
|
||||
t.Fatalf("dialog after second = unread %d read %d top %d, want unread=1 read=%d top=%d",
|
||||
unreadCount, readInboxMaxID, topMessageID, first.RecipientMessage.ID, second.RecipientMessage.ID)
|
||||
}
|
||||
|
||||
if _, err := pool.Exec(ctx, `
|
||||
UPDATE dialogs
|
||||
SET read_inbox_max_id = $4,
|
||||
unread_count = 0
|
||||
WHERE user_id = $1
|
||||
AND peer_type = $2
|
||||
AND peer_id = $3
|
||||
`, recipient.ID, string(domain.PeerTypeUser), sender.ID, domain.MaxMessageBoxID); err != nil {
|
||||
t.Fatalf("corrupt future read watermark: %v", err)
|
||||
}
|
||||
if _, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||||
OwnerUserID: recipient.ID,
|
||||
Peer: peer,
|
||||
MaxID: domain.MaxMessageBoxID,
|
||||
Date: 1700000530,
|
||||
}); err != nil {
|
||||
t.Fatalf("ReadHistory repair future watermark: %v", err)
|
||||
}
|
||||
third, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: sender.ID,
|
||||
RecipientUserID: recipient.ID,
|
||||
RandomID: 823103,
|
||||
Message: "third",
|
||||
Date: 1700000540,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText third: %v", err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT unread_count, read_inbox_max_id, top_message_id
|
||||
FROM dialogs
|
||||
WHERE user_id = $1
|
||||
AND peer_type = $2
|
||||
AND peer_id = $3
|
||||
`, recipient.ID, string(domain.PeerTypeUser), sender.ID).Scan(&unreadCount, &readInboxMaxID, &topMessageID); err != nil {
|
||||
t.Fatalf("load dialog after third send: %v", err)
|
||||
}
|
||||
if unreadCount != 1 || readInboxMaxID != second.RecipientMessage.ID || topMessageID != third.RecipientMessage.ID {
|
||||
t.Fatalf("dialog after repair+third = unread %d read %d top %d, want unread=1 read=%d top=%d",
|
||||
unreadCount, readInboxMaxID, topMessageID, second.RecipientMessage.ID, third.RecipientMessage.ID)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue