go-p2p/node/integration_test.go
Claude bf4ea4b215
test: add Phase 5 integration tests, benchmarks, and bufpool tests
- Full integration test: two nodes on localhost exercising identity
  creation, handshake, encrypted message exchange, UEPS packet routing
  via dispatcher, and graceful shutdown
- Benchmarks: identity key generation, ECDH shared secret derivation,
  KD-tree peer scoring (10/100/1000 peers), message serialisation,
  SMSG encrypt/decrypt, challenge-response, UEPS marshal/unmarshal
- bufpool.go tests: buffer reuse verification, oversized buffer
  discard, concurrent access, MarshalJSON correctness and safety
- Coverage: node/ 72.1% -> 87.5%, ueps/ 88.5% -> 93.1%

Co-Authored-By: Charon <developers@lethean.io>
2026-02-20 11:53:26 +00:00

682 lines
23 KiB
Go

package node
import (
"bufio"
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"time"
"forge.lthn.ai/core/go-p2p/ueps"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ============================================================================
// Full Integration Test — Phase 5
//
// Exercises the complete node lifecycle on localhost:
// 1. Identity creation for two nodes
// 2. WebSocket handshake with challenge-response authentication
// 3. Encrypted message exchange (ping/pong, stats)
// 4. UEPS packet routing via the Dispatcher
// 5. Graceful shutdown with disconnect messages
// ============================================================================
func TestIntegration_FullNodeLifecycle(t *testing.T) {
// ----------------------------------------------------------------
// Step 1: Identity creation
// ----------------------------------------------------------------
controllerNM := testNode(t, "integration-controller", RoleController)
workerNM := testNode(t, "integration-worker", RoleWorker)
controllerIdentity := controllerNM.GetIdentity()
workerIdentity := workerNM.GetIdentity()
require.NotNil(t, controllerIdentity, "controller identity should be initialised")
require.NotNil(t, workerIdentity, "worker identity should be initialised")
assert.NotEmpty(t, controllerIdentity.ID, "controller ID should be non-empty")
assert.NotEmpty(t, workerIdentity.ID, "worker ID should be non-empty")
assert.NotEqual(t, controllerIdentity.ID, workerIdentity.ID,
"two independently generated identities must differ")
assert.Equal(t, RoleController, controllerIdentity.Role)
assert.Equal(t, RoleWorker, workerIdentity.Role)
// ----------------------------------------------------------------
// Step 2: Set up transports, registries, worker, and controller
// ----------------------------------------------------------------
workerReg := testRegistry(t)
controllerReg := testRegistry(t)
workerCfg := DefaultTransportConfig()
workerCfg.PingInterval = 2 * time.Second
workerCfg.PongTimeout = 2 * time.Second
controllerCfg := DefaultTransportConfig()
controllerCfg.PingInterval = 2 * time.Second
controllerCfg.PongTimeout = 2 * time.Second
workerTransport := NewTransport(workerNM, workerReg, workerCfg)
controllerTransport := NewTransport(controllerNM, controllerReg, controllerCfg)
// Register a Worker on the server side.
worker := NewWorker(workerNM, workerTransport)
worker.SetMinerManager(&mockMinerManagerFull{
miners: map[string]*mockMinerFull{
"integration-miner": {
name: "integration-miner",
minerType: "xmrig",
stats: map[string]interface{}{
"hashrate": 5000.0,
"shares": 250,
},
consoleHistory: []string{
"[2026-02-20 12:00:00] miner started",
},
},
},
})
worker.RegisterWithTransport()
// Start the worker transport behind httptest.
mux := http.NewServeMux()
mux.HandleFunc(workerCfg.WSPath, workerTransport.handleWSUpgrade)
ts := httptest.NewServer(mux)
t.Cleanup(func() {
controllerTransport.Stop()
workerTransport.Stop()
ts.Close()
})
u, _ := url.Parse(ts.URL)
workerAddr := u.Host
// Register the worker peer in the controller's registry.
workerPeer := &Peer{
ID: workerIdentity.ID,
Name: "integration-worker",
Address: workerAddr,
Role: RoleWorker,
}
require.NoError(t, controllerReg.AddPeer(workerPeer))
// Create the controller (registers handleResponse on the transport).
controller := NewController(controllerNM, controllerReg, controllerTransport)
// ----------------------------------------------------------------
// Step 3: WebSocket handshake (challenge-response)
// ----------------------------------------------------------------
pc, err := controllerTransport.Connect(workerPeer)
require.NoError(t, err, "handshake should succeed")
require.NotNil(t, pc, "peer connection should be returned")
assert.NotEmpty(t, pc.SharedSecret, "shared secret should be derived after handshake")
// Allow server-side goroutines to register the connection.
time.Sleep(100 * time.Millisecond)
assert.Equal(t, 1, controllerTransport.ConnectedPeers(),
"controller should have 1 connected peer")
assert.Equal(t, 1, workerTransport.ConnectedPeers(),
"worker should have 1 connected peer")
// Verify the peer's real identity is stored.
serverPeerID := workerNM.GetIdentity().ID
conn := controllerTransport.GetConnection(serverPeerID)
require.NotNil(t, conn, "controller should hold a connection keyed by server's real ID")
assert.Equal(t, "integration-worker", conn.Peer.Name)
// ----------------------------------------------------------------
// Step 4: Encrypted message exchange — Ping/Pong
// ----------------------------------------------------------------
rtt, err := controller.PingPeer(serverPeerID)
require.NoError(t, err, "PingPeer should succeed")
assert.Greater(t, rtt, 0.0, "RTT should be positive")
assert.Less(t, rtt, 1000.0, "RTT on loopback should be well under 1s")
// Verify registry metrics were updated.
peerAfterPing := controllerReg.GetPeer(serverPeerID)
require.NotNil(t, peerAfterPing)
assert.Greater(t, peerAfterPing.PingMS, 0.0, "PingMS should be updated")
// ----------------------------------------------------------------
// Step 5: Encrypted message exchange — GetRemoteStats
// ----------------------------------------------------------------
stats, err := controller.GetRemoteStats(serverPeerID)
require.NoError(t, err, "GetRemoteStats should succeed")
require.NotNil(t, stats)
assert.Equal(t, workerIdentity.ID, stats.NodeID)
assert.Equal(t, "integration-worker", stats.NodeName)
assert.Len(t, stats.Miners, 1, "worker should report 1 miner")
assert.Equal(t, "integration-miner", stats.Miners[0].Name)
assert.Equal(t, 5000.0, stats.Miners[0].Hashrate)
// ----------------------------------------------------------------
// Step 6: UEPS packet routing via the Dispatcher
// ----------------------------------------------------------------
dispatcher := NewDispatcher()
var handshakeReceived, computeReceived atomic.Int32
dispatcher.RegisterHandler(IntentHandshake, func(pkt *ueps.ParsedPacket) error {
handshakeReceived.Add(1)
return nil
})
dispatcher.RegisterHandler(IntentCompute, func(pkt *ueps.ParsedPacket) error {
computeReceived.Add(1)
return nil
})
// Build UEPS packets, sign them with the shared secret, parse and dispatch.
sharedSecret := pc.SharedSecret
// 6a. Handshake intent.
pb := ueps.NewBuilder(IntentHandshake, []byte("hello-from-controller"))
wireData, err := pb.MarshalAndSign(sharedSecret)
require.NoError(t, err, "MarshalAndSign should succeed")
parsed, err := ueps.ReadAndVerify(bufio.NewReader(bytes.NewReader(wireData)), sharedSecret)
require.NoError(t, err, "ReadAndVerify should succeed")
require.NoError(t, dispatcher.Dispatch(parsed), "dispatch handshake should succeed")
assert.Equal(t, int32(1), handshakeReceived.Load())
// 6b. Compute intent.
pb2 := ueps.NewBuilder(IntentCompute, []byte(`{"job":"mine-block-42"}`))
wireData2, err := pb2.MarshalAndSign(sharedSecret)
require.NoError(t, err)
parsed2, err := ueps.ReadAndVerify(bufio.NewReader(bytes.NewReader(wireData2)), sharedSecret)
require.NoError(t, err)
require.NoError(t, dispatcher.Dispatch(parsed2))
assert.Equal(t, int32(1), computeReceived.Load())
// 6c. High-threat packet should be rejected by the circuit breaker.
pb3 := ueps.NewBuilder(IntentCompute, []byte("hostile"))
pb3.Header.ThreatScore = ThreatScoreThreshold + 1
wireData3, err := pb3.MarshalAndSign(sharedSecret)
require.NoError(t, err)
parsed3, err := ueps.ReadAndVerify(bufio.NewReader(bytes.NewReader(wireData3)), sharedSecret)
require.NoError(t, err)
err = dispatcher.Dispatch(parsed3)
assert.ErrorIs(t, err, ErrThreatScoreExceeded,
"high-threat packet should be dropped by circuit breaker")
// Compute handler should NOT have been called again.
assert.Equal(t, int32(1), computeReceived.Load())
// ----------------------------------------------------------------
// Step 7: Graceful shutdown
// ----------------------------------------------------------------
disconnectReceived := make(chan *Message, 1)
workerTransport.OnMessage(func(conn *PeerConnection, msg *Message) {
if msg.Type == MsgDisconnect {
disconnectReceived <- msg
}
})
// Gracefully close from the controller side.
pc.GracefulClose("integration test complete", DisconnectNormal)
select {
case msg := <-disconnectReceived:
assert.Equal(t, MsgDisconnect, msg.Type)
var payload DisconnectPayload
require.NoError(t, msg.ParsePayload(&payload))
assert.Equal(t, "integration test complete", payload.Reason)
assert.Equal(t, DisconnectNormal, payload.Code)
case <-time.After(3 * time.Second):
t.Error("timeout waiting for disconnect message on the worker side")
}
// Allow cleanup to propagate.
time.Sleep(200 * time.Millisecond)
// After graceful close, the controller should have 0 peers.
assert.Equal(t, 0, controllerTransport.ConnectedPeers(),
"controller should have 0 peers after graceful close")
}
// TestIntegration_SharedSecretAgreement verifies that two independently created
// nodes derive the same shared secret via ECDH.
func TestIntegration_SharedSecretAgreement(t *testing.T) {
nodeA := testNode(t, "secret-node-a", RoleDual)
nodeB := testNode(t, "secret-node-b", RoleDual)
pubKeyA := nodeA.GetIdentity().PublicKey
pubKeyB := nodeB.GetIdentity().PublicKey
secretFromA, err := nodeA.DeriveSharedSecret(pubKeyB)
require.NoError(t, err)
secretFromB, err := nodeB.DeriveSharedSecret(pubKeyA)
require.NoError(t, err)
assert.Equal(t, secretFromA, secretFromB,
"both nodes should derive identical shared secrets via ECDH")
assert.Equal(t, 32, len(secretFromA), "shared secret should be 32 bytes")
}
// TestIntegration_TwoNodeBidirectionalMessages verifies that both nodes
// can send and receive encrypted messages after the handshake.
func TestIntegration_TwoNodeBidirectionalMessages(t *testing.T) {
controller, _, tp := setupControllerPair(t)
serverID := tp.ServerNode.GetIdentity().ID
// Controller -> Worker: Ping
rtt, err := controller.PingPeer(serverID)
require.NoError(t, err)
assert.Greater(t, rtt, 0.0)
// Controller -> Worker: GetStats
stats, err := controller.GetRemoteStats(serverID)
require.NoError(t, err)
require.NotNil(t, stats)
assert.NotEmpty(t, stats.NodeID)
// Verify multiple sequential round-trips work.
for i := 0; i < 5; i++ {
rtt, err := controller.PingPeer(serverID)
require.NoError(t, err, "sequential ping %d should succeed", i)
assert.Greater(t, rtt, 0.0)
}
}
// TestIntegration_MultiPeerTopology verifies that a controller can
// simultaneously communicate with multiple workers.
func TestIntegration_MultiPeerTopology(t *testing.T) {
controllerNM := testNode(t, "multi-controller", RoleController)
controllerReg := testRegistry(t)
controllerTransport := NewTransport(controllerNM, controllerReg, DefaultTransportConfig())
t.Cleanup(func() { controllerTransport.Stop() })
const numWorkers = 3
workerIDs := make([]string, numWorkers)
for i := 0; i < numWorkers; i++ {
nm, addr, _ := makeWorkerServer(t)
wID := nm.GetIdentity().ID
workerIDs[i] = wID
peer := &Peer{
ID: wID,
Name: "multi-worker",
Address: addr,
Role: RoleWorker,
}
require.NoError(t, controllerReg.AddPeer(peer))
_, err := controllerTransport.Connect(peer)
require.NoError(t, err, "connecting to worker %d should succeed", i)
}
time.Sleep(100 * time.Millisecond)
assert.Equal(t, numWorkers, controllerTransport.ConnectedPeers(),
"controller should be connected to all workers")
controller := NewController(controllerNM, controllerReg, controllerTransport)
// Ping all workers concurrently.
var wg sync.WaitGroup
results := make([]float64, numWorkers)
errs := make([]error, numWorkers)
for i, wID := range workerIDs {
wg.Add(1)
go func(idx int, peerID string) {
defer wg.Done()
results[idx], errs[idx] = controller.PingPeer(peerID)
}(i, wID)
}
wg.Wait()
for i := range numWorkers {
require.NoError(t, errs[i], "ping to worker %d should succeed", i)
assert.Greater(t, results[i], 0.0, "RTT for worker %d should be positive", i)
}
// Fetch stats from all workers in parallel.
allStats := controller.GetAllStats()
assert.Len(t, allStats, numWorkers, "should get stats from all workers")
}
// TestIntegration_IdentityPersistenceAndReload verifies that a node identity
// can be generated, persisted, and reloaded from disk.
func TestIntegration_IdentityPersistenceAndReload(t *testing.T) {
dir := t.TempDir()
keyPath := filepath.Join(dir, "private.key")
configPath := filepath.Join(dir, "node.json")
// Create and persist identity.
nm1, err := NewNodeManagerWithPaths(keyPath, configPath)
require.NoError(t, err)
require.NoError(t, nm1.GenerateIdentity("persistent-node", RoleDual))
original := nm1.GetIdentity()
require.NotNil(t, original)
// Reload from disk.
nm2, err := NewNodeManagerWithPaths(keyPath, configPath)
require.NoError(t, err)
require.True(t, nm2.HasIdentity(), "identity should be loaded from disk")
reloaded := nm2.GetIdentity()
require.NotNil(t, reloaded)
assert.Equal(t, original.ID, reloaded.ID, "ID should persist")
assert.Equal(t, original.Name, reloaded.Name, "Name should persist")
assert.Equal(t, original.PublicKey, reloaded.PublicKey, "PublicKey should persist")
assert.Equal(t, original.Role, reloaded.Role, "Role should persist")
// Verify the reloaded key can derive the same shared secret.
kp, err := stmfGenerateKeyPair()
require.NoError(t, err)
secret1, err := nm1.DeriveSharedSecret(kp)
require.NoError(t, err)
secret2, err := nm2.DeriveSharedSecret(kp)
require.NoError(t, err)
assert.Equal(t, secret1, secret2,
"shared secrets derived from original and reloaded keys should match")
}
// stmfGenerateKeyPair is a helper that generates a keypair and returns
// the public key as base64 (for use in DeriveSharedSecret tests).
func stmfGenerateKeyPair() (string, error) {
dir, _ := filepath.Abs("/tmp/stmf-test-" + time.Now().Format("20060102150405.000"))
nm, err := NewNodeManagerWithPaths(
filepath.Join(dir, "private.key"),
filepath.Join(dir, "node.json"),
)
if err != nil {
return "", err
}
if err := nm.GenerateIdentity("temp-peer", RoleWorker); err != nil {
return "", err
}
return nm.GetIdentity().PublicKey, nil
}
// TestIntegration_UEPSFullRoundTrip exercises a complete UEPS packet
// lifecycle: build, sign, transmit (simulated), read, verify, dispatch.
func TestIntegration_UEPSFullRoundTrip(t *testing.T) {
nodeA := testNode(t, "ueps-node-a", RoleController)
nodeB := testNode(t, "ueps-node-b", RoleWorker)
bPubKey := nodeB.GetIdentity().PublicKey
sharedSecret, err := nodeA.DeriveSharedSecret(bPubKey)
require.NoError(t, err, "shared secret derivation should succeed")
require.Len(t, sharedSecret, 32, "shared secret should be 32 bytes (SHA-256)")
// Build and sign a UEPS packet.
payload := []byte(`{"intent":"compute","job_id":"block-99"}`)
pb := ueps.NewBuilder(IntentCompute, payload)
pb.Header.ThreatScore = 100
wireData, err := pb.MarshalAndSign(sharedSecret)
require.NoError(t, err)
require.NotEmpty(t, wireData)
// Node B derives the same shared secret from A's public key.
aPubKey := nodeA.GetIdentity().PublicKey
sharedSecretB, err := nodeB.DeriveSharedSecret(aPubKey)
require.NoError(t, err)
assert.Equal(t, sharedSecret, sharedSecretB,
"both sides should derive the same shared secret via ECDH")
parsed, err := ueps.ReadAndVerify(
bufio.NewReader(bytes.NewReader(wireData)),
sharedSecretB,
)
require.NoError(t, err, "ReadAndVerify should succeed with the matching shared secret")
assert.Equal(t, byte(0x09), parsed.Header.Version)
assert.Equal(t, IntentCompute, parsed.Header.IntentID)
assert.Equal(t, uint16(100), parsed.Header.ThreatScore)
assert.Equal(t, payload, parsed.Payload)
// Dispatch through the dispatcher.
dispatcher := NewDispatcher()
var dispatched bool
dispatcher.RegisterHandler(IntentCompute, func(pkt *ueps.ParsedPacket) error {
dispatched = true
assert.Equal(t, payload, pkt.Payload)
return nil
})
require.NoError(t, dispatcher.Dispatch(parsed))
assert.True(t, dispatched, "handler should have been called")
}
// TestIntegration_UEPSIntegrityFailure verifies that a tampered UEPS packet
// is rejected by HMAC verification.
func TestIntegration_UEPSIntegrityFailure(t *testing.T) {
nodeA := testNode(t, "integrity-a", RoleController)
nodeB := testNode(t, "integrity-b", RoleWorker)
bPubKey := nodeB.GetIdentity().PublicKey
sharedSecret, err := nodeA.DeriveSharedSecret(bPubKey)
require.NoError(t, err)
pb := ueps.NewBuilder(IntentHandshake, []byte("legitimate data"))
wireData, err := pb.MarshalAndSign(sharedSecret)
require.NoError(t, err)
// Tamper with the payload (last bytes).
tampered := make([]byte, len(wireData))
copy(tampered, wireData)
tampered[len(tampered)-1] ^= 0xFF
aPubKey := nodeA.GetIdentity().PublicKey
sharedSecretB, err := nodeB.DeriveSharedSecret(aPubKey)
require.NoError(t, err)
_, err = ueps.ReadAndVerify(
bufio.NewReader(bytes.NewReader(tampered)),
sharedSecretB,
)
assert.Error(t, err, "tampered packet should fail HMAC verification")
assert.Contains(t, err.Error(), "HMAC mismatch")
}
// TestIntegration_AllowlistHandshakeRejection verifies that a peer not in the
// allowlist is rejected during the WebSocket handshake.
func TestIntegration_AllowlistHandshakeRejection(t *testing.T) {
workerNM := testNode(t, "allowlist-worker", RoleWorker)
workerReg := testRegistry(t)
workerReg.SetAuthMode(PeerAuthAllowlist)
workerTransport := NewTransport(workerNM, workerReg, DefaultTransportConfig())
mux := http.NewServeMux()
mux.HandleFunc("/ws", workerTransport.handleWSUpgrade)
ts := httptest.NewServer(mux)
t.Cleanup(func() {
workerTransport.Stop()
ts.Close()
})
u, _ := url.Parse(ts.URL)
controllerNM := testNode(t, "rejected-controller", RoleController)
controllerReg := testRegistry(t)
controllerTransport := NewTransport(controllerNM, controllerReg, DefaultTransportConfig())
t.Cleanup(func() { controllerTransport.Stop() })
peer := &Peer{
ID: workerNM.GetIdentity().ID,
Name: "worker",
Address: u.Host,
Role: RoleWorker,
}
controllerReg.AddPeer(peer)
_, err := controllerTransport.Connect(peer)
require.Error(t, err, "connection should be rejected by allowlist")
assert.Contains(t, err.Error(), "rejected")
}
// TestIntegration_AllowlistHandshakeAccepted verifies that an allowlisted
// peer can connect successfully.
func TestIntegration_AllowlistHandshakeAccepted(t *testing.T) {
workerNM := testNode(t, "allowlist-worker-ok", RoleWorker)
workerReg := testRegistry(t)
workerReg.SetAuthMode(PeerAuthAllowlist)
controllerNM := testNode(t, "allowed-controller", RoleController)
controllerReg := testRegistry(t)
workerReg.AllowPublicKey(controllerNM.GetIdentity().PublicKey)
workerTransport := NewTransport(workerNM, workerReg, DefaultTransportConfig())
worker := NewWorker(workerNM, workerTransport)
worker.RegisterWithTransport()
mux := http.NewServeMux()
mux.HandleFunc("/ws", workerTransport.handleWSUpgrade)
ts := httptest.NewServer(mux)
t.Cleanup(func() {
workerTransport.Stop()
ts.Close()
})
u, _ := url.Parse(ts.URL)
controllerTransport := NewTransport(controllerNM, controllerReg, DefaultTransportConfig())
t.Cleanup(func() { controllerTransport.Stop() })
peer := &Peer{
ID: workerNM.GetIdentity().ID,
Name: "worker",
Address: u.Host,
Role: RoleWorker,
}
controllerReg.AddPeer(peer)
pc, err := controllerTransport.Connect(peer)
require.NoError(t, err, "allowlisted peer should connect successfully")
assert.NotEmpty(t, pc.SharedSecret)
}
// TestIntegration_DispatcherWithRealUEPSPackets builds real UEPS packets
// from wire bytes and routes them through the dispatcher.
func TestIntegration_DispatcherWithRealUEPSPackets(t *testing.T) {
sharedSecret := make([]byte, 32)
for i := range sharedSecret {
sharedSecret[i] = byte(i ^ 0x42)
}
dispatcher := NewDispatcher()
var results sync.Map
intents := []struct {
id byte
name string
payload string
}{
{IntentHandshake, "handshake", "hello"},
{IntentCompute, "compute", `{"job":"123"}`},
{IntentRehab, "rehab", "pause"},
{IntentCustom, "custom", "app-specific-data"},
}
for _, intent := range intents {
intentID := intent.id
dispatcher.RegisterHandler(intentID, func(pkt *ueps.ParsedPacket) error {
results.Store(pkt.Header.IntentID, string(pkt.Payload))
return nil
})
}
for _, intent := range intents {
t.Run(intent.name, func(t *testing.T) {
pb := ueps.NewBuilder(intent.id, []byte(intent.payload))
wireData, err := pb.MarshalAndSign(sharedSecret)
require.NoError(t, err)
parsed, err := ueps.ReadAndVerify(
bufio.NewReader(bytes.NewReader(wireData)),
sharedSecret,
)
require.NoError(t, err)
require.NoError(t, dispatcher.Dispatch(parsed))
val, ok := results.Load(intent.id)
require.True(t, ok, "handler for %s should have been called", intent.name)
assert.Equal(t, intent.payload, val)
})
}
}
// TestIntegration_MessageSerialiseDeserialise verifies that messages survive
// the full serialisation/encryption/decryption/deserialisation pipeline
// with all fields intact.
func TestIntegration_MessageSerialiseDeserialise(t *testing.T) {
tp := setupTestTransportPair(t)
pc := tp.connectClient(t)
original, err := NewMessage(MsgStats, tp.ClientNode.GetIdentity().ID, tp.ServerNode.GetIdentity().ID, StatsPayload{
NodeID: "test-node",
NodeName: "test-name",
Miners: []MinerStatsItem{
{
Name: "miner-0",
Type: "xmrig",
Hashrate: 9999.9,
Shares: 500,
Rejected: 3,
Uptime: 7200,
Pool: "pool.example.com:3333",
Algorithm: "rx/0",
CPUThreads: 8,
},
},
Uptime: 86400,
})
require.NoError(t, err)
original.ReplyTo = "parent-msg-id-12345"
encrypted, err := tp.Client.encryptMessage(original, pc.SharedSecret)
require.NoError(t, err)
require.NotEmpty(t, encrypted)
decrypted, err := tp.Client.decryptMessage(encrypted, pc.SharedSecret)
require.NoError(t, err)
assert.Equal(t, original.ID, decrypted.ID)
assert.Equal(t, original.Type, decrypted.Type)
assert.Equal(t, original.From, decrypted.From)
assert.Equal(t, original.To, decrypted.To)
assert.Equal(t, original.ReplyTo, decrypted.ReplyTo)
var originalStats, decryptedStats StatsPayload
require.NoError(t, json.Unmarshal(original.Payload, &originalStats))
require.NoError(t, json.Unmarshal(decrypted.Payload, &decryptedStats))
assert.Equal(t, originalStats, decryptedStats)
}
// TestIntegration_GetRemoteStats_EndToEnd tests the full stats retrieval flow
// across a real WebSocket connection.
func TestIntegration_GetRemoteStats_EndToEnd(t *testing.T) {
tp := setupTestTransportPair(t)
worker := NewWorker(tp.ServerNode, tp.Server)
worker.RegisterWithTransport()
controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client)
tp.connectClient(t)
time.Sleep(100 * time.Millisecond)
serverID := tp.ServerNode.GetIdentity().ID
stats, err := controller.GetRemoteStats(serverID)
require.NoError(t, err, "GetRemoteStats should succeed end-to-end")
require.NotNil(t, stats)
assert.Equal(t, serverID, stats.NodeID)
assert.Equal(t, "server", stats.NodeName)
assert.GreaterOrEqual(t, stats.Uptime, int64(0))
}