Ctrl K
ComunidadGuías…

internal/mtprotoedge/helpers_test.go

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

Archivo original ↗Preguntar a KiBot
package mtprotoedge

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"net"
	"testing"
	"time"

	"go.uber.org/zap/zaptest"

	"github.com/gotd/log/logzap"
	"github.com/iamxvbaba/td/bin"
	"github.com/iamxvbaba/td/crypto"
	"github.com/iamxvbaba/td/exchange"
	"github.com/iamxvbaba/td/proto"
	"github.com/iamxvbaba/td/tg"
	"github.com/iamxvbaba/td/tlprofile"
	"github.com/iamxvbaba/td/transport"
)

// legacyCanonicalTestConn explicitly declares the canonical-only profile used
// by old connection-state tests. Production exact-path tests must instead call
// FreezeLayerProfile/SeedLayerProfile with protocol evidence.
func legacyCanonicalTestConn(t testing.TB, c *Conn) *Conn {
	return legacyLayerWireTestConn(t, c, int(tlprofile.ProfileCanonical))
}

// legacyLayerWireTestConn preserves only the old tests' profile setup. It does
// not enable any wire conversion; application values still need an exact
// generated binding at the outbound boundary.
func legacyLayerWireTestConn(t testing.TB, c *Conn, layer int) *Conn {
	t.Helper()
	if c == nil {
		t.Fatal("nil legacy exact-layer test Conn")
	}
	profile, ok := tlprofile.ResolveProfile(layer)
	if !ok {
		t.Fatalf("unsupported generated test Layer %d", layer)
	}
	if err := c.FreezeLayerProfile(profile); err != nil {
		t.Fatalf("freeze generated test Layer %d: %v", layer, err)
	}
	c.setLegacyClientLayer(layer)
	return c
}

// exactTestUpdatesEncoded gives transport/state-machine tests an explicit
// generated session binding without invoking the production fan-out cache.
// Tests which assert wire conversion use layerUpdatesFanout directly instead.
func exactTestUpdatesEncoded(t testing.TB, c *Conn, body []byte) *encodedOutboundMessage {
	t.Helper()
	if c == nil {
		t.Fatal("nil exact test Conn")
	}
	state := c.LayerProfileState()
	if state.Origin == LayerProfileUnknown {
		t.Fatal("exact test Conn has no generated Layer profile")
	}
	return &encodedOutboundMessage{
		body:   append([]byte(nil), body...),
		typeID: tg.UpdatesTooLongTypeID,
		layer: &outboundLayerBinding{
			profile: state.Profile,
			epoch:   state.Epoch,
		},
	}
}

func exactTestUpdatesTooLong(t testing.TB, c *Conn) *encodedOutboundMessage {
	t.Helper()
	var body bin.Buffer
	if err := (&tg.UpdatesTooLong{}).Encode(&body); err != nil {
		t.Fatalf("encode exact test updatesTooLong: %v", err)
	}
	return exactTestUpdatesEncoded(t, c, body.Raw())
}

// opaqueExactTestRPCResult is an explicit request-bound capability for tests
// of compression, retention and delivery mechanics. Semantic result conversion
// is covered by generated dispatcher tests; no production path constructs it.
type opaqueExactTestRPCResult struct{ result bin.Encoder }

func (r *opaqueExactTestRPCResult) Encode(b *bin.Buffer) error { return r.result.Encode(b) }

func (r *opaqueExactTestRPCResult) exactLayerRPCResultBinding() outboundLayerBinding {
	return outboundLayerBinding{
		profile: tlprofile.ProfileCanonical,
		kind:    outboundLayerBindingRequest,
	}
}

func exactTestRPCResult(result bin.Encoder) bin.Encoder {
	if result == nil || isLayerInvariantRPCResultEncoder(result) {
		return result
	}
	if _, ok := result.(exactLayerRPCResultEncoder); ok {
		return result
	}
	return &opaqueExactTestRPCResult{result: result}
}

// startTestServer generates an RSA key, listens on a random port and starts a
// Server, returning the listen address and public key. It automatically cancels
// via t.Cleanup and checks graceful shutdown. opts's RSAKey/Logger/DC are filled
// in with defaults.
func startTestServer(t *testing.T, opts Options) (addr string, pub exchange.PublicKey, srv *Server) {
	t.Helper()
	rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("gen rsa: %v", err)
	}
	opts.RSAKey = rsaKey
	if opts.Logger == nil {
		opts.Logger = zaptest.NewLogger(t)
	}
	if opts.DC == 0 {
		opts.DC = 2
	}

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}

	srv = New(opts)
	ctx, cancel := context.WithCancel(context.Background())
	serveErr := make(chan error, 1)
	go func() { serveErr <- srv.Serve(ctx, ln) }()
	t.Cleanup(func() {
		cancel()
		select {
		case err := <-serveErr:
			if err != nil {
				t.Errorf("serve: %v", err)
			}
		case <-time.After(5 * time.Second):
			t.Error("server did not stop after ctx cancel")
		}
	})

	return ln.Addr().String(), exchange.PublicKey{RSA: &rsaKey.PublicKey}, srv
}

// dialHandshake establishes a TCP connection, completes the intermediate
// negotiation and the MTProto key exchange, and returns the connection, the
// handshake result and the client-side cipher. The connection is closed
// automatically via t.Cleanup.
func dialHandshake(t *testing.T, addr string, dc int, pub exchange.PublicKey) (transport.Conn, exchange.ClientExchangeResult, crypto.Cipher) {
	t.Helper()
	conn := dialTransportOnly(t, addr)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	auth, err := exchange.NewExchanger(conn, dc).
		WithRand(rand.Reader).
		WithLogger(logzap.New(zaptest.NewLogger(t).Named("client"))).
		Client([]exchange.PublicKey{pub}).
		Run(ctx)
	if err != nil {
		t.Fatalf("client exchange: %v", err)
	}
	return conn, auth, crypto.NewClientCipher(rand.Reader)
}

func dialTransportOnly(t *testing.T, addr string) transport.Conn {
	t.Helper()
	raw, err := net.Dial("tcp", addr)
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	conn, err := transport.Intermediate.Handshake(raw)
	if err != nil {
		_ = raw.Close()
		t.Fatalf("transport handshake: %v", err)
	}
	t.Cleanup(func() { _ = conn.Close() })
	return conn
}

// freezeActiveTestSessionProfile gives low-level transport fixtures the exact
// profile that a production invokeWithLayer admission would have proven. It is
// intentionally explicit: handshake/new_session_created alone never implies a
// TL Layer, and production push code must keep failing closed in that state.
func freezeActiveTestSessionProfile(t *testing.T, sessions *SessionManager, authKeyID [8]byte, sessionID int64, profile tlprofile.Profile) {
	t.Helper()
	if sessions == nil {
		t.Fatal("freeze test session profile on nil SessionManager")
	}
	key := sessionKey{authKeyID: authKeyID, sessionID: sessionID}
	sessions.mu.RLock()
	c := sessions.bySession[key]
	sessions.mu.RUnlock()
	if c == nil {
		t.Fatalf("active test session %x/%d is missing", authKeyID, sessionID)
	}
	if err := c.FreezeLayerProfile(profile); err != nil {
		t.Fatalf("freeze active test session profile %d: %v", profile, err)
	}
}

// sendEncrypted encrypts with the client cipher and sends a message with msgID.
func sendEncrypted(t *testing.T, conn transport.Conn, cipher crypto.Cipher, auth exchange.ClientExchangeResult, msgID int64, msg bin.Encoder) {
	t.Helper()
	sendEncryptedWithSalt(t, conn, cipher, auth, auth.ServerSalt, msgID, msg)
}

// sendEncryptedWithSalt encrypts with the given salt and sends a message.
func sendEncryptedWithSalt(t *testing.T, conn transport.Conn, cipher crypto.Cipher, auth exchange.ClientExchangeResult, salt, msgID int64, msg bin.Encoder) {
	t.Helper()
	body, seqNo := encodeClientMessageForTest(t, msg)
	sendEncryptedWithSaltAndSeq(t, conn, cipher, auth, salt, msgID, seqNo, body)
}

func sendEncryptedWithSeq(t *testing.T, conn transport.Conn, cipher crypto.Cipher, auth exchange.ClientExchangeResult, msgID int64, seqNo int32, msg bin.Encoder) {
	t.Helper()
	body := encodeClientMessageBodyForTest(t, msg)
	sendEncryptedWithSaltAndSeq(t, conn, cipher, auth, auth.ServerSalt, msgID, seqNo, body)
}

func sendEncryptedWithSaltAndSeq(t *testing.T, conn transport.Conn, cipher crypto.Cipher, auth exchange.ClientExchangeResult, salt, msgID int64, seqNo int32, body []byte) {
	t.Helper()
	sendEncryptedWithSessionSaltAndSeq(t, conn, cipher, auth, auth.SessionID, salt, msgID, seqNo, body)
}

func sendEncryptedWithSessionSaltAndSeq(t *testing.T, conn transport.Conn, cipher crypto.Cipher, auth exchange.ClientExchangeResult, sessionID, salt, msgID int64, seqNo int32, body []byte) {
	t.Helper()
	var buf bin.Buffer
	if err := cipher.Encrypt(auth.AuthKey, crypto.EncryptedMessageData{
		Salt:                   salt,
		SessionID:              sessionID,
		MessageID:              msgID,
		SeqNo:                  seqNo,
		MessageDataLen:         int32(len(body)),
		MessageDataWithPadding: body,
	}, &buf); err != nil {
		t.Fatalf("encrypt: %v", err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := conn.Send(ctx, &buf); err != nil {
		t.Fatalf("send: %v", err)
	}
}

func encodeClientMessageForTest(t *testing.T, msg bin.Encoder) ([]byte, int32) {
	t.Helper()
	raw := encodeClientMessageBodyForTest(t, msg)
	typeID, err := (&bin.Buffer{Buf: raw}).PeekID()
	if err != nil {
		t.Fatalf("peek encrypted message type: %v", err)
	}
	if container, ok := msg.(*proto.MessageContainer); ok {
		return raw, clientContainerSeqNoForTest(container)
	}
	if clientMessageContentPolicyFor(typeID) == clientMessageContentRequired {
		return raw, 1
	}
	return raw, 0
}

func encodeClientMessageBodyForTest(t *testing.T, msg bin.Encoder) []byte {
	t.Helper()
	var body bin.Buffer
	if err := msg.Encode(&body); err != nil {
		t.Fatalf("encode encrypted message: %v", err)
	}
	return body.Copy()
}

func clientContainerSeqNoForTest(container *proto.MessageContainer) int32 {
	var maxSeq int32
	for _, msg := range container.Messages {
		if seq := int32(msg.SeqNo); seq > maxSeq {
			maxSeq = seq
		}
	}
	if maxSeq%2 != 0 {
		maxSeq++
	}
	return maxSeq
}

// collectReplies reads and decrypts messages the server sends back, collecting
// plaintext buffers by TypeID, until wantID is seen (inclusive) or the cap is
// reached. Used to assert the multiple responses triggered by one request
// (new_session_created / business response / msgs_ack).
func collectReplies(t *testing.T, conn transport.Conn, cipher crypto.Cipher, key crypto.AuthKey, wantID uint32) map[uint32]*bin.Buffer {
	t.Helper()
	got := make(map[uint32]*bin.Buffer)
	for i := 0; i < 8; i++ {
		_, id, plain := readServerMessage(t, conn, cipher, key)
		got[id] = plain
		if id == wantID {
			break
		}
	}
	return got
}

// serverReplyFrame preserves the wire order and encrypted envelope of a server
// reply. Tests which exercise session boundaries must not collapse replies into
// a TypeID-keyed map: both duplicate response types and their order are part of
// the observable protocol behavior.
type serverReplyFrame struct {
	Message *crypto.EncryptedMessageData
	TypeID  uint32
	Plain   *bin.Buffer
}

// collectReplyFrames reads ordered server replies until every requested TypeID
// has been observed the requested number of times. Unrequested frames are kept
// in the returned slice so callers can assert ordering around control messages.
func collectReplyFrames(
	t *testing.T,
	conn transport.Conn,
	cipher crypto.Cipher,
	key crypto.AuthKey,
	wantCounts map[uint32]int,
) []serverReplyFrame {
	t.Helper()

	remaining := make(map[uint32]int, len(wantCounts))
	required := 0
	for typeID, count := range wantCounts {
		if count <= 0 {
			continue
		}
		remaining[typeID] = count
		required += count
	}
	if required == 0 {
		return nil
	}

	// Keep the helper bounded while allowing unrelated control replies (notably
	// msgs_ack) to be interleaved with the frames under test.
	// One shared deadline bounds the whole collection. A per-frame deadline would
	// multiply a missing-result failure by the maximum number of unrelated frames.
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()
	frames := make([]serverReplyFrame, 0, required)
	for i := 0; i < required+16; i++ {
		message, typeID, plain := readServerMessageContext(t, ctx, conn, cipher, key)
		frames = append(frames, serverReplyFrame{
			Message: message,
			TypeID:  typeID,
			Plain:   plain,
		})
		if count, ok := remaining[typeID]; ok {
			if count == 1 {
				delete(remaining, typeID)
			} else {
				remaining[typeID] = count - 1
			}
		}
		if len(remaining) == 0 {
			return frames
		}
	}

	t.Fatalf("missing reply counts after %d frames: %+v", len(frames), remaining)
	return nil
}

func readServerMessage(t *testing.T, conn transport.Conn, cipher crypto.Cipher, key crypto.AuthKey) (*crypto.EncryptedMessageData, uint32, *bin.Buffer) {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	return readServerMessageContext(t, ctx, conn, cipher, key)
}

func readServerMessageContext(t *testing.T, ctx context.Context, conn transport.Conn, cipher crypto.Cipher, key crypto.AuthKey) (*crypto.EncryptedMessageData, uint32, *bin.Buffer) {
	t.Helper()
	var buf bin.Buffer
	err := conn.Recv(ctx, &buf)
	if err != nil {
		t.Fatalf("recv server message: %v", err)
	}
	data, err := cipher.DecryptFromBuffer(key, &buf)
	if err != nil {
		t.Fatalf("decrypt server message: %v", err)
	}
	plain := append([]byte(nil), data.Data()...)
	id, err := (&bin.Buffer{Buf: plain}).PeekID()
	if err != nil {
		t.Fatalf("peek server message: %v", err)
	}
	return data, id, &bin.Buffer{Buf: plain}
}

// mustHave asserts replies contains a message of the given TypeID and returns its buffer.
func mustHave(t *testing.T, replies map[uint32]*bin.Buffer, id uint32, name string) *bin.Buffer {
	t.Helper()
	b, ok := replies[id]
	if !ok {
		t.Fatalf("missing %s (%#x)", name, id)
	}
	return b
}