updateChannelParticipant carries the account's qts per the MTProto spec, but the server always sent Qts: 0, so real clients silently discarded it as a stale duplicate -- the banned/kicked user's channel never vanished locally and no correct "removed by admin" message showed, even though the update was delivered successfully at the transport layer. Add a durable per-device qts queue (channel_participant_event_queue) sharing its qts number space with the existing secret-chat queue (one qts sequence per device, per spec), and use it to stamp a correct, monotonically increasing qts on the update for every device of the affected user -- for channel bans/kicks, admin promotion/demotion, and ownership transfer. A device offline when it happened can now recover the event via updates.getDifference instead of missing it permanently.
456 lines
17 KiB
Go
456 lines
17 KiB
Go
package rpc
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"reflect"
|
||
"sync"
|
||
"testing"
|
||
|
||
"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"
|
||
appsecret "telesrv/internal/app/secretchat"
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/postresponse"
|
||
"telesrv/internal/store/memory"
|
||
)
|
||
|
||
func dispatchForReceivesUpdates(t *testing.T, sessions SessionBinder, wrapWithoutUpdates, loggedIn bool) context.Context {
|
||
t.Helper()
|
||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
||
|
||
var inner bin.Buffer
|
||
if err := (&tg.HelpGetConfigRequest{}).Encode(&inner); err != nil {
|
||
t.Fatalf("encode help.getConfig: %v", err)
|
||
}
|
||
var in bin.Buffer
|
||
if wrapWithoutUpdates {
|
||
in.PutID(tg.InvokeWithoutUpdatesRequestTypeID)
|
||
}
|
||
in.Put(inner.Buf)
|
||
|
||
ctx := postresponse.WithCallbacks(context.Background())
|
||
if loggedIn {
|
||
ctx = WithUserID(ctx, 1000000001)
|
||
}
|
||
if _, err := r.Dispatch(ctx, [8]byte{1}, 42, &in); err != nil {
|
||
t.Fatalf("dispatch: %v", err)
|
||
}
|
||
return ctx
|
||
}
|
||
|
||
// TestDispatchMarksSessionReceivesUpdates 验证已登录连接发出的裸 RPC(未包
|
||
// invokeWithoutUpdates)即视为 updates 接收声明。仅靠 updates.getState/getDifference
|
||
// 置位会漏掉热恢复重连的客户端:它不重建同步基线,置位永不发生时主动推送一直
|
||
// 暂存直至超时丢弃,表现为另一端消息不再实时同步。
|
||
func TestDispatchMarksSessionReceivesUpdates(t *testing.T) {
|
||
sessions := &captureSessions{}
|
||
ctx := dispatchForReceivesUpdates(t, sessions, false, true)
|
||
if sessions.snapshot().receives {
|
||
t.Fatal("plain RPC marked receivesUpdates before rpc_result delivery")
|
||
}
|
||
postresponse.Run(ctx)
|
||
sessions.mu.Lock()
|
||
defer sessions.mu.Unlock()
|
||
if !sessions.receives {
|
||
t.Fatal("plain RPC from logged-in session must mark receivesUpdates")
|
||
}
|
||
if sessions.sessionID != 42 {
|
||
t.Fatalf("marked session_id = %d, want 42", sessions.sessionID)
|
||
}
|
||
if sessions.message != nil {
|
||
t.Fatalf("session readiness emitted unsolicited update %T; readiness must only open delivery", sessions.message)
|
||
}
|
||
}
|
||
|
||
type activationCaptureSessions struct {
|
||
*captureSessions
|
||
activationMu sync.Mutex
|
||
nextToken uint64
|
||
activeToken uint64
|
||
beginCalls int
|
||
grants int
|
||
endCalls int
|
||
bootstrapMu sync.Mutex
|
||
bootstrapNext uint64
|
||
bootstrapActive uint64
|
||
bootstrapBegin int
|
||
bootstrapEnd int
|
||
bootstrapSuccess int
|
||
bootstrapProbed bool
|
||
}
|
||
|
||
func (s *activationCaptureSessions) ReceivesUpdatesForAuthKey([8]byte, int64) bool {
|
||
return s.captureSessions.snapshot().receives
|
||
}
|
||
|
||
func (s *activationCaptureSessions) BeginSessionUpdatesActivation([8]byte, int64) (uint64, bool) {
|
||
s.activationMu.Lock()
|
||
defer s.activationMu.Unlock()
|
||
s.beginCalls++
|
||
if s.activeToken != 0 || s.captureSessions.snapshot().receives {
|
||
return 0, false
|
||
}
|
||
s.nextToken++
|
||
s.activeToken = s.nextToken
|
||
s.grants++
|
||
return s.activeToken, true
|
||
}
|
||
|
||
func (s *activationCaptureSessions) EndSessionUpdatesActivation(_ [8]byte, _ int64, token uint64) {
|
||
s.activationMu.Lock()
|
||
defer s.activationMu.Unlock()
|
||
s.endCalls++
|
||
if s.activeToken == token {
|
||
s.activeToken = 0
|
||
}
|
||
}
|
||
|
||
func (s *activationCaptureSessions) activationSnapshot() (begin, grants, end int, active uint64) {
|
||
s.activationMu.Lock()
|
||
defer s.activationMu.Unlock()
|
||
return s.beginCalls, s.grants, s.endCalls, s.activeToken
|
||
}
|
||
|
||
func (s *activationCaptureSessions) BeginSessionBootstrapProbe([8]byte, int64) (uint64, bool) {
|
||
s.bootstrapMu.Lock()
|
||
defer s.bootstrapMu.Unlock()
|
||
s.bootstrapBegin++
|
||
if s.bootstrapActive != 0 || s.bootstrapProbed {
|
||
return 0, false
|
||
}
|
||
s.bootstrapNext++
|
||
s.bootstrapActive = s.bootstrapNext
|
||
return s.bootstrapActive, true
|
||
}
|
||
|
||
func (s *activationCaptureSessions) EndSessionBootstrapProbe(_ [8]byte, _ int64, token uint64, success bool) {
|
||
s.bootstrapMu.Lock()
|
||
defer s.bootstrapMu.Unlock()
|
||
s.bootstrapEnd++
|
||
if s.bootstrapActive != token {
|
||
return
|
||
}
|
||
s.bootstrapActive = 0
|
||
if success {
|
||
s.bootstrapSuccess++
|
||
s.bootstrapProbed = true
|
||
}
|
||
}
|
||
|
||
func (s *activationCaptureSessions) bootstrapSnapshot() (begin, end, success int, active uint64, probed bool) {
|
||
s.bootstrapMu.Lock()
|
||
defer s.bootstrapMu.Unlock()
|
||
return s.bootstrapBegin, s.bootstrapEnd, s.bootstrapSuccess, s.bootstrapActive, s.bootstrapProbed
|
||
}
|
||
|
||
func TestDispatchCoalescesConcurrentSessionReadinessActivation(t *testing.T) {
|
||
sessions := &activationCaptureSessions{captureSessions: &captureSessions{}}
|
||
first := dispatchForReceivesUpdates(t, sessions, false, true)
|
||
second := dispatchForReceivesUpdates(t, sessions, false, true)
|
||
|
||
if begin, grants, end, active := sessions.activationSnapshot(); begin != 2 || grants != 1 || end != 0 || active == 0 {
|
||
t.Fatalf("activation before delivery = begin:%d grants:%d end:%d active:%d", begin, grants, end, active)
|
||
}
|
||
postresponse.Run(second)
|
||
if got := sessions.snapshot().receivesCalls; got != 0 {
|
||
t.Fatalf("non-owner delivery marked readiness %d times", got)
|
||
}
|
||
postresponse.Run(first)
|
||
if got := sessions.snapshot(); !got.receives || got.receivesCalls != 1 {
|
||
t.Fatalf("owner delivery readiness = receives:%v calls:%d", got.receives, got.receivesCalls)
|
||
}
|
||
if begin, grants, end, active := sessions.activationSnapshot(); begin != 2 || grants != 1 || end != 1 || active != 0 {
|
||
t.Fatalf("activation after delivery = begin:%d grants:%d end:%d active:%d", begin, grants, end, active)
|
||
}
|
||
}
|
||
|
||
func TestSessionReadinessActivationReleasedWhenCallbackRegistrationFails(t *testing.T) {
|
||
sessions := &activationCaptureSessions{captureSessions: &captureSessions{}}
|
||
r := New(Config{}, Deps{Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
||
ctx := WithSessionID(WithRawAuthKeyID(context.Background(), [8]byte{9}), 99)
|
||
r.stageSessionUpdatesReadyAfterDelivery(ctx, 1000000009)
|
||
if begin, grants, end, active := sessions.activationSnapshot(); begin != 1 || grants != 1 || end != 1 || active != 0 {
|
||
t.Fatalf("activation after missing callback registry = begin:%d grants:%d end:%d active:%d", begin, grants, end, active)
|
||
}
|
||
}
|
||
|
||
func TestSuppressSessionActivationReleasesClaimButKeepsCursorWork(t *testing.T) {
|
||
sessions := &activationCaptureSessions{captureSessions: &captureSessions{}}
|
||
bootstrap := &captureBootstrapReadyStore{BootstrapUpdateJobStore: memory.NewBootstrapUpdateJobStore()}
|
||
r := New(Config{}, Deps{Sessions: sessions, BootstrapUpdates: bootstrap}, zaptest.NewLogger(t), clock.System)
|
||
ctx := WithSessionID(WithRawAuthKeyID(context.Background(), [8]byte{8}), 88)
|
||
ctx, plan := withUpdatesDeliveryPlan(ctx)
|
||
r.tryStageSessionUpdatesReady(ctx, plan, 1000000008)
|
||
r.tryStageBootstrapProbe(ctx, plan, 1000000008)
|
||
plan.stageCursor([8]byte{7}, 1000000008, domain.UpdateState{Pts: 7}, domain.UpdateStateCommitDeliveredOnly)
|
||
plan.suppressSessionActivation()
|
||
if plan.markSessionReady || plan.publishBootstrap || !plan.commitCursor {
|
||
t.Fatalf("suppressed plan = ready:%v bootstrap:%v cursor:%v", plan.markSessionReady, plan.publishBootstrap, plan.commitCursor)
|
||
}
|
||
if begin, grants, end, active := sessions.activationSnapshot(); begin != 1 || grants != 1 || end != 1 || active != 0 {
|
||
t.Fatalf("activation after suppression = begin:%d grants:%d end:%d active:%d", begin, grants, end, active)
|
||
}
|
||
if begin, end, success, active, probed := sessions.bootstrapSnapshot(); begin != 1 || end != 1 || success != 0 || active != 0 || probed {
|
||
t.Fatalf("bootstrap after suppression = begin:%d end:%d success:%d active:%d probed:%v", begin, end, success, active, probed)
|
||
}
|
||
}
|
||
|
||
func TestBootstrapProbeReleasedWhenCallbackRegistrationFails(t *testing.T) {
|
||
sessions := &activationCaptureSessions{captureSessions: &captureSessions{}}
|
||
bootstrap := &captureBootstrapReadyStore{BootstrapUpdateJobStore: memory.NewBootstrapUpdateJobStore()}
|
||
r := New(Config{}, Deps{Sessions: sessions, BootstrapUpdates: bootstrap}, zaptest.NewLogger(t), clock.System)
|
||
ctx := WithAuthKeyID(context.Background(), [8]byte{7})
|
||
ctx = WithRawAuthKeyID(ctx, [8]byte{8})
|
||
ctx = WithSessionID(ctx, 88)
|
||
r.stageUpdatesBaselineAfterDelivery(ctx, 1000000008, nil, 0, nil, true)
|
||
if bootstrap.readyCalls != 0 {
|
||
t.Fatalf("bootstrap store called before an attached delivery callback: %d", bootstrap.readyCalls)
|
||
}
|
||
if begin, end, success, active, probed := sessions.bootstrapSnapshot(); begin != 1 || end != 1 || success != 0 || active != 0 || probed {
|
||
t.Fatalf("bootstrap after missing registry = begin:%d end:%d success:%d active:%d probed:%v", begin, end, success, active, probed)
|
||
}
|
||
}
|
||
|
||
// TestDispatchSkipsReceivesUpdatesForInvokeWithoutUpdates 验证 invokeWithoutUpdates
|
||
// 包装的请求(media/temp 连接)不会把该 session 标记为 updates 接收者。
|
||
func TestDispatchSkipsReceivesUpdatesForInvokeWithoutUpdates(t *testing.T) {
|
||
sessions := &captureSessions{}
|
||
ctx := dispatchForReceivesUpdates(t, sessions, true, true)
|
||
postresponse.Run(ctx)
|
||
sessions.mu.Lock()
|
||
defer sessions.mu.Unlock()
|
||
if sessions.receives {
|
||
t.Fatal("invokeWithoutUpdates-wrapped RPC must not mark receivesUpdates")
|
||
}
|
||
}
|
||
|
||
type captureBootstrapReadyStore struct {
|
||
*memory.BootstrapUpdateJobStore
|
||
readyCalls int
|
||
readyErr error
|
||
}
|
||
|
||
func (s *captureBootstrapReadyStore) MarkReadyForSession(ctx context.Context, userID int64, authKeyID [8]byte, sessionID int64) (int, error) {
|
||
s.readyCalls++
|
||
if s.readyErr != nil {
|
||
err := s.readyErr
|
||
s.readyErr = nil
|
||
return 0, err
|
||
}
|
||
return s.BootstrapUpdateJobStore.MarkReadyForSession(ctx, userID, authKeyID, sessionID)
|
||
}
|
||
|
||
func TestBootstrapReadinessProbeIsOneShotAndRetriesFailure(t *testing.T) {
|
||
const userID int64 = 1000000199
|
||
rawAuthKeyID := [8]byte{19}
|
||
businessAuthKeyID := [8]byte{29}
|
||
const sessionID int64 = 199
|
||
sessions := &activationCaptureSessions{captureSessions: &captureSessions{}}
|
||
bootstrap := &captureBootstrapReadyStore{
|
||
BootstrapUpdateJobStore: memory.NewBootstrapUpdateJobStore(),
|
||
readyErr: errors.New("temporary bootstrap lookup failure"),
|
||
}
|
||
r := New(Config{}, Deps{Sessions: sessions, BootstrapUpdates: bootstrap}, zaptest.NewLogger(t), clock.System)
|
||
|
||
baseline := func() {
|
||
ctx := postresponse.WithCallbacks(context.Background())
|
||
ctx = WithAuthKeyID(ctx, businessAuthKeyID)
|
||
ctx = WithRawAuthKeyID(ctx, rawAuthKeyID)
|
||
ctx = WithSessionID(ctx, sessionID)
|
||
ctx = WithUserID(ctx, userID)
|
||
r.stageUpdatesBaselineAfterDelivery(ctx, userID, nil, 0, nil, true)
|
||
postresponse.Run(ctx)
|
||
}
|
||
|
||
baseline() // authoritative query fails: release the physical-generation claim
|
||
baseline() // successful zero-row result: complete the one-shot
|
||
baseline() // no third store call on the same physical generation
|
||
|
||
if bootstrap.readyCalls != 2 {
|
||
t.Fatalf("bootstrap readiness calls = %d, want failure + one successful retry", bootstrap.readyCalls)
|
||
}
|
||
begin, end, success, active, probed := sessions.bootstrapSnapshot()
|
||
if begin != 3 || end != 2 || success != 1 || active != 0 || !probed {
|
||
t.Fatalf("bootstrap probe = begin:%d end:%d success:%d active:%d probed:%v", begin, end, success, active, probed)
|
||
}
|
||
}
|
||
|
||
func TestInvokeWithoutUpdatesBaselineCommitsResultAndSecretEventsWithoutSubscribing(t *testing.T) {
|
||
const userID int64 = 1000000201
|
||
authKeyID := [8]byte{21}
|
||
deviceKey := businessAuthKeyInt64(authKeyID)
|
||
queue := memory.NewEncryptedQueueStore()
|
||
secret := appsecret.NewService(memory.NewSecretChatStore(), queue, queue)
|
||
eventID, err := queue.AppendStateEvent(context.Background(), domain.EncryptedStateEvent{
|
||
TargetUserID: userID,
|
||
ChatID: 77,
|
||
Type: domain.EncryptedStateEventRead,
|
||
MaxDate: 1700000200,
|
||
Date: 1700000201,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("append state event: %v", err)
|
||
}
|
||
sessions := &captureSessions{}
|
||
updates := &captureUpdates{state: domain.UpdateState{Pts: 4, Date: 1700000201}}
|
||
bootstrap := &captureBootstrapReadyStore{BootstrapUpdateJobStore: memory.NewBootstrapUpdateJobStore()}
|
||
r := New(Config{}, Deps{
|
||
Sessions: sessions, Updates: updates, SecretChats: secret, BootstrapUpdates: bootstrap,
|
||
}, zaptest.NewLogger(t), clock.System)
|
||
|
||
var inner bin.Buffer
|
||
if err := (&tg.UpdatesGetDifferenceRequest{Pts: 4, Date: 1700000201}).Encode(&inner); err != nil {
|
||
t.Fatalf("encode getDifference: %v", err)
|
||
}
|
||
var wrapped bin.Buffer
|
||
wrapped.PutID(tg.InvokeWithoutUpdatesRequestTypeID)
|
||
wrapped.Put(inner.Raw())
|
||
ctx := postresponse.WithCallbacks(WithAuthKeyID(WithSessionID(WithUserID(context.Background(), userID), 202), authKeyID))
|
||
if _, err := r.Dispatch(ctx, authKeyID, 202, &wrapped); err != nil {
|
||
t.Fatalf("dispatch wrapped baseline: %v", err)
|
||
}
|
||
if updates.commitCalls != 0 || sessions.snapshot().receivesCalls != 0 {
|
||
t.Fatalf("pre-delivery effects = commits:%d ready_calls:%d", updates.commitCalls, sessions.snapshot().receivesCalls)
|
||
}
|
||
pending, err := queue.ListUndeliveredStateEvents(context.Background(), userID, deviceKey, 10)
|
||
if err != nil || len(pending) != 1 || pending[0].ID != eventID {
|
||
t.Fatalf("pending before delivery = %+v err=%v", pending, err)
|
||
}
|
||
|
||
postresponse.Run(ctx)
|
||
if updates.commitCalls != 1 || updates.committedState.Pts != 4 {
|
||
t.Fatalf("delivered cursor commit = calls:%d state:%+v", updates.commitCalls, updates.committedState)
|
||
}
|
||
if got := sessions.snapshot(); got.receives || got.receivesCalls != 0 {
|
||
t.Fatalf("invokeWithoutUpdates subscribed session: receives=%v calls=%d", got.receives, got.receivesCalls)
|
||
}
|
||
if bootstrap.readyCalls != 0 {
|
||
t.Fatalf("invokeWithoutUpdates released bootstrap %d times", bootstrap.readyCalls)
|
||
}
|
||
pending, err = queue.ListUndeliveredStateEvents(context.Background(), userID, deviceKey, 10)
|
||
if err != nil || len(pending) != 0 {
|
||
t.Fatalf("secret events after delivered wrapped baseline = %+v err=%v", pending, err)
|
||
}
|
||
}
|
||
|
||
// TestDispatchSkipsReceivesUpdatesWhenLoggedOut 验证未登录连接的 RPC 不置位。
|
||
func TestDispatchSkipsReceivesUpdatesWhenLoggedOut(t *testing.T) {
|
||
sessions := &captureSessions{}
|
||
ctx := dispatchForReceivesUpdates(t, sessions, false, false)
|
||
postresponse.Run(ctx)
|
||
sessions.mu.Lock()
|
||
defer sessions.mu.Unlock()
|
||
if sessions.receives {
|
||
t.Fatal("RPC without bound user must not mark receivesUpdates")
|
||
}
|
||
}
|
||
|
||
type fifoFlushCaptureSessions struct {
|
||
*captureSessions
|
||
flushMu sync.Mutex
|
||
pending []int
|
||
flushed []int
|
||
}
|
||
|
||
func (s *fifoFlushCaptureSessions) SetReceivesUpdatesForAuthKey(rawAuthKeyID [8]byte, sessionID int64, receives bool) {
|
||
if receives {
|
||
s.flushMu.Lock()
|
||
s.flushed = append(s.flushed, s.pending...)
|
||
s.pending = nil
|
||
s.flushMu.Unlock()
|
||
}
|
||
s.captureSessions.SetReceivesUpdatesForAuthKey(rawAuthKeyID, sessionID, receives)
|
||
}
|
||
|
||
func (s *fifoFlushCaptureSessions) flushedSnapshot() []int {
|
||
s.flushMu.Lock()
|
||
defer s.flushMu.Unlock()
|
||
return append([]int(nil), s.flushed...)
|
||
}
|
||
|
||
// TestDispatchDefersMembershipAndFIFOFlushUntilPostResponse pins the complete
|
||
// readiness barrier: channel membership and pending updates remain untouched
|
||
// while the rpc_result is only prepared, then the delivery hook rebuilds
|
||
// membership before SetReceivesUpdates drains the original FIFO order.
|
||
func TestDispatchDefersMembershipAndFIFOFlushUntilPostResponse(t *testing.T) {
|
||
const (
|
||
userID = int64(1000000111)
|
||
sessionID = int64(87)
|
||
)
|
||
channelSvc := appchannels.NewService(memory.NewChannelStore())
|
||
created, err := channelSvc.CreateMegagroupFromCreateChat(context.Background(), userID, domain.CreateChannelRequest{
|
||
Title: "delivery barrier",
|
||
Date: 1700000000,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("create channel: %v", err)
|
||
}
|
||
sessions := &fifoFlushCaptureSessions{
|
||
captureSessions: &captureSessions{},
|
||
pending: []int{11, 22, 33},
|
||
}
|
||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||
Sessions: sessions,
|
||
Channels: channelSvc,
|
||
}, zaptest.NewLogger(t), clock.System)
|
||
|
||
var in bin.Buffer
|
||
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
|
||
t.Fatalf("encode help.getConfig: %v", err)
|
||
}
|
||
ctx := postresponse.WithCallbacks(WithUserID(context.Background(), userID))
|
||
if _, err := r.Dispatch(ctx, [8]byte{7}, sessionID, &in); err != nil {
|
||
t.Fatalf("dispatch: %v", err)
|
||
}
|
||
if got := sessions.flushedSnapshot(); len(got) != 0 {
|
||
t.Fatalf("pending flushed before result delivery: %v", got)
|
||
}
|
||
if got := sessions.onlineChannelMemberIDs(created.Channel.ID); len(got) != 0 {
|
||
t.Fatalf("membership synced before result delivery: %v", got)
|
||
}
|
||
if sessions.snapshot().receives {
|
||
t.Fatal("session ready before result delivery")
|
||
}
|
||
|
||
postresponse.Run(ctx)
|
||
if got, want := sessions.flushedSnapshot(), []int{11, 22, 33}; !reflect.DeepEqual(got, want) {
|
||
t.Fatalf("FIFO flush after result delivery = %v, want %v", got, want)
|
||
}
|
||
if got := sessions.onlineChannelMemberIDs(created.Channel.ID); !reflect.DeepEqual(got, []int64{userID}) {
|
||
t.Fatalf("membership after result delivery = %v, want [%d]", got, userID)
|
||
}
|
||
if !sessions.snapshot().receives {
|
||
t.Fatal("session not ready after result delivery")
|
||
}
|
||
}
|
||
|
||
type failingCurrentStateUpdates struct{ *captureUpdates }
|
||
|
||
func (s *failingCurrentStateUpdates) CurrentState(context.Context, int64) (domain.UpdateState, error) {
|
||
return domain.UpdateState{}, errors.New("current state failed")
|
||
}
|
||
|
||
func TestFailedRPCDoesNotRegisterSessionReadyPostResponse(t *testing.T) {
|
||
sessions := &captureSessions{}
|
||
r := New(Config{}, Deps{
|
||
Sessions: sessions,
|
||
Updates: &failingCurrentStateUpdates{captureUpdates: &captureUpdates{}},
|
||
}, zaptest.NewLogger(t), clock.System)
|
||
var in bin.Buffer
|
||
if err := (&tg.UpdatesGetStateRequest{}).Encode(&in); err != nil {
|
||
t.Fatalf("encode updates.getState: %v", err)
|
||
}
|
||
ctx := postresponse.WithCallbacks(WithUserID(context.Background(), 1000000123))
|
||
if _, err := r.Dispatch(ctx, [8]byte{8}, 91, &in); err == nil {
|
||
t.Fatal("updates.getState unexpectedly succeeded")
|
||
}
|
||
postresponse.Run(ctx)
|
||
if sessions.snapshot().receives {
|
||
t.Fatal("failed RPC marked session ready")
|
||
}
|
||
}
|