import { afterEach, describe, expect, it } from "vitest"; import { APIError, Client, DEFAULT_KEEPALIVE, ReconnectBackoff, disableJitterForTest, normalizeMQTTURL, restoreJitterForTest, } from "../src/index.js"; import { FakeTransport } from "../src/fake.js"; afterEach(() => { restoreJitterForTest(); }); async function connectFake(fake: FakeTransport, opts: Record = {}): Promise { const c = new Client(); await c.connect("ws://example.test/mqtt", "ep1", { password: "p" }, { transport: fake, ...opts }); return c; } describe("K00", () => { it("TestK00FirstConnectTimeout", async () => { const fake = new FakeTransport(); fake.autoHello = false; const c = new Client(); await expect( c.connect("ws://example.test/mqtt", "ep1", { password: "p" }, { transport: fake, connectTimeoutMs: 150, }), ).rejects.toMatchObject({ code: "not_connected" }); fake.autoHello = true; await c.connect("ws://example.test/mqtt", "ep1", { password: "p" }, { transport: fake, connectTimeoutMs: 2000, }); await c.close(); }); it("TestK00AuthErrorCodes", async () => { const fake = new FakeTransport(); const c = await connectFake(fake); fake.simulateAuthFail("bad_credentials"); const deadline = Date.now() + 1000; while (Date.now() < deadline && c.lastStopCodeForTest() !== "bad_credentials") { await new Promise((r) => setTimeout(r, 5)); } expect(c.lastStopCodeForTest()).toBe("bad_credentials"); await expect( c.send({ kind: "endpoint", id: "b" }, { enc: "utf8", data: "x" }), ).rejects.toMatchObject({ code: "bad_credentials" }); }); it("TestK00TakenOverReason", async () => { const fake = new FakeTransport(); const c = await connectFake(fake); let got = ""; c.onConnectionHandler((ev) => { if (ev.state === "kicked") got = ev.reason ?? ""; }); fake.simulateKick(); const deadline = Date.now() + 1000; while (Date.now() < deadline && got !== "taken_over") { await new Promise((r) => setTimeout(r, 5)); } expect(got).toBe("taken_over"); expect(c.lastStopCodeForTest()).toBe("taken_over"); }); it("TestK00Disconnect8BRetryable", async () => { const fake = new FakeTransport(); const c = await connectFake(fake); fake.simulateServerDisconnect(0x8b); await new Promise((r) => setTimeout(r, 30)); expect(c.lastStopCodeForTest()).not.toBe("taken_over"); await fake.simulateConnectOK(); await c.close(); }); it("TestK00QueueFull", async () => { const fake = new FakeTransport(); const c = await connectFake(fake, { sendQueueSize: 1 }); const p = c.send({ kind: "endpoint", id: "b" }, { enc: "utf8", data: "1" }); await new Promise((r) => setTimeout(r, 20)); await expect( c.send({ kind: "endpoint", id: "b" }, { enc: "utf8", data: "2" }), ).rejects.toMatchObject({ code: "queue_full" }); fake.replyOK(String(fake.findUp("send")[0]?.rid), { id: "x", state: "accepted" }); await p; await c.close(); }); it("TestK00RequestReturnsData", async () => { const fake = new FakeTransport(); const c = await connectFake(fake); const timer = setInterval(() => { for (const fr of fake.findUp("self.get")) { fake.replyOK(String(fr.rid), { id: "ep1", name: "n", default_delay_ms: 0 }); } }, 5); const info = (await c.getSelf()) as { id: string; name: string }; clearInterval(timer); expect(info.id).toBe("ep1"); expect(info.name).toBe("n"); await c.close(); }); it("TestK00SendAtAndDelayConflict", async () => { const fake = new FakeTransport(); const c = await connectFake(fake); await expect( c.send( { kind: "endpoint", id: "b" }, { enc: "utf8", data: "x" }, { sendAt: new Date(1), delayMs: 1000 }, ), ).rejects.toMatchObject({ code: "bad_request" }); await c.close(); }); it("TestK00SendAfterStopped", async () => { const fake = new FakeTransport(); const c = await connectFake(fake); fake.simulateKick(); await new Promise((r) => setTimeout(r, 30)); await expect( c.send({ kind: "endpoint", id: "b" }, { enc: "utf8", data: "x" }), ).rejects.toMatchObject({ code: "taken_over" }); }); it("TestK00LogoutReturnsError", async () => { const fake = new FakeTransport(); const c = await connectFake(fake); fake.simulateServerDisconnect(0x8b); await new Promise((r) => setTimeout(r, 20)); await expect(c.logout()).rejects.toMatchObject({ code: "not_connected" }); await expect( c.send({ kind: "endpoint", id: "b" }, { enc: "utf8", data: "x" }), ).rejects.toBeInstanceOf(APIError); }); it("TestK00DurationInt64", () => { const sd = JSON.parse(`{"id":"m1","send_at_ms":123,"state":"scheduled"}`) as { id: string; send_at_ms: number; state: string; }; expect(sd.id).toBe("m1"); expect(sd.send_at_ms).toBe(123); expect(sd.state).toBe("scheduled"); const ms = 30 * 24 * 3600 * 1000; expect(ms).toBe(2592000000); }); it("TestK00MaxReceiveBytesMin", async () => { const fake = new FakeTransport(); const c = new Client(); await expect( c.connect("ws://example.test/mqtt", "ep1", { password: "p" }, { transport: fake, maxReceiveBytes: 512, }), ).rejects.toMatchObject({ code: "bad_request" }); }); it("TestK00URLMapping", () => { const a = new URL(normalizeMQTTURL("https://host:7443/", false)); expect(a.protocol).toBe("wss:"); expect(a.pathname).toBe("/mqtt"); const b = new URL(normalizeMQTTURL("http://host/app", false)); expect(b.protocol).toBe("ws:"); expect(b.pathname).toBe("/app"); expect(() => normalizeMQTTURL("mqtt://host:1883", false)).toThrow(); expect(normalizeMQTTURL("mqtt://host:1883", true)).toContain("mqtt:"); }); it("TestK00CancelUnsent", async () => { const fake = new FakeTransport(); fake.autoHello = false; const c = new Client(); const connecting = c .connect("ws://example.test/mqtt", "ep1", { password: "p" }, { transport: fake, connectTimeoutMs: 2000, }) .catch(() => {}); await new Promise((r) => setTimeout(r, 40)); const ac = new AbortController(); const p = c.send({ kind: "endpoint", id: "b" }, { enc: "utf8", data: "x" }, { signal: ac.signal }); ac.abort(); await expect(p).rejects.toBeTruthy(); expect(c.resendPayloadForTest()).toBeUndefined(); await c.close(); await connecting; }); it("TestK00RateLimitedBackoff", async () => { disableJitterForTest(); const fake = new FakeTransport(); const c = await connectFake(fake); const rids: string[] = []; let id0 = ""; let sendAt: unknown; const timer = setInterval(() => { const sends = fake.findUp("send"); if (!sends.length) return; const last = sends[sends.length - 1]!; const rid = String(last.rid); if (!rids.length) { id0 = String(last.id); sendAt = last.send_at_ms; rids.push(rid); fake.replyErr(rid, "rate_limited", "slow"); return; } if (rid === rids[rids.length - 1]) return; rids.push(rid); if (rids.length < 3) { fake.replyErr(rid, "rate_limited", "slow"); return; } expect(last.id).toBe(id0); expect(last.send_at_ms).toBe(sendAt); fake.replyOK(rid, { id: id0, send_at_ms: sendAt, state: "scheduled" }); }, 5); const at = new Date(1_700_000_000_000); await c.send({ kind: "endpoint", id: "b" }, { enc: "utf8", data: "hi" }, { sendAt: at }); clearInterval(timer); expect(rids).toHaveLength(3); expect(new Set(rids).size).toBe(3); await c.close(); }, 10000); it("TestK00ReconnectBackoff", () => { const b = new ReconnectBackoff(); expect(b.nextWaitNoJitter()).toBe(0); const got: number[] = []; for (let i = 0; i < 6; i++) { b.markOffline(); got.push(b.nextWaitNoJitter()); } expect(got).toEqual([1000, 2000, 4000, 8000, 16000, 30000]); b.markOnline(); b.setOnlineAtForTest(Date.now()); b.markOffline(); expect(b.nextWaitNoJitter()).toBe(30000); const b2 = new ReconnectBackoff(); b2.nextWaitNoJitter(); b2.markOnline(); b2.setOnlineAtForTest(Date.now() - 61_000); b2.markOffline(); expect(b2.nextWaitNoJitter()).toBe(1000); }); it("TestK00KeepaliveDefault", () => { expect(DEFAULT_KEEPALIVE).toBe(30); }); it("TestK00NoReceiveMaximum", async () => { const fake = new FakeTransport(); const c = await connectFake(fake); const cs = fake.getConnects(); expect(cs.length).toBeGreaterThan(0); expect(cs[0]?.receiveMaximumSet).toBe(false); await c.close(); }); });