Ctrl K
ComunidadGuías…

internal/mtprotoedge/rpc_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package mtprotoedge

import (
	"context"
	"sync/atomic"
	"testing"
	"time"

	"go.uber.org/zap/zaptest"

	"github.com/iamxvbaba/td/bin"
	"github.com/iamxvbaba/td/clock"
	"github.com/iamxvbaba/td/crypto"
	"github.com/iamxvbaba/td/mt"
	"github.com/iamxvbaba/td/proto"
	"github.com/iamxvbaba/td/tg"
	"github.com/iamxvbaba/td/tgerr"
	"github.com/iamxvbaba/td/transport"

	"github.com/iamxvbaba/td/tlprofile"
	"telesrv/internal/rpc"
)

// TestRPCGetConfig verifies M3: after the handshake the client sends an
// encrypted help.getConfig, and the server routes it through tlprofile.Dispatcher
// and replies with rpc_result (containing the local DC), plus new_session_created + ack.
func TestRPCGetConfig(t *testing.T) {
	const (
		dc      = 2
		advIP   = "127.0.0.1"
		advPort = 12345
	)
	router := rpc.New(rpc.Config{DC: dc, IP: advIP, Port: advPort}, rpc.Deps{}, zaptest.NewLogger(t), clock.System)
	addr, pub, _ := startTestServer(t, Options{DC: dc, legacyRPC: router})
	conn, auth, cipher := dialHandshake(t, addr, dc, pub)

	clientMsgID := proto.NewMessageIDGen(time.Now)
	reqMsgID := clientMsgID.New(proto.MessageFromClient)
	sendEncrypted(t, conn, cipher, auth, reqMsgID, &tg.HelpGetConfigRequest{})

	replies := collectReplies(t, conn, cipher, auth.AuthKey, proto.ResultTypeID)
	if _, ok := replies[mt.MsgsAckTypeID]; !ok {
		for id, b := range collectReplies(t, conn, cipher, auth.AuthKey, mt.MsgsAckTypeID) {
			replies[id] = b
		}
	}
	mustHave(t, replies, mt.NewSessionCreatedTypeID, "new_session_created")
	mustHave(t, replies, mt.MsgsAckTypeID, "msgs_ack")
	resultBuf := mustHave(t, replies, proto.ResultTypeID, "rpc_result")

	var res proto.Result
	if err := res.Decode(resultBuf); err != nil {
		t.Fatalf("decode rpc_result: %v", err)
	}
	if res.RequestMessageID != reqMsgID {
		t.Fatalf("rpc_result req_msg_id = %d, want %d", res.RequestMessageID, reqMsgID)
	}

	var cfg tg.Config
	if err := cfg.Decode(&bin.Buffer{Buf: res.Result}); err != nil {
		t.Fatalf("decode config: %v", err)
	}
	if cfg.ThisDC != dc {
		t.Fatalf("config.ThisDC = %d, want %d", cfg.ThisDC, dc)
	}
	if len(cfg.DCOptions) != 1 {
		t.Fatalf("config.DCOptions = %+v, want one reconnect route", cfg.DCOptions)
	}
	option := cfg.DCOptions[0]
	if option.ID != dc || option.IPAddress != advIP || option.Port != advPort {
		t.Fatalf("config.DCOptions[0] = %+v, want dc=%d at %s:%d", option, dc, advIP, advPort)
	}
}

func TestLayerRPCGetConfigUsesExactAdmittedProfile(t *testing.T) {
	const (
		dc      = 2
		advIP   = "127.0.0.1"
		advPort = 12345
	)
	router := rpc.New(rpc.Config{DC: dc, IP: advIP, Port: advPort}, rpc.Deps{}, zaptest.NewLogger(t), clock.System)
	addr, pub, _ := startTestServer(t, Options{DC: dc, LayerRPC: router})
	conn, auth, cipher := dialHandshake(t, addr, dc, pub)

	clientMsgID := proto.NewMessageIDGen(time.Now)
	reqMsgID := clientMsgID.New(proto.MessageFromClient)
	request := &tg.InvokeWithLayerRequest{
		Layer: int(tlprofile.Profile225),
		Query: &tg.InitConnectionRequest{
			APIID:          123,
			DeviceModel:    "Desktop",
			SystemVersion:  "Windows",
			AppVersion:     "test",
			SystemLangCode: "en",
			LangPack:       "tdesktop",
			LangCode:       "en",
			Query:          &tg.HelpGetConfigRequest{},
		},
	}
	sendEncrypted(t, conn, cipher, auth, reqMsgID, request)

	replies := collectReplies(t, conn, cipher, auth.AuthKey, proto.ResultTypeID)
	resultBuf := mustHave(t, replies, proto.ResultTypeID, "rpc_result")
	var result proto.Result
	if err := result.Decode(resultBuf); err != nil {
		t.Fatalf("decode rpc_result: %v", err)
	}
	if result.RequestMessageID != reqMsgID {
		t.Fatalf("rpc_result req_msg_id = %d, want %d", result.RequestMessageID, reqMsgID)
	}
	exact := &bin.Buffer{Buf: result.Result}
	configObject, err := tlprofile.DecodeObject(tlprofile.Profile225, exact, tlprofile.Limits{})
	if err != nil {
		t.Fatalf("decode layer 225 config: %v", err)
	}
	config, ok := configObject.(*tg.Config)
	if !ok {
		t.Fatalf("layer 225 config = %T, want *tg.Config", configObject)
	}
	if exact.Len() != 0 || config.ThisDC != dc {
		t.Fatalf("layer 225 config = dc:%d remaining:%d", config.ThisDC, exact.Len())
	}
}

func TestInboundRPCQueueFullReturnsWorkerBusy(t *testing.T) {
	const dc = 2
	handler := &blockingRPC{
		started: make(chan struct{}, 1),
		release: make(chan struct{}),
	}
	addr, pub, _ := startTestServer(t, Options{
		DC:             dc,
		legacyRPC:      handler,
		RPCMaxInflight: 1,
		RPCQueueSize:   1,
		RPCTimeout:     5 * time.Second,
	})
	conn, auth, cipher := dialHandshake(t, addr, dc, pub)

	clientMsgID := proto.NewMessageIDGen(time.Now)
	firstReqID := clientMsgID.New(proto.MessageFromClient)
	sendEncryptedWithSeq(t, conn, cipher, auth, firstReqID, 1, &tg.HelpGetConfigRequest{})
	select {
	case <-handler.started:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for first rpc to start")
	}

	secondReqID := clientMsgID.New(proto.MessageFromClient)
	sendEncryptedWithSeq(t, conn, cipher, auth, secondReqID, 3, &tg.HelpGetConfigRequest{})
	thirdReqID := clientMsgID.New(proto.MessageFromClient)
	sendEncryptedWithSeq(t, conn, cipher, auth, thirdReqID, 5, &tg.HelpGetConfigRequest{})

	result := readRPCResultForRequest(t, conn, cipher, auth.AuthKey, thirdReqID)
	var rpcErr mt.RPCError
	if err := rpcErr.Decode(&bin.Buffer{Buf: result.Result}); err != nil {
		t.Fatalf("decode rpc_error: %v", err)
	}
	if rpcErr.ErrorCode != rpcWorkerBusyErrorCode || rpcErr.ErrorMessage != rpcWorkerBusyErrorMessage {
		t.Fatalf("rpc_error = %d %q, want %d %s", rpcErr.ErrorCode, rpcErr.ErrorMessage, rpcWorkerBusyErrorCode, rpcWorkerBusyErrorMessage)
	}
	close(handler.release)
}

func TestInboundRPCQueuedDeadlineReturnsRPCTimeout(t *testing.T) {
	const dc = 2
	handler := &queueDeadlineRPC{
		firstStarted: make(chan struct{}),
		releaseFirst: make(chan struct{}),
	}
	addr, pub, _ := startTestServer(t, Options{
		DC:               dc,
		legacyRPC:        handler,
		RPCMaxInflight:   1,
		RPCQueueSize:     2,
		RPCTimeout:       60 * time.Millisecond,
		RPCGlobalWorkers: 1,
	})
	conn, auth, cipher := dialHandshake(t, addr, dc, pub)

	clientMsgID := proto.NewMessageIDGen(time.Now)
	firstReqID := clientMsgID.New(proto.MessageFromClient)
	sendEncryptedWithSeq(t, conn, cipher, auth, firstReqID, 1, &tg.HelpGetConfigRequest{})
	select {
	case <-handler.firstStarted:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for first rpc to start")
	}

	secondReqID := clientMsgID.New(proto.MessageFromClient)
	sendEncryptedWithSeq(t, conn, cipher, auth, secondReqID, 3, &tg.HelpGetConfigRequest{})
	// The first deliberately ignores its context so the second only gets dequeued after exceeding its own deadline counted from enqueue.
	time.Sleep(120 * time.Millisecond)
	close(handler.releaseFirst)

	result := readRPCResultForRequest(t, conn, cipher, auth.AuthKey, secondReqID)
	var rpcErr mt.RPCError
	if err := rpcErr.Decode(&bin.Buffer{Buf: result.Result}); err != nil {
		t.Fatalf("decode rpc timeout: %v", err)
	}
	if rpcErr.ErrorCode != 500 || rpcErr.ErrorMessage != "RPC_TIMEOUT" {
		t.Fatalf("rpc_error = %d %q, want 500 RPC_TIMEOUT", rpcErr.ErrorCode, rpcErr.ErrorMessage)
	}
	if calls := handler.calls.Load(); calls != 1 {
		t.Fatalf("handler calls = %d, want 1 (expired queued RPC must not dispatch)", calls)
	}
}

func TestInboundRPCRunningDeadlineWaitsForHandlerTerminalResult(t *testing.T) {
	for _, tc := range []struct {
		name         string
		honorContext bool
	}{
		{name: "handler_honors_context", honorContext: true},
		{name: "handler_temporarily_ignores_context", honorContext: false},
	} {
		t.Run(tc.name, func(t *testing.T) {
			const dc = 2
			handler := &runningDeadlineRPC{
				started:      make(chan struct{}),
				release:      make(chan struct{}),
				honorContext: tc.honorContext,
			}
			addr, pub, _ := startTestServer(t, Options{
				DC:               dc,
				legacyRPC:        handler,
				RPCMaxInflight:   1,
				RPCQueueSize:     1,
				RPCTimeout:       60 * time.Millisecond,
				RPCGlobalWorkers: 1,
			})
			conn, auth, cipher := dialHandshake(t, addr, dc, pub)

			clientMsgID := proto.NewMessageIDGen(time.Now)
			reqID := clientMsgID.New(proto.MessageFromClient)
			sendEncryptedWithSeq(t, conn, cipher, auth, reqID, 1, &tg.HelpGetConfigRequest{})
			select {
			case <-handler.started:
			case <-time.After(time.Second):
				t.Fatal("timed out waiting for running rpc")
			}

			if !tc.honorContext {
				// Let the deadline expire while Dispatch is still running. No early timeout
				// may win; after the handler reports committed success, that success is the
				// sole terminal result.
				time.Sleep(120 * time.Millisecond)
				close(handler.release)
			}
			result := readRPCResultForRequest(t, conn, cipher, auth.AuthKey, reqID)
			if tc.honorContext {
				var rpcErr mt.RPCError
				if err := rpcErr.Decode(&bin.Buffer{Buf: result.Result}); err != nil {
					t.Fatalf("decode converged rpc timeout: %v", err)
				}
				if rpcErr.ErrorCode != 500 || rpcErr.ErrorMessage != "RPC_TIMEOUT" {
					t.Fatalf("rpc_error = %d %q, want 500 RPC_TIMEOUT", rpcErr.ErrorCode, rpcErr.ErrorMessage)
				}
				close(handler.release)
			} else {
				var config tg.Config
				if err := config.Decode(&bin.Buffer{Buf: result.Result}); err != nil {
					t.Fatalf("decode committed success after deadline: %v", err)
				}
			}
		})
	}
}

func TestDuplicateRPCResultAcrossReconnectUsesSessionCache(t *testing.T) {
	const dc = 2
	handler := &countingConfigRPC{}
	addr, pub, _ := startTestServer(t, Options{DC: dc, legacyRPC: handler})
	conn, auth, cipher := dialHandshake(t, addr, dc, pub)

	clientMsgID := proto.NewMessageIDGen(time.Now)
	reqMsgID := clientMsgID.New(proto.MessageFromClient)
	sendEncrypted(t, conn, cipher, auth, reqMsgID, &tg.HelpGetConfigRequest{})

	first := readRPCResultForRequest(t, conn, cipher, auth.AuthKey, reqMsgID)
	if calls := handler.calls.Load(); calls != 1 {
		t.Fatalf("handler calls after first request = %d, want 1", calls)
	}
	var firstConfig tg.Config
	if err := firstConfig.Decode(&bin.Buffer{Buf: first.Result}); err != nil {
		t.Fatalf("decode first config: %v", err)
	}
	if firstConfig.ThisDC != dc {
		t.Fatalf("first config.ThisDC = %d, want %d", firstConfig.ThisDC, dc)
	}

	_ = conn.Close()
	replayConn := dialTransportOnly(t, addr)
	sendEncrypted(t, replayConn, cipher, auth, reqMsgID, &tg.HelpGetConfigRequest{})

	second := readRPCResultForRequest(t, replayConn, cipher, auth.AuthKey, reqMsgID)
	if calls := handler.calls.Load(); calls != 1 {
		t.Fatalf("handler calls after replay = %d, want 1", calls)
	}
	if string(second.Result) != string(first.Result) {
		t.Fatalf("replayed rpc_result payload changed")
	}
}

func TestCanceledRPCErrorIsNotCachedAcrossReconnect(t *testing.T) {
	const dc = 2
	handler := &canceledInternalRPC{
		firstStarted: make(chan struct{}),
		firstDone:    make(chan struct{}),
	}
	addr, pub, _ := startTestServer(t, Options{DC: dc, legacyRPC: handler})
	conn, auth, cipher := dialHandshake(t, addr, dc, pub)

	clientMsgID := proto.NewMessageIDGen(time.Now)
	reqMsgID := clientMsgID.New(proto.MessageFromClient)
	sendEncrypted(t, conn, cipher, auth, reqMsgID, &tg.HelpGetConfigRequest{})

	select {
	case <-handler.firstStarted:
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for first rpc to start after session barrier")
	}
	_ = conn.Close()
	select {
	case <-handler.firstDone:
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for canceled first rpc")
	}

	replayConn := dialTransportOnly(t, addr)
	sendEncrypted(t, replayConn, cipher, auth, reqMsgID, &tg.HelpGetConfigRequest{})

	result := readRPCResultForRequest(t, replayConn, cipher, auth.AuthKey, reqMsgID)
	var cfg tg.Config
	if err := cfg.Decode(&bin.Buffer{Buf: result.Result}); err != nil {
		t.Fatalf("decode replay config: %v", err)
	}
	if cfg.ThisDC != dc {
		t.Fatalf("replay config.ThisDC = %d, want %d", cfg.ThisDC, dc)
	}
	if calls := handler.calls.Load(); calls != 2 {
		t.Fatalf("handler calls = %d, want 2 (canceled first result must not be cached)", calls)
	}
}

type countingConfigRPC struct {
	calls atomic.Int32
}

func (h *countingConfigRPC) Dispatch(context.Context, [8]byte, int64, *bin.Buffer) (bin.Encoder, error) {
	h.calls.Add(1)
	return &tg.Config{ThisDC: 2}, nil
}

func (h *countingConfigRPC) NegotiatedLayer([8]byte, int64) (int, bool) { return 227, true }

type blockingRPC struct {
	started chan struct{}
	release chan struct{}
}

func (h *blockingRPC) Dispatch(ctx context.Context, _ [8]byte, _ int64, _ *bin.Buffer) (bin.Encoder, error) {
	select {
	case h.started <- struct{}{}:
	default:
	}
	select {
	case <-h.release:
		return &tg.Config{ThisDC: 2}, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

func (h *blockingRPC) NegotiatedLayer([8]byte, int64) (int, bool) { return 227, true }

type queueDeadlineRPC struct {
	calls        atomic.Int32
	firstStarted chan struct{}
	releaseFirst chan struct{}
}

func (h *queueDeadlineRPC) Dispatch(context.Context, [8]byte, int64, *bin.Buffer) (bin.Encoder, error) {
	if h.calls.Add(1) == 1 {
		close(h.firstStarted)
		<-h.releaseFirst
	}
	return &tg.Config{ThisDC: 2}, nil
}

func (h *queueDeadlineRPC) NegotiatedLayer([8]byte, int64) (int, bool) { return 227, true }

type runningDeadlineRPC struct {
	started      chan struct{}
	release      chan struct{}
	honorContext bool
}

func (h *runningDeadlineRPC) Dispatch(ctx context.Context, _ [8]byte, _ int64, _ *bin.Buffer) (bin.Encoder, error) {
	close(h.started)
	if h.honorContext {
		<-ctx.Done()
		return nil, ctx.Err()
	}
	<-h.release
	return &tg.Config{ThisDC: 2}, nil
}

func (h *runningDeadlineRPC) NegotiatedLayer([8]byte, int64) (int, bool) { return 227, true }

type canceledInternalRPC struct {
	calls        atomic.Int32
	firstStarted chan struct{}
	firstDone    chan struct{}
}

func (h *canceledInternalRPC) Dispatch(ctx context.Context, _ [8]byte, _ int64, _ *bin.Buffer) (bin.Encoder, error) {
	if h.calls.Add(1) == 1 {
		close(h.firstStarted)
		<-ctx.Done()
		close(h.firstDone)
		return nil, tgerr.New(500, "INTERNAL_SERVER_ERROR")
	}
	return &tg.Config{ThisDC: 2}, nil
}

func (h *canceledInternalRPC) NegotiatedLayer([8]byte, int64) (int, bool) { return 227, true }

func readRPCResultForRequest(t *testing.T, conn transport.Conn, cipher crypto.Cipher, key crypto.AuthKey, reqMsgID int64) proto.Result {
	t.Helper()
	for i := 0; i < 12; i++ {
		_, id, plain := readServerMessage(t, conn, cipher, key)
		if id != proto.ResultTypeID {
			continue
		}
		var result proto.Result
		if err := result.Decode(plain); err != nil {
			t.Fatalf("decode rpc_result: %v", err)
		}
		if result.RequestMessageID == reqMsgID {
			return result
		}
	}
	t.Fatalf("missing rpc_result for req_msg_id %d", reqMsgID)
	return proto.Result{}
}