owpengram-server/internal/rpc/router_dispatch_test.go

1410 lines
56 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"
"encoding/binary"
"github.com/gotd/td/bin"
"github.com/gotd/td/clock"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
"go.uber.org/zap/zaptest/observer"
"reflect"
"sync"
appauth "telesrv/internal/app/auth"
appdialogs "telesrv/internal/app/dialogs"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
"testing"
"time"
)
// TestDispatchUnwrapsWrappers 验证 Router 能剥离
// invokeWithLayer(initConnection(help.getConfig)) 并路由到 getConfig handler。
func TestDispatchUnwrapsWrappers(t *testing.T) {
const (
dc = 2
ip = "127.0.0.1"
port = 2398
layer = 225
)
r := New(Config{DC: dc, IP: ip, Port: port}, Deps{}, zaptest.NewLogger(t), clock.System)
req := &tg.InvokeWithLayerRequest{
Layer: layer,
Query: &tg.InitConnectionRequest{
APIID: 123,
DeviceModel: "TestDevice",
SystemVersion: "1.0",
AppVersion: "1.0",
SystemLangCode: "en",
LangPack: "tdesktop",
LangCode: "en",
Query: &tg.HelpGetConfigRequest{},
},
}
var b bin.Buffer
if err := req.Encode(&b); err != nil {
t.Fatalf("encode wrapped request: %v", err)
}
enc, err := r.Dispatch(context.Background(), [8]byte{}, 0, &b)
if err != nil {
t.Fatalf("dispatch: %v", err)
}
cfg, ok := enc.(*tg.Config)
if !ok {
t.Fatalf("result type = %T, want *tg.Config", enc)
}
if cfg.ThisDC != dc {
t.Fatalf("ThisDC = %d, want %d", cfg.ThisDC, dc)
}
if len(cfg.DCOptions) != 0 {
t.Fatalf("DCOptions = %+v, want empty (client uses pinned static address)", cfg.DCOptions)
}
}
func TestDispatchRejectsAuthorizedRPCBeforeLogin(t *testing.T) {
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: &captureAuthService{},
}, zaptest.NewLogger(t), clock.System)
authKeyID := [8]byte{0x68, 0xa7, 0x30, 0x65, 0xc1, 0x11, 0x6e, 0x35}
var dialogs bin.Buffer
if err := (&tg.MessagesGetDialogsRequest{
OffsetPeer: &tg.InputPeerEmpty{},
Limit: 20,
}).Encode(&dialogs); err != nil {
t.Fatalf("encode messages.getDialogs: %v", err)
}
if _, err := r.Dispatch(context.Background(), authKeyID, 1, &dialogs); !tgerr.Is(err, "AUTH_KEY_UNREGISTERED") {
t.Fatalf("messages.getDialogs err = %v, want AUTH_KEY_UNREGISTERED", err)
}
var help bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&help); err != nil {
t.Fatalf("encode help.getConfig: %v", err)
}
if _, err := r.Dispatch(context.Background(), authKeyID, 1, &help); err != nil {
t.Fatalf("help.getConfig should be allowed before login: %v", err)
}
var sendCode bin.Buffer
if err := (&tg.AuthSendCodeRequest{
PhoneNumber: "+15550001111",
APIID: 2040,
APIHash: "test",
Settings: tg.CodeSettings{},
}).Encode(&sendCode); err != nil {
t.Fatalf("encode auth.sendCode: %v", err)
}
if _, err := r.Dispatch(context.Background(), authKeyID, 1, &sendCode); err != nil {
t.Fatalf("auth.sendCode should be allowed before login: %v", err)
}
}
func TestDispatchRemembersLayerAndClientTypeForSession(t *testing.T) {
const layer = 225
core, logs := observer.New(zap.DebugLevel)
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zap.New(core), clock.System)
rawAuthKeyID := [8]byte{0x22, 0x5}
sessionID := int64(88)
initReq := &tg.InvokeWithLayerRequest{
Layer: layer,
Query: &tg.InitConnectionRequest{
APIID: 123,
DeviceModel: "Desktop",
SystemVersion: "Windows",
AppVersion: "6.8.4 x64",
SystemLangCode: "en",
LangPack: "tdesktop",
LangCode: "en",
Query: &tg.HelpGetConfigRequest{},
},
}
var initBuf bin.Buffer
if err := initReq.Encode(&initBuf); err != nil {
t.Fatalf("encode init request: %v", err)
}
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &initBuf); err != nil {
t.Fatalf("dispatch init request: %v", err)
}
sessionCtx := WithSessionID(WithRawAuthKeyID(WithAuthKeyID(context.Background(), rawAuthKeyID), rawAuthKeyID), sessionID)
info, ok, _ := r.clientSessionInfo(sessionCtx)
if !ok {
t.Fatalf("session metadata missing")
}
if info.layer != layer {
t.Fatalf("remembered layer = %d, want %d", info.layer, layer)
}
if !info.hasClientInfo || info.clientInfo.ClientType() != ClientTypeTDesktop {
t.Fatalf("remembered client info = %+v, want tdesktop", info.clientInfo)
}
var plainBuf bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&plainBuf); err != nil {
t.Fatalf("encode plain request: %v", err)
}
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &plainBuf); err != nil {
t.Fatalf("dispatch plain request: %v", err)
}
entries := logs.FilterMessage("RPC inner handled").All()
if len(entries) == 0 {
t.Fatalf("RPC inner handled log missing")
}
fields := entries[len(entries)-1].ContextMap()
if got := intLogField(fields["layer"]); got != layer {
t.Fatalf("logged layer = %d fields=%v, want %d", got, fields, layer)
}
if got := fields["client_type"]; got != string(ClientTypeTDesktop) {
t.Fatalf("logged client_type = %v, want %s", got, ClientTypeTDesktop)
}
if got := fields["app_version"]; got != "6.8.4 x64" {
t.Fatalf("logged app_version = %v, want 6.8.4 x64", got)
}
newSessionID := int64(99)
var newSessionBuf bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&newSessionBuf); err != nil {
t.Fatalf("encode new session request: %v", err)
}
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, newSessionID, &newSessionBuf); err != nil {
t.Fatalf("dispatch new session request: %v", err)
}
entries = logs.FilterMessage("RPC inner handled").All()
fields = entries[len(entries)-1].ContextMap()
if got := intLogField(fields["layer"]); got != layer {
t.Fatalf("new session logged layer = %d fields=%v, want %d", got, fields, layer)
}
if got := fields["client_type"]; got != string(ClientTypeTDesktop) {
t.Fatalf("new session logged client_type = %v, want %s", got, ClientTypeTDesktop)
}
if got := fields["app_version"]; got != "6.8.4 x64" {
t.Fatalf("new session logged app_version = %v, want 6.8.4 x64", got)
}
}
func TestDispatchPersistsPreLoginClientMetadataOnInitConnection(t *testing.T) {
auth := &captureAuthService{}
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zaptest.NewLogger(t), clock.System)
rawAuthKeyID := [8]byte{0x22, 0xdb, 0xcf, 0xc8, 0x0d, 0x4c, 0x77, 0x97}
sessionID := int64(8103956954238395544)
req := &tg.InvokeWithLayerRequest{
Layer: currentClientLayer,
Query: &tg.InitConnectionRequest{
APIID: 4,
DeviceModel: "GooglePixel 9a",
SystemVersion: "SDK 36",
AppVersion: "12.8.1 (69169) pbeta",
SystemLangCode: "en",
LangPack: "android",
LangCode: "en",
Query: &tg.HelpGetConfigRequest{},
},
}
var in bin.Buffer
if err := req.Encode(&in); err != nil {
t.Fatalf("encode init request: %v", err)
}
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &in); err != nil {
t.Fatalf("dispatch init request: %v", err)
}
got, ok := auth.authKeyClientInfos[rawAuthKeyID]
if !ok {
t.Fatalf("auth key client metadata was not persisted")
}
if got.Layer != currentClientLayer {
t.Fatalf("persisted layer = %d, want %d", got.Layer, currentClientLayer)
}
if got.Platform != string(ClientTypeAndroid) {
t.Fatalf("persisted platform = %q, want android", got.Platform)
}
if got.DeviceModel != "GooglePixel 9a" || got.SystemVersion != "SDK 36" || got.APIID != 4 || got.AppVersion != "12.8.1 (69169) pbeta" {
t.Fatalf("persisted client metadata = %+v", got)
}
}
func TestDispatchPersistsPreLoginClientMetadataFromSendCodeAPIID(t *testing.T) {
auth := &captureAuthService{}
rawAuthKeyID := [8]byte{0x33, 0xdb, 0xcf, 0xc8, 0x0d, 0x4c, 0x77, 0x97}
const sessionID = int64(8103956954238395544)
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zaptest.NewLogger(t), clock.System)
var sendCode bin.Buffer
if err := (&tg.AuthSendCodeRequest{
PhoneNumber: "+8618800000020",
APIID: 4,
APIHash: "android",
Settings: tg.CodeSettings{},
}).Encode(&sendCode); err != nil {
t.Fatalf("encode auth.sendCode: %v", err)
}
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &sendCode); err != nil {
t.Fatalf("dispatch auth.sendCode: %v", err)
}
persisted, ok := auth.authKeyClientInfos[rawAuthKeyID]
if !ok {
t.Fatalf("auth.sendCode did not persist auth key client metadata")
}
if persisted.APIID != 4 || persisted.Platform != string(ClientTypeAndroid) {
t.Fatalf("persisted client metadata = %+v, want android api_id=4", persisted)
}
core, logs := observer.New(zap.DebugLevel)
afterRestart := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zap.New(core), clock.System)
var help bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&help); err != nil {
t.Fatalf("encode help.getConfig: %v", err)
}
if _, err := afterRestart.Dispatch(context.Background(), rawAuthKeyID, sessionID+1, &help); err != nil {
t.Fatalf("dispatch help.getConfig after restart: %v", err)
}
entries := logs.FilterMessage("RPC inner handled").All()
if len(entries) == 0 {
t.Fatalf("RPC inner handled log missing")
}
fields := entries[len(entries)-1].ContextMap()
if got := fields["client_type"]; got != string(ClientTypeAndroid) {
t.Fatalf("logged client_type = %v, want %s", got, ClientTypeAndroid)
}
}
func TestSendCodeAPIIDDoesNotOverwriteStrongTWebIdentity(t *testing.T) {
auth := &captureAuthService{}
rawAuthKeyID := [8]byte{0x34, 0xdb, 0xcf, 0xc8, 0x0d, 0x4c, 0x77, 0x97}
const sessionID = int64(8103956954238395545)
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{Auth: auth}, zaptest.NewLogger(t), clock.System)
initReq := &tg.InvokeWithLayerRequest{
Layer: currentClientLayer,
Query: &tg.InitConnectionRequest{
APIID: 2040,
DeviceModel: "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36",
SystemVersion: "Win32",
AppVersion: "2.2",
SystemLangCode: "en-US",
LangPack: "webk",
LangCode: "en",
Query: &tg.HelpGetConfigRequest{},
},
}
var initBuf bin.Buffer
if err := initReq.Encode(&initBuf); err != nil {
t.Fatalf("encode init request: %v", err)
}
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &initBuf); err != nil {
t.Fatalf("dispatch init request: %v", err)
}
var sendCode bin.Buffer
if err := (&tg.AuthSendCodeRequest{
PhoneNumber: "+8618800000021",
APIID: 2040,
APIHash: "tweb-local",
Settings: tg.CodeSettings{},
}).Encode(&sendCode); err != nil {
t.Fatalf("encode auth.sendCode: %v", err)
}
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &sendCode); err != nil {
t.Fatalf("dispatch auth.sendCode: %v", err)
}
persisted := auth.authKeyClientInfos[rawAuthKeyID]
if persisted.Platform != string(ClientTypeTWeb) || persisted.DeviceModel != initReq.Query.(*tg.InitConnectionRequest).DeviceModel {
t.Fatalf("API id fallback overwrote strong TWeb identity: %+v", persisted)
}
}
func TestDispatchRestoresPreLoginAndroidMetadataFromAuthKey(t *testing.T) {
core, logs := observer.New(zap.DebugLevel)
authKeyID := [8]byte{0x22, 0xdb, 0xcf, 0xc8, 0x0d, 0x4c, 0x77, 0x97}
auth := &captureAuthService{
authKeyClientInfos: map[[8]byte]domain.AuthKeyClientInfo{
authKeyID: {
Layer: currentClientLayer,
DeviceModel: "GooglePixel 9a",
Platform: string(ClientTypeAndroid),
SystemVersion: "SDK 36",
APIID: 4,
AppVersion: "12.8.1 (69169) pbeta",
},
},
}
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zap.New(core), clock.System)
var in bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
const sessionID = int64(8103956954238395544)
if _, err := r.Dispatch(context.Background(), authKeyID, sessionID, &in); err != nil {
t.Fatalf("dispatch plain request: %v", err)
}
entries := logs.FilterMessage("RPC inner handled").All()
if len(entries) == 0 {
t.Fatalf("RPC inner handled log missing")
}
fields := entries[len(entries)-1].ContextMap()
if got := intLogField(fields["layer"]); got != currentClientLayer {
t.Fatalf("logged layer = %d fields=%v, want %d", got, fields, currentClientLayer)
}
if got := fields["client_type"]; got != string(ClientTypeAndroid) {
t.Fatalf("logged client_type = %v, want %s", got, ClientTypeAndroid)
}
if got := fields["app_version"]; got != "12.8.1 (69169) pbeta" {
t.Fatalf("logged app_version = %v, want 12.8.1 (69169) pbeta", got)
}
if got, ok := r.NegotiatedLayer(authKeyID, sessionID+1); !ok || got != currentClientLayer {
t.Fatalf("auth-key fallback layer = (%d,%v), want (%d,true)", got, ok, currentClientLayer)
}
}
func TestAndroidLegacyCompatLogsClientMetadataWithoutInit(t *testing.T) {
core, logs := observer.New(zap.DebugLevel)
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zap.New(core), clock.System)
var flags bin.Fields
flags.Set(0)
var in bin.Buffer
in.PutID(0x25939651)
if err := flags.Encode(&in); err != nil {
t.Fatalf("encode flags: %v", err)
}
in.PutInt(10)
in.PutInt(100)
in.PutInt(123456)
in.PutInt(0)
rawAuthKeyID := [8]byte{0x68, 0x25}
sessionID := int64(-3479521421865518854)
if _, err := r.Dispatch(WithUserID(context.Background(), 1780269504), rawAuthKeyID, sessionID, &in); err != nil {
t.Fatalf("dispatch legacy updates.getDifference: %v", err)
}
// The legacy Android constructor is upgraded by layerwire and dispatched
// normally; client metadata is still applied only because IsClientDrift
// positively identified a DrKLO constructor, now surfaced on the standard
// "RPC inner handled" log.
entries := logs.FilterMessage("RPC inner handled").All()
if len(entries) == 0 {
t.Fatalf("RPC inner handled log missing")
}
fields := entries[len(entries)-1].ContextMap()
if got := intLogField(fields["layer"]); got != 0 {
t.Fatalf("logged layer = %d fields=%v, want 0", got, fields)
}
if got := fields["client_type"]; got != string(ClientTypeAndroid) {
t.Fatalf("logged client_type = %v, want %s", got, ClientTypeAndroid)
}
if got, ok := r.NegotiatedLayer(rawAuthKeyID, sessionID); ok || got != currentClientLayer {
t.Fatalf("negotiated layer = (%d,%v), want (%d,false)", got, ok, currentClientLayer)
}
}
// TestNegotiatedLayerStickyContract pins the (layer, ok) contract that keeps the
// edge from clobbering a live connection's layer: unknown ⇒ ok=false (edge keeps
// last-known), a recorded layer ⇒ ok=true, and a reconnect with a NEW session_id
// still inherits the layer via the stable auth_key fallback.
func TestNegotiatedLayerStickyContract(t *testing.T) {
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System)
authKey := [8]byte{1, 2, 3, 4, 5, 6, 7, 8}
const session = int64(42)
if l, ok := r.NegotiatedLayer(authKey, session); ok || l != currentClientLayer {
t.Fatalf("unknown = (%d,%v), want (%d,false)", l, ok, currentClientLayer)
}
ctx := WithSessionID(WithRawAuthKeyID(context.Background(), authKey), session)
r.rememberClientLayer(ctx, 220)
if l, ok := r.NegotiatedLayer(authKey, session); !ok || l != 220 {
t.Fatalf("recorded = (%d,%v), want (220,true)", l, ok)
}
// Reconnect: new session_id, same auth_key → inherits 220 (no re-invokeWithLayer).
if l, ok := r.NegotiatedLayer(authKey, 999); !ok || l != 220 {
t.Fatalf("reconnect new session = (%d,%v), want (220,true)", l, ok)
}
// Unrelated auth_key stays unknown.
if _, ok := r.NegotiatedLayer([8]byte{9, 9}, session); ok {
t.Fatalf("unrelated auth_key reported known")
}
}
func TestObservedClientLayerOverridesStaleAuthFallback(t *testing.T) {
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System)
rawAuthKey := [8]byte{0x68, 0x25, 0x7a, 0x01}
effectiveAuthKey := [8]byte{0x8b, 0x6f, 0x26, 0x17}
ctx := WithAuthKeyID(
WithSessionID(WithRawAuthKeyID(context.Background(), rawAuthKey), 100),
effectiveAuthKey,
)
r.rememberClientSessionInfo(ctx, clientSessionInfo{layer: currentClientLayer})
r.rememberClientLayer(ctx, 225)
if got, ok := r.NegotiatedLayer(rawAuthKey, 100); !ok || got != 225 {
t.Fatalf("exact session layer = (%d,%v), want (225,true)", got, ok)
}
if got, ok := r.NegotiatedLayer(rawAuthKey, 101); !ok || got != 225 {
t.Fatalf("raw auth fallback layer = (%d,%v), want (225,true)", got, ok)
}
if got, ok := r.NegotiatedLayer(effectiveAuthKey, 101); !ok || got != 225 {
t.Fatalf("effective auth fallback layer = (%d,%v), want (225,true)", got, ok)
}
}
func TestInvokeWithLayerPersistsClientLayerUpgrade(t *testing.T) {
authKeyID := [8]byte{0x68, 0x25, 0x7a, 0x02}
userID := int64(1780269504)
auth := &captureAuthService{
userID: userID,
authorizations: []domain.Authorization{{
AuthKeyID: authKeyID,
UserID: userID,
Layer: 225,
}},
}
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zaptest.NewLogger(t), clock.System)
req := &tg.InvokeWithLayerRequest{
Layer: currentClientLayer,
Query: &tg.HelpGetConfigRequest{},
}
var in bin.Buffer
if err := req.Encode(&in); err != nil {
t.Fatalf("encode invokeWithLayer: %v", err)
}
if _, err := r.Dispatch(context.Background(), authKeyID, 100, &in); err != nil {
t.Fatalf("dispatch invokeWithLayer: %v", err)
}
if auth.layerUpdates != 1 {
t.Fatalf("layer update calls = %d, want 1", auth.layerUpdates)
}
if got := auth.authorizations[0].Layer; got != currentClientLayer {
t.Fatalf("persisted layer = %d, want %d", got, currentClientLayer)
}
if got, ok := r.NegotiatedLayer(authKeyID, 101); !ok || got != currentClientLayer {
t.Fatalf("new session negotiated layer = (%d,%v), want (%d,true)", got, ok, currentClientLayer)
}
}
func TestClientTypeDetectionUsesStrongEvidenceBeforeAPIID(t *testing.T) {
tests := []struct {
name string
info ClientInfo
want ClientType
}{
{
name: "iOS 12.8 simulator",
info: ClientInfo{APIID: 1, DeviceModel: "iPhone Simulator", SystemVersion: "26.5", AppVersion: "12.8 (10000)", LangPack: "ios"},
want: ClientTypeIOS,
},
{
name: "restored iOS without lang pack",
info: ClientInfo{DeviceModel: "iPhone 16 Pro", SystemVersion: "18.5", Type: ClientTypeUnknown},
want: ClientTypeIOS,
},
{
name: "TWeb WebK",
info: ClientInfo{APIID: 1025907, DeviceModel: "Mozilla/5.0 Chrome/138.0", SystemVersion: "Win32", LangPack: "webk"},
want: ClientTypeTWeb,
},
{
name: "telegram-tt WebA",
info: ClientInfo{APIID: 2040, DeviceModel: "Mozilla/5.0 Chrome/150.0", SystemVersion: "Windows", AppVersion: "12.0.32 A", LangPack: "weba"},
want: ClientTypeTelegramTT,
},
{
name: "TWeb borrowed TDesktop API id",
info: ClientInfo{APIID: 2040, DeviceModel: "Mozilla/5.0 AppleWebKit/537.36", SystemVersion: "Win32", Type: ClientTypeTDesktop},
want: ClientTypeTWeb,
},
{
name: "mobile TWeb is not native Android",
info: ClientInfo{APIID: 2040, DeviceModel: "Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36", LangPack: "webk"},
want: ClientTypeTWeb,
},
{
name: "Android SDK version",
info: ClientInfo{DeviceModel: "GooglePixel 9a", SystemVersion: "SDK 36", AppVersion: "12.7.3 (67509) pbeta"},
want: ClientTypeAndroid,
},
{name: "DrKLO API fallback", info: ClientInfo{APIID: 4}, want: ClientTypeAndroid},
{name: "TDesktop API fallback", info: ClientInfo{APIID: 2040}, want: ClientTypeTDesktop},
{name: "official iOS API fallback", info: ClientInfo{APIID: 8}, want: ClientTypeIOS},
{name: "official TWeb API fallback", info: ClientInfo{APIID: 2496}, want: ClientTypeTWeb},
{name: "macOS lang pack", info: ClientInfo{LangPack: "macos"}, want: ClientTypeMacOS},
{name: "stored known type", info: ClientInfo{Type: ClientTypeIOS}, want: ClientTypeIOS},
{
name: "gotd remains unknown",
info: ClientInfo{DeviceModel: "go1.26.2", SystemVersion: "windows", AppVersion: "v0.144.0"},
want: ClientTypeUnknown,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeClientInfo(tt.info).ClientType(); got != tt.want {
t.Fatalf("client type = %s, want %s", got, tt.want)
}
})
}
}
func TestRestoredUnknownClientInfoIsNotReclassifiedFromHistoricalFields(t *testing.T) {
info := restoreClientInfo(ClientInfo{
APIID: 2040,
DeviceModel: "Mozilla/5.0 AppleWebKit/537.36",
SystemVersion: "Windows",
AppVersion: "12.0.32 A",
Type: ClientTypeUnknown,
})
if got := info.ClientType(); got != ClientTypeUnknown {
t.Fatalf("restored historical client type = %s, want unknown until fresh initConnection", got)
}
info = restoreClientInfo(ClientInfo{
DeviceModel: "GooglePixel 9a",
SystemVersion: "SDK 36",
Type: ClientTypeUnknown,
})
if got := info.ClientType(); got != ClientTypeUnknown {
t.Fatalf("restored historical Android client type = %s, want unknown until fresh initConnection", got)
}
info = restoreClientInfo(ClientInfo{
DeviceModel: "Mozilla/5.0 AppleWebKit/537.36",
AppVersion: "12.0.32 A",
Type: ClientTypeTelegramTT,
})
if got := info.ClientType(); got != ClientTypeTelegramTT {
t.Fatalf("restored persisted telegram-tt client type = %s, want %s", got, ClientTypeTelegramTT)
}
}
func TestDispatchRestoresClientMetadataFromAuthorization(t *testing.T) {
core, logs := observer.New(zap.DebugLevel)
authKeyID := [8]byte{0x68, 0x25, 0xc2, 0xee, 0xf8, 0x82, 0xef, 0x71}
userID := int64(1780269504)
auth := &captureAuthService{
userID: userID,
authorizations: []domain.Authorization{{
AuthKeyID: authKeyID,
UserID: userID,
Layer: currentClientLayer,
DeviceModel: "Android",
Platform: string(ClientTypeAndroid),
SystemVersion: "Android 15",
APIID: 6,
AppVersion: "12.7.3",
}},
}
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zap.New(core), clock.System)
var in bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
if _, err := r.Dispatch(context.Background(), authKeyID, -3479521421865518854, &in); err != nil {
t.Fatalf("dispatch plain request: %v", err)
}
entries := logs.FilterMessage("RPC inner handled").All()
if len(entries) == 0 {
t.Fatalf("RPC inner handled log missing")
}
fields := entries[len(entries)-1].ContextMap()
if got := intLogField(fields["layer"]); got != currentClientLayer {
t.Fatalf("logged layer = %d fields=%v, want %d", got, fields, currentClientLayer)
}
if got := fields["client_type"]; got != string(ClientTypeAndroid) {
t.Fatalf("logged client_type = %v, want %s", got, ClientTypeAndroid)
}
if got := fields["app_version"]; got != "12.7.3" {
t.Fatalf("logged app_version = %v, want 12.7.3", got)
}
var second bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&second); err != nil {
t.Fatalf("encode second request: %v", err)
}
if _, err := r.Dispatch(context.Background(), authKeyID, -3479521421865518853, &second); err != nil {
t.Fatalf("dispatch second plain request: %v", err)
}
if auth.authorizationLookups != 1 || auth.authorizationLists != 0 {
t.Fatalf("authorization lookups/list calls = %d/%d, want exact-key 1/list 0", auth.authorizationLookups, auth.authorizationLists)
}
}
func TestDispatchCopiesEffectiveAuthLayerToRawTempSession(t *testing.T) {
rawAuthKeyID := [8]byte{0x1a, 0x2d, 0x2d, 0x3d, 0x4b, 0x38, 0x62, 0xc0}
permAuthKeyID := [8]byte{0x5b, 0x1c, 0x12, 0x24, 0x98, 0x85, 0x60, 0xc1}
userID := int64(1780243218)
auth := &captureAuthService{
resolvedAuthKeyID: permAuthKeyID,
hasResolved: true,
userID: userID,
authorizations: []domain.Authorization{{
AuthKeyID: permAuthKeyID,
UserID: userID,
Layer: 225,
DeviceModel: "nubiaNX629J",
Platform: string(ClientTypeAndroid),
SystemVersion: "SDK 30",
AppVersion: "12.7.3 (67509) pbeta",
}},
}
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zaptest.NewLogger(t), clock.System)
var warm bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&warm); err != nil {
t.Fatalf("encode warm request: %v", err)
}
if _, err := r.Dispatch(context.Background(), permAuthKeyID, 11, &warm); err != nil {
t.Fatalf("dispatch warm perm request: %v", err)
}
if got, ok := r.NegotiatedLayer(permAuthKeyID, 12); !ok || got != 225 {
t.Fatalf("perm auth fallback layer = (%d,%v), want (225,true)", got, ok)
}
var firstTempRequest bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&firstTempRequest); err != nil {
t.Fatalf("encode temp request: %v", err)
}
const tempSessionID = int64(5579025282411299519)
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, tempSessionID, &firstTempRequest); err != nil {
t.Fatalf("dispatch first temp request: %v", err)
}
if got, ok := r.NegotiatedLayer(rawAuthKeyID, tempSessionID); !ok || got != 225 {
t.Fatalf("raw temp exact session layer = (%d,%v), want (225,true)", got, ok)
}
if got, ok := r.NegotiatedLayer(rawAuthKeyID, tempSessionID+1); !ok || got != 225 {
t.Fatalf("raw temp auth fallback layer = (%d,%v), want (225,true)", got, ok)
}
hotCtx := WithAuthKeyID(
WithSessionID(WithRawAuthKeyID(context.Background(), rawAuthKeyID), tempSessionID),
permAuthKeyID,
)
info, ok, stored := r.clientSessionInfo(hotCtx)
if !ok || info.layer != 225 {
t.Fatalf("cached temp session info = (%+v,%v), want layer 225", info, ok)
}
if !stored {
t.Fatalf("cached temp session metadata is not fully materialized for hot path")
}
if wrote := r.rememberClientSessionInfoIfMissing(hotCtx, info); wrote {
t.Fatalf("hot path rewrote already materialized client session metadata")
}
}
func TestDispatchRestoresAndroidMetadataFromAuthorizationSDKVersion(t *testing.T) {
core, logs := observer.New(zap.DebugLevel)
authKeyID := [8]byte{0x16, 0x65, 0x54, 0x12, 0xaa, 0xbb, 0xcc, 0xdd}
userID := int64(1780269504)
auth := &captureAuthService{
userID: userID,
authorizations: []domain.Authorization{{
AuthKeyID: authKeyID,
UserID: userID,
DeviceModel: "GooglePixel 9a",
SystemVersion: "SDK 36",
APIID: 4,
AppVersion: "12.7.3 (67509) pbeta",
}},
}
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zap.New(core), clock.System)
var in bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
if _, err := r.Dispatch(context.Background(), authKeyID, -3479521421865518854, &in); err != nil {
t.Fatalf("dispatch plain request: %v", err)
}
entries := logs.FilterMessage("RPC inner handled").All()
if len(entries) == 0 {
t.Fatalf("RPC inner handled log missing")
}
fields := entries[len(entries)-1].ContextMap()
if got := intLogField(fields["layer"]); got != 0 {
t.Fatalf("logged layer = %d fields=%v, want 0", got, fields)
}
if got := fields["client_type"]; got != string(ClientTypeAndroid) {
t.Fatalf("logged client_type = %v, want %s", got, ClientTypeAndroid)
}
if got := fields["app_version"]; got != "12.7.3 (67509) pbeta" {
t.Fatalf("logged app_version = %v, want 12.7.3 (67509) pbeta", got)
}
if auth.authorizationLookups != 1 || auth.authorizationLists != 0 {
t.Fatalf("authorization lookups/list calls = %d/%d, want exact-key 1/list 0", auth.authorizationLookups, auth.authorizationLists)
}
}
func TestDispatchCachesMissingClientMetadataAuthorizationLookup(t *testing.T) {
core, logs := observer.New(zap.DebugLevel)
authKeyID := [8]byte{0x68, 0x25, 0xc2, 0xee, 0xf8, 0x82, 0xef, 0x71}
auth := &captureAuthService{userID: 1780269504}
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zap.New(core), clock.System)
for _, sessionID := range []int64{-3479521421865518854, -3479521421865518853} {
var in bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
if _, err := r.Dispatch(context.Background(), authKeyID, sessionID, &in); err != nil {
t.Fatalf("dispatch plain request: %v", err)
}
}
if auth.authorizationLookups != 1 || auth.authorizationLists != 0 {
t.Fatalf("authorization lookups/list calls = %d/%d, want one cached exact-key empty lookup/list 0", auth.authorizationLookups, auth.authorizationLists)
}
entries := logs.FilterMessage("RPC inner handled").All()
if len(entries) == 0 {
t.Fatalf("RPC inner handled log missing")
}
fields := entries[len(entries)-1].ContextMap()
if got := intLogField(fields["layer"]); got != 0 {
t.Fatalf("logged layer = %d fields=%v, want unknown 0", got, fields)
}
if got := fields["client_type"]; got != string(ClientTypeUnknown) {
t.Fatalf("logged client_type = %v, want %s", got, ClientTypeUnknown)
}
}
func TestDispatchCachesMissingAuthKeyClientMetadataLookup(t *testing.T) {
authKeyID := [8]byte{0x68, 0x25, 0xc2, 0xee, 0xf8, 0x82, 0xef, 0x72}
auth := &captureAuthService{}
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zaptest.NewLogger(t), clock.System)
for _, sessionID := range []int64{101, 102, 103} {
var in bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
if _, err := r.Dispatch(context.Background(), authKeyID, sessionID, &in); err != nil {
t.Fatalf("dispatch session %d: %v", sessionID, err)
}
}
if auth.authKeyInfoLookups != 1 {
t.Fatalf("auth key client info lookups = %d, want 1 cached miss", auth.authKeyInfoLookups)
}
}
func TestCurrentUserIDUsesAuthUserCache(t *testing.T) {
authKeyID := [8]byte{0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42}
auth := &captureAuthService{userID: 1000000001}
r := New(Config{}, Deps{Auth: auth}, zaptest.NewLogger(t), clock.System)
ctx := WithAuthKeyID(context.Background(), authKeyID)
for i := 0; i < 2; i++ {
userID, ok, err := r.currentUserID(ctx)
if err != nil || !ok || userID != auth.userID {
t.Fatalf("currentUserID %d = user %d ok %v err %v, want %d/true", i, userID, ok, err, auth.userID)
}
}
if auth.userIDCount != 1 {
t.Fatalf("auth user lookups = %d, want 1", auth.userIDCount)
}
}
func intLogField(v any) int {
switch n := v.(type) {
case int:
return n
case int64:
return int(n)
case int32:
return int(n)
case float64:
return int(n)
default:
return 0
}
}
func TestDispatchUnwrapsInvokeAfterWrappers(t *testing.T) {
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System)
tests := []struct {
name string
req bin.Encoder
}{
{
name: "invokeAfterMsg",
req: &tg.InvokeAfterMsgRequest{
MsgID: 123,
Query: &tg.HelpGetConfigRequest{},
},
},
{
name: "invokeAfterMsgs",
req: &tg.InvokeAfterMsgsRequest{
MsgIDs: []int64{123, 456},
Query: &tg.HelpGetConfigRequest{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var b bin.Buffer
if err := tt.req.Encode(&b); err != nil {
t.Fatalf("encode: %v", err)
}
enc, err := r.Dispatch(context.Background(), [8]byte{}, 0, &b)
if err != nil {
t.Fatalf("dispatch: %v", err)
}
if _, ok := enc.(*tg.Config); !ok {
t.Fatalf("result type = %T, want *tg.Config", enc)
}
})
}
}
// TestDispatchUnknownReturnsError 验证未注册 RPC 经 fallback 返回 rpc_error。
func TestDispatchUnknownReturnsError(t *testing.T) {
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System)
// help.getCdnConfig 第一阶段未注册,应走 fallback。
var b bin.Buffer
if err := (&tg.HelpGetCDNConfigRequest{}).Encode(&b); err != nil {
t.Fatalf("encode: %v", err)
}
_, err := r.Dispatch(context.Background(), [8]byte{}, 0, &b)
if err == nil {
t.Fatal("expected error for unregistered RPC")
}
}
func TestDispatchResolvesBoundTempAuthKey(t *testing.T) {
var tempAuthKeyID = [8]byte{0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55}
var permAuthKeyID = [8]byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}
tempBindings := memory.NewTempAuthKeyBindingStore()
if err := tempBindings.Save(context.Background(), domain.TempAuthKeyBinding{
TempAuthKeyID: tempAuthKeyID,
PermAuthKeyID: int64(binary.LittleEndian.Uint64(permAuthKeyID[:])),
ExpiresAt: int(time.Now().Add(time.Hour).Unix()),
}); err != nil {
t.Fatalf("save temp binding: %v", err)
}
sessions := &captureSessions{}
r := New(Config{}, Deps{
Auth: appauth.NewService(nil, nil, nil, nil, tempBindings, "12345"),
Sessions: sessions,
}, zaptest.NewLogger(t), clock.System)
req := &tg.HelpGetConfigRequest{}
var in bin.Buffer
if err := req.Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
if _, err := r.Dispatch(context.Background(), tempAuthKeyID, 123, &in); err != nil {
t.Fatalf("dispatch: %v", err)
}
gotSession := sessions.snapshot()
if gotSession.authKeyID != permAuthKeyID || !gotSession.authKeyResolved {
t.Fatalf("session auth key = %x resolved %v, want resolved perm %x", gotSession.authKeyID, gotSession.authKeyResolved, permAuthKeyID)
}
}
func TestDispatchCachesUnauthenticatedIdentity(t *testing.T) {
var rawAuthKeyID = [8]byte{0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77}
auth := &captureAuthService{}
sessions := &captureSessions{}
r := New(Config{}, Deps{
Auth: auth,
Sessions: sessions,
}, zaptest.NewLogger(t), clock.System)
req := &tg.HelpGetConfigRequest{}
for i := 0; i < 2; i++ {
var in bin.Buffer
if err := req.Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, 777, &in); err != nil {
t.Fatalf("dispatch %d: %v", i, err)
}
}
if auth.resolveCount != 1 || auth.userIDCount != 1 {
t.Fatalf("identity lookups = resolve %d user %d, want one-time negative cache", auth.resolveCount, auth.userIDCount)
}
gotSession := sessions.snapshot()
if !gotSession.userResolved || gotSession.userID != 0 {
t.Fatalf("cached unauth identity = user %d resolved %v, want 0/true", gotSession.userID, gotSession.userResolved)
}
}
func TestDispatchCachesTempConnectionIdentity(t *testing.T) {
var rawAuthKeyID = [8]byte{0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55}
var permAuthKeyID = [8]byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}
auth := &captureAuthService{
resolvedAuthKeyID: permAuthKeyID,
hasResolved: true,
userID: 1000000001,
}
sessions := &captureSessions{}
r := New(Config{}, Deps{
Auth: auth,
Sessions: sessions,
Users: staticUsersService{user: domain.User{
ID: 1000000001,
FirstName: "Test",
}},
}, zaptest.NewLogger(t), clock.System)
req := &tg.UsersGetFullUserRequest{ID: &tg.InputUserSelf{}}
for i := 0; i < 2; i++ {
var in bin.Buffer
if err := req.Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, 777, &in); err != nil {
t.Fatalf("dispatch %d: %v", i, err)
}
}
if auth.resolveCount != 2 || auth.userIDCount != 1 {
t.Fatalf("identity lookups = resolve %d user %d, want temp resolve checks and cached user identity", auth.resolveCount, auth.userIDCount)
}
gotSession := sessions.snapshot()
if gotSession.authKeyID != permAuthKeyID || gotSession.userID != 1000000001 {
t.Fatalf("cached identity = auth %x user %d, want perm/user", gotSession.authKeyID, gotSession.userID)
}
}
func TestDispatchSingleflightsAuthUserLookupAcrossStartupRPCs(t *testing.T) {
var authKeyID = [8]byte{0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42}
auth := newBlockingUserAuthService(1000000001)
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Auth: auth,
}, zaptest.NewLogger(t), clock.System)
const calls = 16
errs := make(chan error, calls)
var wg sync.WaitGroup
for i := 0; i < calls; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
var in bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
errs <- err
return
}
_, err := r.Dispatch(context.Background(), authKeyID, int64(100+i), &in)
errs <- err
}(i)
}
select {
case <-auth.started:
case <-time.After(time.Second):
t.Fatal("auth lookup did not start")
}
close(auth.release)
wg.Wait()
close(errs)
for err := range errs {
if err != nil {
t.Fatalf("dispatch: %v", err)
}
}
if got := auth.UserIDCount(); got != 1 {
t.Fatalf("UserID lookups = %d, want singleflighted one lookup", got)
}
for i := 0; i < 3; i++ {
var in bin.Buffer
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
t.Fatalf("encode: %v", err)
}
if _, err := r.Dispatch(context.Background(), authKeyID, int64(200+i), &in); err != nil {
t.Fatalf("cached dispatch %d: %v", i, err)
}
}
if got := auth.UserIDCount(); got != 1 {
t.Fatalf("UserID lookups after cache hits = %d, want still one lookup", got)
}
}
func TestDispatchAnnouncesPresenceWhenSessionIdentityRestored(t *testing.T) {
ctx := context.Background()
alice := domain.User{ID: 1000000001, AccessHash: 11, FirstName: "Alice"}
bob := domain.User{ID: 1000000002, AccessHash: 22, FirstName: "Bob"}
dialogs := memory.NewDialogStore()
// 私聊 dialog 双向建行两侧都存。presence 接收者由「主体自己的 dialog 对端 ∩ 在线」
// 算出bob 改状态时需 bob 侧 dialog 含 alice。
if err := dialogs.SaveList(ctx, alice.ID, domain.DialogList{
Dialogs: []domain.Dialog{{
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID},
TopMessage: 10,
TopMessageDate: 1700000000,
}},
Users: []domain.User{bob},
}); err != nil {
t.Fatalf("save dialogs: %v", err)
}
if err := dialogs.SaveList(ctx, bob.ID, domain.DialogList{
Dialogs: []domain.Dialog{{
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID},
TopMessage: 10,
TopMessageDate: 1700000000,
}},
Users: []domain.User{alice},
}); err != nil {
t.Fatalf("save bob dialogs: %v", err)
}
auth := &captureAuthService{userID: bob.ID}
sessions := &captureSessions{onlineUserIDs: []int64{alice.ID}}
r := New(Config{}, Deps{
Auth: auth,
Dialogs: appdialogs.NewService(dialogs),
Sessions: sessions,
}, zaptest.NewLogger(t), clock.System)
req := &tg.HelpGetConfigRequest{}
for i := 0; i < 2; i++ {
var in bin.Buffer
if err := req.Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
if _, err := r.Dispatch(ctx, [8]byte{0x22}, 333, &in); err != nil {
t.Fatalf("dispatch %d: %v", i, err)
}
}
if auth.userIDCount != 1 {
t.Fatalf("user lookup count = %d, want one restored identity lookup", auth.userIDCount)
}
gotPushes := waitForPushedUserIDs(t, sessions, 2)
if !reflect.DeepEqual(gotPushes, []int64{bob.ID, alice.ID}) {
t.Fatalf("pushed users = %+v, want self and online private dialog peer once", gotPushes)
}
// 用 lastUserPushPushToUser* 的内容)而非 snapshot().message后者还会被
// pushOnlinePeerStatusesToCurrentSession 把 alice 的在线状态推给 bob 当前 session 覆盖。
update := pushedUserStatus(t, waitForLastUserPush(t, sessions))
if update.UserID != bob.ID {
t.Fatalf("status user = %d, want bob", update.UserID)
}
if status, ok := update.Status.(*tg.UserStatusOnline); !ok || status.Expires <= int(time.Now().Unix()) {
t.Fatalf("status = %#v, want online with future expires", update.Status)
}
}
func TestDispatchPushesOnlinePeerStatusesToRestoredSession(t *testing.T) {
ctx := context.Background()
alice := domain.User{ID: 1000000001, AccessHash: 11, FirstName: "Alice"}
bob := domain.User{ID: 1000000002, AccessHash: 22, FirstName: "Bob"}
dialogs := memory.NewDialogStore()
if err := dialogs.SaveList(ctx, alice.ID, domain.DialogList{
Dialogs: []domain.Dialog{{
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID},
TopMessage: 10,
TopMessageDate: 1700000000,
}},
Users: []domain.User{bob},
}); err != nil {
t.Fatalf("save dialogs: %v", err)
}
auth := &captureAuthService{userID: alice.ID}
sessions := &captureSessions{onlineUserIDs: []int64{bob.ID}}
r := New(Config{}, Deps{
Auth: auth,
Dialogs: appdialogs.NewService(dialogs),
Sessions: sessions,
}, zaptest.NewLogger(t), clock.System)
if ok, err := r.onAccountUpdateStatus(WithSessionID(WithUserID(ctx, bob.ID), 22), false); err != nil || !ok {
t.Fatalf("bob account.updateStatus online = %v, %v", ok, err)
}
sessions.clearMessages()
req := &tg.HelpGetConfigRequest{}
for i := 0; i < 2; i++ {
var in bin.Buffer
if err := req.Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
if _, err := r.Dispatch(ctx, [8]byte{0x33}, 444, &in); err != nil {
t.Fatalf("dispatch %d: %v", i, err)
}
}
if auth.userIDCount != 1 {
t.Fatalf("user lookup count = %d, want one restored identity lookup", auth.userIDCount)
}
update := waitForSessionUserStatus(t, sessions, bob.ID)
if update.UserID != bob.ID {
t.Fatalf("status user = %d, want bob", update.UserID)
}
if status, ok := update.Status.(*tg.UserStatusOnline); !ok || status.Expires <= int(time.Now().Unix()) {
t.Fatalf("status = %#v, want online peer status for restored session", update.Status)
}
}
// TestTDesktopStartupRPCsEncode 验证第一阶段 TDesktop 启动 RPC 均能被路由并编码回包。
func TestTDesktopStartupRPCsEncode(t *testing.T) {
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
Users: staticUsersService{user: domain.User{
ID: 1000000001,
AccessHash: 42,
FirstName: "Test",
LastName: "User",
Phone: "15550000000",
}},
}, zaptest.NewLogger(t), clock.System)
tests := []struct {
name string
req bin.Encoder
}{
{name: "auth.bindTempAuthKey", req: &tg.AuthBindTempAuthKeyRequest{PermAuthKeyID: 1, Nonce: 2, ExpiresAt: 3, EncryptedMessage: []byte("binding")}},
{name: "auth.exportLoginToken", req: &tg.AuthExportLoginTokenRequest{APIID: 1, APIHash: "hash"}},
{name: "help.getAppConfig", req: &tg.HelpGetAppConfigRequest{}},
{name: "help.getCountriesList", req: &tg.HelpGetCountriesListRequest{LangCode: "en"}},
{name: "help.getTimezonesList", req: &tg.HelpGetTimezonesListRequest{}},
{name: "help.getPeerColors", req: &tg.HelpGetPeerColorsRequest{}},
{name: "help.getPeerProfileColors", req: &tg.HelpGetPeerProfileColorsRequest{}},
{name: "help.getPromoData", req: &tg.HelpGetPromoDataRequest{}},
{name: "help.getTermsOfServiceUpdate", req: &tg.HelpGetTermsOfServiceUpdateRequest{}},
{name: "help.getPremiumPromo", req: &tg.HelpGetPremiumPromoRequest{}},
{name: "help.getInviteText", req: &tg.HelpGetInviteTextRequest{}},
{name: "help.getAppUpdate", req: &tg.HelpGetAppUpdateRequest{}},
{name: "auth.initPasskeyLogin", req: &tg.AuthInitPasskeyLoginRequest{APIID: 4, APIHash: "test"}},
{name: "account.getPassword", req: &tg.AccountGetPasswordRequest{}},
{name: "account.getNotifySettings", req: &tg.AccountGetNotifySettingsRequest{Peer: &tg.InputNotifyUsers{}}},
{name: "account.resetNotifySettings", req: &tg.AccountResetNotifySettingsRequest{}},
{name: "account.getPrivacy", req: &tg.AccountGetPrivacyRequest{Key: &tg.InputPrivacyKeyStatusTimestamp{}}},
{name: "account.getAuthorizations", req: &tg.AccountGetAuthorizationsRequest{}},
{name: "account.getWebAuthorizations", req: &tg.AccountGetWebAuthorizationsRequest{}},
{name: "account.getNotifyExceptions", req: &tg.AccountGetNotifyExceptionsRequest{}},
{name: "account.getDefaultEmojiStatuses", req: &tg.AccountGetDefaultEmojiStatusesRequest{}},
{name: "account.getRecentEmojiStatuses", req: &tg.AccountGetRecentEmojiStatusesRequest{}},
{name: "account.getCollectibleEmojiStatuses", req: &tg.AccountGetCollectibleEmojiStatusesRequest{}},
{name: "account.getDefaultProfilePhotoEmojis", req: &tg.AccountGetDefaultProfilePhotoEmojisRequest{}},
{name: "account.getDefaultGroupPhotoEmojis", req: &tg.AccountGetDefaultGroupPhotoEmojisRequest{}},
{name: "account.getDefaultBackgroundEmojis", req: &tg.AccountGetDefaultBackgroundEmojisRequest{}},
{name: "account.getChannelDefaultEmojiStatuses", req: &tg.AccountGetChannelDefaultEmojiStatusesRequest{}},
{name: "account.getChannelRestrictedStatusEmojis", req: &tg.AccountGetChannelRestrictedStatusEmojisRequest{}},
{name: "account.getConnectedBots", req: &tg.AccountGetConnectedBotsRequest{}},
{name: "account.getBusinessChatLinks", req: &tg.AccountGetBusinessChatLinksRequest{}},
{name: "account.getReactionsNotifySettings", req: &tg.AccountGetReactionsNotifySettingsRequest{}},
{name: "account.getContactSignUpNotification", req: &tg.AccountGetContactSignUpNotificationRequest{}},
{name: "account.getThemes", req: &tg.AccountGetThemesRequest{Format: "tdesktop"}},
{name: "account.getChatThemes", req: &tg.AccountGetChatThemesRequest{}},
{name: "account.getWallPapers", req: &tg.AccountGetWallPapersRequest{}},
{name: "account.getUniqueGiftChatThemes", req: &tg.AccountGetUniqueGiftChatThemesRequest{Limit: 20}},
{name: "account.getContentSettings", req: &tg.AccountGetContentSettingsRequest{}},
{name: "account.getGlobalPrivacySettings", req: &tg.AccountGetGlobalPrivacySettingsRequest{}},
{name: "account.getPasskeys", req: &tg.AccountGetPasskeysRequest{}},
{name: "account.getAutoDownloadSettings", req: &tg.AccountGetAutoDownloadSettingsRequest{}},
{name: "account.getSavedMusicIds", req: &tg.AccountGetSavedMusicIDsRequest{}},
{name: "account.getSavedRingtones", req: &tg.AccountGetSavedRingtonesRequest{}},
{name: "account.resetPassword", req: &tg.AccountResetPasswordRequest{}},
{name: "account.updateStatus", req: &tg.AccountUpdateStatusRequest{Offline: true}},
{name: "account.updateDeviceLocked", req: &tg.AccountUpdateDeviceLockedRequest{Period: 60}},
{name: "payments.getStarsTopupOptions", req: &tg.PaymentsGetStarsTopupOptionsRequest{}},
{name: "payments.getStarsStatus", req: &tg.PaymentsGetStarsStatusRequest{Peer: &tg.InputPeerSelf{}}},
{name: "updates.getDifference", req: &tg.UpdatesGetDifferenceRequest{}},
{name: "users.getFullUser", req: &tg.UsersGetFullUserRequest{ID: &tg.InputUserSelf{}}},
{name: "users.getRequirementsToContact", req: &tg.UsersGetRequirementsToContactRequest{ID: []tg.InputUserClass{&tg.InputUserSelf{}}}},
{name: "users.getSavedMusic", req: &tg.UsersGetSavedMusicRequest{ID: &tg.InputUserSelf{}, Limit: 20}},
{name: "users.getSavedMusicByID", req: &tg.UsersGetSavedMusicByIDRequest{ID: &tg.InputUserSelf{}, Documents: []tg.InputDocumentClass{}}},
{name: "messages.getDialogFilters", req: &tg.MessagesGetDialogFiltersRequest{}},
{name: "messages.getDialogs", req: &tg.MessagesGetDialogsRequest{OffsetPeer: &tg.InputPeerEmpty{}, Limit: 20}},
{name: "messages.getPinnedDialogs", req: &tg.MessagesGetPinnedDialogsRequest{}},
{name: "messages.getPeerDialogs", req: &tg.MessagesGetPeerDialogsRequest{Peers: []tg.InputDialogPeerClass{&tg.InputDialogPeer{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}}}}},
{name: "messages.getAvailableReactions", req: &tg.MessagesGetAvailableReactionsRequest{}},
{name: "messages.getAvailableEffects", req: &tg.MessagesGetAvailableEffectsRequest{}},
{name: "messages.getStickers", req: &tg.MessagesGetStickersRequest{}},
{name: "messages.getArchivedStickers", req: &tg.MessagesGetArchivedStickersRequest{Limit: 20}},
{name: "messages.getMaskStickers", req: &tg.MessagesGetMaskStickersRequest{}},
{name: "messages.getStickerSet", req: &tg.MessagesGetStickerSetRequest{Stickerset: &tg.InputStickerSetEmpty{}}},
{name: "messages.getEmojiGroups", req: &tg.MessagesGetEmojiGroupsRequest{}},
{name: "messages.getEmojiStatusGroups", req: &tg.MessagesGetEmojiStatusGroupsRequest{}},
{name: "messages.getEmojiStickerGroups", req: &tg.MessagesGetEmojiStickerGroupsRequest{}},
{name: "messages.getEmojiProfilePhotoGroups", req: &tg.MessagesGetEmojiProfilePhotoGroupsRequest{}},
{name: "messages.getEmojiKeywordsLanguages", req: &tg.MessagesGetEmojiKeywordsLanguagesRequest{LangCodes: []string{"en"}}},
{name: "messages.getAttachMenuBots", req: &tg.MessagesGetAttachMenuBotsRequest{}},
{name: "messages.getQuickReplies", req: &tg.MessagesGetQuickRepliesRequest{}},
{name: "messages.getQuickReplyMessages", req: &tg.MessagesGetQuickReplyMessagesRequest{ShortcutID: 1}},
{name: "messages.getSavedHistory", req: &tg.MessagesGetSavedHistoryRequest{Peer: &tg.InputPeerSelf{}, Limit: 20}},
{name: "messages.readSavedHistory", req: &tg.MessagesReadSavedHistoryRequest{ParentPeer: &tg.InputPeerChannel{ChannelID: 1, AccessHash: 1}, Peer: &tg.InputPeerSelf{}}},
{name: "messages.deleteSavedHistory", req: &tg.MessagesDeleteSavedHistoryRequest{Peer: &tg.InputPeerSelf{}}},
{name: "messages.getPeerSettings", req: &tg.MessagesGetPeerSettingsRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}}},
{name: "messages.setChatWallPaper", req: &tg.MessagesSetChatWallPaperRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}, Wallpaper: &tg.InputWallPaperNoFile{ID: 930000000000000000}}},
{name: "messages.getHistory", req: &tg.MessagesGetHistoryRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}, Limit: 20}},
{name: "messages.getRecentLocations", req: &tg.MessagesGetRecentLocationsRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}, Limit: 20}},
{name: "messages.readHistory", req: &tg.MessagesReadHistoryRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}}},
{name: "messages.search", req: &tg.MessagesSearchRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}, Filter: &tg.InputMessagesFilterEmpty{}, Limit: 20}},
{name: "messages.searchGlobal", req: &tg.MessagesSearchGlobalRequest{Q: "login", Filter: &tg.InputMessagesFilterEmpty{}, OffsetPeer: &tg.InputPeerEmpty{}, Limit: 20}},
{name: "messages.getWebPage", req: &tg.MessagesGetWebPageRequest{URL: "https://example.invalid"}},
{name: "messages.getScheduledHistory", req: &tg.MessagesGetScheduledHistoryRequest{Peer: &tg.InputPeerUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}}},
{name: "contacts.getContacts", req: &tg.ContactsGetContactsRequest{}},
{name: "contacts.search", req: &tg.ContactsSearchRequest{Q: "Test", Limit: 20}},
{name: "contacts.getBlocked", req: &tg.ContactsGetBlockedRequest{Limit: 20}},
{name: "contacts.getBirthdays", req: &tg.ContactsGetBirthdaysRequest{}},
{name: "contacts.getTopPeers", req: &tg.ContactsGetTopPeersRequest{Correspondents: true, Limit: 10}},
{name: "contacts.getSponsoredPeers", req: &tg.ContactsGetSponsoredPeersRequest{Q: "Test"}},
{name: "stories.getAllStories", req: &tg.StoriesGetAllStoriesRequest{}},
{name: "stories.getStoriesArchive", req: &tg.StoriesGetStoriesArchiveRequest{Peer: &tg.InputPeerSelf{}, Limit: 20}},
{name: "stories.getPinnedStories", req: &tg.StoriesGetPinnedStoriesRequest{Peer: &tg.InputPeerSelf{}, Limit: 20}},
{name: "stories.exportStoryLink", req: &tg.StoriesExportStoryLinkRequest{Peer: &tg.InputPeerSelf{}, ID: 1}},
{name: "stories.report", req: &tg.StoriesReportRequest{Peer: &tg.InputPeerSelf{}, ID: []int{1}}},
{name: "stories.activateStealthMode", req: &tg.StoriesActivateStealthModeRequest{Past: true, Future: true}},
{name: "stories.searchPosts", req: &tg.StoriesSearchPostsRequest{Hashtag: "storytag", Limit: 20}},
{name: "stories.getAlbums", req: &tg.StoriesGetAlbumsRequest{Peer: &tg.InputPeerSelf{}}},
{name: "stories.getAlbumStories", req: &tg.StoriesGetAlbumStoriesRequest{Peer: &tg.InputPeerSelf{}, AlbumID: 1, Limit: 20}},
{name: "stories.toggleAllStoriesHidden", req: &tg.StoriesToggleAllStoriesHiddenRequest{Hidden: true}},
{name: "stories.reorderAlbums", req: &tg.StoriesReorderAlbumsRequest{Peer: &tg.InputPeerSelf{}, Order: []int{1}}},
{name: "stories.deleteAlbum", req: &tg.StoriesDeleteAlbumRequest{Peer: &tg.InputPeerSelf{}, AlbumID: 1}},
{name: "stories.getAllReadPeerStories", req: &tg.StoriesGetAllReadPeerStoriesRequest{}},
{name: "stories.getPeerMaxIDs", req: &tg.StoriesGetPeerMaxIDsRequest{ID: []tg.InputPeerClass{&tg.InputPeerSelf{}}}},
{name: "stories.getStoriesViews", req: &tg.StoriesGetStoriesViewsRequest{Peer: &tg.InputPeerSelf{}, ID: []int{1}}},
{name: "stories.getChatsToSend", req: &tg.StoriesGetChatsToSendRequest{}},
{name: "payments.getStarGiftActiveAuctions", req: &tg.PaymentsGetStarGiftActiveAuctionsRequest{}},
{name: "payments.getStarGifts", req: &tg.PaymentsGetStarGiftsRequest{}},
{name: "payments.getStarGiftCollections", req: &tg.PaymentsGetStarGiftCollectionsRequest{Peer: &tg.InputPeerSelf{}}},
{name: "payments.getSavedStarGifts", req: &tg.PaymentsGetSavedStarGiftsRequest{Peer: &tg.InputPeerSelf{}, Limit: 20}},
{name: "payments.getSavedStarGift", req: &tg.PaymentsGetSavedStarGiftRequest{Stargift: []tg.InputSavedStarGiftClass{}}},
{name: "payments.getStarsRevenueAdsAccountUrl", req: &tg.PaymentsGetStarsRevenueAdsAccountURLRequest{Peer: &tg.InputPeerSelf{}}},
{name: "payments.getStarsRevenueStats", req: &tg.PaymentsGetStarsRevenueStatsRequest{Ton: true, Peer: &tg.InputPeerSelf{}}},
{name: "bots.getBotRecommendations", req: &tg.BotsGetBotRecommendationsRequest{Bot: &tg.InputUser{UserID: domain.OfficialSystemUserID, AccessHash: domain.OfficialSystemUser().AccessHash}}},
{name: "aicompose.getTones", req: &tg.AicomposeGetTonesRequest{}},
{name: "langpack.getLanguage", req: &tg.LangpackGetLanguageRequest{LangPack: "tdesktop", LangCode: "en"}},
{name: "langpack.getLangPack", req: &tg.LangpackGetLangPackRequest{LangPack: "tdesktop", LangCode: "en"}},
{name: "langpack.getDifference", req: &tg.LangpackGetDifferenceRequest{LangPack: "tdesktop", LangCode: "en", FromVersion: 1}},
{name: "langpack.getStrings", req: &tg.LangpackGetStringsRequest{LangPack: "tdesktop", LangCode: "en", Keys: []string{"lng_intro_about"}}},
{name: "help.getDeepLinkInfo", req: &tg.HelpGetDeepLinkInfoRequest{Path: "join?invite=abc"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var in bin.Buffer
if err := tt.req.Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
ctx := WithUserID(context.Background(), 1000000001)
enc, err := r.Dispatch(ctx, [8]byte{}, 0, &in)
if err != nil {
t.Fatalf("dispatch: %v", err)
}
var out bin.Buffer
if err := enc.Encode(&out); err != nil {
t.Fatalf("encode response: %v", err)
}
if out.Len() == 0 {
t.Fatal("encoded response is empty")
}
})
}
}
// TestHelpGetDeepLinkInfoReturnsEmpty 回归help.getDeepLinkInfo 此前未注册 handler
// 落 fallback 返回 500 NOT_IMPLEMENTED。客户端遇到无法识别的 tg:// 深链就会发该请求
// DrKLO LaunchActivity unsupportedUrl 分支),应返回规范的 deepLinkInfoEmpty 而非报错。
func TestHelpGetDeepLinkInfoReturnsEmpty(t *testing.T) {
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
req := &tg.HelpGetDeepLinkInfoRequest{Path: "resolve?domain=unknown"}
var in bin.Buffer
if err := req.Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
enc, err := r.Dispatch(WithUserID(context.Background(), 1000000001), [8]byte{}, 0, &in)
if err != nil {
t.Fatalf("dispatch help.getDeepLinkInfo err = %v, want nil (未注册时为 500 NOT_IMPLEMENTED)", err)
}
var out bin.Buffer
if err := enc.Encode(&out); err != nil {
t.Fatalf("encode response: %v", err)
}
res, err := tg.DecodeHelpDeepLinkInfo(&out)
if err != nil {
t.Fatalf("decode response: %v", err)
}
if _, ok := res.(*tg.HelpDeepLinkInfoEmpty); !ok {
t.Fatalf("response type = %T, want *tg.HelpDeepLinkInfoEmpty", res)
}
}
func TestStoriesStartLiveDispatchReturnsMethodInvalid(t *testing.T) {
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
req := &tg.StoriesStartLiveRequest{
Peer: &tg.InputPeerSelf{},
PrivacyRules: []tg.InputPrivacyRuleClass{&tg.InputPrivacyValueAllowAll{}},
RandomID: 42,
}
var in bin.Buffer
if err := req.Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
_, err := r.Dispatch(WithUserID(context.Background(), 1000000001), [8]byte{}, 0, &in)
if err == nil || !tgerr.Is(err, "METHOD_INVALID") {
t.Fatalf("dispatch stories.startLive err = %v, want METHOD_INVALID", err)
}
}
func TestStoriesAlbumMutationDispatchReturnsMethodInvalid(t *testing.T) {
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
updateReq := &tg.StoriesUpdateAlbumRequest{
Peer: &tg.InputPeerSelf{},
AlbumID: 1,
}
updateReq.SetTitle("Travel")
tests := []struct {
name string
req bin.Encoder
}{
{name: "stories.createAlbum", req: &tg.StoriesCreateAlbumRequest{
Peer: &tg.InputPeerSelf{},
Title: "Favorites",
Stories: []int{1},
}},
{name: "stories.updateAlbum", req: updateReq},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var in bin.Buffer
if err := tt.req.Encode(&in); err != nil {
t.Fatalf("encode request: %v", err)
}
_, err := r.Dispatch(WithUserID(context.Background(), 1000000001), [8]byte{}, 0, &in)
if err == nil || !tgerr.Is(err, "METHOD_INVALID") {
t.Fatalf("dispatch %s err = %v, want METHOD_INVALID", tt.name, err)
}
})
}
}