chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,572 @@
package rpc
import (
"context"
"strings"
"testing"
"time"
"github.com/gotd/td/clock"
"github.com/gotd/td/proto"
"github.com/gotd/td/tg"
"go.uber.org/zap/zaptest"
appchannels "telesrv/internal/app/channels"
appupdates "telesrv/internal/app/updates"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
func TestPushLoginMessageSendsUpdateNewMessage(t *testing.T) {
sessions := &captureSessions{}
r := New(Config{}, Deps{Sessions: sessions}, zaptest.NewLogger(t), clock.System)
msg := domain.Message{
ID: 99,
OwnerUserID: 1000000001,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
Date: 1700000100,
Body: "Login code: 12345",
Entities: []domain.MessageEntity{{Type: domain.MessageEntityBold, Offset: 0, Length: 11}},
}
event := domain.UpdateEvent{Type: domain.UpdateEventNewMessage, Pts: 4, PtsCount: 1, Date: msg.Date, Message: msg}
state := domain.UpdateState{Pts: 4, Date: 1700000100, Seq: 2}
r.pushLoginMessage(context.Background(), [8]byte{}, 55, event, state)
gotSession := sessions.snapshot()
if gotSession.sessionID != 55 || gotSession.messageType != proto.MessageFromServer {
t.Fatalf("push target = session %d type %v, want session 55 MessageFromServer", gotSession.sessionID, gotSession.messageType)
}
got, ok := gotSession.message.(*tg.Updates)
if !ok {
t.Fatalf("pushed message = %T, want *tg.Updates", gotSession.message)
}
if len(got.Updates) != 1 || len(got.Users) != 1 {
t.Fatalf("updates payload = %+v, want one update and official user", got)
}
update, ok := got.Updates[0].(*tg.UpdateNewMessage)
if !ok {
t.Fatalf("update = %T, want *tg.UpdateNewMessage", got.Updates[0])
}
if update.Pts != event.Pts || update.PtsCount != event.PtsCount || got.Seq != state.Seq {
t.Fatalf("update state = pts %d pts_count %d seq %d, want %d/%d/%d", update.Pts, update.PtsCount, got.Seq, event.Pts, event.PtsCount, state.Seq)
}
message, ok := update.Message.(*tg.Message)
if !ok {
t.Fatalf("update message = %T, want *tg.Message", update.Message)
}
if message.ID != msg.ID || message.Message != msg.Body {
t.Fatalf("message = %+v, want login message %+v", message, msg)
}
user, ok := got.Users[0].(*tg.User)
if !ok || user.ID != domain.OfficialSystemUserID || !user.Verified || !user.Support {
t.Fatalf("user = %#v, want verified official system user", got.Users[0])
}
}
func TestSignInServiceNotificationMatchesEnterpriseShape(t *testing.T) {
var authKeyID [8]byte
authKeyID[0] = 9
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
ctx := WithClientInfo(context.Background(), ClientInfo{
DeviceModel: "Telegram Desktop",
SystemVersion: "Windows",
AppVersion: "6.8.4",
})
got := r.tgSignInServiceNotification(ctx, domain.User{
ID: 1000000001,
FirstName: "Test",
LastName: "User",
}, authKeyID)
if len(got.Updates) != 1 {
t.Fatalf("updates = %+v, want one service notification", got.Updates)
}
update, ok := got.Updates[0].(*tg.UpdateServiceNotification)
if !ok {
t.Fatalf("update = %T, want *tg.UpdateServiceNotification", got.Updates[0])
}
if update.Popup || update.InboxDate == 0 || update.Media == nil {
t.Fatalf("notification flags/media = popup %v inbox %d media %T", update.Popup, update.InboxDate, update.Media)
}
for _, want := range []string{"New login.", "Test User", "Telegram Desktop", "Settings > Devices"} {
if !strings.Contains(update.Message, want) {
t.Fatalf("notification message %q missing %q", update.Message, want)
}
}
if len(update.Entities) < 3 {
t.Fatalf("entities = %+v, want bold entities for title/settings links", update.Entities)
}
}
func TestLogOutClearsSessionAndUpdateState(t *testing.T) {
var authKeyID [8]byte
authKeyID[0] = 5
auth := &captureAuthService{}
updates := &captureUpdates{}
sessions := &captureSessions{authKeyID: authKeyID, authKeyResolved: true, userID: 1000000001, userResolved: true}
r := New(Config{}, Deps{
Auth: auth,
Updates: updates,
Sessions: sessions,
}, zaptest.NewLogger(t), clock.System)
_, err := r.onAuthLogOut(WithAuthKeyID(context.Background(), authKeyID))
if err != nil {
t.Fatalf("auth.logOut: %v", err)
}
if auth.loggedOutAuthKeyID != authKeyID {
t.Fatalf("logged out auth key = %x, want %x", auth.loggedOutAuthKeyID, authKeyID)
}
if updates.clearedAuthKeyID != authKeyID || !updates.cleared {
t.Fatalf("cleared auth key = %x cleared=%v, want %x", updates.clearedAuthKeyID, updates.cleared, authKeyID)
}
gotSession := sessions.snapshot()
if gotSession.userID != 0 || !gotSession.userResolved {
t.Fatalf("session user after logout = %d resolved=%v, want 0/true", gotSession.userID, gotSession.userResolved)
}
}
func TestSignInDifferentUserClearsAuthKeyUpdateState(t *testing.T) {
var authKeyID [8]byte
authKeyID[0] = 6
auth := &captureAuthService{signInUser: domain.User{ID: 1000000002, FirstName: "Two"}}
updates := &captureUpdates{}
sessions := &captureSessions{authKeyID: authKeyID, authKeyResolved: true, userID: 1000000001, userResolved: true}
r := New(Config{}, Deps{
Auth: auth,
Updates: updates,
Sessions: sessions,
}, zaptest.NewLogger(t), clock.System)
ctx := WithUserID(WithAuthKeyID(WithSessionID(context.Background(), 77), authKeyID), 1000000001)
_, err := r.onAuthSignIn(ctx, &tg.AuthSignInRequest{PhoneNumber: "15550000002", PhoneCodeHash: "hash", PhoneCode: "12345"})
if err != nil {
t.Fatalf("auth.signIn: %v", err)
}
if updates.clearedAuthKeyID != authKeyID || !updates.cleared {
t.Fatalf("cleared auth key = %x cleared=%v, want %x", updates.clearedAuthKeyID, updates.cleared, authKeyID)
}
gotSession := sessions.snapshot()
if gotSession.userID != 1000000002 {
t.Fatalf("session user = %d, want new user 1000000002", gotSession.userID)
}
}
func TestUpdatesGetStateMarksSessionReadyForPush(t *testing.T) {
sessions := &captureSessions{}
r := New(Config{}, Deps{
Sessions: sessions,
Updates: &captureUpdates{state: domain.UpdateState{Pts: 3, Date: 1700000000, Seq: 2}},
}, zaptest.NewLogger(t), clock.System)
got, err := r.onUpdatesGetState(WithSessionID(context.Background(), 77))
if err != nil {
t.Fatalf("updates.getState: %v", err)
}
if got.Pts != 3 || got.Seq != 2 {
t.Fatalf("state = %+v, want pts=3 seq=2", got)
}
gotSession := sessions.snapshot()
if gotSession.sessionID != 77 || !gotSession.receives {
t.Fatalf("session ready = id %d receives %v, want 77/true", gotSession.sessionID, gotSession.receives)
}
}
// TestUpdatesGetStateReturnsAccountCurrentState 复现 TDesktop 冷启动未读重复:
// getState 必须返回账号当前最新状态(而非设备旧确认水位),且不得再推
// updatesTooLong 诱导差分——TDesktop 不持久化 pts启动期离线数据由
// getDialogs 快照承载,旧水位差分会把快照里已计入的消息重放一遍
// (实测未读 2→3、dialog 预览被旧消息抢占)。
func TestUpdatesGetStateReturnsAccountCurrentState(t *testing.T) {
sessions := &captureSessions{}
current := domain.UpdateState{Pts: 4, Date: 1700000001}
updates := &captureUpdates{
state: domain.UpdateState{Pts: 3, Date: 1700000000},
currentState: &current,
}
r := New(Config{}, Deps{
Sessions: sessions,
Updates: updates,
}, zaptest.NewLogger(t), clock.System)
got, err := r.onUpdatesGetState(WithSessionID(context.Background(), 77))
if err != nil {
t.Fatalf("updates.getState: %v", err)
}
if got.Pts != 4 {
t.Fatalf("state pts = %d, want account current 4 (per-key stale=3 会触发客户端重放)", got.Pts)
}
if !updates.acknowledged {
t.Fatal("getState 未推进设备确认水位")
}
time.Sleep(500 * time.Millisecond)
if msg := sessions.snapshot().message; msg != nil {
if _, ok := msg.(*tg.UpdatesTooLong); ok {
t.Fatal("getState 后不应再推 updatesTooLong会诱导 TDesktop 重放快照前差分)")
}
}
}
func TestUpdatesDifferenceIncludesLoginMessageAndOfficialUser(t *testing.T) {
msg := domain.Message{
ID: 88,
OwnerUserID: 1000000001,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
Date: 1700000100,
Body: "Login code: 12345",
}
got, ok := tgUpdatesDifference(0, domain.UpdateDifference{
State: domain.UpdateState{Pts: 5, Date: msg.Date, Seq: 4},
Events: []domain.UpdateEvent{{
Type: domain.UpdateEventNewMessage,
Pts: 5,
PtsCount: 1,
Date: msg.Date,
Message: msg,
}},
}).(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", got)
}
if got.State.Pts != 5 || len(got.NewMessages) != 1 || len(got.Users) != 1 {
t.Fatalf("difference = %+v, want one message, one official user, pts=5", got)
}
user, ok := got.Users[0].(*tg.User)
if !ok || user.ID != domain.OfficialSystemUserID {
t.Fatalf("user = %#v, want official system user", got.Users[0])
}
}
func TestUpdatesDifferenceMarksViewerUserAsSelf(t *testing.T) {
viewerID := int64(1000000001)
msg := domain.Message{
ID: 90,
OwnerUserID: viewerID,
Out: true,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002},
From: domain.Peer{Type: domain.PeerTypeUser, ID: viewerID},
Date: 1700000102,
Body: "self probe",
}
got, ok := tgUpdatesDifference(viewerID, domain.UpdateDifference{
State: domain.UpdateState{Pts: 7, Date: msg.Date},
Events: []domain.UpdateEvent{{
UserID: viewerID,
Type: domain.UpdateEventNewMessage,
Pts: 7,
PtsCount: 1,
Date: msg.Date,
Message: msg,
Users: []domain.User{
{ID: viewerID, FirstName: "Me"},
{ID: 1000000002, FirstName: "Peer"},
},
}},
}).(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", got)
}
var sawSelf, sawPeer bool
for _, item := range got.Users {
user, ok := item.(*tg.User)
if !ok {
t.Fatalf("user = %#v, want *tg.User", item)
}
switch user.ID {
case viewerID:
sawSelf = true
if !user.Self {
t.Fatalf("viewer user = %#v, want self flag set: clients persist this object as the current account", user)
}
case 1000000002:
sawPeer = true
if user.Self {
t.Fatalf("peer user = %#v, must not carry self flag", user)
}
}
}
if !sawSelf || !sawPeer {
t.Fatalf("users = %+v, want viewer and peer entries", got.Users)
}
}
func TestUpdatesDifferenceIncludesForwardSourceChannelChat(t *testing.T) {
source := domain.Channel{
ID: 2000000001,
AccessHash: 9001,
Title: "Source Channel",
Broadcast: true,
Date: 1700000000,
}
msg := domain.Message{
ID: 89,
OwnerUserID: 1000000001,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002},
From: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001},
Date: 1700000101,
Body: "forwarded",
Forward: &domain.MessageForward{From: domain.Peer{Type: domain.PeerTypeChannel, ID: source.ID}, Date: 1700000000},
}
got, ok := tgUpdatesDifference(0, domain.UpdateDifference{
State: domain.UpdateState{Pts: 6, Date: msg.Date},
Events: []domain.UpdateEvent{{
UserID: msg.OwnerUserID,
Type: domain.UpdateEventNewMessage,
Pts: 6,
PtsCount: 1,
Date: msg.Date,
Message: msg,
Channels: []domain.Channel{source},
}},
}).(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", got)
}
if len(got.Chats) != 1 {
t.Fatalf("chats = %+v, want source channel", got.Chats)
}
ch, ok := got.Chats[0].(*tg.Channel)
if !ok || ch.ID != source.ID || ch.Title != source.Title {
t.Fatalf("chat = %#v, want source channel", got.Chats[0])
}
}
func TestOutboxEventIncludesForwardSourceChannelChat(t *testing.T) {
source := domain.Channel{
ID: 2000000002,
AccessHash: 9002,
Title: "Forward Source",
Broadcast: true,
Date: 1700000000,
}
update := tgUpdateForOutboxEvent(domain.UpdateEvent{
UserID: 1000000001,
Type: domain.UpdateEventNewMessage,
Pts: 7,
PtsCount: 1,
Date: 1700000102,
Message: domain.Message{
ID: 90,
OwnerUserID: 1000000001,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002},
From: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002},
Date: 1700000102,
Body: "forwarded",
Forward: &domain.MessageForward{From: domain.Peer{Type: domain.PeerTypeChannel, ID: source.ID}, Date: 1700000000},
},
Channels: []domain.Channel{source},
})
if update == nil || len(update.Chats) != 1 {
t.Fatalf("update = %+v, want source channel chat", update)
}
ch, ok := update.Chats[0].(*tg.Channel)
if !ok || ch.ID != source.ID {
t.Fatalf("chat = %#v, want source channel", update.Chats[0])
}
}
func TestOutboxEventChannelViewForumAsMessages(t *testing.T) {
update := tgUpdateForOutboxEvent(domain.UpdateEvent{
UserID: 1000000001,
Type: domain.UpdateEventChannelViewForum,
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 2000000002},
Bool: true,
Date: 1700000200,
})
if update == nil || len(update.Updates) != 1 {
t.Fatalf("update = %+v, want one updateChannelViewForumAsMessages", update)
}
got, ok := update.Updates[0].(*tg.UpdateChannelViewForumAsMessages)
if !ok || got.ChannelID != 2000000002 || !got.Enabled {
t.Fatalf("update = %#v, want channel view-forum-as-messages enabled", update.Updates[0])
}
}
func TestUpdatesDifferenceIncludesChannelTooLongNudge(t *testing.T) {
const viewerID int64 = 1000000002
got, ok := tgUpdatesDifference(viewerID, domain.UpdateDifference{
State: domain.UpdateState{Pts: 8, Date: 1700000250, Seq: 0},
ChannelNudges: []domain.ChannelDifferenceNudge{{
ChannelID: 2000000001,
Pts: 12,
Channel: &domain.ChannelView{
Channel: domain.Channel{
ID: 2000000001,
AccessHash: 9100000001,
Title: "Dirty group",
Megagroup: true,
Date: 1700000200,
},
Self: domain.ChannelMember{
ChannelID: 2000000001,
UserID: viewerID,
Role: domain.ChannelRoleMember,
Status: domain.ChannelMemberActive,
},
},
}},
}).(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", got)
}
if got.State.Pts != 8 || len(got.OtherUpdates) != 1 || len(got.Chats) != 1 {
t.Fatalf("difference = %+v, want one channel nudge, one chat and account pts unchanged", got)
}
update, ok := got.OtherUpdates[0].(*tg.UpdateChannelTooLong)
if !ok || update.ChannelID != 2000000001 {
t.Fatalf("update = %T %+v, want UpdateChannelTooLong", got.OtherUpdates[0], got.OtherUpdates[0])
}
if pts, ok := update.GetPts(); !ok || pts != 12 {
t.Fatalf("channel nudge pts = %d set=%v, want 12", pts, ok)
}
chat, ok := got.Chats[0].(*tg.Channel)
if !ok || chat.ID != 2000000001 || chat.Min {
t.Fatalf("chat = %T %+v, want full channel chat for channel nudge", got.Chats[0], got.Chats[0])
}
}
func TestUpdatesDifferenceChannelNudgeUpgradesExistingMinChat(t *testing.T) {
const viewerID int64 = 1000000001
channel := domain.Channel{
ID: 2000000003,
AccessHash: 9100000003,
CreatorUserID: viewerID,
Title: "Offline created group",
Megagroup: true,
Date: 1700000300,
}
got, ok := tgUpdatesDifference(viewerID, domain.UpdateDifference{
State: domain.UpdateState{Pts: 9, Date: 1700000310, Seq: 0},
Events: []domain.UpdateEvent{{
UserID: viewerID,
Type: domain.UpdateEventChannelState,
Pts: 8,
PtsCount: 1,
Date: 1700000300,
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID},
Channels: []domain.Channel{channel},
}},
ChannelNudges: []domain.ChannelDifferenceNudge{{
ChannelID: channel.ID,
Pts: 3,
Channel: &domain.ChannelView{
Channel: channel,
Self: domain.ChannelMember{
ChannelID: channel.ID,
UserID: viewerID,
Role: domain.ChannelRoleCreator,
Status: domain.ChannelMemberActive,
},
},
}},
}).(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", got)
}
if len(got.OtherUpdates) != 2 || len(got.Chats) != 1 {
t.Fatalf("difference = %+v, want channel state, nudge and one deduped chat", got)
}
chat, ok := got.Chats[0].(*tg.Channel)
if !ok || chat.ID != channel.ID || chat.Min || !chat.Creator {
t.Fatalf("chat = %T %+v, want upgraded full creator channel for Android unknown-channel recovery", got.Chats[0], got.Chats[0])
}
}
func TestUpdatesGetDifferenceChannelNudgeIncludesFullChat(t *testing.T) {
ctx := context.Background()
const (
ownerID int64 = 1000000101
memberID int64 = 1000000102
)
channelStore := memory.NewChannelStore()
channelSvc := appchannels.NewService(channelStore)
created, err := channelSvc.CreateMegagroupFromCreateChat(ctx, ownerID, domain.CreateChannelRequest{
Title: "Android visible group",
Date: 1700000400,
MemberUserIDs: []int64{memberID},
})
if err != nil {
t.Fatalf("CreateMegagroupFromCreateChat: %v", err)
}
updateSvc := appupdates.NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore())
r := New(Config{}, Deps{
Updates: updateSvc,
Channels: channelSvc,
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000410, 0)})
diff, err := r.onUpdatesGetDifference(WithUserID(ctx, memberID), &tg.UpdatesGetDifferenceRequest{
Date: 1700000399,
})
if err != nil {
t.Fatalf("updates.getDifference: %v", err)
}
got, ok := diff.(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", diff)
}
if len(got.OtherUpdates) != 1 || len(got.Chats) != 1 {
t.Fatalf("difference = %+v, want channel nudge with chat", got)
}
update, ok := got.OtherUpdates[0].(*tg.UpdateChannelTooLong)
if !ok || update.ChannelID != created.Channel.ID {
t.Fatalf("update = %T %+v, want updateChannelTooLong for channel %d", got.OtherUpdates[0], got.OtherUpdates[0], created.Channel.ID)
}
chat, ok := got.Chats[0].(*tg.Channel)
if !ok || chat.ID != created.Channel.ID || chat.Min {
t.Fatalf("chat = %T %+v, want full channel chat for Android unknown-channel recovery", got.Chats[0], got.Chats[0])
}
if chat.AccessHash == 0 {
t.Fatalf("chat access_hash = 0, want full channel access hash")
}
}
func TestUpdatesDifferenceIncludesSettingsUpdates(t *testing.T) {
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
got, ok := tgUpdatesDifference(0, domain.UpdateDifference{
State: domain.UpdateState{Pts: 6, Date: 1700000300, Seq: 0},
Events: []domain.UpdateEvent{
{Type: domain.UpdateEventContactsReset, Pts: 1, PtsCount: 1, Date: 1700000300},
{Type: domain.UpdateEventDialogPinned, Pts: 2, PtsCount: 1, Date: 1700000300, Peer: peer, Bool: true},
{Type: domain.UpdateEventPinnedDialogs, Pts: 3, PtsCount: 1, Date: 1700000300, Peers: []domain.Peer{peer}},
{Type: domain.UpdateEventDialogUnreadMark, Pts: 4, PtsCount: 1, Date: 1700000300, Peer: peer, Bool: false},
{Type: domain.UpdateEventPeerSettings, Pts: 5, PtsCount: 1, Date: 1700000300, Peer: peer, Settings: domain.PeerSettings{ShareContact: true}},
{Type: domain.UpdateEventPeerStoryBlocked, Pts: 6, PtsCount: 1, Date: 1700000300, Peer: peer, Bool: true},
},
}).(*tg.UpdatesDifference)
if !ok {
t.Fatalf("difference = %T, want *tg.UpdatesDifference", got)
}
if got.State.Pts != 6 || len(got.OtherUpdates) != 6 {
t.Fatalf("difference = %+v, want six settings updates and pts=6", got)
}
if _, ok := got.OtherUpdates[0].(*tg.UpdateContactsReset); !ok {
t.Fatalf("update[0] = %T, want contacts reset", got.OtherUpdates[0])
}
pinned, ok := got.OtherUpdates[1].(*tg.UpdateDialogPinned)
if !ok || !pinned.Pinned {
t.Fatalf("update[1] = %+v (%T), want pinned dialog", got.OtherUpdates[1], got.OtherUpdates[1])
}
pinnedDialogs, ok := got.OtherUpdates[2].(*tg.UpdatePinnedDialogs)
if !ok || len(pinnedDialogs.Order) != 1 {
t.Fatalf("update[2] = %T, want pinned dialogs", got.OtherUpdates[2])
}
unread, ok := got.OtherUpdates[3].(*tg.UpdateDialogUnreadMark)
if !ok || unread.Unread {
t.Fatalf("update[3] = %+v (%T), want unread=false mark", got.OtherUpdates[3], got.OtherUpdates[3])
}
peerSettings, ok := got.OtherUpdates[4].(*tg.UpdatePeerSettings)
if !ok || !peerSettings.Settings.ShareContact {
t.Fatalf("update[4] = %T, want peer settings", got.OtherUpdates[4])
}
peerBlocked, ok := got.OtherUpdates[5].(*tg.UpdatePeerBlocked)
if !ok || !peerBlocked.Blocked || !peerBlocked.BlockedMyStoriesFrom {
t.Fatalf("update[5] = %+v (%T), want story peer blocked", got.OtherUpdates[5], got.OtherUpdates[5])
}
if peerUser, ok := peerBlocked.PeerID.(*tg.PeerUser); !ok || peerUser.UserID != peer.ID {
t.Fatalf("update[5] peer = %#v, want user %d", peerBlocked.PeerID, peer.ID)
}
}