chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
|
|
@ -57,6 +57,20 @@ func TestMessageStoreSendPrivateTextCreatesBothOwnerBoxes(t *testing.T) {
|
|||
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)
|
||||
|
|
@ -65,11 +79,83 @@ func TestMessageStoreSendPrivateTextCreatesBothOwnerBoxes(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("recipient history: %v", err)
|
||||
}
|
||||
if len(senderHistory.Messages) != 2 || len(recipientHistory.Messages) != 2 {
|
||||
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 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)
|
||||
|
||||
dup, 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 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 boxes", dup)
|
||||
}
|
||||
assertWebViewData("duplicate sender", dup.SenderMessage)
|
||||
|
||||
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 TestMessageStorePrivateMessageReactionsAreSharedAcrossOwnerBoxes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
messages := NewMessageStore()
|
||||
|
|
@ -117,7 +203,7 @@ func TestMessageStorePrivateMessageReactionsAreSharedAcrossOwnerBoxes(t *testing
|
|||
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].Big || got[0].My {
|
||||
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})
|
||||
|
|
@ -145,6 +231,9 @@ func TestMessageStorePrivateMessageReactionsAreSharedAcrossOwnerBoxes(t *testing
|
|||
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,
|
||||
|
|
@ -157,11 +246,84 @@ func TestMessageStorePrivateMessageReactionsAreSharedAcrossOwnerBoxes(t *testing
|
|||
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].My {
|
||||
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()
|
||||
|
|
@ -369,6 +531,270 @@ func TestMessageStoreReadHistoryEmitsInboxAndOutboxReceipts(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
|
@ -419,6 +845,72 @@ func TestMessageStoreReadMessageContentsClearsUnreadContentOnce(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -478,6 +970,59 @@ func TestMessageStoreEditMessageUpdatesBothBoxes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
|
@ -573,3 +1118,185 @@ func TestMessageStoreDeleteHistoryDeletesOrPreservesDialogAndRebuilds(t *testing
|
|||
t.Fatalf("preserved dialogs = %+v messages=%+v, want empty dialog kept after just_clear", preservedDialogs.Dialogs, preservedDialogs.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue