moleculer
Version:
Fast & powerful microservices framework for Node.JS
1,508 lines (1,205 loc) • 47.2 kB
JavaScript
;
const path = require("path");
const Promise = require("bluebird");
const lolex = require("lolex");
const ServiceBroker = require("../../src/service-broker");
const Service = require("../../src/service");
const ServiceRegistry = require("../../src/service-registry");
const Context = require("../../src/context");
const Transit = require("../../src/transit");
const MemoryCacher = require("../../src/cachers/memory");
const JSONSerializer = require("../../src/serializers/json");
const FakeTransporter = require("../../src/transporters/fake");
const { MoleculerError, ServiceNotFoundError, RequestTimeoutError, MaxCallLevelError } = require("../../src/errors");
jest.mock("../../src/utils", () => ({
getNodeID() { return "node-1234"; },
generateToken() { return "1"; }
}));
// Registry strategies
const { STRATEGY_ROUND_ROBIN, STRATEGY_RANDOM } = require("../../src/constants");
describe("Test ServiceBroker constructor", () => {
it("should set default options", () => {
let broker = new ServiceBroker();
expect(broker).toBeDefined();
expect(broker.options).toEqual({
nodeID: null,
logger: null,
logLevel: "info",
transporter: null,
requestTimeout: 0,
requestRetry: 0,
maxCallLevel: 0,
heartbeatInterval: 10,
heartbeatTimeout : 30,
registry: {
strategy: STRATEGY_ROUND_ROBIN,
preferLocal: true
},
circuitBreaker: {
enabled: false,
maxFailures: 5,
halfOpenTime: 10 * 1000,
failureOnTimeout: true,
failureOnReject: true
},
cacher: null,
serializer: null,
validation: true,
metrics: false,
metricsRate: 1,
statistics: false,
internalActions: true
});
expect(broker.Promise).toBe(Promise);
expect(broker.ServiceFactory).toBe(Service);
expect(broker.ContextFactory).toBe(Context);
expect(broker.nodeID).toBe("node-1234");
expect(broker.logger).toBeDefined();
expect(broker.bus).toBeDefined();
expect(broker.services).toBeInstanceOf(Array);
expect(broker.serviceRegistry).toBeInstanceOf(ServiceRegistry);
expect(broker.middlewares).toBeInstanceOf(Array);
expect(broker.cacher).toBeNull();
expect(broker.serializer).toBeInstanceOf(JSONSerializer);
expect(broker.validator).toBeDefined();
expect(broker.transit).toBeUndefined();
expect(broker.statistics).toBeUndefined();
expect(broker.hasAction("$node.list")).toBe(true);
expect(broker.hasAction("$node.services")).toBe(true);
expect(broker.hasAction("$node.actions")).toBe(true);
expect(broker.hasAction("$node.health")).toBe(true);
});
it("should merge options", () => {
let broker = new ServiceBroker( {
heartbeatTimeout: 20,
metrics: true,
metricsRate: 0.5,
statistics: true,
logLevel: "debug",
requestRetry: 3,
requestTimeout: 5000,
maxCallLevel: 10,
registry: {
strategy: STRATEGY_RANDOM,
preferLocal: false
},
circuitBreaker: {
enabled: true,
maxFailures: 2,
failureOnReject: false
},
validation: false,
internalActions: false });
expect(broker).toBeDefined();
expect(broker.options).toEqual({
nodeID: null,
logger: null,
logLevel: "debug",
cacher: null,
serializer: null,
transporter: null,
metrics: true,
metricsRate: 0.5,
statistics: true,
heartbeatTimeout : 20,
heartbeatInterval: 10,
registry: {
strategy: STRATEGY_RANDOM,
preferLocal: false
},
circuitBreaker: {
enabled: true,
maxFailures: 2,
halfOpenTime: 10 * 1000,
failureOnTimeout: true,
failureOnReject: false
},
requestRetry: 3,
requestTimeout: 5000,
maxCallLevel: 10,
validation: false,
internalActions: false });
expect(broker.services).toBeInstanceOf(Array);
expect(broker.serviceRegistry).toBeInstanceOf(ServiceRegistry);
expect(broker.transit).toBeUndefined();
expect(broker.statistics).toBeDefined();
expect(broker.validator).toBeUndefined();
expect(broker.serializer).toBeInstanceOf(JSONSerializer);
expect(broker.nodeID).toBe("node-1234");
expect(broker.hasAction("$node.list")).toBe(false);
expect(broker.hasAction("$node.services")).toBe(false);
expect(broker.hasAction("$node.actions")).toBe(false);
expect(broker.hasAction("$node.health")).toBe(false);
});
it("should create transit if transporter into options", () => {
let broker = new ServiceBroker( {
transporter: new FakeTransporter()
});
expect(broker).toBeDefined();
expect(broker.transit).toBeInstanceOf(Transit);
expect(broker.nodeID).toBe("node-1234");
});
it("should create cacher and call init", () => {
let cacher = new MemoryCacher();
cacher.init = jest.fn();
let broker = new ServiceBroker( {
cacher
});
expect(broker).toBeDefined();
expect(broker.cacher).toBe(cacher);
expect(cacher.init).toHaveBeenCalledTimes(1);
expect(cacher.init).toHaveBeenCalledWith(broker);
});
it("should set serializer and call init", () => {
let serializer = new JSONSerializer();
serializer.init = jest.fn();
let broker = new ServiceBroker( {
serializer
});
expect(broker).toBeDefined();
expect(broker.serializer).toBe(serializer);
expect(serializer.init).toHaveBeenCalledTimes(1);
expect(serializer.init).toHaveBeenCalledWith(broker);
});
});
describe("Test broker.start", () => {
let broker;
let schema = {
name: "test",
started: jest.fn()
};
beforeAll(() => {
broker = new ServiceBroker({
metrics: true,
statistics: true,
transporter: new FakeTransporter()
});
broker.createService(schema);
broker.transit.connect = jest.fn(() => Promise.resolve());
return broker.start();
});
it("should call started of services", () => {
expect(schema.started).toHaveBeenCalledTimes(1);
});
it("should call transit.connect", () => {
expect(broker.transit.connect).toHaveBeenCalledTimes(1);
});
});
describe("Test broker.stop", () => {
let broker;
let schema = {
name: "test",
stopped: jest.fn()
};
beforeAll(() => {
broker = new ServiceBroker({
metrics: true,
statistics: true,
transporter: new FakeTransporter()
});
broker.createService(schema);
broker.transit.connect = jest.fn(() => Promise.resolve());
broker.transit.disconnect = jest.fn(() => Promise.resolve());
return broker.start().then(() => broker.stop());
});
it("should call stopped of services", () => {
expect(schema.stopped).toHaveBeenCalledTimes(1);
});
it("should disconnect transit", () => {
expect(broker.transit.disconnect).toHaveBeenCalledTimes(1);
});
});
describe("Test broker.getLogger", () => {
let broker = new ServiceBroker();
let logger1 = broker.getLogger("test");
let logger2 = broker.getLogger("other");
let logger3 = broker.getLogger("test");
expect(logger1).not.toBe(logger2);
expect(logger1).toBe(logger3);
expect(Object.keys(broker._loggerCache).length).toBe(2 + 1); // +1 logger of broker
});
describe("Test loadServices", () => {
let broker = new ServiceBroker();
broker.loadService = jest.fn();
it("should load 3 services", () => {
let count = broker.loadServices("./test/services");
expect(count).toBe(3);
expect(broker.loadService).toHaveBeenCalledTimes(3);
expect(broker.loadService).toHaveBeenCalledWith("test/services/user.service.js");
expect(broker.loadService).toHaveBeenCalledWith("test/services/post.service.js");
expect(broker.loadService).toHaveBeenCalledWith("test/services/math.service.js");
});
it("should load 1 services", () => {
broker.loadService.mockClear();
let count = broker.loadServices("./test/services", "user.*.js");
expect(count).toBe(1);
expect(broker.loadService).toHaveBeenCalledTimes(1);
expect(broker.loadService).toHaveBeenCalledWith("test/services/user.service.js");
});
it("should load 0 services", () => {
broker.loadService.mockClear();
let count = broker.loadServices();
expect(count).toBe(0);
expect(broker.loadService).toHaveBeenCalledTimes(0);
});
it("should load selected services", () => {
broker.loadService.mockClear();
let count = broker.loadServices("./test/services", ["user.service", "math.service"]);
expect(count).toBe(2);
expect(broker.loadService).toHaveBeenCalledTimes(2);
expect(broker.loadService).toHaveBeenCalledWith(path.join("test", "services", "user.service"));
expect(broker.loadService).toHaveBeenCalledWith(path.join("test", "services", "math.service"));
});
});
describe("Test broker.loadService", () => {
let broker = new ServiceBroker();
broker.createService = jest.fn(svc => svc);
it("should load service from schema", () => {
// Load schema
let service = broker.loadService("./test/services/math.service.js");
expect(service).toBeDefined();
expect(broker.createService).toHaveBeenCalledTimes(1);
//expect(broker.createService).toHaveBeenCalledWith({ name: "math" });
});
it("should call function which returns Service instance", () => {
broker.createService.mockClear();
let service = broker.loadService("./test/services/user.service.js");
expect(service).toBeInstanceOf(Service);
expect(broker.createService).toHaveBeenCalledTimes(0);
});
it("should call function which returns schema", () => {
broker.createService.mockClear();
let service = broker.loadService("./test/services/post.service.js");
expect(service).toBeDefined();
expect(broker.createService).toHaveBeenCalledTimes(1);
});
});
describe("Test broker.createService", () => {
let broker = new ServiceBroker();
broker.ServiceFactory = jest.fn((broker, schema) => schema);
it("should load math service", () => {
let schema = {
name: "test",
actions: {
empty() {}
}
};
let service = broker.createService(schema);
expect(service).toBe(schema);
expect(broker.ServiceFactory).toHaveBeenCalledTimes(1);
expect(broker.ServiceFactory).toHaveBeenCalledWith(broker, schema);
});
it("should call mergeSchema if give schema mods param", () => {
let utils = require("../../src/utils");
utils.mergeSchemas = jest.fn((s1, s2) => s1);
let schema = {
name: "test",
actions: {
empty() {}
}
};
let mods = {
name: "other",
version: 2
};
broker.createService(schema, mods);
expect(utils.mergeSchemas).toHaveBeenCalledTimes(1);
expect(utils.mergeSchemas).toHaveBeenCalledWith(schema, mods);
});
});
describe("Test broker.registerLocalService", () => {
let broker = new ServiceBroker();
broker.emitLocal = jest.fn();
broker.serviceRegistry.registerService = jest.fn();
it("should push to the services & call service registry", () => {
let service = {
name: "test"
};
broker.registerLocalService(service);
expect(broker.services.length).toBe(1);
expect(broker.services[0]).toBe(service);
expect(broker.serviceRegistry.registerService).toHaveBeenCalledTimes(1);
expect(broker.serviceRegistry.registerService).toHaveBeenCalledWith(null, service);
});
});
describe("Test broker.registerRemoteService", () => {
let broker = new ServiceBroker();
broker.serviceRegistry.registerService = jest.fn();
broker.registerAction = jest.fn();
it("should save service & actions", () => {
let service = {
name: "test",
actions: {
hello: {
name: "test.hello",
params: {}
},
hi: {
name: "test.hi"
}
}
};
broker.registerRemoteService("node-2", service);
expect(broker.services.length).toBe(0);
expect(broker.serviceRegistry.registerService).toHaveBeenCalledTimes(1);
expect(broker.serviceRegistry.registerService).toHaveBeenCalledWith("node-2", service);
expect(broker.registerAction).toHaveBeenCalledTimes(2);
expect(broker.registerAction).toHaveBeenCalledWith("node-2", { name: "test.hello", service, params: {} });
expect(broker.registerAction).toHaveBeenCalledWith("node-2", { name: "test.hi", service });
});
});
describe("Test broker.registerAction", () => {
let broker = new ServiceBroker();
broker.wrapAction = jest.fn();
broker.emitLocal = jest.fn();
broker.serviceRegistry.registerAction = jest.fn(() => true);
it("should push to the actions & emit an event without nodeID", () => {
let action = {
name: "user.list",
service: { name: "user" }
};
broker.registerAction(null, action);
expect(broker.wrapAction).toHaveBeenCalledTimes(1);
expect(broker.serviceRegistry.registerAction).toHaveBeenCalledTimes(1);
expect(broker.serviceRegistry.registerAction).toHaveBeenCalledWith(null, action);
// expect(broker.emitLocal).toHaveBeenCalledTimes(1);
// expect(broker.emitLocal).toHaveBeenCalledWith("register.action.user.list", { action, nodeID: null });
});
it("should push to the actions & emit an event with nodeID", () => {
broker.serviceRegistry.registerAction.mockClear();
broker.wrapAction.mockClear();
broker.emitLocal.mockClear();
let action = {
name: "user.update",
service: { name: "user" }
};
broker.registerAction("server-2", action);
expect(broker.wrapAction).toHaveBeenCalledTimes(0);
expect(broker.serviceRegistry.registerAction).toHaveBeenCalledTimes(1);
expect(broker.serviceRegistry.registerAction).toHaveBeenCalledWith("server-2", action);
// expect(broker.emitLocal).toHaveBeenCalledTimes(1);
// expect(broker.emitLocal).toHaveBeenCalledWith("register.action.user.update", { action, nodeID: "server-2" });
});
});
describe("Test broker.wrapAction", () => {
let broker = new ServiceBroker();
it("should not change handler if no middlewares", () => {
let origHandler = jest.fn();
let action = {
name: "list",
handler: origHandler
};
broker.wrapAction(action);
expect(action.handler).toBe(origHandler);
});
it("should wrap middlewares", () => {
let action = {
name: "list",
handler: jest.fn()
};
let mw1 = jest.fn(handler => handler);
let mw2 = jest.fn(handler => handler);
broker.use(mw1);
broker.use(mw2);
broker.wrapAction(action);
expect(mw1).toHaveBeenCalledTimes(1);
expect(mw1).toHaveBeenCalledWith(action.handler, action);
expect(mw2).toHaveBeenCalledTimes(1);
expect(mw2).toHaveBeenCalledWith(action.handler, action);
});
});
/*
describe("Test broker.wrapContextInvoke", () => {
describe("Test wrapping", () => {
let broker = new ServiceBroker();
let origHandler = jest.fn(() => Promise.resolve());
let action = {
name: "list",
handler: origHandler
};
it("should wrap the handler and set to the action", () => {
broker.wrapContextInvoke(action, origHandler);
expect(action.handler).not.toBe(origHandler);
});
});
describe("Test with success response", () => {
let broker = new ServiceBroker();
let origHandler = jest.fn(() => Promise.resolve());
let action = {
name: "list",
handler: origHandler
};
broker.wrapContextInvoke(action, origHandler);
it("should call only origHandler", () => {
let ctx = new Context(broker, action);
ctx._metricStart = jest.fn();
ctx._metricFinish = jest.fn();
return action.handler(ctx).then(() => {
expect(ctx._metricStart).toHaveBeenCalledTimes(0);
expect(ctx._metricFinish).toHaveBeenCalledTimes(0);
expect(origHandler).toHaveBeenCalledTimes(1);
expect(origHandler).toHaveBeenCalledWith(ctx);
});
});
});
describe("Test with success resolve & statistics & metrics", () => {
let broker = new ServiceBroker({
metrics: true,
statistics: true
});
broker.statistics.addRequest = jest.fn();
let origHandler = jest.fn(() => Promise.resolve());
let action = {
name: "list",
handler: origHandler
};
broker.wrapContextInvoke(action, origHandler);
it("should call only origHandler", () => {
let ctx = new Context(broker, action);
ctx.metrics = true;
ctx._metricStart = jest.fn();
ctx._metricFinish = jest.fn();
return action.handler(ctx).then(() => {
expect(ctx._metricStart).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
expect(broker.statistics.addRequest).toHaveBeenCalledTimes(1);
expect(broker.statistics.addRequest).toHaveBeenCalledWith("list", 0, null);
expect(origHandler).toHaveBeenCalledTimes(1);
expect(origHandler).toHaveBeenCalledWith(ctx);
});
});
});
describe("Test with error reject & statistics & metrics", () => {
let broker = new ServiceBroker({
metrics: true,
statistics: true
});
broker.statistics.addRequest = jest.fn();
let origHandler = jest.fn(() => Promise.reject(new MoleculerError("Something went wrong!", 402)));
let action = {
name: "list",
handler: origHandler
};
broker.wrapContextInvoke(action, origHandler);
it("should call metrics & statistics methods & origHandler", () => {
let ctx = new Context(broker, action);
ctx.metrics = true;
ctx._metricStart = jest.fn();
ctx._metricFinish = jest.fn();
return action.handler(ctx).catch(err => {
expect(err).toBeInstanceOf(MoleculerError);
expect(err.message).toBe("Something went wrong!");
expect(err.ctx).toBe(ctx);
expect(ctx._metricStart).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledWith(err);
expect(broker.statistics.addRequest).toHaveBeenCalledTimes(1);
expect(broker.statistics.addRequest).toHaveBeenCalledWith("list", 0, 402);
expect(origHandler).toHaveBeenCalledTimes(1);
expect(origHandler).toHaveBeenCalledWith(ctx);
});
});
});
describe("Test with rejected error message", () => {
let broker = new ServiceBroker();
let origHandler = jest.fn(() => Promise.reject("My custom error message"));
let action = {
name: "list",
handler: origHandler
};
broker.wrapContextInvoke(action, origHandler);
it("should convert error message to MoleculerError", () => {
let ctx = new Context(broker, action);
return action.handler(ctx).catch(err => {
expect(err).toBeInstanceOf(MoleculerError);
expect(err.message).toBe("My custom error message");
expect(err.code).toBe(500);
expect(err.ctx).toBe(ctx);
});
});
});
});
*/
describe("Test broker.unregisterAction", () => {
let broker = new ServiceBroker();
broker.serviceRegistry.unregisterAction = jest.fn(() => true);
let action = {
name: "list"
};
it("should call unregister of serviceRegistry without nodeID", () => {
broker.unregisterAction(null, action);
expect(broker.serviceRegistry.unregisterAction).toHaveBeenCalledTimes(1);
expect(broker.serviceRegistry.unregisterAction).toHaveBeenCalledWith(null, action);
});
it("should call unregister of serviceRegistry with nodeID", () => {
broker.serviceRegistry.unregisterAction.mockClear();
broker.unregisterAction("server-2", action);
expect(broker.serviceRegistry.unregisterAction).toHaveBeenCalledTimes(1);
expect(broker.serviceRegistry.unregisterAction).toHaveBeenCalledWith("server-2", action);
});
});
describe("Test broker.registerInternalActions", () => {
it("should register internal action without statistics", () => {
let broker = new ServiceBroker({
statistics: false,
internalActions: false
});
const service = {"name": "$node"};
broker.registerAction = jest.fn();
broker.registerInternalActions();
expect(broker.registerAction).toHaveBeenCalledTimes(4);
expect(broker.registerAction).toHaveBeenCalledWith(null, { name: "$node.list", cache: false, handler: jasmine.any(Function), service });
expect(broker.registerAction).toHaveBeenCalledWith(null, { name: "$node.services", cache: false, handler: jasmine.any(Function), service });
expect(broker.registerAction).toHaveBeenCalledWith(null, { name: "$node.actions", cache: false, handler: jasmine.any(Function), service });
expect(broker.registerAction).toHaveBeenCalledWith(null, { name: "$node.health", cache: false, handler: jasmine.any(Function), service });
});
it("should register internal action with statistics", () => {
let broker = new ServiceBroker({
statistics: true,
internalActions: false
});
const service = {"name": "$node"};
broker.registerAction = jest.fn();
broker.registerInternalActions();
expect(broker.registerAction).toHaveBeenCalledTimes(5);
expect(broker.registerAction).toHaveBeenCalledWith(null, { name: "$node.stats", cache: false, handler: jasmine.any(Function), service });
});
});
describe("Test broker.on", () => {
let broker = new ServiceBroker();
broker.bus.on = jest.fn();
it("should register handler on this.bus", () => {
broker.on("test.event", jest.fn());
expect(broker.bus.on).toHaveBeenCalledTimes(1);
expect(broker.bus.on).toHaveBeenCalledWith("test.event", jasmine.any(Function));
});
});
describe("Test broker.once", () => {
let broker = new ServiceBroker();
broker.bus.once = jest.fn();
it("should register handler once on this.bus", () => {
broker.once("test.event", jest.fn());
expect(broker.bus.once).toHaveBeenCalledTimes(1);
expect(broker.bus.once).toHaveBeenCalledWith("test.event", jasmine.any(Function));
});
});
describe("Test broker.off", () => {
let broker = new ServiceBroker();
broker.bus.off = jest.fn();
it("should unregister handler on this.bus", () => {
broker.off("test.event", jest.fn());
expect(broker.bus.off).toHaveBeenCalledTimes(1);
expect(broker.bus.off).toHaveBeenCalledWith("test.event", jasmine.any(Function));
});
});
describe("Test broker.getService & hasService", () => {
let broker = new ServiceBroker();
let service = broker.createService({
name: "posts"
});
it("should find the service by name", () => {
let found = broker.getService("posts");
expect(found).toBeDefined();
expect(found).toBe(service);
expect(broker.hasService("posts")).toBe(true);
});
it("should not find the service by name", () => {
let found = broker.getService("other");
expect(found).not.toBeDefined();
expect(broker.hasService("other")).toBe(false);
});
});
describe("Test broker.hasAction", () => {
let broker = new ServiceBroker();
broker.createService({
name: "posts",
actions: {
list: jest.fn()
}
});
it("should find the action by name", () => {
expect(broker.hasAction("posts.list")).toBe(true);
});
it("should not find the action by name", () => {
expect(broker.hasAction("posts.create")).toBe(false);
});
});
describe("Test broker.getAction", () => {
let broker = new ServiceBroker();
const list = {
custom: "hello",
cache: true,
handler: jest.fn()
};
broker.createService({
name: "posts",
actions: {
list
}
});
it("should find the action by name", () => {
const { action } = broker.getAction("posts.list");
expect(action.custom).toBe("hello");
expect(action.cache).toBe(true);
expect(action.handler).toBeInstanceOf(Function);
});
it("should not find the action by name", () => {
expect(broker.getAction("posts.create")).toBe(null);
});
});
describe("Test broker.isActionAvailable", () => {
let broker = new ServiceBroker();
broker.createService({
name: "posts",
actions: {
list: jest.fn()
}
});
it("should find handler for action by name", () => {
expect(broker.hasAction("posts.list")).toBe(true);
expect(broker.isActionAvailable("posts.list")).toBe(true);
});
it("should not find handler for action by name", () => {
broker.unregisterAction(null, { name: "posts.list" });
expect(broker.hasAction("posts.list")).toBe(true);
expect(broker.isActionAvailable("posts.list")).toBe(false);
});
});
describe("Test broker.use (middleware)", () => {
let broker = new ServiceBroker({
validation: false
});
it("should be empty middlewares", () => {
expect(broker.middlewares.length).toBe(0);
});
it("should be contains 2 middlewares", () => {
broker.use(jest.fn());
broker.use();
broker.use(jest.fn());
expect(broker.middlewares.length).toBe(2);
});
it("should be contains plus 2 middlewares", () => {
broker.use(jest.fn(), jest.fn(), null);
expect(broker.middlewares.length).toBe(4);
});
});
describe("Test broker.call method", () => {
describe("Test local call", () => {
let broker = new ServiceBroker({ internalActions: false, metrics: true });
let actionHandler = jest.fn(ctx => ctx);
broker.createService({
name: "posts",
actions: {
find: actionHandler,
noHandler: jest.fn(),
slow() {
return Promise.delay(5000).then(() => "OK");
}
}
});
it("should reject if no action", () => {
return broker.call("posts.noaction").catch(err => {
expect(err).toBeDefined();
expect(err).toBeInstanceOf(ServiceNotFoundError);
expect(err.message).toBe("Service 'posts.noaction' is not available!");
expect(err.data).toEqual({ action: "posts.noaction" });
});
});
it("should reject if no handler", () => {
broker.unregisterAction(null, { name: "posts.noHandler" });
return broker.call("posts.noHandler").catch(err => {
expect(err).toBeDefined();
expect(err).toBeInstanceOf(ServiceNotFoundError);
expect(err.message).toBe("Service 'posts.noHandler' is not available!");
expect(err.data).toEqual({ action: "posts.noHandler" });
});
});
it("should reject if no action on node", () => {
broker.unregisterAction(null, { name: "posts.noHandler" });
return broker.call("posts.noHandler", {}, { nodeID: "node-123"}).catch(err => {
expect(err).toBeDefined();
expect(err).toBeInstanceOf(ServiceNotFoundError);
expect(err.message).toBe("Service 'posts.noHandler' is not available on 'node-123' node!");
expect(err.data).toEqual({ action: "posts.noHandler", nodeID: "node-123" });
});
});
it("should call handler with new Context without params", () => {
let p = broker.call("posts.find");
return p.then(ctx => {
expect(ctx).toBeDefined();
expect(ctx.broker).toBe(broker);
expect(ctx.nodeID).toBeNull();
expect(ctx.level).toBe(1);
expect(ctx.requestID).toBeNull();
expect(ctx.action.name).toBe("posts.find");
expect(ctx.params).toEqual({});
expect(ctx.metrics).toBe(true);
expect(p.ctx).toBe(ctx);
expect(actionHandler).toHaveBeenCalledTimes(1);
expect(actionHandler).toHaveBeenCalledWith(ctx);
});
});
it("should call handler with new Context with params", () => {
actionHandler.mockClear();
let params = { limit: 5, search: "John" };
return broker.call("posts.find", params).then(ctx => {
expect(ctx).toBeDefined();
expect(ctx.action.name).toBe("posts.find");
expect(ctx.params).toEqual(params);
expect(actionHandler).toHaveBeenCalledTimes(1);
expect(actionHandler).toHaveBeenCalledWith(ctx);
});
});
it("should call handler with new Context with requestID", () => {
actionHandler.mockClear();
let params = { limit: 5, search: "John" };
let opts = { requestID: "123", meta: { a: 5 } };
return broker.call("posts.find", params, opts).then(ctx => {
expect(ctx).toBeDefined();
expect(ctx.action.name).toBe("posts.find");
expect(ctx.requestID).toBe("123"); // need enabled `metrics`
expect(ctx.meta).toEqual({ a: 5 });
expect(actionHandler).toHaveBeenCalledTimes(1);
expect(actionHandler).toHaveBeenCalledWith(ctx);
});
});
it("should call handler with a sub Context", () => {
actionHandler.mockClear();
let parentCtx = new Context(broker);
parentCtx.params = { a: 5 };
parentCtx.requestID = "555";
parentCtx.meta = { a: 123 };
parentCtx.metrics = true;
return broker.call("posts.find", { b: 10 }, { parentCtx, meta: { b: "Adam" } }).then(ctx => {
expect(ctx).toBeDefined();
expect(ctx.broker).toBe(broker);
expect(ctx.nodeID).toBeNull();
expect(ctx.level).toBe(2);
expect(ctx.parentID).toBe(parentCtx.id);
expect(ctx.requestID).toBe("555");
expect(ctx.action.name).toBe("posts.find");
expect(ctx.params).toEqual({ b: 10 });
expect(ctx.meta).toEqual({ a: 123, b: "Adam" });
expect(ctx.metrics).toBe(true);
expect(actionHandler).toHaveBeenCalledTimes(1);
expect(actionHandler).toHaveBeenCalledWith(ctx);
});
});
it("should throw Error if reached the 'maxCallLevel'", () => {
actionHandler.mockClear();
broker.options.maxCallLevel = 5;
let parentCtx = new Context(broker);
parentCtx.level = 5;
return broker.call("posts.find", { b: 10 }, { parentCtx }).then(() => {
expect(false).toBe(true);
}).catch(err => {
expect(err).toBeInstanceOf(MaxCallLevelError);
expect(err.code).toBe(500);
expect(err.data).toEqual({"action": "posts.find", "level": 6});
expect(actionHandler).toHaveBeenCalledTimes(0);
});
});
it("should call handler with a reused Context", () => {
actionHandler.mockClear();
let preCtx = new Context(broker, { name: "posts.find" });
preCtx.params = { a: 5 };
preCtx.requestID = "555";
preCtx.meta = { a: 123 };
preCtx.metrics = true;
preCtx._metricStart = jest.fn();
preCtx._metricFinish = jest.fn();
return broker.call("posts.find", { b: 10 }, { ctx: preCtx }).then(ctx => {
expect(ctx).toBe(preCtx);
expect(ctx.broker).toBe(broker);
expect(ctx.nodeID).toBeNull();
expect(ctx.level).toBe(1);
expect(ctx.parentID).toBeNull();
expect(ctx.requestID).toBe("555");
expect(ctx.action.name).toBe("posts.find");
expect(ctx.params).toEqual({ a: 5 }); // params from reused context
expect(ctx.meta).toEqual({ a: 123 });
expect(ctx.metrics).toBe(true);
expect(actionHandler).toHaveBeenCalledTimes(1);
expect(actionHandler).toHaveBeenCalledWith(ctx);
expect(preCtx._metricStart).toHaveBeenCalledTimes(1);
expect(preCtx._metricFinish).toHaveBeenCalledTimes(1);
});
});
it("should call if actionName is an object", () => {
actionHandler.mockClear();
let params = { search: "John" };
let actionItem = broker.getAction("posts.find");
return broker.call(actionItem, params).then(ctx => {
expect(ctx).toBeDefined();
expect(ctx.action.name).toBe("posts.find");
expect(ctx.params).toEqual(params);
expect(actionHandler).toHaveBeenCalledTimes(1);
expect(actionHandler).toHaveBeenCalledWith(ctx);
});
});
it("should call circuitClose if endpoint is in 'half-open' state", () => {
broker.options.circuitBreaker.enabled = true;
actionHandler.mockClear();
let params = { search: "John" };
let actionItem = broker.getAction("posts.find");
actionItem.circuitHalfOpen();
actionItem.circuitClose = jest.fn();
return broker.call(actionItem, params).then(ctx => {
expect(ctx).toBeDefined();
expect(actionHandler).toHaveBeenCalledTimes(1);
expect(actionItem.circuitClose).toHaveBeenCalledTimes(1);
});
});
it("should call _callErrorHandler if call timed out", () => {
let clock = lolex.install();
broker._callErrorHandler = jest.fn((err, ctx, endpoint, opts) => ({ err, ctx, endpoint, opts }));
let p = broker.call("posts.slow", null, { timeout: 1000 });
clock.tick(2000);
p.then(({ err, ctx, endpoint, opts }) => {
expect(err).toBeDefined();
expect(err).toBeInstanceOf(Promise.TimeoutError);
expect(ctx).toBeDefined();
expect(endpoint).toBeDefined();
expect(opts).toEqual({ retryCount: 0, timeout: 1000 });
expect(broker._callErrorHandler).toHaveBeenCalledTimes(1);
//expect(broker._callErrorHandler).toHaveBeenCalledWith(ctx);
clock.uninstall();
});
});
});
describe("Test remote call", () => {
let broker = new ServiceBroker({
transporter: new FakeTransporter(),
internalActions: false,
metrics: true
});
broker.registerAction("server-2", { name: "user.create", service: { name: "user" } });
broker.transit.request = jest.fn((ctx) => Promise.resolve({ ctx }));
it("should call transit.request with new Context without params", () => {
return broker.call("user.create").then(({ ctx }) => {
expect(ctx).toBeDefined();
expect(ctx.broker).toBe(broker);
expect(ctx.nodeID).toBe("server-2");
expect(ctx.level).toBe(1);
expect(ctx.requestID).toBeNull();
expect(ctx.action.name).toBe("user.create");
expect(ctx.params).toEqual({});
expect(ctx.metrics).toBe(true);
expect(ctx.timeout).toBe(0);
expect(ctx.retryCount).toBe(0);
expect(broker.transit.request).toHaveBeenCalledTimes(1);
expect(broker.transit.request).toHaveBeenCalledWith(ctx);
});
});
it("should call transit.request with new Context with params & opts", () => {
broker.transit.request.mockClear();
let params = { limit: 5, search: "John" };
return broker.call("user.create", params, { timeout: 1000 }).then(({ ctx }) => {
expect(ctx).toBeDefined();
expect(ctx.action.name).toBe("user.create");
expect(ctx.params).toEqual(params);
expect(ctx.timeout).toBe(1000);
expect(ctx.retryCount).toBe(0);
expect(broker.transit.request).toHaveBeenCalledTimes(1);
expect(broker.transit.request).toHaveBeenCalledWith(ctx);
// expect(ctx._metricStart).toHaveBeenCalledTimes(1);
// expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
});
});
});
describe("Test direct remote call", () => {
let broker = new ServiceBroker({
transporter: new FakeTransporter(),
internalActions: false,
metrics: true
});
const service = { name: "user" };
broker.registerAction("server-1", { name: "user.create", service });
broker.registerAction("server-2", { name: "user.create", service });
broker.registerAction("server-3", { name: "user.create", service });
broker.registerAction("server-4", { name: "user.create", service });
broker.transit.request = jest.fn((ctx) => Promise.resolve({ ctx }));
it("should call transit.request with nodeID 'server-3'", () => {
return broker.call("user.create", {}, { nodeID: "server-3" }).then(({ ctx }) => {
expect(ctx).toBeDefined();
expect(ctx.broker).toBe(broker);
expect(ctx.nodeID).toBe("server-3");
expect(ctx.level).toBe(1);
expect(ctx.requestID).toBeNull();
expect(ctx.action.name).toBe("user.create");
expect(ctx.params).toEqual({});
expect(broker.transit.request).toHaveBeenCalledTimes(1);
expect(broker.transit.request).toHaveBeenCalledWith(ctx);
});
});
it("should call transit.request with nodeID 'server-3'", () => {
broker.transit.request.mockClear();
return broker.call("user.create", {}, { nodeID: "server-1" }).then(({ ctx }) => {
expect(ctx).toBeDefined();
expect(ctx.broker).toBe(broker);
expect(ctx.nodeID).toBe("server-1");
expect(ctx.level).toBe(1);
expect(ctx.requestID).toBeNull();
expect(ctx.action.name).toBe("user.create");
expect(ctx.params).toEqual({});
expect(broker.transit.request).toHaveBeenCalledTimes(1);
expect(broker.transit.request).toHaveBeenCalledWith(ctx);
});
});
});
});
describe("Test broker._callErrorHandler", () => {
let broker = new ServiceBroker({
transporter: new FakeTransporter(),
metrics: true,
circuitBreaker: {
enabled: true
}
});
broker.call = jest.fn(() => Promise.resolve());
let transit = broker.transit;
let ctx = new Context(broker, { name: "user.create" });
ctx.nodeID = "server-2";
ctx.metrics = true;
let customErr = new MoleculerError("Error", 400);
let timeoutErr = new RequestTimeoutError("user.create", "server-2");
ctx._metricFinish = jest.fn();
transit.removePendingRequest = jest.fn();
let endpoint = new ServiceRegistry.Endpoint(broker, ctx.nodeID, ctx.action);
endpoint.failure = jest.fn();
it("should return error without retryCount & fallbackResponse", () => {
return broker._callErrorHandler(customErr, ctx, endpoint, {}).catch(err => {
expect(err).toBe(customErr);
expect(broker.call).toHaveBeenCalledTimes(0);
expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledWith(err, true);
expect(transit.removePendingRequest).toHaveBeenCalledTimes(1);
expect(transit.removePendingRequest).toHaveBeenCalledWith(ctx.id);
expect(endpoint.failure).toHaveBeenCalledTimes(0);
});
});
it("should call endpoint.failure", () => {
return broker._callErrorHandler(timeoutErr, ctx, endpoint, {}).catch(() => {
expect(endpoint.failure).toHaveBeenCalledTimes(1);
});
});
it("should call endpoint.failure if errorCode >= 500", () => {
endpoint.failure.mockClear();
return broker._callErrorHandler(new MoleculerError("Wrong", 500), ctx, endpoint, {}).catch(() => {
expect(endpoint.failure).toHaveBeenCalledTimes(1);
});
});
it("should call endpoint.failure if errorCode >= 500", () => {
endpoint.failure.mockClear();
broker.options.circuitBreaker.failureOnReject = false;
return broker._callErrorHandler(new MoleculerError("Wrong", 500), ctx, endpoint, {}).catch(() => {
expect(endpoint.failure).toHaveBeenCalledTimes(0);
});
});
it("should convert error text to MoleculerError", () => {
return broker._callErrorHandler("Something happened", ctx, endpoint, {}).catch(err => {
expect(err).toBeInstanceOf(MoleculerError);
expect(broker.call).toHaveBeenCalledTimes(0);
});
});
it("should convert Promise.TimeoutError to RequestTimeoutError", () => {
endpoint.failure.mockClear();
return broker._callErrorHandler(new Promise.TimeoutError, ctx, endpoint, {}).catch(err => {
expect(err).toBeInstanceOf(RequestTimeoutError);
expect(err.message).toBe("Request timed out when call 'user.create' action on 'server-2' node!");
expect(broker.call).toHaveBeenCalledTimes(0);
expect(endpoint.failure).toHaveBeenCalledTimes(1);
});
});
it("should not call endpoint.failure if failureOnTimeout is false", () => {
broker.options.circuitBreaker.failureOnTimeout = false;
endpoint.failure.mockClear();
return broker._callErrorHandler(timeoutErr, ctx, endpoint, {}).catch(() => {
expect(endpoint.failure).toHaveBeenCalledTimes(0);
});
});
it("should retry call if retryCount > 0", () => {
ctx._metricFinish.mockClear();
ctx.retryCount = 2;
return broker._callErrorHandler(timeoutErr, ctx, endpoint, {}).then(() => {
expect(broker.call).toHaveBeenCalledTimes(1);
expect(broker.call).toHaveBeenCalledWith("user.create", {}, { ctx });
expect(ctx.retryCount).toBe(1);
expect(ctx._metricFinish).toHaveBeenCalledTimes(0);
});
});
it("should return with the fallbackResponse data", () => {
ctx._metricFinish.mockClear();
broker.call.mockClear();
let otherRes = {};
return broker._callErrorHandler(customErr, ctx, endpoint, { fallbackResponse: otherRes }).then(res => {
expect(res).toBe(otherRes);
expect(broker.call).toHaveBeenCalledTimes(0);
expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledWith(customErr, true);
});
});
it("should return with the fallbackResponse function returned data", () => {
broker.call.mockClear();
ctx.metrics = false;
ctx._metricFinish.mockClear();
let otherRes = { a: 5 };
let otherFn = jest.fn(() => Promise.resolve(otherRes));
return broker._callErrorHandler(customErr, ctx, endpoint, { fallbackResponse: otherFn }).then(res => {
expect(res).toBe(otherRes);
expect(otherFn).toHaveBeenCalledTimes(1);
expect(otherFn).toHaveBeenCalledWith(ctx);
expect(broker.call).toHaveBeenCalledTimes(0);
expect(ctx._metricFinish).toHaveBeenCalledTimes(0);
});
});
});
describe("Test broker._finishCall", () => {
describe("metrics enabled", () => {
let broker = new ServiceBroker({ metrics: true });
let ctx = new Context(broker, { name: "user.create" });
ctx.nodeID = "server-2";
ctx.metrics = true;
ctx._metricFinish = jest.fn();
it("should call ctx._metricFinish", () => {
broker._finishCall(ctx, null);
expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledWith(null, true);
});
it("should call ctx._metricFinish with error", () => {
ctx._metricFinish.mockClear();
let err = new MoleculerError("");
broker._finishCall(ctx, err);
expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledWith(err, true);
});
});
describe("statistics enabled", () => {
let broker = new ServiceBroker({ metrics: false, statistics: true });
broker.statistics.addRequest = jest.fn();
let ctx = new Context(broker, { name: "user.create" });
ctx.nodeID = "server-2";
ctx.metrics = false;
ctx._metricFinish = jest.fn();
it("should call ctx._metricFinish", () => {
broker._finishCall(ctx, null);
expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledWith(null, false);
expect(broker.statistics.addRequest).toHaveBeenCalledTimes(1);
expect(broker.statistics.addRequest).toHaveBeenCalledWith("user.create", 0, null);
});
it("should call ctx._metricFinish with error", () => {
ctx._metricFinish.mockClear();
broker.statistics.addRequest.mockClear();
let err = new MoleculerError("", 505);
broker._finishCall(ctx, err);
expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledWith(err, false);
expect(broker.statistics.addRequest).toHaveBeenCalledTimes(1);
expect(broker.statistics.addRequest).toHaveBeenCalledWith("user.create", 0, 505);
});
});
describe("metrics & statistics enabled", () => {
let broker = new ServiceBroker({ metrics: true, statistics: true });
broker.statistics.addRequest = jest.fn();
let ctx = new Context(broker, { name: "user.create" });
ctx.nodeID = "server-2";
ctx.metrics = true;
ctx._metricFinish = jest.fn();
it("should call statistics.addRequest", () => {
broker._finishCall(ctx, null);
expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledWith(null, true);
expect(broker.statistics.addRequest).toHaveBeenCalledTimes(1);
expect(broker.statistics.addRequest).toHaveBeenCalledWith("user.create", 0, null);
});
it("should call statistics.addRequest with error", () => {
ctx._metricFinish.mockClear();
broker.statistics.addRequest.mockClear();
let err = new MoleculerError("", 505);
broker._finishCall(ctx, err);
expect(ctx._metricFinish).toHaveBeenCalledTimes(1);
expect(ctx._metricFinish).toHaveBeenCalledWith(err, true);
expect(broker.statistics.addRequest).toHaveBeenCalledTimes(1);
expect(broker.statistics.addRequest).toHaveBeenCalledWith("user.create", 0, 505);
});
});
});
describe("Test broker.emit", () => {
let broker = new ServiceBroker();
broker.emitLocal = jest.fn();
it("should call emitLocal without payload", () => {
broker.emit("test.event");
expect(broker.emitLocal).toHaveBeenCalledTimes(1);
expect(broker.emitLocal).toHaveBeenCalledWith("test.event", undefined);
});
it("should call emitLocal with object payload", () => {
broker.emitLocal.mockClear();
broker.emit("user.event", { name: "John" });
expect(broker.emitLocal).toHaveBeenCalledTimes(1);
expect(broker.emitLocal).toHaveBeenCalledWith("user.event", { name: "John" });
});
});
describe("Test broker.emit with transporter", () => {
let broker = new ServiceBroker({ transporter: new FakeTransporter });
broker.transit.emit = jest.fn();
broker.emitLocal = jest.fn();
it("should call transit.emit with object payload", () => {
broker.transit.emit.mockClear();
broker.emit("user.event", { name: "John" });
expect(broker.transit.emit).toHaveBeenCalledTimes(1);
expect(broker.transit.emit).toHaveBeenCalledWith("user.event", { name: "John" });
expect(broker.emitLocal).toHaveBeenCalledTimes(1);
expect(broker.emitLocal).toHaveBeenCalledWith("user.event", { name: "John" });
});
});
describe("Test broker.shouldMetric", () => {
describe("Test broker.shouldMetric with 0.25", () => {
let broker = new ServiceBroker({ transporter: new FakeTransporter, metrics: true, metricsRate: 0.25 });
it("should return true only all 1/4 calls", () => {
expect(broker.shouldMetric()).toBe(false);
expect(broker.shouldMetric()).toBe(false);
expect(broker.shouldMetric()).toBe(false);
expect(broker.shouldMetric()).toBe(true);
expect(broker.shouldMetric()).toBe(false);
expect(broker.shouldMetric()).toBe(false);
expect(broker.shouldMetric()).toBe(false);
expect(broker.shouldMetric()).toBe(true);
});
});
describe("Test broker.shouldMetric with 1", () => {
let broker = new ServiceBroker({ transporter: new FakeTransporter, metrics: true, metricsRate: 1 });
it("should return true all calls", () => {
expect(broker.shouldMetric()).toBe(true);
expect(broker.shouldMetric()).toBe(true);
expect(broker.shouldMetric()).toBe(true);
expect(broker.shouldMetric()).toBe(true);
expect(broker.shouldMetric()).toBe(true);
});
});
describe("Test broker.shouldMetric with 0", () => {
let broker = new ServiceBroker({ transporter: new FakeTransporter, metrics: true, metricsRate: 0 });
it("should return false all calls", () => {
expect(broker.shouldMetric()).toBe(false);
expect(broker.shouldMetric()).toBe(false);
expect(broker.shouldMetric()).toBe(false);
expect(broker.shouldMetric()).toBe(false);
expect(broker.shouldMetric()).toBe(false);
});
});
});
describe("Test broker.emitLocal", () => {
let broker = new ServiceBroker();
broker.bus.emit = jest.fn();
it("should call bus.emit without payload", () => {
broker.emit("test.event");
expect(broker.bus.emit).toHaveBeenCalledTimes(1);
expect(broker.bus.emit).toHaveBeenCalledWith("test.event", undefined, undefined);
});
it("should call bus.emit with object payload", () => {
broker.bus.emit.mockClear();
broker.emit("user.event", { name: "John" });
expect(broker.bus.emit).toHaveBeenCalledTimes(1);
expect(broker.bus.emit).toHaveBeenCalledWith("user.event", { name: "John" }, undefined);
});
});