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

599 lines
20 KiB
Go

package postgres
import (
"context"
"telesrv/internal/domain"
"testing"
)
func TestMessageStoreDeleteMessagesRecomputesRecipientUnreadAndTop(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
sender, err := users.Create(ctx, domain.User{
AccessHash: 61,
Phone: "+1668" + suffix + "01",
FirstName: "DeleteSender",
})
if err != nil {
t.Fatalf("create sender: %v", err)
}
recipient, err := users.Create(ctx, domain.User{
AccessHash: 62,
Phone: "+1668" + suffix + "02",
FirstName: "DeleteRecipient",
})
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)
bodies := []string{"one", "two", "three"}
sent := make([]domain.SendPrivateTextResult, 0, len(bodies))
for i, body := range bodies {
got, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: sender.ID,
RecipientUserID: recipient.ID,
RandomID: 824000 + int64(i),
Message: body,
Date: 1700000500 + i,
})
if err != nil {
t.Fatalf("SendPrivateText %q: %v", body, err)
}
sent = append(sent, got)
}
deleted, err := messages.DeleteMessages(ctx, domain.DeleteMessagesRequest{
OwnerUserID: sender.ID,
IDs: []int{sent[2].SenderMessage.ID},
Revoke: true,
Date: 1700000510,
})
if err != nil {
t.Fatalf("DeleteMessages revoke latest: %v", err)
}
if len(deleted.Deleted) != 2 || !deleted.Changed() {
t.Fatalf("deleted = %+v, want both owner boxes revoked", deleted)
}
var unreadCount, actualUnread, topMessageID int
var topBody string
if err := pool.QueryRow(ctx, `
WITH target AS (
SELECT d.unread_count, d.top_message_id, d.read_inbox_max_id, COALESCE(m.body, '') AS top_body
FROM dialogs d
LEFT JOIN message_boxes m
ON m.owner_user_id = d.user_id
AND m.box_id = d.top_message_id
AND NOT m.deleted
WHERE d.user_id = $1
AND d.peer_type = $2
AND d.peer_id = $3
),
actual AS (
SELECT COUNT(*)::int AS unread
FROM message_boxes
WHERE owner_user_id = $1
AND peer_type = $2
AND peer_id = $3
AND NOT deleted
AND NOT outgoing
AND box_id > (SELECT read_inbox_max_id FROM target)
)
SELECT target.unread_count, target.top_message_id, target.top_body, actual.unread
FROM target CROSS JOIN actual
`, recipient.ID, string(domain.PeerTypeUser), sender.ID).Scan(&unreadCount, &topMessageID, &topBody, &actualUnread); err != nil {
t.Fatalf("load recipient dialog after revoke: %v", err)
}
if unreadCount != 2 || actualUnread != 2 || topMessageID != sent[1].RecipientMessage.ID || topBody != "two" {
t.Fatalf("recipient dialog after revoke = unread %d actual %d top %d body %q, want unread=2 top=%d body=two",
unreadCount, actualUnread, topMessageID, topBody, sent[1].RecipientMessage.ID)
}
events, err := NewUpdateEventStore(pool).ListAfter(ctx, recipient.ID, 3, 10)
if err != nil {
t.Fatalf("list recipient events after revoke: %v", err)
}
if len(events) != 1 ||
events[0].Type != domain.UpdateEventDeleteMessages ||
events[0].Pts != 4 ||
events[0].PtsCount != 1 ||
!sameInts(events[0].MessageIDs, []int{sent[2].RecipientMessage.ID}) {
t.Fatalf("recipient events after revoke = %+v, want delete only; later unread messages are still live", events)
}
}
func TestMessageStoreDeleteMiddleUnreadKeepsRecipientTop(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
sender, err := users.Create(ctx, domain.User{
AccessHash: 63,
Phone: "+1669" + suffix + "01",
FirstName: "MiddleDeleteSender",
})
if err != nil {
t.Fatalf("create sender: %v", err)
}
recipient, err := users.Create(ctx, domain.User{
AccessHash: 64,
Phone: "+1669" + suffix + "02",
FirstName: "MiddleDeleteRecipient",
})
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)
recipientPeer := domain.Peer{Type: domain.PeerTypeUser, ID: sender.ID}
first, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: sender.ID,
RecipientUserID: recipient.ID,
RandomID: 824999,
Message: "already read",
Date: 1700000590,
})
if err != nil {
t.Fatalf("SendPrivateText first: %v", err)
}
if _, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
OwnerUserID: recipient.ID,
Peer: recipientPeer,
MaxID: first.RecipientMessage.ID,
Date: 1700000595,
}); err != nil {
t.Fatalf("ReadHistory first: %v", err)
}
bodies := []string{"one", "two", "three"}
sent := make([]domain.SendPrivateTextResult, 0, len(bodies))
for i, body := range bodies {
got, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: sender.ID,
RecipientUserID: recipient.ID,
RandomID: 825000 + int64(i),
Message: body,
Date: 1700000600 + i,
})
if err != nil {
t.Fatalf("SendPrivateText %q: %v", body, err)
}
sent = append(sent, got)
}
if _, err := messages.DeleteMessages(ctx, domain.DeleteMessagesRequest{
OwnerUserID: sender.ID,
IDs: []int{sent[1].SenderMessage.ID},
Revoke: true,
Date: 1700000610,
}); err != nil {
t.Fatalf("DeleteMessages revoke middle: %v", err)
}
var unreadCount, actualUnread, topMessageID, readInboxMaxID int
var topBody string
if err := pool.QueryRow(ctx, `
WITH target AS (
SELECT d.unread_count, d.top_message_id, d.read_inbox_max_id, COALESCE(m.body, '') AS top_body
FROM dialogs d
LEFT JOIN message_boxes m
ON m.owner_user_id = d.user_id
AND m.box_id = d.top_message_id
AND NOT m.deleted
WHERE d.user_id = $1
AND d.peer_type = $2
AND d.peer_id = $3
),
actual AS (
SELECT COUNT(*)::int AS unread
FROM message_boxes
WHERE owner_user_id = $1
AND peer_type = $2
AND peer_id = $3
AND NOT deleted
AND NOT outgoing
AND box_id > (SELECT read_inbox_max_id FROM target)
)
SELECT target.unread_count, target.top_message_id, target.read_inbox_max_id, target.top_body, actual.unread
FROM target CROSS JOIN actual
`, recipient.ID, string(domain.PeerTypeUser), sender.ID).Scan(&unreadCount, &topMessageID, &readInboxMaxID, &topBody, &actualUnread); err != nil {
t.Fatalf("load recipient dialog after middle revoke: %v", err)
}
if unreadCount != 2 || actualUnread != 2 || topMessageID != sent[2].RecipientMessage.ID || readInboxMaxID != first.RecipientMessage.ID || topBody != "three" {
t.Fatalf("recipient dialog after middle revoke = unread %d actual %d top %d read %d body %q, want unread=2 top=%d read=%d body=three",
unreadCount, actualUnread, topMessageID, readInboxMaxID, topBody, sent[2].RecipientMessage.ID, first.RecipientMessage.ID)
}
events, err := NewUpdateEventStore(pool).ListAfter(ctx, recipient.ID, 5, 10)
if err != nil {
t.Fatalf("list recipient events after middle revoke: %v", err)
}
if len(events) != 1 ||
events[0].Type != domain.UpdateEventDeleteMessages ||
events[0].Pts != 6 ||
events[0].PtsCount != 1 ||
!sameInts(events[0].MessageIDs, []int{sent[1].RecipientMessage.ID}) {
t.Fatalf("recipient events after middle revoke = %+v, want delete only; first unread is still live", events)
}
}
func TestMessageStoreDeleteFirstUnreadEmitsSafeUnreadCorrection(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
sender, err := users.Create(ctx, domain.User{
AccessHash: 65,
Phone: "+1670" + suffix + "01",
FirstName: "FirstUnreadDeleteSender",
})
if err != nil {
t.Fatalf("create sender: %v", err)
}
recipient, err := users.Create(ctx, domain.User{
AccessHash: 66,
Phone: "+1670" + suffix + "02",
FirstName: "FirstUnreadDeleteRecipient",
})
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)
recipientPeer := domain.Peer{Type: domain.PeerTypeUser, ID: sender.ID}
first, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: sender.ID,
RecipientUserID: recipient.ID,
RandomID: 826000,
Message: "already read",
Date: 1700000640,
})
if err != nil {
t.Fatalf("SendPrivateText first: %v", err)
}
if _, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
OwnerUserID: recipient.ID,
Peer: recipientPeer,
MaxID: first.RecipientMessage.ID,
Date: 1700000645,
}); err != nil {
t.Fatalf("ReadHistory first: %v", err)
}
bodies := []string{"one", "two", "three"}
sent := make([]domain.SendPrivateTextResult, 0, len(bodies))
for i, body := range bodies {
got, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: sender.ID,
RecipientUserID: recipient.ID,
RandomID: 826001 + int64(i),
Message: body,
Date: 1700000650 + i,
})
if err != nil {
t.Fatalf("SendPrivateText %q: %v", body, err)
}
sent = append(sent, got)
}
if _, err := messages.DeleteMessages(ctx, domain.DeleteMessagesRequest{
OwnerUserID: sender.ID,
IDs: []int{sent[0].SenderMessage.ID},
Revoke: true,
Date: 1700000660,
}); err != nil {
t.Fatalf("DeleteMessages revoke first unread: %v", err)
}
events, err := NewUpdateEventStore(pool).ListAfter(ctx, recipient.ID, 5, 10)
if err != nil {
t.Fatalf("list recipient events after first unread revoke: %v", err)
}
if len(events) != 2 ||
events[0].Type != domain.UpdateEventDeleteMessages ||
events[0].Pts != 6 ||
events[0].PtsCount != 1 ||
!sameInts(events[0].MessageIDs, []int{sent[0].RecipientMessage.ID}) ||
events[1].Type != domain.UpdateEventReadHistoryInbox ||
events[1].Pts != 7 ||
events[1].PtsCount != 1 ||
events[1].MaxID != sent[0].RecipientMessage.ID ||
events[1].StillUnreadCount != 2 {
t.Fatalf("recipient events after first unread revoke = %+v, want delete then prefix unread correction", events)
}
}
func TestMessageStoreDeleteHistoryRebuildsDialogAndEmitsDeleteUpdates(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
sender := createTestUser(t, ctx, users, "+1991"+suffix+"01", "DeleteSender", "")
recipient := createTestUser(t, ctx, users, "+1991"+suffix+"02", "DeleteRecipient", "")
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{sender.ID, recipient.ID})
})
messages := NewMessageStore(pool)
for i := 0; i < 2; i++ {
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: sender.ID,
RecipientUserID: recipient.ID,
RandomID: int64(7000 + i),
Message: "history",
Date: 1700000700 + i,
}); err != nil {
t.Fatalf("seed send %d: %v", i, err)
}
}
peer := domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID}
deleted, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
OwnerUserID: sender.ID,
Peer: peer,
Date: 1700000800,
})
if err != nil {
t.Fatalf("DeleteHistory: %v", err)
}
if self := deleted.Self(); self.Event.Pts != 4 || self.Event.PtsCount != 2 || len(self.MessageIDs) != 2 {
t.Fatalf("delete result = %+v, want sender delete range pts=4 count=2 ids", self)
}
senderHistory, err := messages.ListByUser(ctx, sender.ID, domain.MessageFilter{HasPeer: true, Peer: peer, Limit: 10})
if err != nil {
t.Fatalf("sender history: %v", err)
}
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(senderHistory.Messages) != 0 || len(recipientHistory.Messages) != 2 {
t.Fatalf("history sizes sender=%d recipient=%d, want sender cleared only", len(senderHistory.Messages), len(recipientHistory.Messages))
}
senderDialogs, err := NewDialogStore(pool).ListByUser(ctx, sender.ID, domain.DialogFilter{Limit: 10})
if err != nil {
t.Fatalf("sender dialogs after delete: %v", err)
}
if len(senderDialogs.Dialogs) != 0 {
t.Fatalf("sender dialogs = %+v, want empty after full history delete", senderDialogs.Dialogs)
}
events, err := NewUpdateEventStore(pool).ListAfter(ctx, sender.ID, 2, 10)
if err != nil {
t.Fatalf("list sender events: %v", err)
}
if len(events) != 1 || events[0].Type != domain.UpdateEventDeleteMessages || events[0].Pts != 4 || events[0].PtsCount != 2 || len(events[0].MessageIDs) != 2 {
t.Fatalf("events = %+v, want delete messages event pts=4 pts_count=2", events)
}
rebuilt, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: sender.ID,
RecipientUserID: recipient.ID,
RandomID: 8000,
Message: "after clear",
Date: 1700000900,
})
if err != nil {
t.Fatalf("send after delete: %v", err)
}
senderDialogs, err = NewDialogStore(pool).ListByUser(ctx, sender.ID, domain.DialogFilter{Limit: 10})
if err != nil {
t.Fatalf("sender dialogs after rebuild: %v", err)
}
if len(senderDialogs.Dialogs) != 1 || senderDialogs.Dialogs[0].Peer != peer || senderDialogs.Dialogs[0].TopMessage != rebuilt.SenderMessage.ID {
t.Fatalf("rebuilt dialogs = %+v, want new top message %d", senderDialogs.Dialogs, rebuilt.SenderMessage.ID)
}
revoked, err := messages.DeleteMessages(ctx, domain.DeleteMessagesRequest{
OwnerUserID: sender.ID,
IDs: []int{rebuilt.SenderMessage.ID},
Revoke: true,
Date: 1700001000,
})
if err != nil {
t.Fatalf("DeleteMessages revoke: %v", err)
}
if len(revoked.Deleted) != 2 || !revoked.Changed() {
t.Fatalf("revoked = %+v, want delete events for both owners", revoked)
}
senderHistory, err = messages.ListByUser(ctx, sender.ID, domain.MessageFilter{HasPeer: true, Peer: peer, Limit: 10})
if err != nil {
t.Fatalf("sender history after revoke: %v", err)
}
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 after revoke: %v", err)
}
if len(senderHistory.Messages) != 0 || len(recipientHistory.Messages) != 2 {
t.Fatalf("history sizes after revoke sender=%d recipient=%d, want new message removed from both owners", len(senderHistory.Messages), len(recipientHistory.Messages))
}
}
func TestMessageStoreDeleteHistoryJustClearPreservesEmptyDialog(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
owner := createTestUser(t, ctx, users, "+1992"+suffix+"01", "ClearOwner", "")
peerUser := createTestUser(t, ctx, users, "+1992"+suffix+"02", "ClearPeer", "")
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, peerUser.ID})
})
messages := NewMessageStore(pool)
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: owner.ID,
RecipientUserID: peerUser.ID,
RandomID: 9000,
Message: "clear but keep dialog",
Date: 1700001100,
}); err != nil {
t.Fatalf("seed send: %v", err)
}
peer := domain.Peer{Type: domain.PeerTypeUser, ID: peerUser.ID}
if _, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
OwnerUserID: owner.ID,
Peer: peer,
JustClear: true,
Date: 1700001200,
}); err != nil {
t.Fatalf("DeleteHistory just_clear: %v", err)
}
dialogs, err := NewDialogStore(pool).ListByUser(ctx, owner.ID, domain.DialogFilter{Limit: 10})
if err != nil {
t.Fatalf("dialogs after just_clear: %v", err)
}
if len(dialogs.Dialogs) != 1 || dialogs.Dialogs[0].Peer != peer || dialogs.Dialogs[0].TopMessage != 0 || len(dialogs.Messages) != 0 {
t.Fatalf("dialogs = %+v messages=%+v, want empty dialog preserved after just_clear", dialogs.Dialogs, dialogs.Messages)
}
history, err := messages.ListByUser(ctx, owner.ID, domain.MessageFilter{HasPeer: true, Peer: peer, Limit: 10, NeedTotalCount: true})
if err != nil {
t.Fatalf("history after just_clear: %v", err)
}
if len(history.Messages) != 0 {
t.Fatalf("history = %+v, want cleared", history.Messages)
}
}
func TestMessageStoreDeleteHistoryBatchesHugeMaxID(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
owner := createTestUser(t, ctx, users, "+1993"+suffix+"01", "BulkOwner", "")
peerUser := createTestUser(t, ctx, users, "+1993"+suffix+"02", "BulkPeer", "")
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, peerUser.ID})
})
total := domain.MaxDeleteHistoryBatch + 2
if _, err := pool.Exec(ctx, `
WITH src AS (
SELECT generate_series(1, $3::int) AS g
),
pm AS (
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
)
SELECT
$1::bigint,
$2::bigint,
910000000 + g,
decode(repeat('00', 32), 'hex'),
false,
g,
g,
0,
0,
1700002000 + g,
'bulk history',
'[]'::jsonb
FROM src
RETURNING id, random_id, message_date
)
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
)
SELECT
$1::bigint,
(random_id - 910000000)::int,
id,
$1::bigint,
'user',
$2::bigint,
$1::bigint,
message_date,
true,
'bulk history',
'[]'::jsonb,
(random_id - 910000000)::int
FROM pm
`, owner.ID, peerUser.ID, total); err != nil {
t.Fatalf("seed bulk history: %v", err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO dialogs (
user_id,
peer_type,
peer_id,
top_message_id,
top_message_date,
read_outbox_max_id,
unread_count
) VALUES ($1, 'user', $2, $3, $4, $3, 0)
`, owner.ID, peerUser.ID, total, 1700002000+total); err != nil {
t.Fatalf("seed dialog: %v", err)
}
messages := NewMessageStore(pool)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: peerUser.ID}
first, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
OwnerUserID: owner.ID,
Peer: peer,
MaxID: domain.MaxMessageBoxID,
Date: 1700003000,
})
if err != nil {
t.Fatalf("DeleteHistory first batch: %v", err)
}
self := first.Self()
if first.Offset != 1 || self.Event.Pts != domain.MaxDeleteHistoryBatch || self.Event.PtsCount != domain.MaxDeleteHistoryBatch || len(self.MessageIDs) != domain.MaxDeleteHistoryBatch {
t.Fatalf("first batch = %+v self=%+v, want offset=1 and exactly %d deleted ids", first, self, domain.MaxDeleteHistoryBatch)
}
history, err := messages.ListByUser(ctx, owner.ID, domain.MessageFilter{HasPeer: true, Peer: peer, Limit: 10, NeedTotalCount: true})
if err != nil {
t.Fatalf("history after first batch: %v", err)
}
if history.Count != 2 || len(history.Messages) != 2 || history.Messages[0].ID != 2 {
t.Fatalf("history after first batch = %+v, want only two oldest messages left", history)
}
second, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
OwnerUserID: owner.ID,
Peer: peer,
MaxID: domain.MaxMessageBoxID,
Date: 1700003001,
})
if err != nil {
t.Fatalf("DeleteHistory second batch: %v", err)
}
if second.Offset != 0 || second.Self().Event.PtsCount != 2 {
t.Fatalf("second batch = %+v, want final offset=0 pts_count=2", second)
}
}