Add tests

This commit is contained in:
Endeavorance 2025-01-07 15:17:51 -05:00
parent ba73b0a1f7
commit 428852d619
6 changed files with 163 additions and 13 deletions

View file

@ -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",
}

View file

@ -10,7 +10,7 @@ type SocketDataHandler = (data: unknown) => void | Promise<void>;
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;

View file

@ -84,7 +84,7 @@ function prepare<T>(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,
};

76
src/test/messages.test.ts Normal file
View file

@ -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);
});
});

72
src/test/types.test.ts Normal file
View file

@ -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 });
});
});

View file

@ -23,5 +23,7 @@
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
},
"include": ["src"],
"exclude": ["src/test"],
}