WizardConnect/packages/core/src/message-queue.test.ts

222 lines
6.5 KiB
TypeScript
Raw Normal View History

2026-02-26 11:19:47 +01:00
// Copyright (C) 2026 Whiterun LLC,
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
import { describe, it, expect, beforeEach, vi } from "vitest";
import { MessageQueue } from "./message-queue.js";
import { ProtocolMessage, RelayMsgAction } from "./protocols/hdwalletv1.js";
describe("MessageQueue", () => {
let queue: MessageQueue;
let publishFn: ReturnType<typeof vi.fn>;
beforeEach(() => {
queue = new MessageQueue({ logActivity: false });
publishFn = vi.fn().mockResolvedValue(undefined);
});
describe("initialization", () => {
it("should start with ready state false", () => {
expect(queue.getReady()).toBe(false);
});
it("should start with empty queue", () => {
expect(queue.getQueueLength()).toBe(0);
});
});
describe("enqueue", () => {
it("should queue messages when not ready", async () => {
const message: ProtocolMessage = {
action: RelayMsgAction.DappReady,
time: Math.floor(Date.now() / 1000),
};
const promise = queue.enqueue(message);
expect(queue.getQueueLength()).toBe(1);
expect(queue.getReady()).toBe(false);
// Message should be queued, promise should not resolve yet
await expect(
Promise.race([
promise,
new Promise((resolve) => setTimeout(() => resolve("timeout"), 10)),
]),
).resolves.toBe("timeout");
});
it("should return resolved promise when already ready", async () => {
queue.setReady(publishFn);
const message: ProtocolMessage = {
action: RelayMsgAction.DappReady,
time: Math.floor(Date.now() / 1000),
};
await expect(queue.enqueue(message)).resolves.toBeUndefined();
expect(queue.getQueueLength()).toBe(0);
});
});
describe("setReady", () => {
it("should mark queue as ready", () => {
queue.setReady(publishFn);
expect(queue.getReady()).toBe(true);
});
it("should process queued messages when set to ready", async () => {
const message1: ProtocolMessage = {
action: RelayMsgAction.DappReady,
time: Math.floor(Date.now() / 1000),
};
const message2: ProtocolMessage = {
action: RelayMsgAction.WalletReady,
time: Math.floor(Date.now() / 1000),
};
const promise1 = queue.enqueue(message1);
const promise2 = queue.enqueue(message2);
expect(queue.getQueueLength()).toBe(2);
await queue.setReady(publishFn);
expect(publishFn).toHaveBeenCalledTimes(2);
expect(publishFn).toHaveBeenCalledWith(message1);
expect(publishFn).toHaveBeenCalledWith(message2);
expect(queue.getQueueLength()).toBe(0);
// Promises should resolve
await expect(promise1).resolves.toBeUndefined();
await expect(promise2).resolves.toBeUndefined();
});
it("should reject queued messages if publish fails", async () => {
const error = new Error("Publish failed");
publishFn.mockRejectedValueOnce(error);
const message: ProtocolMessage = {
action: RelayMsgAction.DappReady,
time: Math.floor(Date.now() / 1000),
};
const promise = queue.enqueue(message);
await queue.setReady(publishFn);
await expect(promise).rejects.toThrow("Publish failed");
});
it("should process messages in order", async () => {
const messages: ProtocolMessage[] = [];
const callOrder: number[] = [];
publishFn.mockImplementation(async (msg: ProtocolMessage) => {
const index = messages.length;
messages.push(msg);
callOrder.push(index);
});
for (let i = 0; i < 5; i++) {
const message: ProtocolMessage = {
action: RelayMsgAction.DappReady,
time: Math.floor(Date.now() / 1000),
};
queue.enqueue(message);
}
await queue.setReady(publishFn);
expect(callOrder).toEqual([0, 1, 2, 3, 4]);
expect(messages).toHaveLength(5);
});
});
describe("setNotReady", () => {
it("should mark queue as not ready", () => {
queue.setReady(publishFn);
expect(queue.getReady()).toBe(true);
queue.setNotReady();
expect(queue.getReady()).toBe(false);
});
it("should reject all queued messages", async () => {
const message1: ProtocolMessage = {
action: RelayMsgAction.DappReady,
time: Math.floor(Date.now() / 1000),
};
const message2: ProtocolMessage = {
action: RelayMsgAction.WalletReady,
time: Math.floor(Date.now() / 1000),
};
const promise1 = queue.enqueue(message1);
const promise2 = queue.enqueue(message2);
queue.setNotReady("Custom error message");
await expect(promise1).rejects.toThrow("Custom error message");
await expect(promise2).rejects.toThrow("Custom error message");
expect(queue.getQueueLength()).toBe(0);
});
it("should use default error message if not provided", async () => {
const message: ProtocolMessage = {
action: RelayMsgAction.DappReady,
time: Math.floor(Date.now() / 1000),
};
const promise = queue.enqueue(message);
queue.setNotReady();
await expect(promise).rejects.toThrow(
"Connection closed before message could be sent",
);
});
});
describe("clear", () => {
it("should clear all queued messages", async () => {
const message: ProtocolMessage = {
action: RelayMsgAction.DappReady,
time: Math.floor(Date.now() / 1000),
};
const promise = queue.enqueue(message);
expect(queue.getQueueLength()).toBe(1);
queue.clear();
expect(queue.getQueueLength()).toBe(0);
await expect(promise).rejects.toThrow("Queue cleared");
});
it("should not affect ready state", () => {
queue.setReady(publishFn);
expect(queue.getReady()).toBe(true);
queue.clear();
expect(queue.getReady()).toBe(true);
});
});
describe("getQueueLength", () => {
it("should return correct queue length", () => {
expect(queue.getQueueLength()).toBe(0);
queue.enqueue({
action: RelayMsgAction.DappReady,
time: Math.floor(Date.now() / 1000),
});
expect(queue.getQueueLength()).toBe(1);
queue.enqueue({
action: RelayMsgAction.WalletReady,
time: Math.floor(Date.now() / 1000),
});
expect(queue.getQueueLength()).toBe(2);
});
});
});