Sync telesrv c779c48 (feat(branding): unify visible product naming). Skipped private www files and preserved public README files; mapped orange appearance seed names to public default names.
858 lines
33 KiB
Go
858 lines
33 KiB
Go
package rpc
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/iamxvbaba/td/bin"
|
||
"github.com/iamxvbaba/td/clock"
|
||
"github.com/iamxvbaba/td/tg"
|
||
"go.uber.org/zap/zaptest"
|
||
|
||
appchannels "telesrv/internal/app/channels"
|
||
appupdates "telesrv/internal/app/updates"
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/postresponse"
|
||
"telesrv/internal/store/memory"
|
||
)
|
||
|
||
type bootstrapOrderUpdates struct {
|
||
*captureUpdates
|
||
sessions *captureSessions
|
||
publishedAfterReady bool
|
||
}
|
||
|
||
func (s *bootstrapOrderUpdates) PublishNewMessage(ctx context.Context, userID int64, msg domain.Message) (domain.UpdateEvent, domain.UpdateState, error) {
|
||
s.publishedAfterReady = s.sessions.snapshot().receives
|
||
return s.captureUpdates.PublishNewMessage(ctx, userID, msg)
|
||
}
|
||
|
||
func TestSignUpBootstrapLoginMessagePublishesNewMessageAfterReady(t *testing.T) {
|
||
bootstrap := memory.NewBootstrapUpdateJobStore()
|
||
updates := &captureUpdates{state: domain.UpdateState{Pts: 3, Date: 1700000000}}
|
||
messages := &captureMessages{
|
||
list: domain.MessageList{Messages: []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}},
|
||
}}},
|
||
}
|
||
r := New(Config{}, Deps{BootstrapUpdates: bootstrap, Updates: updates, Messages: messages}, 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}},
|
||
}
|
||
authKeyID := [8]byte{1, 2, 3}
|
||
sessionID := int64(55)
|
||
ctx := WithSessionID(WithAuthKeyID(context.Background(), authKeyID), sessionID)
|
||
r.enqueueLoginMessageBootstrap(ctx, msg)
|
||
claimed := r.publishReadyBootstrapUpdates(context.Background(), 10, time.Second, zaptest.NewLogger(t))
|
||
if claimed != 0 || len(updates.events) != 0 {
|
||
t.Fatalf("published before ready = claimed %d events %d, want none", claimed, len(updates.events))
|
||
}
|
||
|
||
ready, err := bootstrap.MarkReadyForSession(context.Background(), msg.OwnerUserID, authKeyID, sessionID)
|
||
if err != nil {
|
||
t.Fatalf("mark ready: %v", err)
|
||
}
|
||
if ready != 1 {
|
||
t.Fatalf("ready jobs = %d, want 1", ready)
|
||
}
|
||
claimed = r.publishReadyBootstrapUpdates(context.Background(), 10, time.Second, zaptest.NewLogger(t))
|
||
if claimed != 1 {
|
||
t.Fatalf("claimed jobs = %d, want 1", claimed)
|
||
}
|
||
if len(updates.events) != 1 {
|
||
t.Fatalf("published events = %d, want 1", len(updates.events))
|
||
}
|
||
event := updates.events[0]
|
||
if event.Type != domain.UpdateEventNewMessage || event.Message.ID != msg.ID || event.Pts != 4 {
|
||
t.Fatalf("event = %+v, want login message pts 4", event)
|
||
}
|
||
}
|
||
|
||
func TestSignUpBootstrapPendingJobFollowsSameAuthKeyReconnect(t *testing.T) {
|
||
bootstrap := memory.NewBootstrapUpdateJobStore()
|
||
updates := &captureUpdates{state: domain.UpdateState{Pts: 0, Date: 1700000000}}
|
||
msg := domain.Message{
|
||
ID: 7, OwnerUserID: 1780243777,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
|
||
From: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
|
||
Date: 1700000100, Body: "Login code: 12345",
|
||
}
|
||
r := New(Config{}, Deps{
|
||
BootstrapUpdates: bootstrap,
|
||
Updates: updates,
|
||
Messages: &captureMessages{list: domain.MessageList{Messages: []domain.Message{msg}}},
|
||
}, zaptest.NewLogger(t), clock.System)
|
||
authKeyID := [8]byte{4, 5, 6}
|
||
oldSessionID := int64(1001)
|
||
newSessionID := int64(2002)
|
||
r.enqueueLoginMessageBootstrap(
|
||
WithSessionID(WithAuthKeyID(context.Background(), authKeyID), oldSessionID),
|
||
msg,
|
||
)
|
||
|
||
if ready, err := bootstrap.MarkReadyForSession(context.Background(), msg.OwnerUserID, [8]byte{9}, newSessionID); err != nil || ready != 0 {
|
||
t.Fatalf("different auth-key ready=%d err=%v, want 0/nil", ready, err)
|
||
}
|
||
ready, err := bootstrap.MarkReadyForSession(context.Background(), msg.OwnerUserID, authKeyID, newSessionID)
|
||
if err != nil || ready != 1 {
|
||
t.Fatalf("same auth-key reconnect ready=%d err=%v, want 1/nil", ready, err)
|
||
}
|
||
if claimed := r.publishReadyBootstrapUpdates(context.Background(), 1, time.Second, zaptest.NewLogger(t)); claimed != 1 {
|
||
t.Fatalf("published after same-auth reconnect = %d, want 1", claimed)
|
||
}
|
||
if len(updates.events) != 1 || updates.events[0].Message.ID != msg.ID {
|
||
t.Fatalf("events after reconnect = %+v", updates.events)
|
||
}
|
||
}
|
||
|
||
func TestUpdatesGetStatePublishesSignUpBootstrapAfterRPCResult(t *testing.T) {
|
||
bootstrap := memory.NewBootstrapUpdateJobStore()
|
||
sessions := &captureSessions{}
|
||
updates := &bootstrapOrderUpdates{
|
||
captureUpdates: &captureUpdates{state: domain.UpdateState{Pts: 0, Date: 1700000000}},
|
||
sessions: sessions,
|
||
}
|
||
msg := domain.Message{
|
||
ID: 1,
|
||
OwnerUserID: 1780243001,
|
||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
|
||
From: domain.Peer{Type: domain.PeerTypeUser, ID: domain.OfficialSystemUserID},
|
||
Date: 1700000100,
|
||
Body: "Login code: 12345",
|
||
}
|
||
messages := &captureMessages{list: domain.MessageList{Messages: []domain.Message{msg}}}
|
||
r := New(Config{}, Deps{BootstrapUpdates: bootstrap, Updates: updates, Messages: messages, Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
||
authKeyID := [8]byte{9, 8, 7}
|
||
sessionID := int64(5723482677041206318)
|
||
ctx := postresponse.WithCallbacks(
|
||
WithClientInfo(
|
||
WithUserID(
|
||
WithSessionID(WithAuthKeyID(context.Background(), authKeyID), sessionID),
|
||
msg.OwnerUserID,
|
||
),
|
||
ClientInfo{Type: ClientTypeTDesktop},
|
||
),
|
||
)
|
||
r.enqueueLoginMessageBootstrap(ctx, msg)
|
||
|
||
var request bin.Buffer
|
||
if err := (&tg.UpdatesGetStateRequest{}).Encode(&request); err != nil {
|
||
t.Fatalf("encode updates.getState: %v", err)
|
||
}
|
||
encoded, err := r.Dispatch(ctx, authKeyID, sessionID, &request)
|
||
if err != nil {
|
||
t.Fatalf("getState: %v", err)
|
||
}
|
||
state, ok := encoded.(*tg.UpdatesState)
|
||
if !ok {
|
||
t.Fatalf("getState response = %T, want *tg.UpdatesState", encoded)
|
||
}
|
||
if state.Pts != 0 {
|
||
t.Fatalf("state pts = %d, want 0 before bootstrap publish", state.Pts)
|
||
}
|
||
if claimed := r.publishReadyBootstrapUpdates(context.Background(), 10, time.Second, zaptest.NewLogger(t)); claimed != 0 {
|
||
t.Fatalf("bootstrap published before post-response callback: %d", claimed)
|
||
}
|
||
if len(updates.events) != 0 {
|
||
t.Fatalf("events before post-response = %d, want 0", len(updates.events))
|
||
}
|
||
if got := sessions.snapshot(); got.receives || got.receivesCalls != 0 {
|
||
t.Fatalf("session readiness before post-response = receives:%v calls:%d, want false/0", got.receives, got.receivesCalls)
|
||
}
|
||
|
||
postresponse.Run(ctx)
|
||
if len(updates.events) != 1 {
|
||
t.Fatalf("events after post-response = %d, want 1", len(updates.events))
|
||
}
|
||
if got := updates.events[0]; got.Type != domain.UpdateEventNewMessage || got.Message.ID != msg.ID {
|
||
t.Fatalf("event after post-response = %+v, want login message", got)
|
||
}
|
||
if !updates.publishedAfterReady {
|
||
t.Fatal("bootstrap update published before session readiness/FIFO flush barrier")
|
||
}
|
||
if got := sessions.snapshot(); !got.receives || got.receivesCalls != 1 {
|
||
t.Fatalf("merged baseline callback readiness = receives:%v calls:%d, want true/1", got.receives, got.receivesCalls)
|
||
}
|
||
}
|
||
|
||
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", "Telesrv 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 TestSignInDifferentUserDoesNotClearFreshlyBoundUpdateState(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.cleared {
|
||
t.Fatalf("router cleared auth key %x after Bind; this would delete the new user's retained-floor baseline", updates.clearedAuthKeyID)
|
||
}
|
||
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)
|
||
|
||
ctx := postresponse.WithCallbacks(WithClientInfo(WithUserID(WithSessionID(context.Background(), 77), 1000000001), ClientInfo{Type: ClientTypeTDesktop}))
|
||
got, err := r.onUpdatesGetState(ctx)
|
||
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)
|
||
}
|
||
if gotSession := sessions.snapshot(); gotSession.receives {
|
||
t.Fatal("session became ready before getState rpc_result delivery")
|
||
}
|
||
postresponse.Run(ctx)
|
||
gotSession := sessions.snapshot()
|
||
if gotSession.sessionID != 77 || !gotSession.receives {
|
||
t.Fatalf("session ready = id %d receives %v, want 77/true", gotSession.sessionID, gotSession.receives)
|
||
}
|
||
}
|
||
|
||
func TestUpdatesGetDifferenceMarksSessionReadyOnlyAfterRPCResult(t *testing.T) {
|
||
sessions := &captureSessions{}
|
||
r := New(Config{}, Deps{
|
||
Sessions: sessions,
|
||
Updates: &captureUpdates{state: domain.UpdateState{Pts: 7, Date: 1700000007}},
|
||
}, zaptest.NewLogger(t), clock.System)
|
||
|
||
ctx := postresponse.WithCallbacks(
|
||
WithSessionID(
|
||
WithAuthKeyID(WithUserID(context.Background(), 1000000001), [8]byte{7}),
|
||
79,
|
||
),
|
||
)
|
||
diff, err := r.onUpdatesGetDifference(ctx, &tg.UpdatesGetDifferenceRequest{Pts: 7, Date: 1700000007})
|
||
if err != nil {
|
||
t.Fatalf("updates.getDifference: %v", err)
|
||
}
|
||
if _, ok := diff.(*tg.UpdatesDifferenceEmpty); !ok {
|
||
t.Fatalf("difference = %T, want UpdatesDifferenceEmpty", diff)
|
||
}
|
||
if sessions.snapshot().receives {
|
||
t.Fatal("session became ready before getDifference rpc_result delivery")
|
||
}
|
||
updates := r.deps.Updates.(*captureUpdates)
|
||
if updates.observedCalls != 1 || updates.observedRequest.Pts != 7 || updates.commitCalls != 0 {
|
||
t.Fatalf("pre-delivery cursors = observed_calls:%d observed:%+v commits:%d", updates.observedCalls, updates.observedRequest, updates.commitCalls)
|
||
}
|
||
|
||
postresponse.Run(ctx)
|
||
gotSession := sessions.snapshot()
|
||
if gotSession.sessionID != 79 || !gotSession.receives {
|
||
t.Fatalf("session ready = id %d receives %v, want 79/true", gotSession.sessionID, gotSession.receives)
|
||
}
|
||
if updates.commitCalls != 1 || updates.committedState.Pts != 7 || updates.commitMode != domain.UpdateStateCommitDeliveredOnly {
|
||
t.Fatalf("delivered difference commit = calls:%d state:%+v mode:%d", updates.commitCalls, updates.committedState, updates.commitMode)
|
||
}
|
||
}
|
||
|
||
func TestUpdatesDifferenceTooLongObservesRequestAndCommitsReturnedCursorAfterDelivery(t *testing.T) {
|
||
updates := &captureUpdates{currentState: &domain.UpdateState{Pts: 10, Date: 1700000010}}
|
||
r := New(Config{}, Deps{Updates: updates, Sessions: &captureSessions{}}, zaptest.NewLogger(t), clock.System)
|
||
ctx := postresponse.WithCallbacks(WithAuthKeyID(WithUserID(context.Background(), 1000000001), [8]byte{10}))
|
||
req := &tg.UpdatesGetDifferenceRequest{Pts: 1, Qts: 3, Date: 1700000001}
|
||
req.SetPtsTotalLimit(2)
|
||
got, err := r.onUpdatesGetDifference(ctx, req)
|
||
if err != nil {
|
||
t.Fatalf("getDifference tooLong: %v", err)
|
||
}
|
||
tooLong, ok := got.(*tg.UpdatesDifferenceTooLong)
|
||
if !ok || tooLong.Pts != 10 {
|
||
t.Fatalf("tooLong result = %T %+v", got, got)
|
||
}
|
||
if updates.observedCalls != 1 || updates.observedRequest.Pts != 1 || updates.commitCalls != 0 {
|
||
t.Fatalf("pre-delivery cursors = observed:%+v calls:%d commits:%d", updates.observedRequest, updates.observedCalls, updates.commitCalls)
|
||
}
|
||
postresponse.Run(ctx)
|
||
if updates.commitCalls != 1 || updates.committedState.Pts != 10 || updates.committedState.Qts != req.Qts || updates.committedState.Date != req.Date || updates.commitMode != domain.UpdateStateCommitDeliveredOnly {
|
||
t.Fatalf("delivered tooLong commit = calls:%d state:%+v mode:%d", updates.commitCalls, updates.committedState, updates.commitMode)
|
||
}
|
||
}
|
||
|
||
func TestUpdatesDifferenceEmptyCommitsRequestCursorNotInternalCandidate(t *testing.T) {
|
||
updates := &captureUpdates{state: domain.UpdateState{Pts: 100, Date: 1700000100}}
|
||
r := New(Config{}, Deps{Updates: updates, Sessions: &captureSessions{}}, zaptest.NewLogger(t), clock.System)
|
||
ctx := postresponse.WithCallbacks(WithAuthKeyID(WithUserID(context.Background(), 1000000002), [8]byte{11}))
|
||
got, err := r.onUpdatesGetDifference(ctx, &tg.UpdatesGetDifferenceRequest{Pts: 5, Qts: 6, Date: 1700000005})
|
||
if err != nil {
|
||
t.Fatalf("getDifference empty: %v", err)
|
||
}
|
||
if _, ok := got.(*tg.UpdatesDifferenceEmpty); !ok {
|
||
t.Fatalf("difference = %T, want empty", got)
|
||
}
|
||
postresponse.Run(ctx)
|
||
if updates.committedState.Pts != 5 || updates.committedState.Qts != 6 {
|
||
t.Fatalf("empty delivered cursor = %+v, want request pts/qts 5/6 (wire has neither)", updates.committedState)
|
||
}
|
||
}
|
||
|
||
// 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: ¤t,
|
||
}
|
||
r := New(Config{}, Deps{
|
||
Sessions: sessions,
|
||
Updates: updates,
|
||
}, zaptest.NewLogger(t), clock.System)
|
||
|
||
ctx := postresponse.WithCallbacks(WithClientInfo(
|
||
WithUserID(WithSessionID(context.Background(), 77), 1000000001),
|
||
ClientInfo{Type: ClientTypeTDesktop},
|
||
))
|
||
got, err := r.onUpdatesGetState(ctx)
|
||
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 在 rpc_result 物理交付前推进了设备确认水位")
|
||
}
|
||
postresponse.Run(ctx)
|
||
if !updates.acknowledged || updates.commitMode != domain.UpdateStateCommitDeliveredAndObservedBaseline || updates.committedState.Pts != 4 {
|
||
t.Fatalf("delivered getState commit = acknowledged:%v mode:%d state:%+v", updates.acknowledged, updates.commitMode, updates.committedState)
|
||
}
|
||
time.Sleep(500 * time.Millisecond)
|
||
if msg := sessions.snapshot().message; msg != nil {
|
||
if _, ok := msg.(*tg.UpdatesTooLong); ok {
|
||
t.Fatal("getState 后不应再推 updatesTooLong(会诱导 TDesktop 重放快照前差分)")
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestUpdatesGetStateUnknownClientDoesNotAdvanceObservedBaseline(t *testing.T) {
|
||
sessions := &captureSessions{}
|
||
current := domain.UpdateState{Pts: 9, Date: 1700000009}
|
||
updates := &captureUpdates{
|
||
state: domain.UpdateState{Pts: 3, Date: 1700000003},
|
||
currentState: ¤t,
|
||
}
|
||
r := New(Config{}, Deps{Sessions: sessions, Updates: updates}, zaptest.NewLogger(t), clock.System)
|
||
|
||
ctx := postresponse.WithCallbacks(WithUserID(WithSessionID(context.Background(), 78), 1000000001))
|
||
got, err := r.onUpdatesGetState(ctx)
|
||
if err != nil {
|
||
t.Fatalf("updates.getState unknown client: %v", err)
|
||
}
|
||
if got.Pts != current.Pts {
|
||
t.Fatalf("state pts = %d, want current %d", got.Pts, current.Pts)
|
||
}
|
||
if updates.acknowledged {
|
||
t.Fatal("unknown client advanced the observed getState baseline")
|
||
}
|
||
if sessions.snapshot().receives {
|
||
t.Fatal("unknown client was enabled before getState rpc_result delivery")
|
||
}
|
||
postresponse.Run(ctx)
|
||
if !sessions.snapshot().receives {
|
||
t.Fatal("unknown client was not enabled for subsequent updates")
|
||
}
|
||
if updates.commitMode != domain.UpdateStateCommitDeliveredOnly || updates.committedState.Pts != current.Pts {
|
||
t.Fatalf("unknown client delivered commit = mode:%d state:%+v", updates.commitMode, updates.committedState)
|
||
}
|
||
}
|
||
|
||
func TestUpdatesGetStateDrKLOEstablishesObservedBaseline(t *testing.T) {
|
||
updates := &captureUpdates{currentState: &domain.UpdateState{Pts: 6, Date: 1700000006}}
|
||
r := New(Config{}, Deps{Sessions: &captureSessions{}, Updates: updates}, zaptest.NewLogger(t), clock.System)
|
||
ctx := postresponse.WithCallbacks(WithClientInfo(WithUserID(context.Background(), 1000000001), ClientInfo{Type: ClientTypeAndroid, AppVersion: "12.8.1"}))
|
||
|
||
if _, err := r.onUpdatesGetState(ctx); err != nil {
|
||
t.Fatalf("updates.getState DrKLO: %v", err)
|
||
}
|
||
if updates.acknowledged {
|
||
t.Fatal("DrKLO getState established baseline before rpc_result delivery")
|
||
}
|
||
postresponse.Run(ctx)
|
||
if !updates.acknowledged || updates.commitMode != domain.UpdateStateCommitDeliveredAndObservedBaseline {
|
||
t.Fatalf("DrKLO delivered baseline = acknowledged:%v mode:%d", updates.acknowledged, updates.commitMode)
|
||
}
|
||
}
|
||
|
||
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 TestUpdatesGetDifferenceEmptyRetentionCheckpointStaysDifferenceSlice(t *testing.T) {
|
||
updates := &captureUpdates{difference: &domain.UpdateDifference{
|
||
State: domain.UpdateState{Pts: 42, Date: 1700000442},
|
||
Partial: true,
|
||
}}
|
||
r := New(Config{}, Deps{Updates: updates}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000443, 0)})
|
||
authKeyID := [8]byte{42}
|
||
ctx := WithUserID(WithAuthKeyID(context.Background(), authKeyID), 1000000042)
|
||
|
||
diff, err := r.onUpdatesGetDifference(ctx, &tg.UpdatesGetDifferenceRequest{Pts: 1, Date: 1700000400})
|
||
if err != nil {
|
||
t.Fatalf("updates.getDifference: %v", err)
|
||
}
|
||
slice, ok := diff.(*tg.UpdatesDifferenceSlice)
|
||
if !ok {
|
||
t.Fatalf("difference = %T, want *tg.UpdatesDifferenceSlice (never Empty/TooLong)", diff)
|
||
}
|
||
if slice.IntermediateState.Pts != 42 || slice.IntermediateState.Date != 1700000442 || len(slice.NewMessages) != 0 || len(slice.OtherUpdates) != 0 {
|
||
t.Fatalf("checkpoint slice = %+v, want empty payload at pts/date 42/1700000442", slice)
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|