owpengram-server/internal/rpc/encrypted_messages_test.go
2026-07-24 11:56:59 +08:00

387 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"testing"
"github.com/iamxvbaba/td/tg"
appmoderation "telesrv/internal/app/moderation"
"telesrv/internal/domain"
"telesrv/internal/postresponse"
"telesrv/internal/store/memory"
)
// acceptChat 跑完 request→accept返回 normal 态密聊 id 与 participant 视角 access_hash。
func (f *encryptedFixture) acceptChat(t *testing.T) (chatID int, partAccessHash int64) {
t.Helper()
res, err := f.router.onMessagesRequestEncryption(f.adminCtx(), &tg.MessagesRequestEncryptionRequest{
UserID: &tg.InputUser{UserID: f.participant.ID, AccessHash: f.participant.AccessHash},
RandomID: 4242,
GA: dhParam(0x55),
})
if err != nil {
t.Fatalf("request: %v", err)
}
chatID = res.(*tg.EncryptedChatWaiting).ID
chat, _, _ := f.store.GetSecretChat(f.ctx, chatID)
if _, err := f.router.onMessagesAcceptEncryption(f.participantCtx(), &tg.MessagesAcceptEncryptionRequest{
Peer: tg.InputEncryptedChat{ChatID: chatID, AccessHash: chat.ParticipantAccessHash},
GB: dhParam(0x66),
KeyFingerprint: 7,
}); err != nil {
t.Fatalf("accept: %v", err)
}
return chatID, chat.ParticipantAccessHash
}
func encNewMessagePayload(t *testing.T, rec phonePushRecord) *tg.UpdateNewEncryptedMessage {
t.Helper()
updates, ok := rec.msg.(*tg.Updates)
if !ok || len(updates.Updates) != 1 {
t.Fatalf("pushed msg = %T, want single-update tg.Updates", rec.msg)
}
upd, ok := updates.Updates[0].(*tg.UpdateNewEncryptedMessage)
if !ok {
t.Fatalf("pushed update = %T, want UpdateNewEncryptedMessage", updates.Updates[0])
}
return upd
}
func TestReportEncryptedSpamPersistsMetadataOnly(t *testing.T) {
f := newEncryptedFixture(t)
chatID, participantAccessHash := f.acceptChat(t)
reports := memory.NewModerationReportStore()
f.router.deps.Moderation = appmoderation.NewService(reports)
ok, err := f.router.onMessagesReportEncryptedSpam(
f.participantCtx(),
tg.InputEncryptedChat{ChatID: chatID, AccessHash: participantAccessHash},
)
if err != nil || !ok {
t.Fatalf("report encrypted spam ok=%v err=%v", ok, err)
}
stored := reports.Reports()
if len(stored) != 1 ||
stored[0].Source != domain.ModerationSourceEncryptedSpam ||
stored[0].ReporterUserID != f.participant.ID ||
stored[0].Target != (domain.Peer{Type: domain.PeerTypeUser, ID: f.admin.ID}) ||
len(stored[0].Items) != 1 ||
stored[0].Items[0].Kind != domain.ModerationItemEncryptedChat {
t.Fatalf("stored encrypted report=%+v", stored)
}
if string(stored[0].Items[0].Evidence) == "" ||
string(stored[0].Items[0].Evidence) == "plaintext" {
t.Fatalf("encrypted metadata evidence=%s", stored[0].Items[0].Evidence)
}
}
func TestSendEncryptedRPCFlow(t *testing.T) {
f := newEncryptedFixture(t)
chatID, _ := f.acceptChat(t)
chat, _, _ := f.store.GetSecretChat(f.ctx, chatID)
f.sessions.reset()
// admin 发加密消息 → 投给 participant 设备。
data := []byte{0xDE, 0xAD, 0xBE, 0xEF}
sent, err := f.router.onMessagesSendEncrypted(f.adminCtx(), &tg.MessagesSendEncryptedRequest{
Peer: tg.InputEncryptedChat{ChatID: chatID, AccessHash: chat.AdminAccessHash},
RandomID: 88888,
Data: data,
})
if err != nil {
t.Fatalf("sendEncrypted: %v", err)
}
sentMsg, ok := sent.(*tg.MessagesSentEncryptedMessage)
if !ok || sentMsg.Date == 0 {
t.Fatalf("send response = %T %+v, want SentEncryptedMessage{date}", sent, sent)
}
recs := f.sessions.records()
if len(recs) != 1 || recs[0].userID != f.participant.ID {
t.Fatalf("send push = %+v, want single push to participant", recs)
}
upd := encNewMessagePayload(t, recs[0])
if upd.Qts != 1 {
t.Fatalf("pushed qts = %d, want 1", upd.Qts)
}
em, ok := upd.Message.(*tg.EncryptedMessage)
if !ok || string(em.Bytes) != string(data) || em.ChatID != chatID {
t.Fatalf("pushed message = %+v, want EncryptedMessage bytes verbatim", upd.Message)
}
// participant getState设备 qts = 1。
st, err := f.router.onUpdatesGetState(f.participantCtx())
if err != nil {
t.Fatalf("getState: %v", err)
}
if st.Qts != 1 {
t.Fatalf("participant getState qts = %d, want 1", st.Qts)
}
// participant 离线补差分:从 qts=0 拿回该加密消息。
differenceCtx := postresponse.WithCallbacks(f.participantCtx())
diff, err := f.router.onUpdatesGetDifference(differenceCtx, &tg.UpdatesGetDifferenceRequest{Qts: 0})
if err != nil {
t.Fatalf("getDifference: %v", err)
}
full, ok := diff.(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", diff)
}
if len(full.NewEncryptedMessages) != 1 || full.State.Qts != 1 {
t.Fatalf("difference enc msgs = %d state.qts = %d, want 1/1", len(full.NewEncryptedMessages), full.State.Qts)
}
gotEM, ok := full.NewEncryptedMessages[0].(*tg.EncryptedMessage)
if !ok || string(gotEM.Bytes) != string(data) {
t.Fatalf("difference message = %+v, want bytes verbatim", full.NewEncryptedMessages[0])
}
postresponse.Run(differenceCtx)
// receivedQueue确认到 qts=1返回空 Vector。
rq, err := f.router.onMessagesReceivedQueue(f.participantCtx(), 1)
if err != nil {
t.Fatalf("receivedQueue: %v", err)
}
if len(rq) != 0 {
t.Fatalf("receivedQueue = %v, want empty vector", rq)
}
// 确认后再补差分qts=1已无新消息 → DifferenceEmpty。
diff2, err := f.router.onUpdatesGetDifference(f.participantCtx(), &tg.UpdatesGetDifferenceRequest{Qts: 1})
if err != nil {
t.Fatalf("getDifference 2: %v", err)
}
if _, ok := diff2.(*tg.UpdatesDifferenceEmpty); !ok {
t.Fatalf("difference after ack = %T, want UpdatesDifferenceEmpty", diff2)
}
// 幂等重发同 random_id → 返回首次 date不产生新 qts。
f.sessions.reset()
sent2, err := f.router.onMessagesSendEncrypted(f.adminCtx(), &tg.MessagesSendEncryptedRequest{
Peer: tg.InputEncryptedChat{ChatID: chatID, AccessHash: chat.AdminAccessHash},
RandomID: 88888,
Data: data,
})
if err != nil {
t.Fatalf("idempotent resend: %v", err)
}
if sent2.(*tg.MessagesSentEncryptedMessage).Date != sentMsg.Date {
t.Fatalf("idempotent resend date = %d, want %d (首次落库 date)", sent2.(*tg.MessagesSentEncryptedMessage).Date, sentMsg.Date)
}
}
func encOtherUpdate[T tg.UpdateClass](t *testing.T, diff tg.UpdatesDifferenceClass) T {
t.Helper()
full, ok := diff.(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", diff)
}
for _, u := range full.OtherUpdates {
if got, ok := u.(T); ok {
return got
}
}
var zero T
t.Fatalf("OtherUpdates %+v missing %T", full.OtherUpdates, zero)
return zero
}
// TestEncryptionStateEventOfflineDeliveryparticipant 在 requestEncryption 时离线,
// 重连 getDifference 经 durable 状态事件补回 updateEncryption(requested),且只补一次。
func TestEncryptionStateEventOfflineDelivery(t *testing.T) {
f := newEncryptedFixture(t)
res, err := f.router.onMessagesRequestEncryption(f.adminCtx(), &tg.MessagesRequestEncryptionRequest{
UserID: &tg.InputUser{UserID: f.participant.ID, AccessHash: f.participant.AccessHash},
RandomID: 5151,
GA: dhParam(0x55),
})
if err != nil {
t.Fatalf("request: %v", err)
}
chatID := res.(*tg.EncryptedChatWaiting).ID
// participant 离线补差分:拿到 updateEncryption(encryptedChatRequested, 携 g_a)。
differenceCtx := postresponse.WithCallbacks(f.participantCtx())
diff, err := f.router.onUpdatesGetDifference(differenceCtx, &tg.UpdatesGetDifferenceRequest{Qts: 0})
if err != nil {
t.Fatalf("getDifference: %v", err)
}
upd := encOtherUpdate[*tg.UpdateEncryption](t, diff)
requested, ok := upd.Chat.(*tg.EncryptedChatRequested)
if !ok || requested.ID != chatID || len(requested.GA) == 0 {
t.Fatalf("offline handshake update = %+v, want EncryptedChatRequested with g_a", upd.Chat)
}
deviceKey := businessAuthKeyInt64(encPartAuthKey)
beforeDelivery, err := f.queue.ListUndeliveredStateEvents(f.ctx, f.participant.ID, deviceKey, 10)
if err != nil || len(beforeDelivery) == 0 {
t.Fatalf("state events before difference delivery = %v err=%v, want pending", beforeDelivery, err)
}
postresponse.Run(differenceCtx)
afterDelivery, err := f.queue.ListUndeliveredStateEvents(f.ctx, f.participant.ID, deviceKey, 10)
if err != nil || len(afterDelivery) != 0 {
t.Fatalf("state events after difference delivery = %v err=%v, want none", afterDelivery, err)
}
// 再次补差分:已投递 → 不重复DifferenceEmpty
diff2, err := f.router.onUpdatesGetDifference(f.participantCtx(), &tg.UpdatesGetDifferenceRequest{Qts: 0})
if err != nil {
t.Fatalf("getDifference 2: %v", err)
}
if _, ok := diff2.(*tg.UpdatesDifferenceEmpty); !ok {
t.Fatalf("redelivery: difference = %T, want UpdatesDifferenceEmpty", diff2)
}
}
func TestEncryptedDifferenceAcknowledgesOnlyProjectedStateEvents(t *testing.T) {
f := newEncryptedFixture(t)
chatID, _ := f.acceptChat(t)
deviceKey := businessAuthKeyInt64(encPartAuthKey)
// Retire handshake events created while establishing the fixture; the test
// below owns the complete pending set.
prior, err := f.queue.ListUndeliveredStateEvents(f.ctx, f.participant.ID, deviceKey, 100)
if err != nil {
t.Fatalf("list prior events: %v", err)
}
priorIDs := make([]int64, 0, len(prior))
for _, event := range prior {
priorIDs = append(priorIDs, event.ID)
}
if err := f.queue.MarkStateEventsDelivered(context.Background(), deviceKey, priorIDs); err != nil {
t.Fatalf("retire prior events: %v", err)
}
validID, err := f.queue.AppendStateEvent(f.ctx, domain.EncryptedStateEvent{
TargetUserID: f.participant.ID,
ChatID: chatID,
Type: domain.EncryptedStateEventRead,
MaxDate: 1700001000,
Date: 1700001001,
})
if err != nil {
t.Fatalf("append valid event: %v", err)
}
missingChatID, err := f.queue.AppendStateEvent(f.ctx, domain.EncryptedStateEvent{
TargetUserID: f.participant.ID,
ChatID: 999999,
Type: domain.EncryptedStateEventEncryption,
Date: 1700001002,
})
if err != nil {
t.Fatalf("append missing-chat event: %v", err)
}
unknownID, err := f.queue.AppendStateEvent(f.ctx, domain.EncryptedStateEvent{
TargetUserID: f.participant.ID,
ChatID: chatID,
Type: domain.EncryptedStateEventType(99),
Date: 1700001003,
})
if err != nil {
t.Fatalf("append unknown event: %v", err)
}
ctx := postresponse.WithCallbacks(f.participantCtx())
diff, err := f.router.onUpdatesGetDifference(ctx, &tg.UpdatesGetDifferenceRequest{})
if err != nil {
t.Fatalf("getDifference: %v", err)
}
read := encOtherUpdate[*tg.UpdateEncryptedMessagesRead](t, diff)
if read.ChatID != chatID {
t.Fatalf("projected read chat_id = %d, want %d", read.ChatID, chatID)
}
postresponse.Run(ctx)
pending, err := f.queue.ListUndeliveredStateEvents(f.ctx, f.participant.ID, deviceKey, 100)
if err != nil {
t.Fatalf("list pending after delivery: %v", err)
}
remaining := make(map[int64]bool, len(pending))
for _, event := range pending {
remaining[event.ID] = true
}
if remaining[validID] || !remaining[missingChatID] || !remaining[unknownID] {
t.Fatalf("pending IDs after delivery = %v; valid=%d missing=%d unknown=%d", remaining, validID, missingChatID, unknownID)
}
}
// TestDiscardStateEventOfflineDeliveryadmin 在 participant 未接受时 discard账号级
// participant 离线设备 getDifference 补回 encryptedChatDiscarded。
func TestDiscardStateEventOfflineDelivery(t *testing.T) {
f := newEncryptedFixture(t)
res, err := f.router.onMessagesRequestEncryption(f.adminCtx(), &tg.MessagesRequestEncryptionRequest{
UserID: &tg.InputUser{UserID: f.participant.ID, AccessHash: f.participant.AccessHash},
RandomID: 6262,
GA: dhParam(0x55),
})
if err != nil {
t.Fatalf("request: %v", err)
}
chatID := res.(*tg.EncryptedChatWaiting).ID
// admin 撤回邀请。
if _, err := f.router.onMessagesDiscardEncryption(f.adminCtx(), &tg.MessagesDiscardEncryptionRequest{ChatID: chatID, DeleteHistory: false}); err != nil {
t.Fatalf("discard: %v", err)
}
// participant 离线补差分:现态是 discarded重建出 encryptedChatDiscarded。
diff, err := f.router.onUpdatesGetDifference(f.participantCtx(), &tg.UpdatesGetDifferenceRequest{Qts: 0})
if err != nil {
t.Fatalf("getDifference: %v", err)
}
upd := encOtherUpdate[*tg.UpdateEncryption](t, diff)
if _, ok := upd.Chat.(*tg.EncryptedChatDiscarded); !ok {
t.Fatalf("offline discard update = %T, want EncryptedChatDiscarded", upd.Chat)
}
}
func TestReadEncryptedHistoryPushesPeer(t *testing.T) {
f := newEncryptedFixture(t)
chatID, _ := f.acceptChat(t)
chat, _, _ := f.store.GetSecretChat(f.ctx, chatID)
f.sessions.reset()
ok, err := f.router.onMessagesReadEncryptedHistory(f.adminCtx(), &tg.MessagesReadEncryptedHistoryRequest{
Peer: tg.InputEncryptedChat{ChatID: chatID, AccessHash: chat.AdminAccessHash},
MaxDate: 1500,
})
if err != nil || !ok {
t.Fatalf("readEncryptedHistory: ok=%v err=%v", ok, err)
}
recs := f.sessions.records()
if len(recs) != 1 || recs[0].userID != f.participant.ID {
t.Fatalf("read push = %+v, want single push to participant", recs)
}
updates := recs[0].msg.(*tg.Updates)
rd, ok := updates.Updates[0].(*tg.UpdateEncryptedMessagesRead)
if !ok || rd.ChatID != chatID || rd.MaxDate != 1500 {
t.Fatalf("read update = %+v, want UpdateEncryptedMessagesRead{chat,max_date=1500}", updates.Updates[0])
}
}
func TestReadEncryptedHistoryInvalidMaxDate(t *testing.T) {
f := newEncryptedFixture(t)
chatID, _ := f.acceptChat(t)
chat, _, _ := f.store.GetSecretChat(f.ctx, chatID)
_, err := f.router.onMessagesReadEncryptedHistory(f.adminCtx(), &tg.MessagesReadEncryptedHistoryRequest{
Peer: tg.InputEncryptedChat{ChatID: chatID, AccessHash: chat.AdminAccessHash},
MaxDate: 0,
})
assertPhoneRPCErr(t, err, "MAX_DATE_INVALID")
}
func TestSetEncryptedTypingPushesPeer(t *testing.T) {
f := newEncryptedFixture(t)
chatID, _ := f.acceptChat(t)
chat, _, _ := f.store.GetSecretChat(f.ctx, chatID)
f.sessions.reset()
ok, err := f.router.onMessagesSetEncryptedTyping(f.adminCtx(), &tg.MessagesSetEncryptedTypingRequest{
Peer: tg.InputEncryptedChat{ChatID: chatID, AccessHash: chat.AdminAccessHash},
Typing: true,
})
if err != nil || !ok {
t.Fatalf("setEncryptedTyping: ok=%v err=%v", ok, err)
}
recs := f.sessions.records()
if len(recs) != 1 || recs[0].userID != f.participant.ID {
t.Fatalf("typing push = %+v, want single push to participant", recs)
}
if _, ok := recs[0].msg.(*tg.Updates).Updates[0].(*tg.UpdateEncryptedChatTyping); !ok {
t.Fatalf("typing update = %T, want UpdateEncryptedChatTyping", recs[0].msg.(*tg.Updates).Updates[0])
}
}