robotty/dank-twitch-irc

View on GitHub
lib/await/await-response.spec.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { assert } from "chai";
import * as sinon from "sinon";
import { ConnectionError, MessageError } from "../client/errors";
import { assertErrorChain, fakeConnection } from "../helpers.spec";
import { parseTwitchMessage } from "../message/parser/twitch-message";
import { BaseError } from "../utils/base-error";
import { ignoreErrors } from "../utils/ignore-errors";
import { awaitResponse, ResponseAwaiter } from "./await-response";
import { TimeoutError } from "./timeout-error";

describe("./await/await-response", function () {
  describe("ResponseAwaiter", function () {
    it("should add itself to list of waiters", function () {
      const { client, end } = fakeConnection();

      const awaiter1 = new ResponseAwaiter(client, {
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter 1 failure",
      });
      awaiter1.promise.catch(ignoreErrors);

      const awaiter2 = new ResponseAwaiter(client, {
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter 2 failure",
      });
      awaiter2.promise.catch(ignoreErrors);

      assert.deepStrictEqual(client.pendingResponses, [awaiter1, awaiter2]);

      end();
    });

    it("should resolve on matching incoming message", async function () {
      const { client, end } = fakeConnection();

      const wantedMsg = parseTwitchMessage("PONG :tmi.twitch.tv");

      const promise = awaitResponse(client, {
        success: (msg) => msg === wantedMsg,
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter failure",
      });

      client.emitMessage(wantedMsg);

      end();

      assert.strictEqual(await promise, wantedMsg);
      assert.deepStrictEqual(client.pendingResponses, []);
    });

    it("should reject on matching incoming message", async function () {
      const { client, clientError, emitAndEnd } = fakeConnection();

      const wantedMsg = "PONG :tmi.twitch.tv";

      const promise = awaitResponse(client, {
        failure: (msg) => msg.rawSource === wantedMsg,
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter failure",
      });

      emitAndEnd(wantedMsg);

      await assertErrorChain(
        promise,
        BaseError,
        "test awaiter failure: Bad response message: PONG :tmi.twitch.tv",
        MessageError,
        "Bad response message: PONG :tmi.twitch.tv"
      );
      assert.deepStrictEqual(client.pendingResponses, []);

      await assertErrorChain(
        clientError,
        BaseError,
        "test awaiter failure: Bad response message: PONG :tmi.twitch.tv",
        MessageError,
        "Bad response message: PONG :tmi.twitch.tv"
      );
    });

    it("should reject on connection close (no error)", async function () {
      const { client, end, clientError } = fakeConnection();

      const promise = awaitResponse(client, {
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter failure",
      });

      end();

      const clientErrorAfterClose = new Promise((resolve, reject) => {
        client.once("error", reject);
      });

      await assertErrorChain(
        [promise, clientErrorAfterClose],
        BaseError,
        "test awaiter failure: Connection closed with no error",
        ConnectionError,
        "Connection closed with no error"
      );

      // the client is closed so the error occurring after close is not
      // emitted -> clientError is resolved because on("close") happens
      // before our ResponseAwaiter emits the error
      await clientError;
    });

    it("should reject on connection close (with error)", async function () {
      const { client, end, clientError } = fakeConnection();

      const promise = awaitResponse(client, {
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter failure",
      });

      end(new Error("peer reset connection"));

      // TODO create a utility to await error no #N on arbitrary EventEmitter
      const clientErrorAfterClose = new Promise((resolve, reject) => {
        let counter = 0;
        const target = 1;
        client.on("error", (e) => {
          if (counter++ === target) {
            reject(e);
          }
        });
      });

      await assertErrorChain(
        promise,
        BaseError,
        "test awaiter failure: Connection closed due to error: Error occurred in transport layer: peer reset connection",
        ConnectionError,
        "Connection closed due to error: Error occurred in transport layer: peer reset connection",
        ConnectionError,
        "Error occurred in transport layer: peer reset connection",
        Error,
        "peer reset connection"
      );

      await assertErrorChain(
        clientError,
        ConnectionError,
        "Error occurred in transport layer: peer reset connection",
        Error,
        "peer reset connection"
      );

      await assertErrorChain(
        clientErrorAfterClose,
        BaseError,
        "test awaiter failure: Connection closed due to error: Error occurred in transport layer: peer reset connection",
        ConnectionError,
        "Connection closed due to error: Error occurred in transport layer: peer reset connection",
        ConnectionError,
        "Error occurred in transport layer: peer reset connection",
        Error,
        "peer reset connection"
      );
    });

    it("should timeout after specified timeout (noResponseAction = failure)", async function () {
      sinon.useFakeTimers();
      const { client, clientError } = fakeConnection();

      // awaiter is going to be the only awaiter in the queue so
      // it starts the timeout
      // awaiters should wait until they are at the head of the queue
      // to start their timeout
      const promise = awaitResponse(client, {
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter failure",
        timeout: 3000,
      });

      sinon.clock.tick(3000);

      await assertErrorChain(
        [promise, clientError],
        BaseError,
        "test awaiter failure: Timed out after waiting for response for 3000 milliseconds",
        TimeoutError,
        "Timed out after waiting for response for 3000 milliseconds"
      );
    });

    it("should timeout after specified timeout (noResponseAction = success)", async function () {
      sinon.useFakeTimers();
      const { client, clientError, end } = fakeConnection();

      // awaiter is going to be the only awaiter in the queue so
      // it starts the timeout
      // awaiters should wait until they are at the head of the queue
      // to start their timeout
      const promise = awaitResponse(client, {
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter failure",
        timeout: 3000,
        noResponseAction: "success",
      });

      sinon.clock.tick(3000);
      end();

      await Promise.all([promise, clientError]);
    });

    it("should begin timeout only once awaiter is moved to head of queue", async function () {
      sinon.useFakeTimers();
      const { client, clientError } = fakeConnection();

      const promise1 = awaitResponse(client, {
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter1 failure",
        timeout: 1000,
      });

      const promise2 = awaitResponse(client, {
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter2 failure",
        timeout: 1000,
      });

      sinon.clock.tick(1000);

      await assertErrorChain(
        [promise1, clientError],
        BaseError,
        "test awaiter1 failure: Timed out after waiting for response for 1000 milliseconds",
        TimeoutError,
        "Timed out after waiting for response for 1000 milliseconds"
      );

      sinon.clock.tick(1000);

      await assertErrorChain(
        promise2,
        BaseError,
        "test awaiter2 failure: Timed out after waiting for response for 1000 milliseconds",
        TimeoutError,
        "Timed out after waiting for response for 1000 milliseconds"
      );
    });

    it("should notify other awaiters that they are outpaced", async function () {
      const { client, emitAndEnd, clientError } = fakeConnection();

      const promise1 = awaitResponse(client, {
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter1 failure",
      });
      const expectedMsg = "PONG :tmi.twitch.tv";

      const promise2 = awaitResponse(client, {
        success: (msg) => msg.rawSource === expectedMsg,
        errorType: (message, cause) => new BaseError(message, cause),
        errorMessage: "test awaiter2 failure",
      });

      // awaiter2 will resolve -> awaiter1 will be rejected because it was
      // outpaced

      emitAndEnd(expectedMsg);

      await assertErrorChain(
        [promise1, clientError],
        BaseError,
        "test awaiter1 failure: A response to a command issued later than this command was received",
        TimeoutError,
        "A response to a command issued later than this command was received"
      );

      const matchedMsg = await promise2;
      assert.strictEqual(matchedMsg.rawSource, expectedMsg);
    });
  });
});