Add tests
This commit is contained in:
parent
ba73b0a1f7
commit
428852d619
|
@ -25,7 +25,7 @@ export const NON_RECONNECT_CODES = [
|
||||||
|
|
||||||
export enum ManagedSocketErrorType {
|
export enum ManagedSocketErrorType {
|
||||||
INVALID_MESSAGE_SHAPE = "Invalid message shape.",
|
INVALID_MESSAGE_SHAPE = "Invalid message shape.",
|
||||||
CATOSTROPHIC_CLOSE = "Catostrophic close code",
|
CATASTROPHIC_CLOSE = "Catastrophic close code",
|
||||||
SOCKET_ERROR = "WebSocket error",
|
SOCKET_ERROR = "WebSocket error",
|
||||||
CONNECTION_REJECTED = "Connection rejected",
|
CONNECTION_REJECTED = "Connection rejected",
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ type SocketDataHandler = (data: unknown) => void | Promise<void>;
|
||||||
type SocketErrorHandler = (error: ManagedSocketError) => void;
|
type SocketErrorHandler = (error: ManagedSocketError) => void;
|
||||||
type SocketOpenHandler = () => void;
|
type SocketOpenHandler = () => void;
|
||||||
type SocketCloseHandler = () => void;
|
type SocketCloseHandler = () => void;
|
||||||
type SocketCatostrophicErrorHandler = (error: ManagedSocketError) => void;
|
type SocketCatastrophicErrorHandler = (error: ManagedSocketError) => void;
|
||||||
|
|
||||||
export type ManagedSocketConnectionState =
|
export type ManagedSocketConnectionState =
|
||||||
| "connecting"
|
| "connecting"
|
||||||
|
@ -29,7 +29,7 @@ export class ManagedSocket {
|
||||||
private onErrorHandler: SocketErrorHandler | null = null;
|
private onErrorHandler: SocketErrorHandler | null = null;
|
||||||
private onOpenHandler: SocketOpenHandler | null = null;
|
private onOpenHandler: SocketOpenHandler | null = null;
|
||||||
private onCloseHandler: SocketCloseHandler | null = null;
|
private onCloseHandler: SocketCloseHandler | null = null;
|
||||||
private onCatostrophicErrorHandler: SocketCatostrophicErrorHandler | null =
|
private onCatastrophicErrorHandler: SocketCatastrophicErrorHandler | null =
|
||||||
null;
|
null;
|
||||||
|
|
||||||
private reconnectAttempts: number;
|
private reconnectAttempts: number;
|
||||||
|
@ -100,9 +100,9 @@ export class ManagedSocket {
|
||||||
this.debugMessage(code);
|
this.debugMessage(code);
|
||||||
this.onCloseHandler?.();
|
this.onCloseHandler?.();
|
||||||
|
|
||||||
// If the code received was catostrophic, terminate the connection
|
// If the code received was catastrophic, terminate the connection
|
||||||
if (SocketSpeak.isCatatrophicCloseCode(code)) {
|
if (SocketSpeak.isCatastrophicCloseCode(code)) {
|
||||||
this.handleCatostophicError(code);
|
this.handleCatastrophicError(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = "closed";
|
this.state = "closed";
|
||||||
|
@ -158,7 +158,7 @@ export class ManagedSocket {
|
||||||
return this.ws;
|
return this.ws;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCatostophicError(code: SocketDisconnectCode) {
|
handleCatastrophicError(code: SocketDisconnectCode) {
|
||||||
this.state = "terminated";
|
this.state = "terminated";
|
||||||
this.attemptReconnect = false;
|
this.attemptReconnect = false;
|
||||||
this.debugMessage(
|
this.debugMessage(
|
||||||
|
@ -168,11 +168,11 @@ export class ManagedSocket {
|
||||||
const socketError = new ManagedSocketError(
|
const socketError = new ManagedSocketError(
|
||||||
"Socket connection terminated due to non-reconnect close code",
|
"Socket connection terminated due to non-reconnect close code",
|
||||||
new Error(code.toString()),
|
new Error(code.toString()),
|
||||||
ManagedSocketErrorType.CATOSTROPHIC_CLOSE,
|
ManagedSocketErrorType.CATASTROPHIC_CLOSE,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.onCatostrophicErrorHandler !== null) {
|
if (this.onCatastrophicErrorHandler !== null) {
|
||||||
return this.onCatostrophicErrorHandler(socketError);
|
return this.onCatastrophicErrorHandler(socketError);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw socketError;
|
throw socketError;
|
||||||
|
|
|
@ -84,7 +84,7 @@ function prepare<T>(data: T): SerializedSocketMessage {
|
||||||
* @param code - The close code to check.
|
* @param code - The close code to check.
|
||||||
* @returns `true` if the code is in the list of non-reconnect codes, otherwise `false`.
|
* @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);
|
return (NON_RECONNECT_CODES as readonly number[]).includes(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +93,6 @@ export const SocketSpeak = {
|
||||||
createHeartbeatMessage,
|
createHeartbeatMessage,
|
||||||
serialize,
|
serialize,
|
||||||
deserialize,
|
deserialize,
|
||||||
isCatatrophicCloseCode,
|
isCatastrophicCloseCode,
|
||||||
prepare,
|
prepare,
|
||||||
};
|
};
|
||||||
|
|
76
src/test/messages.test.ts
Normal file
76
src/test/messages.test.ts
Normal 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
72
src/test/types.test.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
|
@ -23,5 +23,7 @@
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false
|
||||||
}
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["src/test"],
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue