Files
NixMsg/sdk/js/test/client.test.ts
T

153 lines
4.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { Client, FakeTransport, APIError, buildCleanConnectFlags, register } from "../src/index.js";
import { createServer } from "node:http";
async function connectFake(fake: FakeTransport): Promise<Client> {
const c = new Client();
await c.connect("ws://example.test/mqtt", "ep1", { password: "secret" }, { transport: fake });
return c;
}
describe("nixmsg sdk", () => {
it("Clean Start every connect", async () => {
const fake = new FakeTransport();
const c = await connectFake(fake);
await fake.simulateReconnect();
await fake.simulateReconnect();
const cs = fake.getConnects();
expect(cs.length).toBeGreaterThanOrEqual(3);
for (const x of cs) {
expect(x.cleanStart).toBe(true);
expect(x.sessionExpiry).toBe(0);
}
await c.close();
});
it("session token callback", async () => {
const fake = new FakeTransport();
fake.helloToken = "nst_abc";
const c = new Client();
let got = "";
c.onSessionHandler((t) => {
got = t;
});
await c.connect("ws://example.test/mqtt", "ep1", { password: "p" }, { transport: fake });
// 等待串行回调
await new Promise((r) => setTimeout(r, 20));
expect(got).toBe("nst_abc");
await c.close();
});
it("dedup then re-ack", async () => {
const fake = new FakeTransport();
const c = await connectFake(fake);
let calls = 0;
c.onMessageHandler(() => {
calls++;
});
const replyAcks = setInterval(() => {
for (const fr of fake.findUp("ack")) {
fake.replyOK(String(fr.rid), { result: "accepted" });
}
}, 5);
const msg = JSON.stringify({
v: 1,
type: "msg",
id: "m1",
from: "a",
to: { kind: "endpoint", id: "ep1" },
body: { enc: "utf8", data: "hi" },
send_at_ms: 1,
});
fake.injectDown(msg);
fake.injectDown(msg);
await new Promise((r) => setTimeout(r, 80));
fake.injectDown(msg);
await new Promise((r) => setTimeout(r, 80));
clearInterval(replyAcks);
expect(calls).toBe(1);
expect(fake.findUp("ack").length).toBeGreaterThanOrEqual(2);
await c.close();
});
it("body too large locally", async () => {
const fake = new FakeTransport();
fake.maxBodyBytes = 16;
const c = await connectFake(fake);
await expect(
c.send(
{ kind: "endpoint", id: "b" },
{ enc: "utf8", data: "x".repeat(64) },
),
).rejects.toMatchObject({ code: "body_too_large" });
await c.close();
});
it("resend keeps id and send_at_ms", async () => {
const fake = new FakeTransport();
const c = await connectFake(fake);
const at = new Date(1_700_000_000_000);
let firstId = "";
let firstSendAt: unknown;
let replied = false;
const timer = setInterval(() => {
const sends = fake.findUp("send");
if (!sends.length) return;
if (!replied) {
replied = true;
firstId = String(sends[0].id);
firstSendAt = sends[0].send_at_ms;
fake.replyErr(String(sends[0].rid), "rate_limited", "slow");
return;
}
if (sends.length >= 2) {
expect(sends[1].id).toBe(firstId);
expect(sends[1].send_at_ms).toBe(firstSendAt);
fake.replyOK(String(sends[1].rid), {
id: firstId,
send_at_ms: firstSendAt,
state: "scheduled",
});
clearInterval(timer);
}
}, 20);
const res = await c.send(
{ kind: "endpoint", id: "b" },
{ enc: "utf8", data: "hi" },
{ sendAt: at },
);
expect(res.id).toBe(firstId);
await c.close();
}, 10000);
it("register HTTP from ws url", async () => {
const srv = createServer((req, res) => {
expect(req.url).toBe("/api/client/register");
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ ok: true, data: { id: "e_1", login_password: "gen" } }));
});
await new Promise<void>((r) => srv.listen(0, "127.0.0.1", r));
const addr = srv.address();
if (!addr || typeof addr === "string") throw new Error("addr");
const ws = `ws://127.0.0.1:${addr.port}/mqtt`;
const res = await register(ws, "code", { name: "n" });
expect(res.id).toBe("e_1");
expect(res.loginPassword).toBe("gen");
await new Promise<void>((r) => srv.close(() => r()));
});
it("buildCleanConnectFlags", () => {
expect(buildCleanConnectFlags()).toEqual({ cleanStart: true, sessionExpiry: 0 });
});
it("APIError shape", () => {
const e = new APIError("body_too_large", "x");
expect(e.code).toBe("body_too_large");
});
});