You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
183 lines
4.6 KiB
183 lines
4.6 KiB
import type { Socket } from "node:net";
|
|
import { Duplex } from "node:stream";
|
|
import { Types, Utils } from "@meshtastic/core";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { runTransportContract } from "../../../tests/utils/transportContract.ts";
|
|
import { TransportNode } from "./transport.ts";
|
|
|
|
function isStatusEvent(
|
|
out: Types.DeviceOutput | undefined,
|
|
): out is Extract<Types.DeviceOutput, { type: "status" }> {
|
|
return !!out && (out as any).type === "status";
|
|
}
|
|
|
|
class FakeSocket extends Duplex {
|
|
public lastWritten: Uint8Array | undefined;
|
|
|
|
constructor() {
|
|
super({ objectMode: false });
|
|
}
|
|
|
|
_read() {}
|
|
|
|
_write(
|
|
chunk: Buffer,
|
|
_encoding: BufferEncoding,
|
|
callback: (error?: Error | null) => void,
|
|
) {
|
|
this.lastWritten = new Uint8Array(
|
|
chunk.buffer,
|
|
chunk.byteOffset,
|
|
chunk.byteLength,
|
|
);
|
|
callback();
|
|
}
|
|
|
|
pushIncoming(data: Uint8Array) {
|
|
const buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
this.push(buf);
|
|
}
|
|
|
|
emitErrorOnce(message = "simulated error") {
|
|
this.emit("error", new Error(message));
|
|
}
|
|
|
|
emitClose() {
|
|
this.emit("close");
|
|
}
|
|
|
|
override destroy(error?: Error) {
|
|
super.destroy(error);
|
|
this.emit("close");
|
|
return this;
|
|
}
|
|
}
|
|
|
|
function stubCoreTransforms() {
|
|
const toDevice = new TransformStream<Uint8Array, Uint8Array>({
|
|
transform(chunk, controller) {
|
|
controller.enqueue(chunk);
|
|
},
|
|
});
|
|
|
|
const fromDeviceFactory = () =>
|
|
new TransformStream<Uint8Array, Types.DeviceOutput>({
|
|
transform(chunk, controller) {
|
|
controller.enqueue({ type: "packet", data: chunk });
|
|
},
|
|
});
|
|
|
|
vi.spyOn(Utils, "toDeviceStream", "get").mockReturnValue(
|
|
toDevice as unknown as typeof Utils.toDeviceStream,
|
|
);
|
|
|
|
vi.spyOn(Utils, "fromDeviceStream").mockImplementation(
|
|
() =>
|
|
fromDeviceFactory() as unknown as TransformStream<
|
|
Uint8Array,
|
|
Types.DeviceOutput
|
|
>,
|
|
);
|
|
|
|
return {
|
|
restore: () => vi.restoreAllMocks(),
|
|
};
|
|
}
|
|
|
|
describe("TransportNode (contract)", () => {
|
|
let transformsStub: { restore: () => void } | undefined;
|
|
|
|
beforeEach(() => {
|
|
transformsStub = stubCoreTransforms();
|
|
});
|
|
|
|
afterEach(() => {
|
|
transformsStub?.restore();
|
|
});
|
|
|
|
runTransportContract({
|
|
name: "TransportNode",
|
|
setup: () => {},
|
|
teardown: () => {
|
|
vi.restoreAllMocks();
|
|
},
|
|
create: async () => {
|
|
const fakeSocket = new FakeSocket();
|
|
const transport = new TransportNode(fakeSocket as unknown as Socket);
|
|
await Promise.resolve();
|
|
(globalThis as unknown as { __nodeSock: FakeSocket }).__nodeSock =
|
|
fakeSocket;
|
|
return transport;
|
|
},
|
|
pushIncoming: async (bytes) => {
|
|
(
|
|
globalThis as unknown as { __nodeSock: FakeSocket }
|
|
).__nodeSock.pushIncoming(bytes);
|
|
await Promise.resolve();
|
|
},
|
|
assertLastWritten: (bytes) => {
|
|
const sock = (globalThis as unknown as { __nodeSock: FakeSocket })
|
|
.__nodeSock;
|
|
expect(sock.lastWritten).toBeDefined();
|
|
expect(sock.lastWritten).toEqual(bytes);
|
|
},
|
|
triggerDisconnect: async () => {
|
|
(
|
|
globalThis as unknown as { __nodeSock: FakeSocket }
|
|
).__nodeSock.emitErrorOnce("test-disconnect");
|
|
await Promise.resolve();
|
|
},
|
|
});
|
|
});
|
|
|
|
describe("TransportNode (extras)", () => {
|
|
let transformsStub: { restore: () => void } | undefined;
|
|
|
|
beforeEach(() => {
|
|
transformsStub = stubCoreTransforms();
|
|
});
|
|
|
|
afterEach(() => {
|
|
transformsStub?.restore();
|
|
});
|
|
|
|
it("emits DeviceDisconnected with reason 'socket-closed' on close event", async () => {
|
|
const fakeSocket = new FakeSocket();
|
|
const transport = new TransportNode(fakeSocket as unknown as Socket);
|
|
const reader = transport.fromDevice.getReader();
|
|
|
|
await Promise.resolve();
|
|
|
|
const first = await reader.read();
|
|
expect(isStatusEvent(first.value)).toBe(true);
|
|
if (isStatusEvent(first.value)) {
|
|
expect(first.value.data.status).toBe(
|
|
Types.DeviceStatusEnum.DeviceConnecting,
|
|
);
|
|
}
|
|
|
|
const second = await reader.read();
|
|
expect(isStatusEvent(second.value)).toBe(true);
|
|
if (isStatusEvent(second.value)) {
|
|
expect(second.value.data.status).toBe(
|
|
Types.DeviceStatusEnum.DeviceConnected,
|
|
);
|
|
}
|
|
|
|
fakeSocket.emitClose();
|
|
await Promise.resolve();
|
|
|
|
let sawClosed = false;
|
|
for (let i = 0; i < 6; i++) {
|
|
const { value } = await reader.read();
|
|
if (isStatusEvent(value) && value.data.reason === "socket-closed") {
|
|
sawClosed = true;
|
|
break;
|
|
}
|
|
}
|
|
expect(sawClosed).toBe(true);
|
|
|
|
reader.releaseLock();
|
|
await transport.disconnect();
|
|
});
|
|
});
|
|
|