mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Host not connected, host shutting down and instance not running are now UnavailableError (503) instead of ConflictError. The request error migration notes move to a new 2.1 guide, since this ships in 2.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
830 lines
32 KiB
JavaScript
830 lines
32 KiB
JavaScript
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")
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|