current commit
260727.8 · test: ensure assertions are reached on all tests
// Flags: --experimental-quic --no-warnings

// Test: session close/destroy with application and transport error codes
// .
// Application error propagated as ERR_QUIC_APPLICATION_ERROR.
// Session close with specific app error code — peer receives it.
// Verifies that close() and destroy() with { code, type, reason } options
// send the correct CONNECTION_CLOSE frame, and the peer receives the
// correct error type, code, and reason.

import { hasQuic, skip, mustCall } from '../common/index.mjs';
import assert from 'node:assert';
import { setTimeout } from 'node:timers/promises';

if (!hasQuic) {
  skip('QUIC is not enabled');
}

const { listen, connect } = await import('../common/quic.mjs');

// --- Test 1: close() with application error code ---
// The client closes with an application error. The server receives
// ERR_QUIC_APPLICATION_ERROR with the exact code and reason.
{
  const serverGot = Promise.withResolvers();

  const serverEndpoint = await listen(mustCall(async (serverSession) => {
    serverSession.onerror = mustCall((err) => {
      assert.strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR');
      assert.strictEqual(err.message.includes('42'), true); // Error message should contain the code
      assert.strictEqual(err.message.includes('client shutdown'), true); // Error message should contain the reason
      assert.strictEqual(err.errorCode, 42n);
      assert.strictEqual(err.type, 'application');
      assert.strictEqual(err.reason, 'client shutdown');
    });
    await assert.rejects(serverSession.closed, {
      code: 'ERR_QUIC_APPLICATION_ERROR',
      errorCode: 42n,
      reason: 'client shutdown',
    });
    serverGot.resolve();
  }));

  const clientSession = await connect(serverEndpoint.address, {
    reuseEndpoint: false,
  });
  await clientSession.opened;

  // Small delay to ensure handshake is fully confirmed so ngtcp2
  // generates the 1-RTT APPLICATION CONNECTION_CLOSE frame.
  await setTimeout(100);

  // close() with application error — the client's closed promise
  // resolves because the close was locally initiated (intentional).
  // The peer receives the error code, but the local side is not in error.
  await clientSession.close({
    code: 42n,
    type: 'application',
    reason: 'client shutdown',
  });

  await serverGot.promise;
  await serverEndpoint.close();
}

// --- Test 2: close() with transport error code ---
// The client closes with a transport error. The server receives
// ERR_QUIC_TRANSPORT_ERROR with the exact code.
{
  const serverGot = Promise.withResolvers();

  const serverEndpoint = await listen(mustCall(async (serverSession) => {
    serverSession.onerror = mustCall((err) => {
      assert.strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR');
      assert.strictEqual(err.message.includes('1'), true); // Error message should contain the code
      assert.strictEqual(err.errorCode, 1n);
      assert.strictEqual(err.type, 'transport');
    });
    await assert.rejects(serverSession.closed, {
      code: 'ERR_QUIC_TRANSPORT_ERROR',
    });
    serverGot.resolve();
  }));

  const clientSession = await connect(serverEndpoint.address, {
    reuseEndpoint: false,
  });
  await clientSession.opened;
  await setTimeout(100);

  // close() with transport error — resolves locally (intentional).
  await clientSession.close({ code: 1n });

  await serverGot.promise;
  await serverEndpoint.close();
}

// --- Test 3: destroy() with application error code ---
// The client destroys with both a JS error and a QUIC error code.
// The peer receives the QUIC application error.
{
  const serverGot = Promise.withResolvers();

  const serverEndpoint = await listen(mustCall(async (serverSession) => {
    serverSession.onerror = mustCall((err) => {
      assert.strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR');
      assert.strictEqual(err.message.includes('99'), true);
      assert.strictEqual(err.errorCode, 99n);
      assert.strictEqual(err.type, 'application');
      assert.strictEqual(err.reason, 'destroy with code');
    });
    await assert.rejects(serverSession.closed, {
      code: 'ERR_QUIC_APPLICATION_ERROR',
    });
    serverGot.resolve();
  }));

  const clientSession = await connect(serverEndpoint.address, {
    reuseEndpoint: false,
    onerror: mustCall((err) => {
      // The JS error passed to destroy is delivered via onerror.
      assert.strictEqual(err.message, 'fatal error');
    }),
  });
  await clientSession.opened;
  await setTimeout(100);

  const jsError = new Error('fatal error');
  clientSession.destroy(jsError, {
    code: 99n,
    type: 'application',
    reason: 'destroy with code',
  });

  // The closed promise rejects with the JS error, not the QUIC error.
  await assert.rejects(clientSession.closed, jsError);

  await serverGot.promise;
  await serverEndpoint.close();
}

// --- Test 4: close() with no options (default behavior) ---
// Verify the default close sends NO_ERROR and the peer closes cleanly.
{
  const serverGot = Promise.withResolvers();

  const serverEndpoint = await listen(mustCall(async (serverSession) => {
    // No onerror — clean close should not trigger errors.
    await serverSession.closed;
    serverGot.resolve();
  }));

  const clientSession = await connect(serverEndpoint.address, {
    reuseEndpoint: false,
  });
  await clientSession.opened;
  await setTimeout(100);

  // Default close — no error code, clean shutdown.
  await clientSession.close();

  await serverGot.promise;
  await serverEndpoint.close();
}