"use strict"; // device.raise(): an event nobody asked for goes to /event, and one that would not arrive is refused. const { test } = require("node:test"); const assert = require("node:assert/strict"); const { Alex2MQTT, AlexaInterfaceType, DoorbellEventSource, MemoryPublisher, MessageError, PowerController, SceneController, SimpleEventSource, asset, topics, } = require("alex2node"); const { endpoint } = require("../helpers/endpoint.js"); const { doc } = require("../helpers/fixtures.js"); const { UUID_V4 } = require("../helpers/harness.js"); function doorbell() { const sent = new MemoryPublisher(); const errors = []; const bridge = new Alex2MQTT("u", "p", "root", false, { publisher: sent, log: () => {} }); bridge.on("error", (err) => errors.push(err)); const device = bridge.addDevice({ endpointId: "door-1", name: "Front door", categories: ["DOORBELL"] }); device.add(DoorbellEventSource); return { bridge, device, sent, errors }; } const button = (name, events) => ({ instance: `Remote.${name}`, friendlyNames: [asset(`Alexa.Button.${name}`)], supportedEvents: events.map((push) => ({ id: `Button.${push}.1`, friendlyNames: [asset(`Alexa.Button.${push}`)] })), }); const refused = (raise, problem) => assert.throws(raise, (err) => err instanceof MessageError && err.problem === problem); test("raise() publishes the event on /event, with the endpoint, a new messageId, the time and the usual cause", async () => { const { device, sent } = doorbell(); const printed = doc("alexa-doorbelleventsource", "DoorbellPress.event").event; const before = Date.now(); assert.deepEqual(await device.raise(DoorbellEventSource, "DoorbellPress"), { ok: true, topic: "root/event" }); await device.raise(DoorbellEventSource, "DoorbellPress", printed.payload); await device.raise(DoorbellEventSource, "DoorbellPress", { cause: { type: "APP_INTERACTION" } }, { messageId: "press-3" }); assert.equal(topics.event("root"), "root/event"); assert.deepEqual(sent.published.map(({ topic }) => topic), ["root/event", "root/event", "root/event"]); const [first, second, third] = sent.published.map(({ message }) => message.event); for (const event of [first, second, third]) { assert.deepEqual(event.endpoint, { endpointId: "door-1" }); const { messageId, ...header } = event.header; assert.deepEqual(header, { namespace: printed.header.namespace, name: printed.header.name, payloadVersion: printed.header.payloadVersion }); } assert.match(first.header.messageId, UUID_V4); assert.match(second.header.messageId, UUID_V4); assert.notEqual(first.header.messageId, second.header.messageId); assert.equal(third.header.messageId, "press-3"); assert.deepEqual(first.payload.cause, { type: "PHYSICAL_INTERACTION" }); assert.ok(Date.parse(first.payload.timestamp) >= before && Date.parse(first.payload.timestamp) <= Date.now(), first.payload.timestamp); assert.deepEqual(second.payload, printed.payload); assert.deepEqual(third.payload.cause, { type: "APP_INTERACTION" }); }); test("raise() for an instance: the Event of a button has the instance in its header and the version of the interface", async () => { const sent = new MemoryPublisher(); const remote = endpoint("remote-1", "Remote", ["REMOTE"]); remote.publisher = sent; const top = remote.add(SimpleEventSource, button("TopButton", ["SinglePush", "DoublePush"])); remote.add(SimpleEventSource, button("BottomButton", ["SinglePush"])); const printed = doc("alexa-simpleeventsource", "Event.event").event; await remote.raise(SimpleEventSource, "Event", printed.payload, { instance: top.instance, messageId: "push-1" }); assert.deepEqual(sent.published, [{ topic: "root/event", message: { event: { header: { namespace: "Alexa.SimpleEventSource", name: "Event", instance: "Remote.TopButton", messageId: "push-1", payloadVersion: "1.0" }, endpoint: { endpointId: "remote-1" }, payload: printed.payload, }, }, }]); assert.equal(printed.header.payloadVersion, "1.0"); const raised = "remote-1: Alexa.SimpleEventSource.Event was not raised: "; assert.throws(() => remote.raise(SimpleEventSource, "Event", { id: "Button.SinglePush.1" }), { name: "MessageError", message: `${raised}the device did not declare Alexa.SimpleEventSource. Declare it with add() first, and pass the instance that raises the event`, }); assert.throws(() => remote.raise(SimpleEventSource, "Event", { id: "Button.SinglePush.1" }, { instance: "Remote.LeftButton" }), { message: `${raised}the device did not declare the instance "Remote.LeftButton" of Alexa.SimpleEventSource. Declare it with add() first`, }); assert.throws(() => remote.raise(SimpleEventSource, "Event", {}, { instance: top.instance }), { message: `${raised}payload.id: expected a string of 1 or more characters, got nothing`, }); assert.equal(sent.published.length, 1); }); test("raise() refuses an event the device has no capability for, and one that would not arrive", () => { const { device, sent } = doorbell(); device.add(PowerController); const scene = endpoint("scene-1", "Evening", ["SCENE_TRIGGER"]); scene.setDescription("Evening scene"); scene.publisher = sent; scene.add(SceneController); const was = (name) => `${name} was not raised: `; refused(() => scene.raise(DoorbellEventSource, "DoorbellPress"), `${was("Alexa.DoorbellEventSource.DoorbellPress")}the device did not declare Alexa.DoorbellEventSource. Declare it with add() first`); refused(() => device.raise(DoorbellEventSource, "Knock"), `${was("Alexa.DoorbellEventSource.Knock")}it is not an event of the interface, which has DoorbellPress`); refused(() => device.raise(PowerController, "TurnedOn"), `${was("Alexa.PowerController.TurnedOn")}it is not an event of the interface, which has none`); refused( () => scene.raise(SceneController, "ActivationStarted"), `${was("Alexa.SceneController.ActivationStarted")}it answers a directive. Send it with respond() in the handler of the directive` ); refused( () => device.raise(DoorbellEventSource, "DoorbellPress", { timestamp: "just now" }), `${was("Alexa.DoorbellEventSource.DoorbellPress")}payload.timestamp: expected a UTC time like 2017-08-30T01:18:21Z, got "just now"` ); refused( () => device.raise(DoorbellEventSource, "DoorbellPress", { cause: { type: "KNOCK" } }), `${was("Alexa.DoorbellEventSource.DoorbellPress")}payload.cause.type: expected APP_INTERACTION | PERIODIC_POLL | PHYSICAL_INTERACTION | RULE_TRIGGER | VOICE_INTERACTION, got "KNOCK"` ); refused( () => device.raise(DoorbellEventSource, "DoorbellPress", { picture: "x".repeat(topics.EVENT_BYTES) }), `${was("Alexa.DoorbellEventSource.DoorbellPress")}it is 16296 bytes as JSON, Alex2MQTT takes 16000` ); assert.deepEqual(sent.published, []); }); test("raise() of a doorbell declared the 1.x way, of a device on no bridge and with a broker that is gone", async () => { const sent = new MemoryPublisher(); const bell = endpoint("door-2", "Back door", ["DOORBELL"]); bell.addCapability(AlexaInterfaceType.DOORBELL_EVENT_SOURCE); const reported = []; bell.onPublishError = (err) => reported.push(err.message); const nowhere = await bell.raise(DoorbellEventSource, "DoorbellPress"); assert.equal(nowhere.ok, false); assert.deepEqual(reported, ["nothing was published to root/event: the device is on no bridge, register it with addDevice() or registerDevice()"]); bell.publisher = sent; assert.equal((await bell.raise(DoorbellEventSource, "DoorbellPress")).ok, true); assert.equal(sent.published[0].message.event.endpoint.endpointId, "door-2"); const { device, sent: broker, errors } = doorbell(); broker.failWith = new Error("the broker is gone"); const result = await device.raise(DoorbellEventSource, "DoorbellPress"); assert.deepEqual([result.ok, result.topic, result.error.message], [false, "root/event", "the broker is gone"]); assert.deepEqual(errors.map((err) => err.message), ["the broker is gone"]); });