1589 lines
57 KiB
Go
1589 lines
57 KiB
Go
package memory
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"reflect"
|
||
"testing"
|
||
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
var validRichMessageBlocks = []byte{
|
||
0x15, 0xc4, 0xb5, 0x1c, // vector
|
||
0x01, 0x00, 0x00, 0x00, // count
|
||
0x8a, 0x7e, 0x56, 0x13, // pageBlockUnsupported
|
||
}
|
||
|
||
func TestMessageStoreSendPrivateTextCreatesBothOwnerBoxes(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
req := domain.SendPrivateTextRequest{
|
||
SenderUserID: 1000000001,
|
||
RecipientUserID: 1000000002,
|
||
RandomID: 99,
|
||
Message: "hello",
|
||
Date: 1700000100,
|
||
}
|
||
|
||
got, err := messages.SendPrivateText(ctx, req)
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
if got.SenderMessage.ID != 1 || got.SenderMessage.OwnerUserID != req.SenderUserID || !got.SenderMessage.Out || got.SenderMessage.Pts != 1 {
|
||
t.Fatalf("sender message = %+v, want first outgoing box with pts=1", got.SenderMessage)
|
||
}
|
||
if got.RecipientMessage.ID != 1 || got.RecipientMessage.OwnerUserID != req.RecipientUserID || got.RecipientMessage.Out || got.RecipientMessage.Pts != 1 {
|
||
t.Fatalf("recipient message = %+v, want first incoming box with pts=1", got.RecipientMessage)
|
||
}
|
||
if got.SenderMessage.UID == 0 || got.SenderMessage.UID != got.RecipientMessage.UID {
|
||
t.Fatalf("uid = sender %d recipient %d, want shared private message uid", got.SenderMessage.UID, got.RecipientMessage.UID)
|
||
}
|
||
|
||
second, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: req.SenderUserID,
|
||
RecipientUserID: req.RecipientUserID,
|
||
RandomID: 100,
|
||
Message: "again",
|
||
Date: 1700000110,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText second: %v", err)
|
||
}
|
||
if second.SenderMessage.ID != 2 || second.SenderMessage.Pts != 2 || second.RecipientMessage.ID != 2 || second.RecipientMessage.Pts != 2 {
|
||
t.Fatalf("second send = %+v/%+v, want per-owner box_id and pts to advance", second.SenderMessage, second.RecipientMessage)
|
||
}
|
||
|
||
dup, err := messages.SendPrivateText(ctx, req)
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText duplicate: %v", err)
|
||
}
|
||
if !dup.Duplicate || dup.SenderMessage.ID != got.SenderMessage.ID || dup.RecipientMessage.ID != got.RecipientMessage.ID {
|
||
t.Fatalf("duplicate = %+v, want original message boxes", dup)
|
||
}
|
||
|
||
afterDup, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: req.SenderUserID,
|
||
RecipientUserID: req.RecipientUserID,
|
||
RandomID: 101,
|
||
Message: "after duplicate",
|
||
Date: 1700000111,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText after duplicate: %v", err)
|
||
}
|
||
if afterDup.SenderMessage.ID != 3 || afterDup.SenderMessage.Pts != 3 || afterDup.RecipientMessage.ID != 3 || afterDup.RecipientMessage.Pts != 3 {
|
||
t.Fatalf("send after duplicate = %+v/%+v, want next contiguous box_id and pts", afterDup.SenderMessage, afterDup.RecipientMessage)
|
||
}
|
||
|
||
senderHistory, err := messages.ListByUser(ctx, req.SenderUserID, domain.MessageFilter{HasPeer: true, Peer: got.SenderMessage.Peer, Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("sender history: %v", err)
|
||
}
|
||
recipientHistory, err := messages.ListByUser(ctx, req.RecipientUserID, domain.MessageFilter{HasPeer: true, Peer: got.RecipientMessage.Peer, Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("recipient history: %v", err)
|
||
}
|
||
if len(senderHistory.Messages) != 3 || len(recipientHistory.Messages) != 3 {
|
||
t.Fatalf("history sizes = sender %d recipient %d, want both owner partitions populated", len(senderHistory.Messages), len(recipientHistory.Messages))
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreSendPrivateTextContentInvariant(t *testing.T) {
|
||
ctx := context.Background()
|
||
messages := NewMessageStore()
|
||
|
||
richOnly, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1000000001,
|
||
RecipientUserID: 1000000002,
|
||
RandomID: 191,
|
||
Date: 1700000191,
|
||
RichMessage: &domain.MessageRichMessage{Blocks: validRichMessageBlocks},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText rich-only: %v", err)
|
||
}
|
||
if richOnly.SenderMessage.Body != "" || richOnly.SenderMessage.RichMessage.IsZero() {
|
||
t.Fatalf("rich-only sender message = %+v, want empty body with rich payload", richOnly.SenderMessage)
|
||
}
|
||
|
||
_, err = messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1000000001,
|
||
RecipientUserID: 1000000002,
|
||
RandomID: 192,
|
||
Date: 1700000192,
|
||
})
|
||
if !errors.Is(err, domain.ErrMessageEmpty) {
|
||
t.Fatalf("SendPrivateText empty err = %v, want ErrMessageEmpty", err)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreEditRichOnlyMessageUsesFinalContentState(t *testing.T) {
|
||
ctx := context.Background()
|
||
messages := NewMessageStore()
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1000000001,
|
||
RecipientUserID: 1000000002,
|
||
RandomID: 201,
|
||
Date: 1700000200,
|
||
RichMessage: &domain.MessageRichMessage{Blocks: validRichMessageBlocks},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText rich-only: %v", err)
|
||
}
|
||
|
||
edited, err := messages.EditMessage(ctx, domain.EditMessageRequest{
|
||
OwnerUserID: 1000000001,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002},
|
||
ID: sent.SenderMessage.ID,
|
||
Message: "",
|
||
HideEdited: true,
|
||
EditDate: 1700000210,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("EditMessage rich-only hide-edit: %v", err)
|
||
}
|
||
if len(edited.Edited) != 2 {
|
||
t.Fatalf("edited boxes = %d, want 2", len(edited.Edited))
|
||
}
|
||
for _, item := range edited.Edited {
|
||
if item.Message.RichMessage.IsZero() {
|
||
t.Fatalf("edited rich-only box for user %d lost rich message: %+v", item.UserID, item.Message)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreWebViewDataServiceActionRoundTrip(t *testing.T) {
|
||
ctx := context.Background()
|
||
messages := NewMessageStore()
|
||
req := domain.SendPrivateTextRequest{
|
||
SenderUserID: 1000000001,
|
||
RecipientUserID: 1000000002,
|
||
RandomID: 199,
|
||
Date: 1700000120,
|
||
Media: &domain.MessageMedia{
|
||
Kind: domain.MessageMediaKindService,
|
||
ServiceAction: &domain.MessageServiceAction{
|
||
Kind: domain.MessageServiceActionWebViewDataSent,
|
||
WebViewData: &domain.MessageWebViewDataAction{
|
||
ButtonText: "Open",
|
||
Data: `{"ok":true}`,
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
got, err := messages.SendPrivateText(ctx, req)
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
assertWebViewData := func(name string, msg domain.Message) {
|
||
if msg.Media == nil || msg.Media.ServiceAction == nil ||
|
||
msg.Media.ServiceAction.Kind != domain.MessageServiceActionWebViewDataSent ||
|
||
msg.Media.ServiceAction.WebViewData == nil {
|
||
t.Fatalf("%s media = %+v, want webview data service action", name, msg.Media)
|
||
}
|
||
if data := msg.Media.ServiceAction.WebViewData; data.ButtonText != "Open" || data.Data != `{"ok":true}` {
|
||
t.Fatalf("%s webview data = %+v, want Open/data", name, data)
|
||
}
|
||
}
|
||
assertWebViewData("sender", got.SenderMessage)
|
||
assertWebViewData("recipient", got.RecipientMessage)
|
||
|
||
_, err = messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: req.SenderUserID,
|
||
RecipientUserID: req.RecipientUserID,
|
||
RandomID: req.RandomID,
|
||
Date: 1700000121,
|
||
Media: &domain.MessageMedia{
|
||
Kind: domain.MessageMediaKindService,
|
||
ServiceAction: &domain.MessageServiceAction{
|
||
Kind: domain.MessageServiceActionWebViewDataSent,
|
||
WebViewData: &domain.MessageWebViewDataAction{
|
||
ButtonText: "Changed",
|
||
Data: `{"ok":false}`,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
if !errors.Is(err, domain.ErrMessageRandomIDDuplicate) {
|
||
t.Fatalf("changed-media duplicate err = %v, want ErrMessageRandomIDDuplicate", err)
|
||
}
|
||
|
||
recipientHistory, err := messages.ListByUser(ctx, req.RecipientUserID, domain.MessageFilter{
|
||
HasPeer: true,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: req.SenderUserID},
|
||
Limit: 10,
|
||
})
|
||
if err != nil || len(recipientHistory.Messages) != 1 {
|
||
t.Fatalf("recipient history = %+v err=%v, want one message", recipientHistory, err)
|
||
}
|
||
assertWebViewData("recipient history", recipientHistory.Messages[0])
|
||
}
|
||
|
||
func TestMessageStoreRequestedPeerDisclosureSnapshotRoundTrip(t *testing.T) {
|
||
ctx := context.Background()
|
||
messages := NewMessageStore()
|
||
photo := domain.Photo{ID: 8101, Sizes: []domain.PhotoSize{{
|
||
Kind: domain.PhotoSizeKindDefault, Type: "m", W: 320, H: 320, Size: 4096,
|
||
}}}
|
||
requestedPeer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000003}
|
||
req := domain.SendPrivateTextRequest{
|
||
SenderUserID: 1000000001, RecipientUserID: 1000000002, RandomID: 200, Date: 1700000121,
|
||
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: &domain.MessageServiceAction{
|
||
Kind: domain.MessageServiceActionRequestedPeer,
|
||
RequestedPeer: &domain.MessageRequestedPeerAction{
|
||
ButtonID: 77, Peers: []domain.Peer{requestedPeer},
|
||
Details: []domain.MessageRequestedPeerDetails{{
|
||
Peer: requestedPeer, FirstName: "Shared", Username: "shared_user", Photo: &photo,
|
||
}},
|
||
NameRequested: true, UsernameRequested: true, PhotoRequested: true,
|
||
},
|
||
}},
|
||
}
|
||
|
||
got, err := messages.SendPrivateText(ctx, req)
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
assertSnapshot := func(name string, msg domain.Message) {
|
||
t.Helper()
|
||
if msg.Media == nil || msg.Media.ServiceAction == nil || msg.Media.ServiceAction.RequestedPeer == nil {
|
||
t.Fatalf("%s media=%+v, want requested-peer action", name, msg.Media)
|
||
}
|
||
action := msg.Media.ServiceAction.RequestedPeer
|
||
if action.ButtonID != 77 || len(action.Peers) != 1 || action.Peers[0] != requestedPeer ||
|
||
len(action.Details) != 1 || action.Details[0].FirstName != "Shared" ||
|
||
action.Details[0].Username != "shared_user" || action.Details[0].Photo == nil ||
|
||
len(action.Details[0].Photo.Sizes) != 1 || action.Details[0].Photo.Sizes[0].W != 320 ||
|
||
!action.NameRequested || !action.UsernameRequested || !action.PhotoRequested {
|
||
t.Fatalf("%s requested-peer=%+v", name, action)
|
||
}
|
||
}
|
||
assertSnapshot("sender", got.SenderMessage)
|
||
assertSnapshot("recipient", got.RecipientMessage)
|
||
|
||
// Mutating either the request or one returned box must not alter the other
|
||
// box or the immutable store snapshot.
|
||
req.Media.ServiceAction.RequestedPeer.Details[0].FirstName = "mutated-request"
|
||
req.Media.ServiceAction.RequestedPeer.Details[0].Photo.Sizes[0].W = 1
|
||
got.SenderMessage.Media.ServiceAction.RequestedPeer.Details[0].FirstName = "mutated-result"
|
||
got.SenderMessage.Media.ServiceAction.RequestedPeer.Details[0].Photo.Sizes[0].W = 2
|
||
assertSnapshot("isolated recipient result", got.RecipientMessage)
|
||
|
||
for _, owner := range []int64{req.SenderUserID, req.RecipientUserID} {
|
||
history, err := messages.ListByUser(ctx, owner, domain.MessageFilter{Limit: 10})
|
||
if err != nil || len(history.Messages) != 1 {
|
||
t.Fatalf("owner %d history=%+v err=%v", owner, history, err)
|
||
}
|
||
assertSnapshot("stored history", history.Messages[0])
|
||
}
|
||
}
|
||
|
||
func TestMessageStorePrivateMessageReactionsAreSharedAcrossOwnerBoxes(t *testing.T) {
|
||
ctx := context.Background()
|
||
messages := NewMessageStore()
|
||
aliceID := int64(1000000001)
|
||
bobID := int64(1000000002)
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: aliceID,
|
||
RecipientUserID: bobID,
|
||
RandomID: 101,
|
||
Message: "react to me",
|
||
Date: 1700000100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
reaction := domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "\U0001f44d"}
|
||
res, err := messages.SetMessageReactions(ctx, domain.SetPrivateMessageReactionsRequest{
|
||
UserID: bobID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: aliceID},
|
||
MessageID: sent.RecipientMessage.ID,
|
||
Reactions: []domain.MessageReaction{
|
||
reaction,
|
||
},
|
||
Big: true,
|
||
Date: 1700000200,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SetMessageReactions: %v", err)
|
||
}
|
||
if len(res.Messages) != 2 {
|
||
t.Fatalf("reaction result messages = %d, want both owner boxes", len(res.Messages))
|
||
}
|
||
|
||
aliceReactions, err := messages.GetMessageReactions(ctx, domain.PrivateMessageReactionsRequest{
|
||
OwnerUserID: aliceID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||
IDs: []int{sent.SenderMessage.ID},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("alice GetMessageReactions: %v", err)
|
||
}
|
||
if len(aliceReactions.Messages) != 1 || aliceReactions.Messages[0].Reactions == nil {
|
||
t.Fatalf("alice reactions = %+v, want one enriched message", aliceReactions)
|
||
}
|
||
if got := aliceReactions.Messages[0].Reactions.Results; len(got) != 1 || got[0].Reaction != reaction || got[0].Count != 1 || got[0].ChosenOrder != 0 {
|
||
t.Fatalf("alice reaction counts = %+v, want one peer reaction without chosen order", got)
|
||
}
|
||
if got := aliceReactions.Messages[0].Reactions.Recent; len(got) != 1 || got[0].UserID != bobID || got[0].SenderUserID != aliceID || !got[0].Unread || !got[0].Big || got[0].My {
|
||
t.Fatalf("alice recent reactions = %+v, want bob non-my big reaction", got)
|
||
}
|
||
aliceBox, err := messages.GetByIDs(ctx, aliceID, []int{sent.SenderMessage.ID})
|
||
if err != nil {
|
||
t.Fatalf("alice GetByIDs after reaction: %v", err)
|
||
}
|
||
if len(aliceBox.Messages) != 1 || !aliceBox.Messages[0].ReactionUnread {
|
||
t.Fatalf("alice box after reaction = %+v, want reaction_unread", aliceBox.Messages)
|
||
}
|
||
read, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
|
||
OwnerUserID: aliceID,
|
||
IDs: []int{sent.SenderMessage.ID},
|
||
Date: 1700000210,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ReadMessageContents reaction: %v", err)
|
||
}
|
||
if !reflect.DeepEqual(read.MessageIDs, []int{sent.SenderMessage.ID}) || read.Event.Type != domain.UpdateEventReadMessageContents || read.Event.Pts == 0 {
|
||
t.Fatalf("read reaction contents = %+v, want one read_message_contents event", read)
|
||
}
|
||
aliceBox, err = messages.GetByIDs(ctx, aliceID, []int{sent.SenderMessage.ID})
|
||
if err != nil {
|
||
t.Fatalf("alice GetByIDs after read reaction: %v", err)
|
||
}
|
||
if len(aliceBox.Messages) != 1 || aliceBox.Messages[0].ReactionUnread {
|
||
t.Fatalf("alice box after read reaction = %+v, want reaction_unread cleared", aliceBox.Messages)
|
||
}
|
||
if got := aliceBox.Messages[0].Reactions.Recent; len(got) != 1 || got[0].Unread || got[0].SenderUserID != aliceID {
|
||
t.Fatalf("alice recent reactions after read = %+v, want unread flag cleared", got)
|
||
}
|
||
|
||
bobReactions, err := messages.GetMessageReactions(ctx, domain.PrivateMessageReactionsRequest{
|
||
OwnerUserID: bobID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: aliceID},
|
||
IDs: []int{sent.RecipientMessage.ID},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("bob GetMessageReactions: %v", err)
|
||
}
|
||
if got := bobReactions.Messages[0].Reactions.Results; len(got) != 1 || got[0].ChosenOrder != 1 {
|
||
t.Fatalf("bob reaction counts = %+v, want own chosen order", got)
|
||
}
|
||
if got := bobReactions.Messages[0].Reactions.Recent; len(got) != 1 || got[0].UserID != bobID || got[0].SenderUserID != aliceID || got[0].Unread || !got[0].My {
|
||
t.Fatalf("bob recent reactions = %+v, want my reaction", got)
|
||
}
|
||
}
|
||
|
||
func TestMessageStorePrivateReactionEnrichesDialogTopMessages(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
aliceID := int64(1000000001)
|
||
bobID := int64(1000000002)
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: aliceID,
|
||
RecipientUserID: bobID,
|
||
RandomID: 111,
|
||
Message: "latest from alice",
|
||
Date: 1700000300,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
reaction := domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "\u2764"}
|
||
if _, err := messages.SetMessageReactions(ctx, domain.SetPrivateMessageReactionsRequest{
|
||
UserID: bobID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: aliceID},
|
||
MessageID: sent.RecipientMessage.ID,
|
||
Reactions: []domain.MessageReaction{
|
||
reaction,
|
||
},
|
||
Date: 1700000310,
|
||
}); err != nil {
|
||
t.Fatalf("SetMessageReactions: %v", err)
|
||
}
|
||
|
||
aliceDialogs, err := dialogs.ListByUser(ctx, aliceID, domain.DialogFilter{Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("alice ListByUser: %v", err)
|
||
}
|
||
if len(aliceDialogs.Dialogs) != 1 || aliceDialogs.Dialogs[0].TopMessage != sent.SenderMessage.ID || aliceDialogs.Dialogs[0].UnreadReactions != 1 {
|
||
t.Fatalf("alice dialog = %+v, want top message with one unread reaction", aliceDialogs.Dialogs)
|
||
}
|
||
if len(aliceDialogs.Messages) != 1 || aliceDialogs.Messages[0].ID != sent.SenderMessage.ID || aliceDialogs.Messages[0].Reactions == nil {
|
||
t.Fatalf("alice dialog messages = %+v, want enriched top message", aliceDialogs.Messages)
|
||
}
|
||
if got := aliceDialogs.Messages[0].Reactions.Recent; len(got) != 1 || got[0].UserID != bobID || got[0].SenderUserID != aliceID || !got[0].Unread || got[0].My {
|
||
t.Fatalf("alice dialog recent reactions = %+v, want bob unread non-my reaction", got)
|
||
}
|
||
|
||
bobDialogs, err := dialogs.ListByPeers(ctx, bobID, []domain.Peer{{Type: domain.PeerTypeUser, ID: aliceID}})
|
||
if err != nil {
|
||
t.Fatalf("bob ListByPeers: %v", err)
|
||
}
|
||
if len(bobDialogs.Messages) != 1 || bobDialogs.Messages[0].Reactions == nil {
|
||
t.Fatalf("bob peer dialog messages = %+v, want enriched top message", bobDialogs.Messages)
|
||
}
|
||
if got := bobDialogs.Messages[0].Reactions.Recent; len(got) != 1 || got[0].UserID != bobID || got[0].SenderUserID != aliceID || got[0].Unread || !got[0].My {
|
||
t.Fatalf("bob peer dialog recent reactions = %+v, want my read reaction", got)
|
||
}
|
||
|
||
if _, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
|
||
OwnerUserID: aliceID,
|
||
IDs: []int{sent.SenderMessage.ID},
|
||
Date: 1700000320,
|
||
}); err != nil {
|
||
t.Fatalf("ReadMessageContents: %v", err)
|
||
}
|
||
aliceDialogs, err = dialogs.ListByUser(ctx, aliceID, domain.DialogFilter{Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("alice ListByUser after read: %v", err)
|
||
}
|
||
if len(aliceDialogs.Dialogs) != 1 || aliceDialogs.Dialogs[0].UnreadReactions != 0 {
|
||
t.Fatalf("alice dialog after read = %+v, want no unread reactions", aliceDialogs.Dialogs)
|
||
}
|
||
if got := aliceDialogs.Messages[0].Reactions.Recent; len(got) != 1 || got[0].Unread {
|
||
t.Fatalf("alice dialog recent reactions after read = %+v, want unread cleared", got)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreSendPrivateTextReplyAndForwardMetadata(t *testing.T) {
|
||
ctx := context.Background()
|
||
messages := NewMessageStore()
|
||
aliceID := int64(1000000001)
|
||
bobID := int64(1000000002)
|
||
first, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: aliceID,
|
||
RecipientUserID: bobID,
|
||
RandomID: 501,
|
||
Message: "first",
|
||
Date: 1700000100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("seed SendPrivateText: %v", err)
|
||
}
|
||
reply, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: aliceID,
|
||
RecipientUserID: bobID,
|
||
RandomID: 502,
|
||
Message: "reply",
|
||
Silent: true,
|
||
NoForwards: true,
|
||
ReplyTo: &domain.MessageReply{
|
||
MessageID: first.SenderMessage.ID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||
QuoteText: "fir",
|
||
QuoteOffset: 0,
|
||
},
|
||
Date: 1700000110,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("reply SendPrivateText: %v", err)
|
||
}
|
||
if reply.SenderMessage.ReplyTo == nil || reply.SenderMessage.ReplyTo.MessageID != first.SenderMessage.ID {
|
||
t.Fatalf("sender reply = %+v, want sender-side message id", reply.SenderMessage.ReplyTo)
|
||
}
|
||
if reply.RecipientMessage.ReplyTo == nil || reply.RecipientMessage.ReplyTo.MessageID != first.RecipientMessage.ID {
|
||
t.Fatalf("recipient reply = %+v, want translated recipient-side message id", reply.RecipientMessage.ReplyTo)
|
||
}
|
||
if !reply.SenderMessage.Silent || !reply.SenderMessage.NoForwards {
|
||
t.Fatalf("reply flags = silent %v noforwards %v, want true/true", reply.SenderMessage.Silent, reply.SenderMessage.NoForwards)
|
||
}
|
||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: aliceID,
|
||
RecipientUserID: bobID,
|
||
RandomID: 504,
|
||
Message: "bad quote offset",
|
||
ReplyTo: &domain.MessageReply{
|
||
MessageID: first.SenderMessage.ID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||
QuoteText: "fir",
|
||
QuoteOffset: domain.MaxMessageReplyQuoteOffset + 1,
|
||
},
|
||
Date: 1700000115,
|
||
}); !errors.Is(err, domain.ErrReplyMessageIDInvalid) {
|
||
t.Fatalf("bad quote offset err = %v, want ErrReplyMessageIDInvalid", err)
|
||
}
|
||
|
||
forwarded, err := messages.ForwardPrivateMessages(ctx, domain.ForwardPrivateMessagesRequest{
|
||
OwnerUserID: aliceID,
|
||
FromPeer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||
ToUserID: bobID,
|
||
MessageIDs: []int{first.SenderMessage.ID},
|
||
RandomIDs: []int64{503},
|
||
ReplyTo: &domain.MessageReply{
|
||
MessageID: first.SenderMessage.ID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||
},
|
||
Date: 1700000120,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ForwardPrivateMessages: %v", err)
|
||
}
|
||
if len(forwarded.SenderMessages) != 1 || forwarded.SenderMessages[0].Forward == nil || forwarded.SenderMessages[0].Forward.From.ID != aliceID {
|
||
t.Fatalf("forwarded messages = %+v, want original author header", forwarded.SenderMessages)
|
||
}
|
||
if forwarded.SenderMessages[0].ReplyTo == nil || forwarded.SenderMessages[0].ReplyTo.MessageID != first.SenderMessage.ID {
|
||
t.Fatalf("forward reply = %+v, want target dialog reply header", forwarded.SenderMessages[0].ReplyTo)
|
||
}
|
||
if _, err := messages.ForwardPrivateMessages(ctx, domain.ForwardPrivateMessagesRequest{
|
||
OwnerUserID: aliceID,
|
||
FromPeer: domain.Peer{Type: domain.PeerTypeUser, ID: bobID},
|
||
ToUserID: aliceID,
|
||
MessageIDs: []int{reply.SenderMessage.ID},
|
||
RandomIDs: []int64{504},
|
||
Date: 1700000130,
|
||
}); err != domain.ErrChatForwardsRestricted {
|
||
t.Fatalf("forward protected err=%v, want ErrChatForwardsRestricted", err)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreListByUserSupportsForwardAndAroundHistoryOffsets(t *testing.T) {
|
||
ctx := context.Background()
|
||
messages := NewMessageStore()
|
||
aliceID := int64(1000000001)
|
||
bobID := int64(1000000002)
|
||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: aliceID}
|
||
|
||
for i := 1; i <= 6; i++ {
|
||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: aliceID,
|
||
RecipientUserID: bobID,
|
||
RandomID: int64(600 + i),
|
||
Message: "history",
|
||
Date: 1700000000 + i,
|
||
}); err != nil {
|
||
t.Fatalf("seed message %d: %v", i, err)
|
||
}
|
||
}
|
||
|
||
around, err := messages.ListByUser(ctx, bobID, domain.MessageFilter{
|
||
HasPeer: true,
|
||
Peer: peer,
|
||
OffsetID: 3,
|
||
AddOffset: -3,
|
||
Limit: 6,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("around history: %v", err)
|
||
}
|
||
if got := messageIDs(around.Messages); !sameInts(got, []int{6, 5, 4, 3, 2, 1}) {
|
||
t.Fatalf("around ids = %v, want unread/newer side plus older context", got)
|
||
}
|
||
|
||
forward, err := messages.ListByUser(ctx, bobID, domain.MessageFilter{
|
||
HasPeer: true,
|
||
Peer: peer,
|
||
OffsetID: 3,
|
||
AddOffset: -3,
|
||
Limit: 3,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("forward history: %v", err)
|
||
}
|
||
if got := messageIDs(forward.Messages); !sameInts(got, []int{6, 5, 4}) {
|
||
t.Fatalf("forward ids = %v, want messages newer than offset", got)
|
||
}
|
||
|
||
hugePositive, err := messages.ListByUser(ctx, bobID, domain.MessageFilter{
|
||
HasPeer: true,
|
||
Peer: peer,
|
||
AddOffset: 1 << 30,
|
||
Limit: 3,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("huge positive add_offset history: %v", err)
|
||
}
|
||
if len(hugePositive.Messages) != 0 {
|
||
t.Fatalf("huge positive add_offset ids = %v, want bounded empty page", messageIDs(hugePositive.Messages))
|
||
}
|
||
|
||
hugeNegative, err := messages.ListByUser(ctx, bobID, domain.MessageFilter{
|
||
HasPeer: true,
|
||
Peer: peer,
|
||
OffsetID: 3,
|
||
AddOffset: -1 << 30,
|
||
Limit: 3,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("huge negative add_offset history: %v", err)
|
||
}
|
||
if got := messageIDs(hugeNegative.Messages); !sameInts(got, []int{6, 5, 4}) {
|
||
t.Fatalf("huge negative add_offset ids = %v, want clamped forward page", got)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreReadHistoryEmitsInboxAndOutboxReceipts(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
senderID := int64(1000000001)
|
||
recipientID := int64(1000000002)
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 101,
|
||
Message: "hello",
|
||
Date: 1700000100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
|
||
read, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||
OwnerUserID: recipientID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: senderID},
|
||
Date: 1700000200,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ReadHistory: %v", err)
|
||
}
|
||
if !read.Changed || read.InboxEvent.Type != domain.UpdateEventReadHistoryInbox || read.InboxEvent.Pts != 2 || read.InboxEvent.MaxID != sent.RecipientMessage.ID {
|
||
t.Fatalf("inbox read = %+v, want recipient inbox pts=2 max recipient id", read)
|
||
}
|
||
if !read.OutboxChanged || read.OutboxUserID != senderID || read.OutboxEvent.Type != domain.UpdateEventReadHistoryOutbox || read.OutboxEvent.MaxID != sent.SenderMessage.ID {
|
||
t.Fatalf("outbox read = %+v, want sender outbox receipt with sender message id", read)
|
||
}
|
||
date, err := messages.GetOutboxReadDate(ctx, domain.OutboxReadDateRequest{
|
||
OwnerUserID: senderID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: recipientID},
|
||
ID: sent.SenderMessage.ID,
|
||
})
|
||
if err != nil || date != 1700000200 {
|
||
t.Fatalf("outbox read date = %d err=%v, want read date", date, err)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreReadHistoryStaleUnreadRepairDoesNotAdvancePts(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
senderID := int64(1000000101)
|
||
recipientID := int64(1000000102)
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 201,
|
||
Message: "hello",
|
||
Date: 1700000100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: senderID}
|
||
if _, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||
OwnerUserID: recipientID,
|
||
Peer: peer,
|
||
MaxID: sent.RecipientMessage.ID,
|
||
Date: 1700000200,
|
||
}); err != nil {
|
||
t.Fatalf("first ReadHistory: %v", err)
|
||
}
|
||
|
||
dialogs.mu.Lock()
|
||
list := dialogs.m[recipientID]
|
||
for i := range list.Dialogs {
|
||
if list.Dialogs[i].Peer != peer {
|
||
continue
|
||
}
|
||
list.Dialogs[i].UnreadCount = 1
|
||
list.Dialogs[i].UnreadMentions = 1
|
||
list.Dialogs[i].UnreadReactions = 1
|
||
list.Dialogs[i].UnreadMark = true
|
||
}
|
||
dialogs.m[recipientID] = list
|
||
dialogs.mu.Unlock()
|
||
|
||
read, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||
OwnerUserID: recipientID,
|
||
Peer: peer,
|
||
MaxID: sent.RecipientMessage.ID,
|
||
Date: 1700000300,
|
||
})
|
||
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)
|
||
}
|
||
|
||
dialogs.mu.RLock()
|
||
repaired := dialogs.m[recipientID].Dialogs[0]
|
||
dialogs.mu.RUnlock()
|
||
// readHistory 重算 UnreadCount、清 mentions/mark,但不清 reaction 角标(与 PG
|
||
// 对齐;reaction 未读由 readReactions/readMessageContents 单独清),故 UnreadReactions
|
||
// 保留为 1。
|
||
if repaired.UnreadCount != 0 || repaired.UnreadMentions != 0 || repaired.UnreadReactions != 1 || repaired.UnreadMark {
|
||
t.Fatalf("dialog after repair = %+v, want unread/mentions/mark cleared and reactions preserved", repaired)
|
||
}
|
||
next, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 202,
|
||
Message: "next",
|
||
Date: 1700000400,
|
||
})
|
||
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 TestMessageStoreSendPrivateTextRecomputesInboxUnreadFromReadMax(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
senderID := int64(1000000201)
|
||
recipientID := int64(1000000202)
|
||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: senderID}
|
||
|
||
first, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 301,
|
||
Message: "already read",
|
||
Date: 1700000500,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText first: %v", err)
|
||
}
|
||
if _, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||
OwnerUserID: recipientID,
|
||
Peer: peer,
|
||
MaxID: first.RecipientMessage.ID,
|
||
Date: 1700000510,
|
||
}); err != nil {
|
||
t.Fatalf("ReadHistory: %v", err)
|
||
}
|
||
|
||
dialogs.mu.Lock()
|
||
list := dialogs.m[recipientID]
|
||
for i := range list.Dialogs {
|
||
if list.Dialogs[i].Peer == peer {
|
||
list.Dialogs[i].UnreadCount = 2
|
||
}
|
||
}
|
||
dialogs.m[recipientID] = list
|
||
dialogs.mu.Unlock()
|
||
|
||
bodies := []string{"one", "two", "three"}
|
||
var last domain.SendPrivateTextResult
|
||
for i, body := range bodies {
|
||
last, err = messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 310 + int64(i),
|
||
Message: body,
|
||
Date: 1700000520 + i,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText %q: %v", body, err)
|
||
}
|
||
}
|
||
|
||
dialogs.mu.RLock()
|
||
var got domain.Dialog
|
||
for _, dialog := range dialogs.m[recipientID].Dialogs {
|
||
if dialog.Peer == peer {
|
||
got = dialog
|
||
break
|
||
}
|
||
}
|
||
dialogs.mu.RUnlock()
|
||
if got.UnreadCount != len(bodies) || got.TopMessage != last.RecipientMessage.ID || got.ReadInboxMaxID != first.RecipientMessage.ID {
|
||
t.Fatalf("dialog = %+v, want unread=3 top=%d read=%d", got, last.RecipientMessage.ID, first.RecipientMessage.ID)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreDeleteMessagesRecomputesRecipientUnreadAndTop(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
senderID := int64(1000000301)
|
||
recipientID := int64(1000000302)
|
||
recipientPeer := domain.Peer{Type: domain.PeerTypeUser, ID: senderID}
|
||
|
||
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: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 330 + int64(i),
|
||
Message: body,
|
||
Date: 1700000600 + i,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText %q: %v", body, err)
|
||
}
|
||
sent = append(sent, got)
|
||
}
|
||
|
||
deleted, err := messages.DeleteMessages(ctx, domain.DeleteMessagesRequest{
|
||
OwnerUserID: senderID,
|
||
IDs: []int{sent[2].SenderMessage.ID},
|
||
Revoke: true,
|
||
Date: 1700000610,
|
||
})
|
||
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)
|
||
}
|
||
|
||
dialogList, err := dialogs.ListByUser(ctx, recipientID, domain.DialogFilter{Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("recipient dialogs: %v", err)
|
||
}
|
||
var got domain.Dialog
|
||
for _, dialog := range dialogList.Dialogs {
|
||
if dialog.Peer == recipientPeer {
|
||
got = dialog
|
||
break
|
||
}
|
||
}
|
||
if got.UnreadCount != 2 || got.TopMessage != sent[1].RecipientMessage.ID {
|
||
t.Fatalf("recipient dialog after revoke = %+v, want unread=2 top=%d", got, sent[1].RecipientMessage.ID)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreDeleteMiddleUnreadKeepsRecipientTop(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
senderID := int64(1000000401)
|
||
recipientID := int64(1000000402)
|
||
recipientPeer := domain.Peer{Type: domain.PeerTypeUser, ID: senderID}
|
||
|
||
first, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 429,
|
||
Message: "already read",
|
||
Date: 1700000690,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText first: %v", err)
|
||
}
|
||
if _, err := messages.ReadHistory(ctx, domain.ReadHistoryRequest{
|
||
OwnerUserID: recipientID,
|
||
Peer: recipientPeer,
|
||
MaxID: first.RecipientMessage.ID,
|
||
Date: 1700000695,
|
||
}); 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: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 430 + int64(i),
|
||
Message: body,
|
||
Date: 1700000700 + i,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText %q: %v", body, err)
|
||
}
|
||
sent = append(sent, got)
|
||
}
|
||
|
||
if _, err := messages.DeleteMessages(ctx, domain.DeleteMessagesRequest{
|
||
OwnerUserID: senderID,
|
||
IDs: []int{sent[1].SenderMessage.ID},
|
||
Revoke: true,
|
||
Date: 1700000710,
|
||
}); err != nil {
|
||
t.Fatalf("DeleteMessages revoke middle: %v", err)
|
||
}
|
||
|
||
dialogList, err := dialogs.ListByUser(ctx, recipientID, domain.DialogFilter{Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("recipient dialogs: %v", err)
|
||
}
|
||
var got domain.Dialog
|
||
for _, dialog := range dialogList.Dialogs {
|
||
if dialog.Peer == recipientPeer {
|
||
got = dialog
|
||
break
|
||
}
|
||
}
|
||
if got.UnreadCount != 2 || got.TopMessage != sent[2].RecipientMessage.ID || got.ReadInboxMaxID != first.RecipientMessage.ID {
|
||
t.Fatalf("recipient dialog after middle revoke = %+v, want unread=2 top=%d read=%d", got, sent[2].RecipientMessage.ID, first.RecipientMessage.ID)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreReadMessageContentsClearsUnreadContentOnce(t *testing.T) {
|
||
ctx := context.Background()
|
||
messages := NewMessageStore()
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1001,
|
||
RecipientUserID: 1002,
|
||
RandomID: 88,
|
||
Message: "voice placeholder",
|
||
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindDocument, Voice: true},
|
||
Date: 1700000300,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
if !sent.RecipientMessage.MediaUnread {
|
||
t.Fatalf("recipient MediaUnread = false, want true for incoming media")
|
||
}
|
||
got, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
|
||
OwnerUserID: 1002,
|
||
IDs: []int{sent.RecipientMessage.ID, domain.MaxMessageBoxID},
|
||
Date: 1700000400,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ReadMessageContents: %v", err)
|
||
}
|
||
if !reflect.DeepEqual(got.MessageIDs, []int{sent.RecipientMessage.ID}) {
|
||
t.Fatalf("MessageIDs = %v, want unread recipient id", got.MessageIDs)
|
||
}
|
||
if got.Event.Type != domain.UpdateEventReadMessageContents || got.Event.Pts == 0 || got.Event.PtsCount != 1 {
|
||
t.Fatalf("Event = %+v, want read_message_contents pts update", got.Event)
|
||
}
|
||
repeated, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
|
||
OwnerUserID: 1002,
|
||
IDs: []int{sent.RecipientMessage.ID},
|
||
Date: 1700000500,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ReadMessageContents repeat: %v", err)
|
||
}
|
||
if len(repeated.MessageIDs) != 0 || repeated.Event.Pts != 0 {
|
||
t.Fatalf("repeat = %+v, want no affected messages and no pts", repeated)
|
||
}
|
||
if _, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
|
||
OwnerUserID: 1002,
|
||
IDs: []int{0},
|
||
}); !errors.Is(err, domain.ErrMessageIDInvalid) {
|
||
t.Fatalf("invalid id error = %v, want ErrMessageIDInvalid", err)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreReadMessageContentsNotifiesVoiceSender(t *testing.T) {
|
||
ctx := context.Background()
|
||
messages := NewMessageStore()
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1001,
|
||
RecipientUserID: 1002,
|
||
RandomID: 99,
|
||
Message: "",
|
||
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindDocument, Voice: true},
|
||
Date: 1700000300,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
if !sent.SenderMessage.MediaUnread {
|
||
t.Fatalf("sender voice MediaUnread = false, want true until the peer listens")
|
||
}
|
||
photo, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1001,
|
||
RecipientUserID: 1002,
|
||
RandomID: 100,
|
||
Message: "",
|
||
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &domain.Photo{ID: 7}},
|
||
Date: 1700000301,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText photo: %v", err)
|
||
}
|
||
if photo.SenderMessage.MediaUnread || photo.RecipientMessage.MediaUnread {
|
||
t.Fatalf("photo media_unread sender=%v recipient=%v, want false: only voice/round carry unread payload",
|
||
photo.SenderMessage.MediaUnread, photo.RecipientMessage.MediaUnread)
|
||
}
|
||
read, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
|
||
OwnerUserID: 1002,
|
||
IDs: []int{sent.RecipientMessage.ID},
|
||
Date: 1700000400,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ReadMessageContents: %v", err)
|
||
}
|
||
if len(read.SenderEvents) != 1 {
|
||
t.Fatalf("SenderEvents = %+v, want one sender receipt", read.SenderEvents)
|
||
}
|
||
receipt := read.SenderEvents[0]
|
||
if receipt.UserID != 1001 || receipt.Type != domain.UpdateEventReadMessageContents || receipt.Pts == 0 {
|
||
t.Fatalf("receipt = %+v, want sender-side read_message_contents", receipt)
|
||
}
|
||
if !reflect.DeepEqual(receipt.MessageIDs, []int{sent.SenderMessage.ID}) {
|
||
t.Fatalf("receipt ids = %v, want sender box id %d", receipt.MessageIDs, sent.SenderMessage.ID)
|
||
}
|
||
if receipt.Date != 1700000400 {
|
||
t.Fatalf("receipt date = %d, want read time 1700000400", receipt.Date)
|
||
}
|
||
repeat, err := messages.ReadMessageContents(ctx, domain.ReadMessageContentsRequest{
|
||
OwnerUserID: 1002,
|
||
IDs: []int{sent.RecipientMessage.ID},
|
||
Date: 1700000500,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ReadMessageContents repeat: %v", err)
|
||
}
|
||
if len(repeat.SenderEvents) != 0 {
|
||
t.Fatalf("repeat SenderEvents = %+v, want none", repeat.SenderEvents)
|
||
}
|
||
}
|
||
|
||
func messageIDs(messages []domain.Message) []int {
|
||
out := make([]int, 0, len(messages))
|
||
for _, msg := range messages {
|
||
out = append(out, msg.ID)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func sameInts(got, want []int) bool {
|
||
if len(got) != len(want) {
|
||
return false
|
||
}
|
||
for i := range got {
|
||
if got[i] != want[i] {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func TestMessageStoreEditMessageUpdatesBothBoxes(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
senderID := int64(1000000001)
|
||
recipientID := int64(1000000002)
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 102,
|
||
Message: "before",
|
||
Date: 1700000100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
|
||
edited, err := messages.EditMessage(ctx, domain.EditMessageRequest{
|
||
OwnerUserID: senderID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: recipientID},
|
||
ID: sent.SenderMessage.ID,
|
||
Message: "after",
|
||
EditDate: 1700000200,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("EditMessage: %v", err)
|
||
}
|
||
if len(edited.Edited) != 2 || edited.Self().Message.Body != "after" || edited.Self().Event.Type != domain.UpdateEventEditMessage {
|
||
t.Fatalf("edited = %+v, want both owner boxes and self edit event", edited)
|
||
}
|
||
recipientHistory, err := messages.ListByUser(ctx, recipientID, domain.MessageFilter{HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: senderID}, Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("recipient history: %v", err)
|
||
}
|
||
if len(recipientHistory.Messages) != 1 || recipientHistory.Messages[0].Body != "after" || recipientHistory.Messages[0].EditDate != 1700000200 {
|
||
t.Fatalf("recipient history = %+v, want edited body/date", recipientHistory.Messages)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreEditViaBotMessageUpdatesBothBoxes(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
senderID := int64(1000000001)
|
||
recipientID := int64(1000000002)
|
||
viaBotID := int64(1000000900)
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 103,
|
||
Message: "via before",
|
||
ViaBotID: viaBotID,
|
||
Date: 1700000100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SendPrivateText: %v", err)
|
||
}
|
||
|
||
if _, err := messages.EditMessage(ctx, domain.EditMessageRequest{
|
||
OwnerUserID: recipientID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: senderID},
|
||
ID: sent.RecipientMessage.ID,
|
||
Message: "bad bot",
|
||
EditDate: 1700000190,
|
||
ViaBotEditBotID: viaBotID + 1,
|
||
}); err != domain.ErrMessageAuthorRequired {
|
||
t.Fatalf("EditMessage wrong via bot err = %v, want ErrMessageAuthorRequired", err)
|
||
}
|
||
|
||
edited, err := messages.EditMessage(ctx, domain.EditMessageRequest{
|
||
OwnerUserID: recipientID,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: senderID},
|
||
ID: sent.RecipientMessage.ID,
|
||
Message: "via after",
|
||
EditDate: 1700000200,
|
||
ViaBotEditBotID: viaBotID,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("EditMessage via bot: %v", err)
|
||
}
|
||
if len(edited.Edited) != 2 {
|
||
t.Fatalf("edited boxes = %d, want 2", len(edited.Edited))
|
||
}
|
||
senderHistory, err := messages.ListByUser(ctx, senderID, domain.MessageFilter{HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: recipientID}, Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("sender history: %v", err)
|
||
}
|
||
if len(senderHistory.Messages) != 1 || senderHistory.Messages[0].Body != "via after" || senderHistory.Messages[0].ViaBotID != viaBotID {
|
||
t.Fatalf("sender history = %+v, want via after with via bot", senderHistory.Messages)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreDeleteHistoryDeletesOrPreservesDialogAndRebuilds(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
senderID := int64(1000000001)
|
||
recipientID := int64(1000000002)
|
||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: recipientID}
|
||
|
||
for i := 0; i < 2; i++ {
|
||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: int64(100 + i),
|
||
Message: "hello",
|
||
Date: 1700000200 + i,
|
||
}); err != nil {
|
||
t.Fatalf("seed send %d: %v", i, err)
|
||
}
|
||
}
|
||
deleted, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||
OwnerUserID: senderID,
|
||
Peer: peer,
|
||
Date: 1700000300,
|
||
})
|
||
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", self)
|
||
}
|
||
senderHistory, err := messages.ListByUser(ctx, senderID, domain.MessageFilter{HasPeer: true, Peer: peer, Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("sender history: %v", err)
|
||
}
|
||
recipientHistory, err := messages.ListByUser(ctx, recipientID, domain.MessageFilter{HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: senderID}, 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 := dialogs.ListByUser(ctx, senderID, 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 dialog deleted after full history delete", senderDialogs.Dialogs)
|
||
}
|
||
|
||
rebuilt, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: senderID,
|
||
RecipientUserID: recipientID,
|
||
RandomID: 200,
|
||
Message: "rebuilt",
|
||
Date: 1700000400,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("send after delete: %v", err)
|
||
}
|
||
senderDialogs, err = dialogs.ListByUser(ctx, senderID, 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 one dialog with new top message %d", senderDialogs.Dialogs, rebuilt.SenderMessage.ID)
|
||
}
|
||
|
||
preservedOwner := int64(1000000003)
|
||
preservedPeerID := int64(1000000004)
|
||
preservedPeer := domain.Peer{Type: domain.PeerTypeUser, ID: preservedPeerID}
|
||
var preservedTop domain.Message
|
||
for i := 0; i < 2; i++ {
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: preservedOwner,
|
||
RecipientUserID: preservedPeerID,
|
||
RandomID: int64(300 + i),
|
||
Message: "clear but keep dialog",
|
||
Date: 1700000500 + i,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("seed preserved send %d: %v", i, err)
|
||
}
|
||
preservedTop = sent.SenderMessage
|
||
}
|
||
clearResult, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||
OwnerUserID: preservedOwner,
|
||
Peer: preservedPeer,
|
||
JustClear: true,
|
||
Date: 1700000600,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("DeleteHistory just_clear: %v", err)
|
||
}
|
||
clearSelf := clearResult.Self()
|
||
if clearSelf.Pts != 5 || clearSelf.PtsCount != 3 || len(clearSelf.MessageIDs) != 1 || len(clearSelf.Events) != 3 {
|
||
t.Fatalf("just_clear result = %+v, want delete+read+edit ending pts=5 count=3", clearSelf)
|
||
}
|
||
if clearSelf.Events[0].Type != domain.UpdateEventDeleteMessages ||
|
||
clearSelf.Events[1].Type != domain.UpdateEventReadHistoryInbox ||
|
||
clearSelf.Events[2].Type != domain.UpdateEventEditMessage {
|
||
t.Fatalf("just_clear events = %+v, want delete/read/edit order", clearSelf.Events)
|
||
}
|
||
preservedDialogs, err := dialogs.ListByUser(ctx, preservedOwner, domain.DialogFilter{Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("preserved dialogs: %v", err)
|
||
}
|
||
if len(preservedDialogs.Dialogs) != 1 || preservedDialogs.Dialogs[0].Peer != preservedPeer ||
|
||
preservedDialogs.Dialogs[0].TopMessage != preservedTop.ID || len(preservedDialogs.Messages) != 1 {
|
||
t.Fatalf("preserved dialogs = %+v messages=%+v, want history-clear top %d", preservedDialogs.Dialogs, preservedDialogs.Messages, preservedTop.ID)
|
||
}
|
||
clearMessage := preservedDialogs.Messages[0]
|
||
if !domain.IsHistoryClearServiceMessage(clearMessage) || clearMessage.ID != preservedTop.ID ||
|
||
!clearMessage.Out || clearMessage.From.ID != preservedOwner || clearMessage.Body != "" ||
|
||
clearMessage.ReplyTo != nil || clearMessage.Forward != nil || clearMessage.MediaUnread ||
|
||
clearMessage.ReactionUnread || clearMessage.Pinned {
|
||
t.Fatalf("history clear anchor = %+v, want clean owner-local service message", clearMessage)
|
||
}
|
||
repeated, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||
OwnerUserID: preservedOwner,
|
||
Peer: preservedPeer,
|
||
JustClear: true,
|
||
Date: 1700000601,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("repeat DeleteHistory just_clear: %v", err)
|
||
}
|
||
if repeated.Changed() || len(repeated.Deleted) != 0 || messages.nextPts[preservedOwner] != 5 {
|
||
t.Fatalf("repeat just_clear = %+v pts=%d, want idempotent no-op", repeated, messages.nextPts[preservedOwner])
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreDeleteHistoryJustClearRevokeKeepsPerOwnerAnchors(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
const alice, bob = int64(1101), int64(1102)
|
||
var sent domain.SendPrivateTextResult
|
||
for i := 0; i < 2; i++ {
|
||
var err error
|
||
sent, err = messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: alice, RecipientUserID: bob, RandomID: int64(800 + i),
|
||
Message: "revoke clear", Date: 1700000700 + i,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("send %d: %v", i, err)
|
||
}
|
||
}
|
||
res, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||
OwnerUserID: alice,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bob},
|
||
JustClear: true,
|
||
Revoke: true,
|
||
Date: 1700000800,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("revoke just_clear: %v", err)
|
||
}
|
||
if len(res.Deleted) != 2 {
|
||
t.Fatalf("deleted owners = %+v, want alice and bob", res.Deleted)
|
||
}
|
||
for _, tc := range []struct {
|
||
userID int64
|
||
peerID int64
|
||
topID int
|
||
}{
|
||
{alice, bob, sent.SenderMessage.ID},
|
||
{bob, alice, sent.RecipientMessage.ID},
|
||
} {
|
||
history, err := messages.ListByUser(ctx, tc.userID, domain.MessageFilter{
|
||
HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: tc.peerID}, Limit: 10,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("history user %d: %v", tc.userID, err)
|
||
}
|
||
if len(history.Messages) != 1 || history.Messages[0].ID != tc.topID ||
|
||
!domain.IsHistoryClearServiceMessage(history.Messages[0]) ||
|
||
history.Messages[0].From.ID != tc.userID || !history.Messages[0].Out {
|
||
t.Fatalf("history user %d = %+v, want owner-local anchor %d", tc.userID, history.Messages, tc.topID)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreDeleteHistoryDateRangeDoesNotCreateHistoryClearAnchor(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
const owner, peerID = int64(1201), int64(1202)
|
||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: peerID}
|
||
for i, date := range []int{100, 200} {
|
||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: owner, RecipientUserID: peerID, RandomID: int64(900 + i),
|
||
Message: "dated", Date: date,
|
||
}); err != nil {
|
||
t.Fatalf("send %d: %v", i, err)
|
||
}
|
||
}
|
||
if _, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||
OwnerUserID: owner, Peer: peer, JustClear: true, MinDate: 150, MaxDate: 250, Date: 300,
|
||
}); err != nil {
|
||
t.Fatalf("date delete: %v", err)
|
||
}
|
||
history, err := messages.ListByUser(ctx, owner, domain.MessageFilter{HasPeer: true, Peer: peer, Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("history: %v", err)
|
||
}
|
||
if len(history.Messages) != 1 || history.Messages[0].Date != 100 || domain.IsHistoryClearServiceMessage(history.Messages[0]) {
|
||
t.Fatalf("date history = %+v, want surviving ordinary message only", history.Messages)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreDeleteHistoryJustClearKeepsAnchorAcrossBatches(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
const owner, peerID = int64(1301), int64(1302)
|
||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: peerID}
|
||
total := domain.MaxDeleteHistoryBatch + 2
|
||
var topID int
|
||
for i := 0; i < total; i++ {
|
||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: owner, RecipientUserID: peerID, RandomID: int64(10000 + i),
|
||
Message: "batch clear", Date: 1700010000 + i,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("send %d: %v", i, err)
|
||
}
|
||
topID = sent.SenderMessage.ID
|
||
}
|
||
first, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||
OwnerUserID: owner, Peer: peer, JustClear: true, Date: 1700020000,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("first clear: %v", err)
|
||
}
|
||
if first.Offset == 0 || len(first.Self().MessageIDs) != domain.MaxDeleteHistoryBatch ||
|
||
first.Self().PtsCount != domain.MaxDeleteHistoryBatch+2 {
|
||
t.Fatalf("first clear = %+v, want full batch plus one read/edit", first.Self())
|
||
}
|
||
second, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||
OwnerUserID: owner, Peer: peer, JustClear: true, Date: 1700020001,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("second clear: %v", err)
|
||
}
|
||
if second.Offset != 0 || len(second.Self().MessageIDs) != 1 || second.Self().PtsCount != 1 ||
|
||
len(second.Self().Events) != 1 || second.Self().Events[0].Type != domain.UpdateEventDeleteMessages {
|
||
t.Fatalf("second clear = %+v, want remaining delete only", second.Self())
|
||
}
|
||
history, err := messages.ListByUser(ctx, owner, domain.MessageFilter{HasPeer: true, Peer: peer, Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("history: %v", err)
|
||
}
|
||
if len(history.Messages) != 1 || history.Messages[0].ID != topID ||
|
||
!domain.IsHistoryClearServiceMessage(history.Messages[0]) {
|
||
t.Fatalf("history = %+v, want stable top anchor %d", history.Messages, topID)
|
||
}
|
||
}
|
||
|
||
func TestMessageStoreRevokeHistorySweepsPeerSideAfterLocalClear(t *testing.T) {
|
||
ctx := context.Background()
|
||
messages := NewMessageStore()
|
||
for i := 0; i < 3; i++ {
|
||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1001,
|
||
RecipientUserID: 1002,
|
||
RandomID: int64(700 + i),
|
||
Message: "history",
|
||
Date: 1700000600 + i,
|
||
}); err != nil {
|
||
t.Fatalf("SendPrivateText %d: %v", i, err)
|
||
}
|
||
}
|
||
// 先单向清空自己侧。
|
||
if _, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||
OwnerUserID: 1001,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1002},
|
||
Date: 1700000700,
|
||
}); err != nil {
|
||
t.Fatalf("local clear: %v", err)
|
||
}
|
||
// 再双向清史:反查模型对"我方已无 box"的消息失效,必须直扫对端。
|
||
res, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||
OwnerUserID: 1001,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1002},
|
||
Revoke: true,
|
||
Date: 1700000800,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("revoke clear: %v", err)
|
||
}
|
||
var peerEvent bool
|
||
for _, d := range res.Deleted {
|
||
if d.UserID == 1002 && len(d.MessageIDs) == 3 {
|
||
peerEvent = true
|
||
}
|
||
}
|
||
if !peerEvent {
|
||
t.Fatalf("revoke deleted = %+v, want peer-side sweep of all 3 messages", res.Deleted)
|
||
}
|
||
peerHistory, err := messages.ListByUser(ctx, 1002, domain.MessageFilter{
|
||
HasPeer: true,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1001},
|
||
Limit: 10,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("peer history: %v", err)
|
||
}
|
||
if len(peerHistory.Messages) != 0 {
|
||
t.Fatalf("peer history after revoke = %+v, want empty", peerHistory.Messages)
|
||
}
|
||
}
|
||
|
||
func TestReorderPinnedForceScopedToFolder(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
mainPeer := domain.Peer{Type: domain.PeerTypeUser, ID: 2001}
|
||
archivedPeer := domain.Peer{Type: domain.PeerTypeUser, ID: 2002}
|
||
if err := dialogs.Upsert(ctx, 1001, domain.Dialog{Peer: mainPeer, TopMessage: 1, TopMessageDate: 10}); err != nil {
|
||
t.Fatalf("upsert main: %v", err)
|
||
}
|
||
if err := dialogs.Upsert(ctx, 1001, domain.Dialog{Peer: archivedPeer, FolderID: domain.DialogArchiveFolderID, TopMessage: 2, TopMessageDate: 20}); err != nil {
|
||
t.Fatalf("upsert archived: %v", err)
|
||
}
|
||
if _, _, err := dialogs.SetPinned(ctx, 1001, mainPeer, true); err != nil {
|
||
t.Fatalf("pin main: %v", err)
|
||
}
|
||
if _, folderID, err := dialogs.SetPinned(ctx, 1001, archivedPeer, true); err != nil || folderID != domain.DialogArchiveFolderID {
|
||
t.Fatalf("pin archived folder = %d err %v, want archive", folderID, err)
|
||
}
|
||
// 归档列表内 force 重排:绝不允许清掉主列表的置顶。
|
||
if changed, err := dialogs.ReorderPinned(ctx, 1001, domain.DialogArchiveFolderID, []domain.Peer{archivedPeer}, true); err != nil || changed {
|
||
t.Fatalf("reorder archive = changed %v err %v, want no-op", changed, err)
|
||
}
|
||
list, err := dialogs.ListByUser(ctx, 1001, domain.DialogFilter{Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("list main: %v", err)
|
||
}
|
||
for _, dialog := range list.Dialogs {
|
||
if dialog.Peer == mainPeer && !dialog.Pinned {
|
||
t.Fatalf("main pin cleared by archive force reorder: %+v", dialog)
|
||
}
|
||
}
|
||
// 主列表 force 重排同样不得波及归档置顶。
|
||
if changed, err := dialogs.ReorderPinned(ctx, 1001, domain.DialogMainFolderID, []domain.Peer{mainPeer}, true); err != nil || changed {
|
||
t.Fatalf("reorder main = changed %v err %v, want no-op", changed, err)
|
||
}
|
||
archived, err := dialogs.ListByUser(ctx, 1001, domain.DialogFilter{HasFolderID: true, FolderID: domain.DialogArchiveFolderID, Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("list archive: %v", err)
|
||
}
|
||
for _, dialog := range archived.Dialogs {
|
||
if dialog.Peer == archivedPeer && !dialog.Pinned {
|
||
t.Fatalf("archive pin cleared by main force reorder: %+v", dialog)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSendPrivateTextClearsSenderUnreadMark(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
first, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1000000001,
|
||
RecipientUserID: 1000000002,
|
||
RandomID: 1,
|
||
Message: "hi",
|
||
Date: 1700000100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("seed send: %v", err)
|
||
}
|
||
peer := first.SenderMessage.Peer
|
||
if _, err := dialogs.SetUnreadMark(ctx, 1000000001, peer, true); err != nil {
|
||
t.Fatalf("mark unread: %v", err)
|
||
}
|
||
// 发送方向发出消息即清手动未读标记(对齐 postgres UpsertOutboxDialog
|
||
// 与 channel 发送路径)。
|
||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1000000001,
|
||
RecipientUserID: 1000000002,
|
||
RandomID: 2,
|
||
Message: "again",
|
||
Date: 1700000200,
|
||
}); err != nil {
|
||
t.Fatalf("send after mark: %v", err)
|
||
}
|
||
marks, err := dialogs.ListUnreadMarked(ctx, 1000000001)
|
||
if err != nil {
|
||
t.Fatalf("list unread marks: %v", err)
|
||
}
|
||
if len(marks) != 0 {
|
||
t.Fatalf("unread marks after send = %+v, want cleared", marks)
|
||
}
|
||
}
|
||
|
||
func TestSendPrivateTextPreservesPinnedDialog(t *testing.T) {
|
||
ctx := context.Background()
|
||
dialogs := NewDialogStore()
|
||
messages := NewMessageStore(dialogs)
|
||
first, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1000000001,
|
||
RecipientUserID: 1000000002,
|
||
RandomID: 1,
|
||
Message: "hi",
|
||
Date: 1700000100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("seed send: %v", err)
|
||
}
|
||
peer := first.SenderMessage.Peer
|
||
if _, _, err := dialogs.SetPinned(ctx, 1000000001, peer, true); err != nil {
|
||
t.Fatalf("pin: %v", err)
|
||
}
|
||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||
SenderUserID: 1000000001,
|
||
RecipientUserID: 1000000002,
|
||
RandomID: 2,
|
||
Message: "again",
|
||
Date: 1700000200,
|
||
}); err != nil {
|
||
t.Fatalf("send after pin: %v", err)
|
||
}
|
||
list, err := dialogs.ListByUser(ctx, 1000000001, domain.DialogFilter{Limit: 10})
|
||
if err != nil {
|
||
t.Fatalf("list: %v", err)
|
||
}
|
||
found := false
|
||
for _, dialog := range list.Dialogs {
|
||
if dialog.Peer == peer {
|
||
found = true
|
||
if !dialog.Pinned || dialog.PinnedOrder == 0 {
|
||
t.Fatalf("dialog after send = %+v, want pinned preserved", dialog)
|
||
}
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("dialog not found after send")
|
||
}
|
||
}
|