current commit
260916.1 · lib,src: apply multiple updates to dtls implementation
// Flags: --experimental-dtls --no-warnings

// Test: DTLS endpoint and session stats increment with data transfer.

import { hasCrypto, skip, mustCall } from '../common/index.mjs';
import assert from 'node:assert';
import * as fixtures from '../common/fixtures.mjs';

if (!hasCrypto) {
  skip('missing crypto');
}

if (!process.features.dtls) {
  skip('DTLS is not enabled');
}

const { listen, connect } = await import('node:dtls');

const serverCert = fixtures.readKey('agent1-cert.pem');
const serverKey = fixtures.readKey('agent1-key.pem');
const ca = fixtures.readKey('ca1-cert.pem');

const serverReceivedData = Promise.withResolvers();
const clientReceivedData = Promise.withResolvers();

let serverSession;

// Start server.
const endpoint = listen(mustCall((session) => {
  serverSession = session;

  session.onmessage = mustCall((data) => {
    assert.strictEqual(data.toString(), 'hello from client');
    session.send('hello from server');
    serverReceivedData.resolve();
  });

  session.onhandshake = mustCall();
}), {
  cert: serverCert.toString(),
  key: serverKey.toString(),
  port: 0,
  host: '127.0.0.1',
});

// --- Endpoint stats should be available immediately ---

const epStats = endpoint.stats;
assert.ok(epStats, 'endpoint.stats should be defined');
assert.ok(epStats.isConnected, 'stats should be connected');
assert.ok(epStats.createdAt > 0n, 'createdAt should be set');
assert.strictEqual(epStats.destroyedAt, 0n);
assert.strictEqual(epStats.clientSessions, 0n);
assert.strictEqual(epStats.serverSessions, 0n);
assert.strictEqual(epStats.serverBusyCount, 0n);

// Connect client.
const clientSession = connect('127.0.0.1', endpoint.address.port, {
  ca: [ca.toString()],
  rejectUnauthorized: false,
});

clientSession.onmessage = mustCall((data) => {
  assert.strictEqual(data.toString(), 'hello from server');
  clientReceivedData.resolve();
});

clientSession.onhandshake = mustCall();

// Wait for handshake.
await clientSession.opened;

// --- Client session stats after handshake ---

const csStats = clientSession.stats;
assert.ok(csStats, 'session.stats should be defined');
assert.ok(csStats.isConnected, 'session stats should be connected');
assert.ok(csStats.createdAt > 0n, 'createdAt should be set');
assert.ok(csStats.handshakeCompletedAt > 0n, 'handshake timestamp should be set');
assert.ok(csStats.handshakeCompletedAt >= csStats.createdAt,
          'handshake should complete after creation');
assert.strictEqual(csStats.closingAt, 0n);
assert.strictEqual(csStats.destroyedAt, 0n);

// Record bytes before sending application data.
const csBytesSentBefore = csStats.bytesSent;
const csMessagesSentBefore = csStats.messagesSent;

// Send data.
clientSession.send('hello from client');

// Wait for bidirectional exchange.
await Promise.all([serverReceivedData.promise, clientReceivedData.promise]);

// --- Client session stats after data exchange ---

assert.ok(csStats.bytesSent > csBytesSentBefore,
          'bytesSent should increase after send');
assert.ok(csStats.messagesSent > csMessagesSentBefore,
          'messagesSent should increase after send');
assert.ok(csStats.bytesReceived > 0n, 'bytesReceived should be non-zero');
assert.ok(csStats.messagesReceived > 0n, 'messagesReceived should be non-zero');

// --- Server session stats after data exchange ---

assert.ok(serverSession, 'server session should exist');
const ssStats = serverSession.stats;
assert.ok(ssStats.bytesReceived > 0n, 'server bytesReceived should be non-zero');
assert.ok(ssStats.messagesReceived > 0n, 'server messagesReceived should be non-zero');
assert.ok(ssStats.bytesSent > 0n, 'server bytesSent should be non-zero');
assert.ok(ssStats.messagesSent > 0n, 'server messagesSent should be non-zero');
assert.ok(ssStats.handshakeCompletedAt > 0n, 'server handshake timestamp should be set');

// --- Endpoint stats after data exchange ---

assert.ok(epStats.bytesReceived > 0n, 'endpoint bytesReceived should be non-zero');
assert.ok(epStats.bytesSent > 0n, 'endpoint bytesSent should be non-zero');
assert.ok(epStats.packetsReceived > 0n, 'endpoint packetsReceived should be non-zero');
assert.ok(epStats.packetsSent > 0n, 'endpoint packetsSent should be non-zero');
assert.strictEqual(epStats.serverSessions, 1n);

// The client's own endpoint should track the client session.
const clientEpStats = clientSession.endpoint.stats;
assert.strictEqual(clientEpStats.clientSessions, 1n);
assert.ok(clientEpStats.bytesSent > 0n);
assert.ok(clientEpStats.bytesReceived > 0n);

// --- toJSON / toString ---

const epJson = epStats.toJSON();
assert.ok(epJson);
assert.strictEqual(typeof epJson.bytesReceived, 'string');
assert.strictEqual(typeof epJson.bytesSent, 'string');
assert.strictEqual(typeof epJson.connected, 'boolean');

const ssJson = csStats.toJSON();
assert.ok(ssJson);
assert.strictEqual(typeof ssJson.handshakeCompletedAt, 'string');
assert.strictEqual(typeof ssJson.messagesReceived, 'string');

const epStr = epStats.toString();
assert.ok(typeof epStr === 'string');
assert.ok(epStr.includes('bytesReceived'));

// Clean up.
await clientSession.close();

// After close, session closing timestamp should be set.
assert.notStrictEqual(csStats.closingAt, 0n);

await endpoint.close();

// Stats say when they have stopped tracking anything.
//
// isConnected is documented as false once the stats are stale, and nothing
// ever set it: kFinishClose was defined on both stats classes and imported,
// and no caller in the module ever invoked it. The numbers stayed readable
// and stopped moving, and isConnected went on saying they were live.
{
  const server = listen(() => {}, {
    cert: serverCert, key: serverKey, host: '127.0.0.1', port: 0,
  });
  const client = connect('127.0.0.1', server.address.port, {
    rejectUnauthorized: false,
  });
  await client.opened;

  const sessionStats = client.stats;
  const endpointStats = server.stats;
  assert.strictEqual(sessionStats.isConnected, true);
  assert.strictEqual(endpointStats.isConnected, true);

  // The last values are kept rather than lost, which is the point of taking
  // a copy rather than simply marking the object dead.
  const createdAt = sessionStats.createdAt;

  await client.close();
  assert.strictEqual(sessionStats.isConnected, false);
  assert.strictEqual(sessionStats.createdAt, createdAt);

  await server.close();
  assert.strictEqual(endpointStats.isConnected, false);
}

// destroy() reaches them too, not only the graceful path.
{
  const server = listen(() => {}, {
    cert: serverCert, key: serverKey, host: '127.0.0.1', port: 0,
  });
  const client = connect('127.0.0.1', server.address.port, {
    rejectUnauthorized: false,
  });
  await client.opened;

  const sessionStats = client.stats;
  const endpointStats = server.stats;

  client.destroy();
  assert.strictEqual(sessionStats.isConnected, false);

  server.destroy();
  assert.strictEqual(endpointStats.isConnected, false);
}