socket.js

import {log} from './utils.js'
import stream from 'stream'
import util from 'util'

// A mock socket is two duplex HalfSockets, each of which is connected to the
// other with a crossover cable.

// Summary of node stream events:
// end() => 'finish' (the send side)
// push(null) => 'end' (the receive side)
// 'finish' + 'end' => 'close'

/**
 * Data was written to a HalfSocket.
 *
 * @event HalfSocket#written
 * @param {Buffer|string} data The data that was written.
 * @param {string} encoding The encoding for the data.
 */
const WRITTEN_EVENT = 'written'

/**
 * One end of a socket, either client or server.  Serves as a pass-through to
 * the other side.
 *
 * @fires {HalfSocket#written} When data is written to this side of the socket
 *   to be sent.
 */
export class HalfSocket extends stream.Duplex {
  /**
   * Creates an instance of HalfSocket.
   *
   * @param {string} name The name of this socket end, for debugging.
   * @param {stream.DuplexOptions} opts Options for the socket end.
   */
  constructor(name, opts) {
    opts = {
      // These have slightly different values between Duplex and Socket
      // on node 12 and 16.
      allowHalfOpen: false,
      autoDestroy: true,
      emitClose: true,
      ...opts,
    }
    super(opts)
    this.name = name
  }

  /**
   * Add debug logging to EventEmitter.  Start with NODE_DEBUG=mock-tls-server
   * to see all events.
   *
   * @param {string} eventName The name of the event.
   * @param {any[]} args The arguments to be passed to listeners.
   * @returns {boolean} Returns `true` if the event had listeners, `false`
   *   otherwise.
   */
  emit(eventName, ...args) {
    log(this.name, eventName, ...args)
    return super.emit(eventName, ...args)
  }

  _write(chunk, encoding, cb) {
    // De-couple the halves
    this.emit(WRITTEN_EVENT, chunk, encoding)
    cb()
  }

  // eslint-disable-next-line class-methods-use-this, no-empty-function
  _read(sz) {
  }

  [util.inspect.custom]() {
    return `[HalfSocket "${this.name}"]`
  }
}

/**
 * A double-ended mock socket, for use in testing.  No actual network
 * connections are created, so this can be used for testing no matter what
 * permissions you have in your CI system.
 */
export class MockSocket extends stream.Duplex {
  /**
   * Creates an instance of MockSocket.
   *
   * @param {stream.DuplexOptions} opts Options for both server and client
   *   sides.
   */
  constructor(opts) {
    super(opts)
    this.clientSocket = new HalfSocket('cli', opts)
    this.serverSocket = new HalfSocket('srv', opts)

    // Simulate the TCP flavor of the Internet.
    // If we were going to add delays, throttling, or Nagel-like coalescing,
    // this would be the place to do it.
    this.clientSocket.on(WRITTEN_EVENT, (chunk, encoding) => {
      this.serverSocket.push(chunk)
    })
    this.serverSocket.on(WRITTEN_EVENT, (chunk, encoding) => {
      this.clientSocket.push(chunk)
    })

    // This is from the song by Jimmy Buffer:
    // FINs to the left.
    this.clientSocket.on('finish', () => {
      this.serverSocket.push(null)
    })
    // FINs to the right.
    this.serverSocket.on('finish', () => {
      this.clientSocket.push(null)
    })
  }
}