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