inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
353 lines (321 loc) • 11 kB
text/typescript
import { assertZWaveError, ZWaveErrorCodes } from "@zwave-js/core";
import { createTestingHost, TestingHost } from "@zwave-js/host";
import { FunctionType, MessageType } from "./Constants";
import type { INodeQuery } from "./INodeQuery";
import { Message, messageTypes } from "./Message";
describe("lib/message", () => {
let host: TestingHost;
beforeEach(() => {
host = createTestingHost();
});
describe("Message", () => {
it("should deserialize and serialize correctly", () => {
// actual messages from OZW
const okayMessages = [
Buffer.from([
0x01, 0x09, 0x00, 0x13, 0x03, 0x02, 0x00, 0x00, 0x25, 0x0b,
0xca,
]),
Buffer.from([0x01, 0x05, 0x00, 0x47, 0x04, 0x20, 0x99]),
Buffer.from([0x01, 0x06, 0x00, 0x46, 0x0c, 0x0d, 0x32, 0x8c]),
Buffer.from([
0x01, 0x0a, 0x00, 0x13, 0x03, 0x03, 0x8e, 0x02, 0x04, 0x25,
0x40, 0x0b,
]),
];
for (const original of okayMessages) {
const parsed = new Message(host, { data: original });
expect(parsed.serialize()).toEqual(original);
}
});
it("should serialize correctly when the payload is null", () => {
// synthetic message
const expected = Buffer.from([0x01, 0x03, 0x00, 0xff, 0x03]);
const message = new Message(host, {
type: MessageType.Request,
functionType: 0xff,
});
expect(message.serialize()).toEqual(expected);
});
it("should throw the correct error when parsing a faulty message", () => {
// fake messages to produce certain errors
const brokenMessages: [Buffer, string, ZWaveErrorCodes][] = [
// too short (<5 bytes)
[
Buffer.from([0x01, 0x02, 0x00, 0x00]),
"truncated",
ZWaveErrorCodes.PacketFormat_Truncated,
],
// no SOF
[
Buffer.from([0x00, 0x03, 0x00, 0x00, 0x00]),
"start with SOF",
ZWaveErrorCodes.PacketFormat_Invalid,
],
// too short for the provided data length
[
Buffer.from([0x01, 0x04, 0x00, 0x00, 0x00]),
"truncated",
ZWaveErrorCodes.PacketFormat_Truncated,
],
// invalid checksum
[
Buffer.from([0x01, 0x03, 0x00, 0x00, 0x00]),
"checksum",
ZWaveErrorCodes.PacketFormat_Checksum,
],
// invalid checksum (once more with a real packet)
[
Buffer.from([0x01, 0x05, 0x00, 0x47, 0x04, 0x20, 0x98]),
"checksum",
ZWaveErrorCodes.PacketFormat_Checksum,
],
];
for (const [message, msg, code] of brokenMessages) {
assertZWaveError(() => new Message(host, { data: message }), {
messageMatches: msg,
errorCode: code,
});
}
});
it("isComplete() should work correctly", () => {
// actual messages from OZW
const okayMessages = [
Buffer.from([
0x01, 0x09, 0x00, 0x13, 0x03, 0x02, 0x00, 0x00, 0x25, 0x0b,
0xca,
]),
Buffer.from([0x01, 0x05, 0x00, 0x47, 0x04, 0x20, 0x99]),
Buffer.from([0x01, 0x06, 0x00, 0x46, 0x0c, 0x0d, 0x32, 0x8c]),
Buffer.from([
0x01, 0x0a, 0x00, 0x13, 0x03, 0x03, 0x8e, 0x02, 0x04, 0x25,
0x40, 0x0b,
]),
];
for (const msg of okayMessages) {
expect(Message.isComplete(msg)).toBe(true); // `${msg.toString("hex")} should be detected as complete`
}
// truncated messages
const truncatedMessages = [
undefined,
Buffer.from([]),
Buffer.from([0x01]),
Buffer.from([0x01, 0x09]),
Buffer.from([0x01, 0x09, 0x00]),
Buffer.from([
0x01, 0x09, 0x00, 0x13, 0x03, 0x02, 0x00, 0x00, 0x25, 0x0b,
]),
];
for (const msg of truncatedMessages) {
expect(Message.isComplete(msg)).toBe(false); // `${msg ? msg.toString("hex") : "null"} should be detected as incomplete`
}
// faulty but non-truncated messages should be detected as complete
const faultyMessages = [
Buffer.from([
0x01, 0x09, 0x00, 0x13, 0x03, 0x02, 0x00, 0x00, 0x25, 0x0b,
0xca,
]),
Buffer.from([0x01, 0x05, 0x00, 0x47, 0x04, 0x20, 0x99]),
Buffer.from([0x01, 0x06, 0x00, 0x46, 0x0c, 0x0d, 0x32, 0x8c]),
Buffer.from([
0x01, 0x0a, 0x00, 0x13, 0x03, 0x03, 0x8e, 0x02, 0x04, 0x25,
0x40, 0x0b,
]),
];
for (const msg of faultyMessages) {
expect(Message.isComplete(msg)).toBe(true); // `${msg.toString("hex")} should be detected as complete`
}
// actual messages from OZW, appended with some random data
const tooLongMessages = [
Buffer.from([
0x01, 0x09, 0x00, 0x13, 0x03, 0x02, 0x00, 0x00, 0x25, 0x0b,
0xca, 0x00,
]),
Buffer.from([
0x01, 0x05, 0x00, 0x47, 0x04, 0x20, 0x99, 0x01, 0x02,
]),
Buffer.from([
0x01, 0x06, 0x00, 0x46, 0x0c, 0x0d, 0x32, 0x8c, 0xab, 0xcd,
0xef,
]),
Buffer.from([
0x01, 0x0a, 0x00, 0x13, 0x03, 0x03, 0x8e, 0x02, 0x04, 0x25,
0x40, 0x0b, 0x12,
]),
];
for (const msg of tooLongMessages) {
expect(Message.isComplete(msg)).toBe(true); // `${msg.toString("hex")} should be detected as complete`
}
});
it("toJSON() should return a semi-readable JSON representation", () => {
const msg1 = new Message(host, {
type: MessageType.Request,
functionType: FunctionType.GetControllerVersion,
});
const json1 = {
name: msg1.constructor.name,
type: "Request",
functionType: "GetControllerVersion",
payload: "",
};
const msg2 = new Message(host, {
type: MessageType.Request,
functionType: FunctionType.GetControllerVersion,
payload: Buffer.from("aabbcc", "hex"),
});
const json2 = {
name: msg2.constructor.name,
type: "Request",
functionType: "GetControllerVersion",
payload: "aabbcc",
};
const msg3 = new Message(host, {
type: MessageType.Response,
functionType: FunctionType.GetControllerVersion,
expectedResponse: FunctionType.GetControllerVersion,
});
const json3 = {
name: msg3.constructor.name,
type: "Response",
functionType: "GetControllerVersion",
expectedResponse: "GetControllerVersion",
payload: "",
};
const msg4 = new Message(host, {
type: MessageType.Request,
functionType: FunctionType.GetControllerVersion,
expectedResponse: FunctionType.GetControllerVersion,
payload: Buffer.from("aabbcc", "hex"),
});
const json4 = {
name: msg4.constructor.name,
type: "Request",
functionType: "GetControllerVersion",
expectedResponse: "GetControllerVersion",
payload: "aabbcc",
};
expect(msg1.toJSON()).toEqual(json1);
expect(msg2.toJSON()).toEqual(json2);
expect(msg3.toJSON()).toEqual(json3);
expect(msg4.toJSON()).toEqual(json4);
});
it("getConstructor() should return `Message` for an unknown packet type", () => {
const unknown = Buffer.from([0x01, 0x03, 0x00, 0x00, 0xfc]);
expect(Message.getConstructor(unknown)).toBe(Message);
});
// it(`when the expectedResponse is defined, testResponse() should return "final" or "unexpected"`, () => {
// const msg = new Message(fakeDriver, {
// type: MessageType.Request,
// functionType: 0xff,
// expectedResponse: FunctionType.ApplicationCommand,
// });
// const final = new Message(fakeDriver, {
// type: MessageType.Response,
// functionType: FunctionType.ApplicationCommand,
// });
// expect(msg.testResponse(final)).toBe("final");
// // wrong function type
// const unexpected1 = new Message(fakeDriver, {
// type: MessageType.Response,
// functionType: FunctionType.SendData,
// });
// expect(msg.testResponse(unexpected1)).toBe("unexpected");
// // not a response
// const unexpected2 = new Message(fakeDriver, {
// type: MessageType.Request,
// functionType: 0xff,
// });
// expect(msg.testResponse(unexpected2)).toBe("unexpected");
// });
// it(`when the message has a callbackId, testResponse() should return "unexpected" for requests that don't match it`, () => {
// const msg = new Message(fakeDriver, {
// type: MessageType.Request,
// functionType: 0xff,
// expectedCallback: FunctionType.GetSUCNodeId,
// callbackId: 5,
// });
// const final = new Message(fakeDriver, {
// type: MessageType.Request,
// functionType: FunctionType.GetSUCNodeId,
// callbackId: 5,
// });
// expect(msg.testResponse(final)).toBe("final");
// // wrong callback id
// const unexpected1 = new Message(fakeDriver, {
// type: MessageType.Request,
// functionType: FunctionType.GetSUCNodeId,
// callbackId: 4,
// });
// expect(msg.testResponse(unexpected1)).toBe("unexpected");
// // missing callback id
// const unexpected2 = new Message(fakeDriver, {
// type: MessageType.Request,
// functionType: FunctionType.GetSUCNodeId,
// });
// expect(msg.testResponse(unexpected2)).toBe("unexpected");
// // sanity check: the function type should still be checked
// const unexpected3 = new Message(fakeDriver, {
// type: MessageType.Request,
// functionType: FunctionType.RequestNodeInfo, // does not match
// callbackId: 5, // matches
// });
// expect(msg.testResponse(unexpected3)).toBe("unexpected");
// });
it(`the constructor should throw when no message type is specified`, () => {
assertZWaveError(() => new Message(host, { functionType: 0xff }), {
errorCode: ZWaveErrorCodes.Argument_Invalid,
messageMatches: /message type/i,
});
(undefined as any, 0xff)
class FakeMessageWithoutMessageType extends Message {}
assertZWaveError(() => new FakeMessageWithoutMessageType(host), {
errorCode: ZWaveErrorCodes.Argument_Invalid,
messageMatches: /message type/i,
});
});
it(`the constructor should throw when no function type is specified`, () => {
assertZWaveError(
() => new Message(host, { type: MessageType.Request }),
{
errorCode: ZWaveErrorCodes.Argument_Invalid,
messageMatches: /function type/i,
},
);
(MessageType.Request, undefined as any)
class FakeMessageWithoutFunctionType extends Message {}
assertZWaveError(() => new FakeMessageWithoutFunctionType(host), {
errorCode: ZWaveErrorCodes.Argument_Invalid,
messageMatches: /function type/i,
});
});
describe("getNodeUnsafe()", () => {
it("returns undefined when the controller is not initialized yet", () => {
const msg = new Message(host, {
type: MessageType.Request,
functionType: 0xff,
});
expect(msg.getNodeUnsafe(host)).toBeUndefined();
});
it("returns undefined when the message is no node query", () => {
const msg = new Message(host, {
type: MessageType.Request,
functionType: 0xff,
});
expect(msg.getNodeUnsafe(host)).toBeUndefined();
});
it("returns the associated node otherwise", () => {
host.nodes.set(1, {} as any);
const msg = new Message(host, {
type: MessageType.Request,
functionType: 0xff,
});
// This node exists
(msg as any as INodeQuery).nodeId = 1;
expect(msg.getNodeUnsafe(host)).toBe(host.nodes.get(1));
// This one does
(msg as any as INodeQuery).nodeId = 2;
expect(msg.getNodeUnsafe(host)).toBeUndefined();
});
});
});
});