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

428 lines
15 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"
)
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 TestMessageStoreEditCanHideEditedBadge(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
sender, err := users.Create(ctx, domain.User{
AccessHash: 331,
Phone: "+1666" + suffix + "31",
FirstName: "HiddenEditSender",
})
if err != nil {
t.Fatalf("create sender: %v", err)
}
recipient, err := users.Create(ctx, domain.User{
AccessHash: 332,
Phone: "+1666" + suffix + "32",
FirstName: "HiddenEditRecipient",
})
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: 223399,
Message: "...",
Date: 1700000400,
})
if err != nil {
t.Fatalf("SendPrivateText: %v", 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: "streamed answer",
EditDate: 1700000405,
HideEdited: true,
})
if err != nil {
t.Fatalf("EditMessage: %v", err)
}
if self := edited.Self(); self.Message.Body != "streamed answer" || !self.Message.HideEdited {
t.Fatalf("self hidden edit = %+v, want hidden edited message", self)
}
history, 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(history.Messages) != 1 || history.Messages[0].Body != "streamed answer" || !history.Messages[0].HideEdited {
t.Fatalf("recipient history = %+v, want hidden edited message", history.Messages)
}
events, err := NewUpdateEventStore(pool).ListAfter(ctx, recipient.ID, 0, 10)
if err != nil {
t.Fatalf("recipient events: %v", err)
}
if len(events) != 2 || events[1].Type != domain.UpdateEventEditMessage || !events[1].Message.HideEdited {
t.Fatalf("recipient events = %+v, want hidden edit event", events)
}
}
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 markreaction 角标
// 由 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)
}
}