From 428852d6195655f79e8f3bd2740ede0263c69865 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Tue, 7 Jan 2025 15:17:51 -0500 Subject: [PATCH] Add tests --- src/constants.ts | 2 +- src/managed-socket.ts | 18 +++++----- src/messages.ts | 4 +-- src/test/messages.test.ts | 76 +++++++++++++++++++++++++++++++++++++++ src/test/types.test.ts | 72 +++++++++++++++++++++++++++++++++++++ tsconfig.json | 4 ++- 6 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 src/test/messages.test.ts create mode 100644 src/test/types.test.ts diff --git a/src/constants.ts b/src/constants.ts index 5cc91b9..1244f14 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -25,7 +25,7 @@ export const NON_RECONNECT_CODES = [ export enum ManagedSocketErrorType { INVALID_MESSAGE_SHAPE = "Invalid message shape.", - CATOSTROPHIC_CLOSE = "Catostrophic close code", + CATASTROPHIC_CLOSE = "Catastrophic close code", SOCKET_ERROR = "WebSocket error", CONNECTION_REJECTED = "Connection rejected", } diff --git a/src/managed-socket.ts b/src/managed-socket.ts index f9cdc54..d6d518b 100644 --- a/src/managed-socket.ts +++ b/src/managed-socket.ts @@ -10,7 +10,7 @@ type SocketDataHandler = (data: unknown) => void | Promise; type SocketErrorHandler = (error: ManagedSocketError) => void; type SocketOpenHandler = () => void; type SocketCloseHandler = () => void; -type SocketCatostrophicErrorHandler = (error: ManagedSocketError) => void; +type SocketCatastrophicErrorHandler = (error: ManagedSocketError) => void; export type ManagedSocketConnectionState = | "connecting" @@ -29,7 +29,7 @@ export class ManagedSocket { private onErrorHandler: SocketErrorHandler | null = null; private onOpenHandler: SocketOpenHandler | null = null; private onCloseHandler: SocketCloseHandler | null = null; - private onCatostrophicErrorHandler: SocketCatostrophicErrorHandler | null = + private onCatastrophicErrorHandler: SocketCatastrophicErrorHandler | null = null; private reconnectAttempts: number; @@ -100,9 +100,9 @@ export class ManagedSocket { this.debugMessage(code); this.onCloseHandler?.(); - // If the code received was catostrophic, terminate the connection - if (SocketSpeak.isCatatrophicCloseCode(code)) { - this.handleCatostophicError(code); + // If the code received was catastrophic, terminate the connection + if (SocketSpeak.isCatastrophicCloseCode(code)) { + this.handleCatastrophicError(code); } this.state = "closed"; @@ -158,7 +158,7 @@ export class ManagedSocket { return this.ws; } - handleCatostophicError(code: SocketDisconnectCode) { + handleCatastrophicError(code: SocketDisconnectCode) { this.state = "terminated"; this.attemptReconnect = false; this.debugMessage( @@ -168,11 +168,11 @@ export class ManagedSocket { const socketError = new ManagedSocketError( "Socket connection terminated due to non-reconnect close code", new Error(code.toString()), - ManagedSocketErrorType.CATOSTROPHIC_CLOSE, + ManagedSocketErrorType.CATASTROPHIC_CLOSE, ); - if (this.onCatostrophicErrorHandler !== null) { - return this.onCatostrophicErrorHandler(socketError); + if (this.onCatastrophicErrorHandler !== null) { + return this.onCatastrophicErrorHandler(socketError); } throw socketError; diff --git a/src/messages.ts b/src/messages.ts index 8c42a16..4bc6fcd 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -84,7 +84,7 @@ function prepare(data: T): SerializedSocketMessage { * @param code - The close code to check. * @returns `true` if the code is in the list of non-reconnect codes, otherwise `false`. */ -function isCatatrophicCloseCode(code: number) { +function isCatastrophicCloseCode(code: number) { return (NON_RECONNECT_CODES as readonly number[]).includes(code); } @@ -93,6 +93,6 @@ export const SocketSpeak = { createHeartbeatMessage, serialize, deserialize, - isCatatrophicCloseCode, + isCatastrophicCloseCode, prepare, }; diff --git a/src/test/messages.test.ts b/src/test/messages.test.ts new file mode 100644 index 0000000..be31bbe --- /dev/null +++ b/src/test/messages.test.ts @@ -0,0 +1,76 @@ +import { test, describe, expect } from "bun:test"; + +import { SocketSpeak } from "../messages"; +import { ManagedSocketError } from "../errors"; +import { NON_RECONNECT_CODES, SocketDisconnectCode } from "../constants"; + +describe("createDataMessage()", () => { + test("wraps the provided data in a message object", () => { + const data = { foo: "bar" }; + const message = SocketSpeak.createDataMessage(data); + expect(message).toEqual({ type: "message", data }); + }); +}); + +describe("createHeartbeatMessage()", () => { + test("creates a heartbeat message with the current timestamp", () => { + const message = SocketSpeak.createHeartbeatMessage(); + expect(message.type).toBe("heartbeat"); + expect(message.data).toBeTypeOf("string"); + }); +}); + +describe("serialize()", () => { + test("serializes a SocketMessage object into a JSON string", () => { + const message = SocketSpeak.createDataMessage({ foo: "bar" }); + const serialized = SocketSpeak.serialize(message) as string; + expect(serialized).toBe(JSON.stringify(message)); + }); +}); + +describe("deserialize()", () => { + test("deserializes a JSON string into a SocketMessage object", () => { + const message = SocketSpeak.createDataMessage({ foo: "bar" }); + const serialized = SocketSpeak.serialize(message); + const deserialized = SocketSpeak.deserialize(serialized); + expect(deserialized).toEqual(message); + }); + + test("throws an error if the JSON string cannot be parsed", () => { + const invalidData = "invalid"; + expect(() => SocketSpeak.deserialize(invalidData)).toThrow( + ManagedSocketError, + ); + }); + + test("throws an error if the message shape is invalid", () => { + const invalidData = JSON.stringify({ type: "invalid" }); + expect(() => SocketSpeak.deserialize(invalidData)).toThrow( + ManagedSocketError, + ); + }); +}); + +describe("prepare()", () => { + test("serializes and wraps the provided data in a message object", () => { + const data = { foo: "bar" }; + const message = SocketSpeak.prepare(data); + expect(message).toEqual(SocketSpeak.serialize({ type: "message", data })); + }); +}); + +describe("isCatatrophicCloseCode()", () => { + test("returns for codes in the catastrophic list", () => { + for (const code of NON_RECONNECT_CODES) { + expect(SocketSpeak.isCatastrophicCloseCode(code)).toBe(true); + } + }); + + test("returns false for other close codes", () => { + expect( + SocketSpeak.isCatastrophicCloseCode( + SocketDisconnectCode.CLOSE_SERVICE_RESTART, + ), + ).toBe(false); + }); +}); diff --git a/src/test/types.test.ts b/src/test/types.test.ts new file mode 100644 index 0000000..c532458 --- /dev/null +++ b/src/test/types.test.ts @@ -0,0 +1,72 @@ +import { test, describe, expect } from "bun:test"; + +import { parseAnyScoketMessage, parseMessageType } from "../types"; +import { ManagedSocketError } from "../errors"; + +describe("parseMessageType()", () => { + test("errors if the value is not a string", () => { + expect(() => parseMessageType(123)).toThrowError(ManagedSocketError); + }); + + test("errors if the value is a string but not a valid message type", () => { + expect(() => parseMessageType("invalid")).toThrowError(ManagedSocketError); + }); + + test("returns the message type when the value is a valid message type", () => { + expect(parseMessageType("heartbeat")).toBe("heartbeat"); + expect(parseMessageType("message")).toBe("message"); + }); +}); + +describe("parseAnyScoketMessage()", () => { + test("throws when the value is null", () => { + expect(() => parseAnyScoketMessage(null)).toThrowError(ManagedSocketError); + }); + + test("throws when the value is not an object", () => { + expect(() => parseAnyScoketMessage("string")).toThrowError( + ManagedSocketError, + ); + expect(() => parseAnyScoketMessage(() => {})).toThrowError( + ManagedSocketError, + ); + expect(() => parseAnyScoketMessage(false)).toThrowError(ManagedSocketError); + expect(() => parseAnyScoketMessage([])).toThrowError(ManagedSocketError); + expect(() => parseAnyScoketMessage(Symbol())).toThrowError( + ManagedSocketError, + ); + }); + + test("throws when the object does not have the correct shape", () => { + expect(() => parseAnyScoketMessage({})).toThrowError(ManagedSocketError); + expect(() => parseAnyScoketMessage({ type: "message" })).toThrowError( + ManagedSocketError, + ); + expect(() => parseAnyScoketMessage({ data: "data" })).toThrowError( + ManagedSocketError, + ); + }); + + test("throws when the message type is invalid", () => { + expect(() => + parseAnyScoketMessage({ type: "invalid", data: "data" }), + ).toThrowError(ManagedSocketError); + }); + + test("throws when the type is 'heartbeat' and the value is not a string", () => { + expect(() => + parseAnyScoketMessage({ type: "heartbeat", data: 123 }), + ).toThrowError(ManagedSocketError); + }); + + test("returns a valid message object when the type is 'heartbeat'", () => { + const dateStr = new Date().toISOString(); + const message = parseAnyScoketMessage({ + type: "heartbeat", + data: dateStr, + }); + const dataMessage = parseAnyScoketMessage({ type: "message", data: 123 }); + expect(message).toEqual({ type: "heartbeat", data: dateStr }); + expect(dataMessage).toEqual({ type: "message", data: 123 }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 238655f..9195094 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,7 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false - } + }, + "include": ["src"], + "exclude": ["src/test"], }