import assert from "node:assert/strict"; import events from "node:events"; import * as lib from "@clusterio/lib"; import * as mock from "../../mock.js"; import "../../setup_logging.js"; const addr = lib.Address.fromShorthand; describe("lib/link/link", function() { function throwSimple(message) { let err = new Error(message); err.stack = message; throw err; } describe("class Link", function() { let testConnector; /** @type {lib.Link} */ let testLink; let src = addr({ controlId: 1 }); let dst = addr("controller"); class SimpleRequest { static type = "request"; static src = ["controller", "control"]; static dst = ["controller", "control"]; static permission = null; } lib.Link.register(SimpleRequest); class NumberRequest { static type = "request"; static src = ["controller", "control"]; static dst = ["controller", "control"]; static permission = null; constructor(value) { this.value = value; } static jsonSchema = { type: "number" }; toJSON() { return this.value; } static fromJSON(json) { return new this(json); } } NumberRequest.Response = class { constructor(value) { this.value = value; } static jsonSchema = { type: "number" }; toJSON() { return this.value; } static fromJSON(json) { return new this(json); } }; lib.Link.register(NumberRequest); class SimpleEvent { static type = "event"; static src = ["controller", "control"]; static dst = ["controller", "control"]; static permission = null; } lib.Link.register(SimpleEvent); class NumberEvent { static type = "event"; static src = ["controller", "control"]; static dst = ["controller", "control"]; static permission = null; constructor(value) { this.value = value; } static jsonSchema = { type: "number" }; toJSON() { return this.value; } static fromJSON(json) { return new this(json); } } lib.Link.register(NumberEvent); beforeEach(function() { testConnector = new mock.MockConnector(src, dst); testLink = new lib.Link(testConnector); }); it("should handle unknown message", async function() { testConnector.emit("message", { type: "unknown" }); }); describe("Request handling", function() { it("should give an error response back on unrecognized request", function() { testConnector.emit("message", new lib.MessageRequest(1, dst, src, "UnhandledRequest")); assert.deepEqual(testConnector.sentMessages, [ new lib.MessageResponseError( testConnector._seq - 1, src, dst, new lib.ResponseError("Unrecognized request UnhandledRequest", "InvalidMessage") ), ]); }); it("should give an error response back on unhandled request", function() { class UnhandledRequest { static type = "request"; static src = "controller"; static dst = "control"; } lib.Link.register(UnhandledRequest); testConnector.emit("message", new lib.MessageRequest(1, dst, src, "UnhandledRequest")); assert.deepEqual(testConnector.sentMessages, [ new lib.MessageResponseError( testConnector._seq - 1, src, dst, new lib.ResponseError("No handler for UnhandledRequest") ), ]); }); it("should give an error response back on invalid src", function() { class InvalidSrcRequest { static type = "request"; static src = "host"; static dst = "control"; } lib.Link.register(InvalidSrcRequest); testConnector.emit("message", new lib.MessageRequest(1, dst, src, "InvalidSrcRequest")); assert.deepEqual(testConnector.sentMessages, [ new lib.MessageResponseError( testConnector._seq - 1, src, dst, new lib.ResponseError( "Source [Address controller:0] is not allowed for InvalidSrcRequest", "InvalidMessage" ) ), ]); }); it("should give an error response back on invalid dst", function() { class InvalidDstRequest { static type = "request"; static src = "controller"; static dst = "host"; } lib.Link.register(InvalidDstRequest); testConnector.emit("message", new lib.MessageRequest(1, dst, src, "InvalidDstRequest")); assert.deepEqual(testConnector.sentMessages, [ new lib.MessageResponseError( testConnector._seq - 1, src, dst, new lib.ResponseError( "Destination [Address control:1] is not allowed for InvalidDstRequest", "InvalidMessage", ) ), ]); }); it("should give an error response back on broadcast src", function() { testConnector.emit("message", new lib.MessageRequest(1, addr("allControls"), src, "SimpleRequest")); assert.deepEqual(testConnector.sentMessages, [ new lib.MessageResponseError( testConnector._seq - 1, src, addr("allControls"), new lib.ResponseError("Message src may not be broadcast", "InvalidMessage") ), ]); }); it("should give an error response back on requst with broadcast dst", function() { testConnector.emit("message", new lib.MessageRequest(1, dst, addr("allControls"), "SimpleRequest")); assert.deepEqual(testConnector.sentMessages, [ new lib.MessageResponseError( testConnector._seq - 1, src, dst, new lib.ResponseError( "Destination [Address 4:control] is not allowed for SimpleRequest", "InvalidMessage", ) ), ]); }); }); describe("Event handling", function() { it("should ignore unrecognized event", function() { testConnector.emit("message", new lib.MessageEvent(1, dst, src, "UnhandledEvent")); assert.deepEqual(testConnector.sentMessages, []); }); it("should ignore unhandled event", function() { class UnhandledEvent { static type = "event"; static src = "controller"; static dst = "control"; } lib.Link.register(UnhandledEvent); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "UnhandledEvent")); assert.deepEqual(testConnector.sentMessages, []); }); it("should ignore invalid src", function() { class InvalidSrcEvent { static type = "event"; static src = "host"; static dst = "control"; } let handled = false; lib.Link.register(InvalidSrcEvent); testLink.snoopEvent(InvalidSrcEvent, async () => { handled = true; }); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "InvalidSrcEvent")); assert(!handled, "event was not ignored"); assert.deepEqual(testConnector.sentMessages, []); }); it("should ignore invalid dst", function() { class InvalidDstEvent { static type = "event"; static src = "controller"; static dst = "host"; } lib.Link.register(InvalidDstEvent); let handled = false; testLink.snoopEvent(InvalidDstEvent, async () => { handled = true; }); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "InvalidDstEvent")); testConnector.emit("message", new lib.MessageEvent(1, dst, addr("allControls"), "InvalidDstEvent")); assert(!handled, "event was not ignored"); assert.deepEqual(testConnector.sentMessages, []); }); it("should ignore broadcast src", function() { let handled = false; testLink.snoopEvent(SimpleEvent, async () => { handled = true; }); testConnector.emit("message", new lib.MessageEvent(1, addr("allControls"), src, "SimpleEvent")); assert(!handled, "event was not ignored"); assert.deepEqual(testConnector.sentMessages, []); }); it("should handle event with broadcast dst", function() { let handled = false; testLink.snoopEvent(SimpleEvent, async () => { handled = true; }); testConnector.emit("message", new lib.MessageEvent(1, dst, addr("allControls"), "SimpleEvent")); assert(handled, "event was ignored"); assert.deepEqual(testConnector.sentMessages, []); }); }); it("should send ready on connector prepareDisconnect", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); testConnector.emit("disconnectPrepare"); assert.deepEqual(await message, [new lib.MessageDisconnect("ready")]); }); it("should send ready on connector prepareDisconnect if an error occurs", async function() { testLink.prepareDisconnect = async () => { throwSimple("Error occured"); }; let message = events.once(testConnector, "send"); message.catch(() => {}); testConnector.emit("disconnectPrepare"); assert.deepEqual(await message, [new lib.MessageDisconnect("ready")]); }); it("should reject pending requests on close", async function() { let pending = testLink.send(new SimpleRequest()); pending.catch(() => {}); testConnector.emit("close"); await assert.rejects(pending, { message: "Session Closed" }); }); it("should reject pending requests on invalidate", async function() { let pending = testLink.send(new SimpleRequest()); pending.catch(() => {}); testConnector.emit("invalidate"); await assert.rejects(pending, { message: "Session Lost" }); }); describe(".send()", function() { it("should send request to the other side of the link", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); testLink.send(new SimpleRequest()); let srcReq = new lib.Address(lib.Address.control, 1, 1); assert.deepEqual( await message, [new lib.MessageRequest(1, srcReq, dst, "SimpleRequest", undefined)] ); }); it("should send request with data to the other side of the link", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); testLink.send(new NumberRequest(22)); let srcReq = new lib.Address(lib.Address.control, 1, 1); assert.deepEqual( await message, [new lib.MessageRequest(1, srcReq, dst, "NumberRequest", new NumberRequest(22))] ); }); it("should send request and resolve when response is received", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); let request = testLink.send(new SimpleRequest()); let srcReq = new lib.Address(lib.Address.control, 1, 1); testConnector.emit("message", new lib.MessageResponse(1, dst, srcReq)); await request; assert.deepEqual(testLink._pendingRequests, new Map()); }); it("should send request and reject with error when error response is received", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); let request = testLink.send(new SimpleRequest()); let srcReq = new lib.Address(lib.Address.control, 1, 1); testConnector.emit( "message", new lib.MessageResponseError( 1, dst, srcReq, new lib.ResponseError("Error") ) ); await assert.rejects( request, { message: "Error" } ); assert.deepEqual(testLink._pendingRequests, new Map()); }); it("should send request and resolve with data when response is received", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); let request = testLink.send(new NumberRequest(22)); let srcReq = new lib.Address(lib.Address.control, 1, 1); testConnector.emit("message", new lib.MessageResponse(1, dst, srcReq, 44)); assert.deepEqual(await request, new NumberRequest.Response(44)); assert.deepEqual(testLink._pendingRequests, new Map()); }); it("should send event to the other side of the link", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); testLink.send(new SimpleEvent()); assert.deepEqual( await message, [new lib.MessageEvent(1, src, dst, "SimpleEvent", undefined)] ); }); it("should send event with data to the other side of the link", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); testLink.send(new NumberEvent(22)); assert.deepEqual( await message, [new lib.MessageEvent(1, src, dst, "NumberEvent", new NumberEvent(22))] ); }); }); describe(".handle()", function() { it("should register a request handler", function() { let handled = false; testLink.handle(SimpleRequest, async () => { handled = true; }); assert(testLink._requestHandlers.has(SimpleRequest), "request handler was not registered"); testConnector.emit("message", new lib.MessageRequest(1, dst, src, "SimpleRequest")); assert(handled, "request was not handled"); }); it("should send response error from request handler throwing", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); testLink.handle(SimpleRequest, async () => { throwSimple("Error"); }); testConnector.emit("message", new lib.MessageRequest(1, dst, src, "SimpleRequest")); assert.deepEqual( await message, [new lib.MessageResponseError( 1, src, dst, new lib.ResponseError("Error", "InternalServerError", "Error") )] ); }); it("should send response error on request validation failing", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); testLink.handle(NumberRequest, async () => 1); testConnector.emit("message", new lib.MessageRequest(1, dst, src, "NumberRequest", "not a number")); let response = (await message)[0]; assert.deepEqual( response, new lib.MessageResponseError( 1, src, dst, new lib.ResponseError( "Request NumberRequest failed validation", "InvalidMessage", response.data.stack, ) ) ); }); it("should send response error on response validation failing", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); testLink.handle(NumberRequest, async () => "not a number"); testConnector.emit("message", new lib.MessageRequest(1, dst, src, "NumberRequest", 1)); let response = (await message)[0]; assert.deepEqual( response, new lib.MessageResponseError( 1, src, dst, new lib.ResponseError( "Response for request NumberRequest failed validation", response.data.code, response.data.stack, ) ) ); }); it("should send value returned from request handler", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); testLink.handle(NumberRequest, async (request) => request.value + 4); testConnector.emit("message", new lib.MessageRequest(1, dst, src, "NumberRequest", 1)); let response = (await message)[0]; assert.deepEqual( response, new lib.MessageResponse(1, src, dst, 5) ); }); it("should register an event handler", function() { let handled = false; testLink.handle(SimpleEvent, async () => { handled = true; }); assert(testLink._eventHandlers.has(SimpleEvent), "event handler was not registered"); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "SimpleEvent")); assert(handled, "event was not handled"); }); it("should pass value to event handler", async function() { let value; testLink.handle(NumberEvent, async (event) => { value = event.value; }); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "NumberEvent", 9)); assert.deepEqual(value, 9); }); it("should log errors from event handler", function() { testLink.handle(SimpleEvent, async () => { throwSimple("Error"); }); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "SimpleEvent")); }); it("should log validation errors from event handler", async function() { let logged = []; const originalError = lib.logger.error; lib.logger.error = msg => { logged.push(msg); }; try { testLink.handle(SimpleEvent, async () => { testLink.sendEvent(new NumberEvent("not a number"), dst); }); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "SimpleEvent")); await new Promise(resolve => setImmediate(resolve)); } finally { lib.logger.error = originalError; } assert.equal(logged.length, 1); assert(logged[0].startsWith( "Unexpected error handling SimpleEvent:\nError: Event NumberEvent failed validation\n" )); assert(logged[0].includes('"message": "must be number"')); }); it("should fall back to message when event handler error has no stack", async function() { let logged = []; const originalError = lib.logger.error; lib.logger.error = msg => { logged.push(msg); }; try { testLink.handle(SimpleEvent, async () => { let err = new Error("no stack"); err.stack = undefined; throw err; }); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "SimpleEvent")); await new Promise(resolve => setImmediate(resolve)); } finally { lib.logger.error = originalError; } assert.deepEqual(logged, ["Unexpected error handling SimpleEvent:\nno stack"]); }); it("should throw on unknown type", function() { assert.throws( () => testLink.handle({ name: "Bad", type: "bad" }), { message: "Class Bad has unrecognized type bad" } ); }); it("should throw on double registration", function() { testLink.handle(SimpleRequest); assert.throws( () => testLink.handle(SimpleRequest), new Error("Request SimpleRequest is already registered") ); testLink.handle(SimpleEvent); assert.throws( () => testLink.handle(SimpleEvent), new Error("Event SimpleEvent is already registered") ); }); }); describe("static .register()", function() { it("should throw if Request has only one of jsonSchema and fromJSON", function() { class BadRequest1 { static type = "request"; static src = "controller"; static dst = "control"; static jsonSchema = {}; } assert.throws( () => lib.Link.register(BadRequest1), new Error("Request BadRequest1 has static jsonSchema but is missing static fromJSON") ); class BadRequest2 { static type = "request"; static src = "controller"; static dst = "control"; static fromJSON() {}; } assert.throws( () => lib.Link.register(BadRequest2), new Error("Request BadRequest2 has static fromJSON but is missing static jsonSchema") ); }); it("should throw if Event has only one of jsonSchema and fromJSON", function() { class BadEvent1 { static type = "event"; static src = "controller"; static dst = "control"; static jsonSchema = {}; } assert.throws( () => lib.Link.register(BadEvent1), new Error("Event BadEvent1 has static jsonSchema but is missing static fromJSON") ); class BadEvent2 { static type = "event"; static src = "controller"; static dst = "control"; static fromJSON() {}; } assert.throws( () => lib.Link.register(BadEvent2), new Error("Event BadEvent2 has static fromJSON but is missing static jsonSchema") ); }); }); describe("._processMessage()", function() { it("should throw on unhandled type", function() { assert.throws( () => testLink._processMessage({ type: "invalid" }), { message: "Unhandled message type invalid" } ); }); it("should throw on Event failing validation", function() { class StringEvent { static type = "event"; static src = "controller"; static dst = "control"; constructor(value) { this.value = value; } static jsonSchema = { type: "string" }; static fromJSON(json) { return new this(json); }; } lib.Link.register(StringEvent); testLink.handle(StringEvent, () => {}); assert.throws( () => testLink._processMessage(new lib.MessageEvent(1, dst, src, "StringEvent", 99)), { message: "Event StringEvent failed validation" } ); }); describe("permission checking", function() { // A control connection has connector.dst.type === control, which is // what gates permission checking, see link.js _processMessage. function controlSetup() { const connector = new mock.MockConnector(dst, src); const link = new lib.Link(connector); const message = new lib.MessageRequest(1, src, dst, "SimpleRequest"); return { connector, link, message }; } it("should silently deny on ClientError from the permission check", function() { const { connector, link, message } = controlSetup(); let handled = false; let errored = false; connector.on("error", () => { errored = true; }); link.handle(SimpleRequest, async () => { handled = true; }); link.validatePermission = () => { throw new lib.PermissionError("Denied"); }; connector.emit("message", message); assert(!handled, "handler ran despite denial"); assert(!errored, "connector emitted error on denial"); assert.deepEqual(connector.sentMessages, [], "denial sent a message"); }); it("should not crash when the permission check throws a non-ClientError", function() { const { connector, link, message } = controlSetup(); let handled = false; let errored = false; const logged = []; const originalError = lib.logger.error; lib.logger.error = msg => { logged.push(msg); }; connector.on("error", () => { errored = true; }); try { link.handle(SimpleRequest, async () => { handled = true; }); link.validatePermission = () => { throwSimple("check is broken"); }; connector.emit("message", message); } finally { lib.logger.error = originalError; } assert(!errored, "non-ClientError escaped to the connector error path"); assert(!handled, "handler ran after the permission check threw"); assert.equal(logged.length, 1); assert(logged[0].startsWith("Unexpected error checking permission for SimpleRequest:\n")); }); }); describe("Broadcast handling", function() { // A broadcast is addressed to every link of the type it targets, // so a link it reaches has to pass it on as well as handle it, // see #575. function mockRouter(record) { return { forwardMessage: () => { record.routed = true; return true; } }; } it("should route a broadcast addressed to this link as well as handle it", function() { const record = {}; let handled = false; testLink.router = mockRouter(record); testLink.handle(SimpleEvent, async () => { handled = true; }); testConnector.emit("message", new lib.MessageEvent(1, dst, addr("allControls"), "SimpleEvent")); assert(record.routed, "broadcast was not routed onwards"); assert(handled, "broadcast was not handled locally"); }); it("should handle a broadcast on a link which does not route", function() { // Instances have no router, so routing unconditionally would // throw here and the event would never be handled. let handled = false; testLink.handle(SimpleEvent, async () => { handled = true; }); testConnector.emit("message", new lib.MessageEvent(1, dst, addr("allControls"), "SimpleEvent")); assert(handled, "broadcast was not handled locally"); }); it("should not route a non broadcast addressed to this link", function() { const record = {}; testLink.router = mockRouter(record); testLink.handle(SimpleEvent, async () => {}); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "SimpleEvent")); assert(!record.routed, "message addressed to this link was routed onwards"); }); }); }); describe(".forwardRequest()", function() { // A request is recorded so that its response can be sent back the // way it came. If it never left there is nothing to send back, and // leaving it recorded has it answered twice, see #650. function forwardingSetup() { const originConnector = new mock.MockConnector(dst, src); const origin = new lib.Link(originConnector); const nextHopConnector = new mock.MockConnector(src, dst); const nextHop = new lib.Link(nextHopConnector); const message = new lib.MessageRequest(1, src, dst, "SimpleRequest"); return { origin, originConnector, nextHop, nextHopConnector, message }; } it("should record a request it forwarded", function() { const { origin, nextHop, nextHopConnector, message } = forwardingSetup(); nextHop.forwardRequest(message, origin); assert.equal(nextHop.pendingRequestCount, 1); assert.equal(nextHopConnector.sentMessages.length, 1); }); it("should not record a request it failed to forward", function() { const { origin, nextHop, nextHopConnector, message } = forwardingSetup(); nextHopConnector.send = () => throwSimple("Session Closed"); assert.throws(() => nextHop.forwardRequest(message, origin), { message: "Session Closed" }); assert.equal(nextHop.pendingRequestCount, 0, "failed forward was left pending"); }); it("should not answer a request it failed to forward a second time", function() { const { origin, originConnector, nextHop, nextHopConnector, message } = forwardingSetup(); nextHopConnector.send = () => throwSimple("Session Closed"); // Routers answer the origin themselves when forwarding throws. assert.throws(() => nextHop.forwardRequest(message, origin), { message: "Session Closed" }); originConnector.sendResponseError(new lib.ResponseError("Session Closed"), message.src); const answers = originConnector.sentMessages.length; nextHop._clearPendingRequests(new lib.SessionLost("Session Lost")); assert.equal(originConnector.sentMessages.length, answers, "request was answered twice"); }); }); describe("Error responses", function() { class ErrorTestRequest { static type = "request"; static src = "control"; static dst = "host"; static permission = null; } lib.Link.register(ErrorTestRequest); class TestPluginError extends lib.ConflictError { static plugin = "error_test"; } lib.registerPluginMessages([{ name: "error_test", errors: [TestPluginError] }]); // control 1 <-> controller <-> host 1, with the controller forwarding between the two function forwardingSetup() { const [clientConnector, controlConnector] = lib.VirtualConnector.makePair( addr({ controlId: 1 }), addr("controller") ); const [hostSideConnector, hostConnector] = lib.VirtualConnector.makePair( addr("controller"), addr({ hostId: 1 }) ); const client = new lib.Link(clientConnector); const controlSide = new lib.Link(controlConnector); const hostSide = new lib.Link(hostSideConnector); const host = new lib.Link(hostConnector); controlSide.router = { forwardMessage(origin, message) { hostSide.forwardRequest(message, origin); return true; } }; hostSide.router = { forwardMessage(origin, message) { controlSide.connector.forward(message); return true; } }; return { client, host }; } async function rejectionFor(err) { const { client, host } = forwardingSetup(); host.handle(ErrorTestRequest, async () => { throw err; }); const logged = []; const originalError = lib.logger.error; lib.logger.error = msg => { logged.push(msg); }; try { await client.sendTo({ hostId: 1 }, new ErrorTestRequest()); } catch (received) { return { received, logged }; } finally { lib.logger.error = originalError; } throw new Error("Request did not reject"); } it("should reject with the class thrown by the handler", async function() { const err = new lib.NotFoundError("Thing does not exist"); const { received, logged } = await rejectionFor(err); assert(received instanceof lib.NotFoundError); assert.equal(received.message, "Thing does not exist"); assert.equal(received.stack, err.stack); assert.deepEqual(logged, []); }); it("should reject with a registered plugin error class", async function() { const { received, logged } = await rejectionFor(new TestPluginError("Plugin conflict")); assert(received instanceof TestPluginError); assert.equal(received.message, "Plugin conflict"); assert.deepEqual(logged, []); }); it("should reject with the closest registered class of an unregistered error", async function() { class UnregisteredError extends lib.ConflictError { } const { received } = await rejectionFor(new UnregisteredError("Busy")); assert.equal(received.constructor, lib.ConflictError); }); it("should reject with InternalServerError and log for unregistered errors", async function() { const err = new TypeError("x is not a function"); const { received, logged } = await rejectionFor(err); assert(received instanceof lib.InternalServerError); assert.equal(received.message, "x is not a function"); assert.equal(received.stack, err.stack); assert.equal(logged.length, 1); assert(logged[0].startsWith("Unexpected error responding to ErrorTestRequest:\n")); }); it("should reject with InternalServerError for an unknown error name", async function() { const received = lib.Link.errorFromResponse(new lib.ResponseError("Gone", "other_plugin:GoneError")); assert(received instanceof lib.InternalServerError); assert.equal(received.message, "Gone"); }); it("should require plugin errors to set the plugin property", function() { class MissingPluginError extends lib.ClientError { } assert.throws( () => lib.registerPluginMessages([{ name: "error_test_2", errors: [MissingPluginError] }]), new Error( "Expected error MissingPluginError from error_test_2 to have a static plugin property " + "set to \"error_test_2\"" ) ); }); it("should throw when registering an error name twice", function() { assert.throws( () => lib.Link.registerError(lib.NotFoundError), new Error("Error NotFoundError is already registered") ); }); }); describe(".snoopEvent()", function() { it("should snoop an event", function() { let handled = false; testLink.snoopEvent(SimpleEvent, async () => { handled = true; }); assert(testLink._eventSnoopers.has(SimpleEvent), "event was not snooped"); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "SimpleEvent")); assert(handled, "event was not handled"); }); it("should log errors from snoop handler", function() { testLink.snoopEvent(SimpleEvent, async () => { throwSimple("Error"); }); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "SimpleEvent")); }); it("should log validation errors from snoop handler", async function() { let logged = []; const originalError = lib.logger.error; lib.logger.error = msg => { logged.push(msg); }; try { testLink.handle(SimpleEvent, async () => {}); testLink.snoopEvent(SimpleEvent, async () => { testLink.sendEvent(new NumberEvent("not a number"), dst); }); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "SimpleEvent")); await new Promise(resolve => setImmediate(resolve)); } finally { lib.logger.error = originalError; } assert.equal(logged.length, 1); assert(logged[0].startsWith( "Unexpected error snooping SimpleEvent:\nError: Event NumberEvent failed validation\n" )); assert(logged[0].includes('"message": "must be number"')); }); it("should fall back to message when snoop handler error has no stack", async function() { let logged = []; const originalError = lib.logger.error; lib.logger.error = msg => { logged.push(msg); }; try { testLink.handle(SimpleEvent, async () => {}); testLink.snoopEvent(SimpleEvent, async () => { let err = new Error("no stack"); err.stack = undefined; throw err; }); testConnector.emit("message", new lib.MessageEvent(1, dst, src, "SimpleEvent")); await new Promise(resolve => setImmediate(resolve)); } finally { lib.logger.error = originalError; } assert.deepEqual(logged, ["Unexpected error snooping SimpleEvent:\nno stack"]); }); it("should throw on double registration", function() { testLink.snoopEvent(SimpleEvent); assert.throws( () => testLink.snoopEvent(SimpleEvent), new Error("Event SimpleEvent is already snooped") ); }); }); }); });