Files
Hornwitser 123148e277 Fix pending request not deleted on response
Fix memory leak due to the recorded _pendingRequests entry for a request
not being deleted when the response for the request has been received.
2024-01-22 20:48:01 +01:00

529 lines
19 KiB
JavaScript

"use strict";
const assert = require("assert").strict;
const events = require("events");
const lib = require("@clusterio/lib");
const mock = require("../../mock");
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", undefined, "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",
response.data.code,
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 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(".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 throw on double registration", function() {
testLink.snoopEvent(SimpleEvent);
assert.throws(
() => testLink.snoopEvent(SimpleEvent),
new Error("Event SimpleEvent is already snooped")
);
});
});
});
});